mirror of
https://github.com/unraid/api.git
synced 2026-02-06 16:08:52 -06:00
Reorganizes files, but keeps logic (and Nest dependency relationships) unchanged. * **Documentation** * Update README to reflect the new directory structure. * **Refactor** * Update import paths to match the new feature-based directory structure. * Rename `SystemModule` to `NetworkModule` for smaller scope and increased clarity. * Reorganize source code into directories of feature-related concerns. * **Chores** * Delete unused demo configuration and related code for a cleaner codebase. * **Other** * No functional changes to end-user features or behaviors.
305 lines
13 KiB
TypeScript
305 lines
13 KiB
TypeScript
import { ConfigService } from '@nestjs/config';
|
|
|
|
import { faker } from '@faker-js/faker';
|
|
import * as fc from 'fast-check';
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import { ConnectConfigPersister } from '../config/config.persistence.js';
|
|
import { DynamicRemoteAccessType, MyServersConfig } from '../config/connect.config.js';
|
|
|
|
describe('MyServersConfig Validation', () => {
|
|
let persister: ConnectConfigPersister;
|
|
let validConfig: Partial<MyServersConfig>;
|
|
|
|
beforeEach(() => {
|
|
const configService = {
|
|
getOrThrow: vi.fn().mockReturnValue('/mock/path'),
|
|
get: vi.fn(),
|
|
set: vi.fn(),
|
|
changes$: {
|
|
pipe: vi.fn(() => ({
|
|
subscribe: vi.fn(),
|
|
})),
|
|
},
|
|
} as any;
|
|
|
|
persister = new ConnectConfigPersister(configService as any);
|
|
|
|
validConfig = {
|
|
wanaccess: false,
|
|
wanport: 0,
|
|
upnpEnabled: false,
|
|
apikey: 'test-api-key',
|
|
localApiKey: 'test-local-key',
|
|
email: 'test@example.com',
|
|
username: 'testuser',
|
|
avatar: 'https://example.com/avatar.jpg',
|
|
regWizTime: '2024-01-01T00:00:00Z',
|
|
dynamicRemoteAccessType: DynamicRemoteAccessType.DISABLED,
|
|
upnpStatus: null,
|
|
};
|
|
});
|
|
|
|
describe('Email validation', () => {
|
|
it('should accept valid email addresses', async () => {
|
|
const config = { ...validConfig, email: 'user@example.com' };
|
|
const result = await persister.validate(config);
|
|
expect(result.email).toBe('user@example.com');
|
|
});
|
|
|
|
it('should accept empty string for email', async () => {
|
|
const config = { ...validConfig, email: '' };
|
|
const result = await persister.validate(config);
|
|
expect(result.email).toBe('');
|
|
});
|
|
|
|
it('should accept null for email', async () => {
|
|
const config = { ...validConfig, email: null };
|
|
const result = await persister.validate(config);
|
|
expect(result.email).toBeNull();
|
|
});
|
|
|
|
it('should reject invalid email addresses', async () => {
|
|
const config = { ...validConfig, email: 'invalid-email' };
|
|
await expect(persister.validate(config)).rejects.toThrow();
|
|
});
|
|
|
|
it('should reject malformed email addresses', async () => {
|
|
const config = { ...validConfig, email: '@example.com' };
|
|
await expect(persister.validate(config)).rejects.toThrow();
|
|
});
|
|
});
|
|
|
|
describe('Boolean field validation', () => {
|
|
it('should accept boolean values for wanaccess', async () => {
|
|
const config = { ...validConfig, wanaccess: true };
|
|
const result = await persister.validate(config);
|
|
expect(result.wanaccess).toBe(true);
|
|
});
|
|
|
|
it('should accept boolean values for upnpEnabled', async () => {
|
|
const config = { ...validConfig, upnpEnabled: true };
|
|
const result = await persister.validate(config);
|
|
expect(result.upnpEnabled).toBe(true);
|
|
});
|
|
|
|
it('should reject non-boolean values for wanaccess', async () => {
|
|
const config = { ...validConfig, wanaccess: 'yes' as any };
|
|
await expect(persister.validate(config)).rejects.toThrow();
|
|
});
|
|
|
|
it('should reject non-boolean values for upnpEnabled', async () => {
|
|
const config = { ...validConfig, upnpEnabled: 'no' as any };
|
|
await expect(persister.validate(config)).rejects.toThrow();
|
|
});
|
|
});
|
|
|
|
describe('Number field validation', () => {
|
|
it('should accept number values for wanport', async () => {
|
|
const config = { ...validConfig, wanport: 8080 };
|
|
const result = await persister.validate(config);
|
|
expect(result.wanport).toBe(8080);
|
|
});
|
|
|
|
it('should accept null for optional number fields', async () => {
|
|
const config = { ...validConfig, wanport: null };
|
|
const result = await persister.validate(config);
|
|
expect(result.wanport).toBeNull();
|
|
});
|
|
|
|
it('should reject non-number values for wanport', async () => {
|
|
const config = { ...validConfig, wanport: '8080' as any };
|
|
await expect(persister.validate(config)).rejects.toThrow();
|
|
});
|
|
});
|
|
|
|
describe('String field validation', () => {
|
|
it('should accept string values for required string fields', async () => {
|
|
const config = { ...validConfig };
|
|
const result = await persister.validate(config);
|
|
expect(result.apikey).toBe(validConfig.apikey);
|
|
expect(result.localApiKey).toBe(validConfig.localApiKey);
|
|
expect(result.username).toBe(validConfig.username);
|
|
});
|
|
|
|
it('should reject non-string values for required string fields', async () => {
|
|
const config = { ...validConfig, apikey: 123 as any };
|
|
await expect(persister.validate(config)).rejects.toThrow();
|
|
});
|
|
});
|
|
|
|
describe('Enum validation', () => {
|
|
it('should accept valid enum values for dynamicRemoteAccessType', async () => {
|
|
const config = { ...validConfig, dynamicRemoteAccessType: DynamicRemoteAccessType.STATIC };
|
|
const result = await persister.validate(config);
|
|
expect(result.dynamicRemoteAccessType).toBe(DynamicRemoteAccessType.STATIC);
|
|
});
|
|
|
|
it('should reject invalid enum values for dynamicRemoteAccessType', async () => {
|
|
const config = { ...validConfig, dynamicRemoteAccessType: 'INVALID' as any };
|
|
await expect(persister.validate(config)).rejects.toThrow();
|
|
});
|
|
});
|
|
|
|
describe('Property-based validation testing', () => {
|
|
it('should accept valid email addresses generated by faker', () => {
|
|
fc.assert(
|
|
fc.asyncProperty(
|
|
fc.constant(null).map(() => faker.internet.email()),
|
|
async (email) => {
|
|
const config = { ...validConfig, email };
|
|
const result = await persister.validate(config);
|
|
expect(result.email).toBe(email);
|
|
}
|
|
),
|
|
{ numRuns: 20 }
|
|
);
|
|
});
|
|
|
|
it('should handle various boolean combinations', () => {
|
|
fc.assert(
|
|
fc.asyncProperty(fc.boolean(), fc.boolean(), async (wanaccess, upnpEnabled) => {
|
|
const config = { ...validConfig, wanaccess, upnpEnabled };
|
|
const result = await persister.validate(config);
|
|
expect(result.wanaccess).toBe(wanaccess);
|
|
expect(result.upnpEnabled).toBe(upnpEnabled);
|
|
}),
|
|
{ numRuns: 10 }
|
|
);
|
|
});
|
|
|
|
it('should handle valid port numbers', () => {
|
|
fc.assert(
|
|
fc.asyncProperty(fc.integer({ min: 0, max: 65535 }), async (port) => {
|
|
const config = { ...validConfig, wanport: port };
|
|
const result = await persister.validate(config);
|
|
expect(result.wanport).toBe(port);
|
|
expect(typeof result.wanport).toBe('number');
|
|
}),
|
|
{ numRuns: 20 }
|
|
);
|
|
});
|
|
|
|
it('should handle various usernames and API keys', () => {
|
|
fc.assert(
|
|
fc.asyncProperty(
|
|
fc.constant(null).map(() => faker.internet.username()),
|
|
fc.constant(null).map(() => `unraid_${faker.string.alphanumeric({ length: 32 })}`),
|
|
fc.constant(null).map(() => faker.string.alphanumeric({ length: 64 })),
|
|
async (username, apikey, localApiKey) => {
|
|
const config = { ...validConfig, username, apikey, localApiKey };
|
|
const result = await persister.validate(config);
|
|
expect(result.username).toBe(username);
|
|
expect(result.apikey).toBe(apikey);
|
|
expect(result.localApiKey).toBe(localApiKey);
|
|
}
|
|
),
|
|
{ numRuns: 15 }
|
|
);
|
|
});
|
|
|
|
it('should handle various enum values for dynamicRemoteAccessType', () => {
|
|
fc.assert(
|
|
fc.asyncProperty(
|
|
fc.constantFrom(
|
|
DynamicRemoteAccessType.DISABLED,
|
|
DynamicRemoteAccessType.STATIC,
|
|
DynamicRemoteAccessType.UPNP
|
|
),
|
|
async (dynamicRemoteAccessType) => {
|
|
const config = { ...validConfig, dynamicRemoteAccessType };
|
|
const result = await persister.validate(config);
|
|
expect(result.dynamicRemoteAccessType).toBe(dynamicRemoteAccessType);
|
|
}
|
|
),
|
|
{ numRuns: 10 }
|
|
);
|
|
});
|
|
|
|
it('should reject invalid enum values', () => {
|
|
fc.assert(
|
|
fc.asyncProperty(
|
|
fc
|
|
.string({ minLength: 1 })
|
|
.filter((s) => !Object.values(DynamicRemoteAccessType).includes(s as any)),
|
|
async (invalidEnumValue) => {
|
|
const config = { ...validConfig, dynamicRemoteAccessType: invalidEnumValue };
|
|
await expect(persister.validate(config)).rejects.toThrow();
|
|
}
|
|
),
|
|
{ numRuns: 10 }
|
|
);
|
|
});
|
|
|
|
it('should reject invalid email formats using fuzzing', () => {
|
|
fc.assert(
|
|
fc.asyncProperty(
|
|
fc
|
|
.string({ minLength: 1 })
|
|
.filter((s) => !s.includes('@') || s.startsWith('@') || s.endsWith('@')),
|
|
async (invalidEmail) => {
|
|
const config = { ...validConfig, email: invalidEmail };
|
|
await expect(persister.validate(config)).rejects.toThrow();
|
|
}
|
|
),
|
|
{ numRuns: 15 }
|
|
);
|
|
});
|
|
|
|
it('should accept any number values for wanport (range validation is done at form level)', () => {
|
|
fc.assert(
|
|
fc.asyncProperty(fc.integer({ min: -100000, max: 100000 }), async (port) => {
|
|
const config = { ...validConfig, wanport: port };
|
|
const result = await persister.validate(config);
|
|
expect(result.wanport).toBe(port);
|
|
expect(typeof result.wanport).toBe('number');
|
|
}),
|
|
{ numRuns: 10 }
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('Complete config validation', () => {
|
|
it('should validate a complete valid config', async () => {
|
|
const result = await persister.validate(validConfig);
|
|
expect(result).toBeDefined();
|
|
expect(result.email).toBe(validConfig.email);
|
|
expect(result.username).toBe(validConfig.username);
|
|
expect(result.wanaccess).toBe(validConfig.wanaccess);
|
|
expect(result.upnpEnabled).toBe(validConfig.upnpEnabled);
|
|
});
|
|
|
|
it('should validate config with minimal required fields using faker data', () => {
|
|
fc.assert(
|
|
fc.asyncProperty(
|
|
fc.constant(null).map(() => ({
|
|
email: faker.internet.email(),
|
|
username: faker.internet.username(),
|
|
apikey: `unraid_${faker.string.alphanumeric({ length: 32 })}`,
|
|
localApiKey: faker.string.alphanumeric({ length: 64 }),
|
|
avatar: faker.image.avatarGitHub(),
|
|
regWizTime: faker.date.past().toISOString(),
|
|
})),
|
|
async (fakerData) => {
|
|
const minimalConfig = {
|
|
wanaccess: false,
|
|
upnpEnabled: false,
|
|
wanport: 0,
|
|
dynamicRemoteAccessType: DynamicRemoteAccessType.DISABLED,
|
|
upnpStatus: null,
|
|
...fakerData,
|
|
};
|
|
|
|
const result = await persister.validate(minimalConfig);
|
|
expect(result.email).toBe(fakerData.email);
|
|
expect(result.username).toBe(fakerData.username);
|
|
expect(result.apikey).toBe(fakerData.apikey);
|
|
expect(result.localApiKey).toBe(fakerData.localApiKey);
|
|
}
|
|
),
|
|
{ numRuns: 10 }
|
|
);
|
|
});
|
|
});
|
|
});
|