refactor: test mocks (#16008)

This commit is contained in:
Jason Rasmussen 2025-02-10 18:47:42 -05:00 committed by GitHub
parent 8794c84e9d
commit 735f8d661e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
74 changed files with 3820 additions and 4043 deletions

View file

@ -12,13 +12,11 @@ import {
VideoCodec,
VideoContainer,
} from 'src/enum';
import { IEventRepository } from 'src/interfaces/event.interface';
import { QueueName } from 'src/interfaces/job.interface';
import { SystemConfigService } from 'src/services/system-config.service';
import { DeepPartial, IConfigRepository, ILoggingRepository, ISystemMetadataRepository } from 'src/types';
import { DeepPartial } from 'src/types';
import { mockEnvData } from 'test/repositories/config.repository.mock';
import { newTestService } from 'test/utils';
import { Mocked } from 'vitest';
import { newTestService, ServiceMocks } from 'test/utils';
const partialConfig = {
ffmpeg: { crf: 30 },
@ -198,14 +196,10 @@ const updatedConfig = Object.freeze<SystemConfig>({
describe(SystemConfigService.name, () => {
let sut: SystemConfigService;
let configMock: Mocked<IConfigRepository>;
let eventMock: Mocked<IEventRepository>;
let loggerMock: Mocked<ILoggingRepository>;
let systemMock: Mocked<ISystemMetadataRepository>;
let mocks: ServiceMocks;
beforeEach(() => {
({ sut, configMock, eventMock, loggerMock, systemMock } = newTestService(SystemConfigService));
({ sut, mocks } = newTestService(SystemConfigService));
});
it('should work', () => {
@ -214,22 +208,22 @@ describe(SystemConfigService.name, () => {
describe('getDefaults', () => {
it('should return the default config', () => {
systemMock.get.mockResolvedValue(partialConfig);
mocks.systemMetadata.get.mockResolvedValue(partialConfig);
expect(sut.getDefaults()).toEqual(defaults);
expect(systemMock.get).not.toHaveBeenCalled();
expect(mocks.systemMetadata.get).not.toHaveBeenCalled();
});
});
describe('getConfig', () => {
it('should return the default config', async () => {
systemMock.get.mockResolvedValue({});
mocks.systemMetadata.get.mockResolvedValue({});
await expect(sut.getSystemConfig()).resolves.toEqual(defaults);
});
it('should merge the overrides', async () => {
systemMock.get.mockResolvedValue({
mocks.systemMetadata.get.mockResolvedValue({
ffmpeg: { crf: 30 },
oauth: { autoLaunch: true },
trash: { days: 10 },
@ -240,17 +234,17 @@ describe(SystemConfigService.name, () => {
});
it('should load the config from a json file', async () => {
configMock.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
systemMock.readFile.mockResolvedValue(JSON.stringify(partialConfig));
mocks.config.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
mocks.systemMetadata.readFile.mockResolvedValue(JSON.stringify(partialConfig));
await expect(sut.getSystemConfig()).resolves.toEqual(updatedConfig);
expect(systemMock.readFile).toHaveBeenCalledWith('immich-config.json');
expect(mocks.systemMetadata.readFile).toHaveBeenCalledWith('immich-config.json');
});
it('should transform booleans', async () => {
configMock.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
systemMock.readFile.mockResolvedValue(JSON.stringify({ ffmpeg: { twoPass: 'false' } }));
mocks.config.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
mocks.systemMetadata.readFile.mockResolvedValue(JSON.stringify({ ffmpeg: { twoPass: 'false' } }));
await expect(sut.getSystemConfig()).resolves.toMatchObject({
ffmpeg: expect.objectContaining({ twoPass: false }),
@ -258,8 +252,8 @@ describe(SystemConfigService.name, () => {
});
it('should transform numbers', async () => {
configMock.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
systemMock.readFile.mockResolvedValue(JSON.stringify({ ffmpeg: { threads: '42' } }));
mocks.config.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
mocks.systemMetadata.readFile.mockResolvedValue(JSON.stringify({ ffmpeg: { threads: '42' } }));
await expect(sut.getSystemConfig()).resolves.toMatchObject({
ffmpeg: expect.objectContaining({ threads: 42 }),
@ -267,8 +261,10 @@ describe(SystemConfigService.name, () => {
});
it('should accept valid cron expressions', async () => {
configMock.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
systemMock.readFile.mockResolvedValue(JSON.stringify({ library: { scan: { cronExpression: '0 0 * * *' } } }));
mocks.config.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
mocks.systemMetadata.readFile.mockResolvedValue(
JSON.stringify({ library: { scan: { cronExpression: '0 0 * * *' } } }),
);
await expect(sut.getSystemConfig()).resolves.toMatchObject({
library: {
@ -281,8 +277,8 @@ describe(SystemConfigService.name, () => {
});
it('should reject invalid cron expressions', async () => {
configMock.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
systemMock.readFile.mockResolvedValue(JSON.stringify({ library: { scan: { cronExpression: 'foo' } } }));
mocks.config.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
mocks.systemMetadata.readFile.mockResolvedValue(JSON.stringify({ library: { scan: { cronExpression: 'foo' } } }));
await expect(sut.getSystemConfig()).rejects.toThrow(
'library.scan.cronExpression has failed the following constraints: cronValidator',
@ -290,22 +286,22 @@ describe(SystemConfigService.name, () => {
});
it('should log errors with the config file', async () => {
configMock.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
mocks.config.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
systemMock.readFile.mockResolvedValue(`{ "ffmpeg2": true, "ffmpeg2": true }`);
mocks.systemMetadata.readFile.mockResolvedValue(`{ "ffmpeg2": true, "ffmpeg2": true }`);
await expect(sut.getSystemConfig()).rejects.toBeInstanceOf(Error);
expect(systemMock.readFile).toHaveBeenCalledWith('immich-config.json');
expect(loggerMock.error).toHaveBeenCalledTimes(2);
expect(loggerMock.error.mock.calls[0][0]).toEqual('Unable to load configuration file: immich-config.json');
expect(loggerMock.error.mock.calls[1][0].toString()).toEqual(
expect(mocks.systemMetadata.readFile).toHaveBeenCalledWith('immich-config.json');
expect(mocks.logger.error).toHaveBeenCalledTimes(2);
expect(mocks.logger.error.mock.calls[0][0]).toEqual('Unable to load configuration file: immich-config.json');
expect(mocks.logger.error.mock.calls[1][0].toString()).toEqual(
expect.stringContaining('YAMLException: duplicated mapping key (1:20)'),
);
});
it('should load the config from a yaml file', async () => {
configMock.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.yaml' }));
mocks.config.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.yaml' }));
const partialConfig = `
ffmpeg:
crf: 30
@ -316,26 +312,26 @@ describe(SystemConfigService.name, () => {
user:
deleteDelay: 15
`;
systemMock.readFile.mockResolvedValue(partialConfig);
mocks.systemMetadata.readFile.mockResolvedValue(partialConfig);
await expect(sut.getSystemConfig()).resolves.toEqual(updatedConfig);
expect(systemMock.readFile).toHaveBeenCalledWith('immich-config.yaml');
expect(mocks.systemMetadata.readFile).toHaveBeenCalledWith('immich-config.yaml');
});
it('should accept an empty configuration file', async () => {
configMock.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
systemMock.readFile.mockResolvedValue(JSON.stringify({}));
mocks.config.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
mocks.systemMetadata.readFile.mockResolvedValue(JSON.stringify({}));
await expect(sut.getSystemConfig()).resolves.toEqual(defaults);
expect(systemMock.readFile).toHaveBeenCalledWith('immich-config.json');
expect(mocks.systemMetadata.readFile).toHaveBeenCalledWith('immich-config.json');
});
it('should allow underscores in the machine learning url', async () => {
configMock.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
mocks.config.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
const partialConfig = { machineLearning: { urls: ['immich_machine_learning'] } };
systemMock.readFile.mockResolvedValue(JSON.stringify(partialConfig));
mocks.systemMetadata.readFile.mockResolvedValue(JSON.stringify(partialConfig));
const config = await sut.getSystemConfig();
expect(config.machineLearning.urls).toEqual(['immich_machine_learning']);
@ -349,9 +345,9 @@ describe(SystemConfigService.name, () => {
for (const { should, externalDomain, result } of externalDomainTests) {
it(`should normalize an external domain ${should}`, async () => {
configMock.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
mocks.config.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
const partialConfig = { server: { externalDomain } };
systemMock.readFile.mockResolvedValue(JSON.stringify(partialConfig));
mocks.systemMetadata.readFile.mockResolvedValue(JSON.stringify(partialConfig));
const config = await sut.getSystemConfig();
expect(config.server.externalDomain).toEqual(result ?? 'https://demo.immich.app');
@ -359,14 +355,14 @@ describe(SystemConfigService.name, () => {
}
it('should warn for unknown options in yaml', async () => {
configMock.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.yaml' }));
mocks.config.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.yaml' }));
const partialConfig = `
unknownOption: true
`;
systemMock.readFile.mockResolvedValue(partialConfig);
mocks.systemMetadata.readFile.mockResolvedValue(partialConfig);
await sut.getSystemConfig();
expect(loggerMock.warn).toHaveBeenCalled();
expect(mocks.logger.warn).toHaveBeenCalled();
});
const tests = [
@ -380,12 +376,12 @@ describe(SystemConfigService.name, () => {
for (const test of tests) {
it(`should ${test.should}`, async () => {
configMock.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
systemMock.readFile.mockResolvedValue(JSON.stringify(test.config));
mocks.config.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
mocks.systemMetadata.readFile.mockResolvedValue(JSON.stringify(test.config));
if (test.warn) {
await sut.getSystemConfig();
expect(loggerMock.warn).toHaveBeenCalled();
expect(mocks.logger.warn).toHaveBeenCalled();
} else {
await expect(sut.getSystemConfig()).rejects.toBeInstanceOf(Error);
}
@ -395,19 +391,19 @@ describe(SystemConfigService.name, () => {
describe('updateConfig', () => {
it('should update the config and emit an event', async () => {
systemMock.get.mockResolvedValue(partialConfig);
mocks.systemMetadata.get.mockResolvedValue(partialConfig);
await expect(sut.updateSystemConfig(updatedConfig)).resolves.toEqual(updatedConfig);
expect(eventMock.emit).toHaveBeenCalledWith(
expect(mocks.event.emit).toHaveBeenCalledWith(
'config.update',
expect.objectContaining({ oldConfig: expect.any(Object), newConfig: updatedConfig }),
);
});
it('should throw an error if a config file is in use', async () => {
configMock.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
systemMock.readFile.mockResolvedValue(JSON.stringify({}));
mocks.config.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
mocks.systemMetadata.readFile.mockResolvedValue(JSON.stringify({}));
await expect(sut.updateSystemConfig(defaults)).rejects.toBeInstanceOf(BadRequestException);
expect(systemMock.set).not.toHaveBeenCalled();
expect(mocks.systemMetadata.set).not.toHaveBeenCalled();
});
});