refactor(server): system config (#9517)

This commit is contained in:
Jason Rasmussen 2024-05-15 18:58:23 -04:00 committed by GitHub
parent 7f0f016f2e
commit 984aa8fb41
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
46 changed files with 599 additions and 770 deletions

View file

@ -12,24 +12,25 @@ import {
VideoCodec,
defaults,
} from 'src/config';
import { SystemConfigEntity, SystemConfigKey } from 'src/entities/system-config.entity';
import { SystemMetadataKey } from 'src/entities/system-metadata.entity';
import { IEventRepository, ServerEvent } from 'src/interfaces/event.interface';
import { QueueName } from 'src/interfaces/job.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { ISearchRepository } from 'src/interfaces/search.interface';
import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { SystemConfigService } from 'src/services/system-config.service';
import { newEventRepositoryMock } from 'test/repositories/event.repository.mock';
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
import { newSystemConfigRepositoryMock } from 'test/repositories/system-config.repository.mock';
import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
import { DeepPartial } from 'typeorm';
import { Mocked } from 'vitest';
const updates: SystemConfigEntity[] = [
{ key: SystemConfigKey.FFMPEG_CRF, value: 30 },
{ key: SystemConfigKey.OAUTH_AUTO_LAUNCH, value: true },
{ key: SystemConfigKey.TRASH_DAYS, value: 10 },
{ key: SystemConfigKey.USER_DELETE_DELAY, value: 15 },
];
const partialConfig = {
ffmpeg: { crf: 30 },
oauth: { autoLaunch: true },
trash: { days: 10 },
user: { deleteDelay: 15 },
} satisfies DeepPartial<SystemConfig>;
const updatedConfig = Object.freeze<SystemConfig>({
job: {
@ -171,17 +172,17 @@ const updatedConfig = Object.freeze<SystemConfig>({
describe(SystemConfigService.name, () => {
let sut: SystemConfigService;
let configMock: Mocked<ISystemConfigRepository>;
let systemMock: Mocked<ISystemMetadataRepository>;
let eventMock: Mocked<IEventRepository>;
let loggerMock: Mocked<ILoggerRepository>;
let smartInfoMock: Mocked<ISearchRepository>;
beforeEach(() => {
delete process.env.IMMICH_CONFIG_FILE;
configMock = newSystemConfigRepositoryMock();
systemMock = newSystemMetadataRepositoryMock();
eventMock = newEventRepositoryMock();
loggerMock = newLoggerRepositoryMock();
sut = new SystemConfigService(configMock, eventMock, loggerMock, smartInfoMock);
sut = new SystemConfigService(systemMock, eventMock, loggerMock, smartInfoMock);
});
it('should work', () => {
@ -190,44 +191,39 @@ describe(SystemConfigService.name, () => {
describe('getDefaults', () => {
it('should return the default config', () => {
configMock.load.mockResolvedValue(updates);
systemMock.get.mockResolvedValue(partialConfig);
expect(sut.getDefaults()).toEqual(defaults);
expect(configMock.load).not.toHaveBeenCalled();
expect(systemMock.get).not.toHaveBeenCalled();
});
});
describe('getConfig', () => {
it('should return the default config', async () => {
configMock.load.mockResolvedValue([]);
systemMock.get.mockResolvedValue({});
await expect(sut.getConfig()).resolves.toEqual(defaults);
});
it('should merge the overrides', async () => {
configMock.load.mockResolvedValue([
{ key: SystemConfigKey.FFMPEG_CRF, value: 30 },
{ key: SystemConfigKey.OAUTH_AUTO_LAUNCH, value: true },
{ key: SystemConfigKey.TRASH_DAYS, value: 10 },
{ key: SystemConfigKey.USER_DELETE_DELAY, value: 15 },
]);
systemMock.get.mockResolvedValue({
ffmpeg: { crf: 30 },
oauth: { autoLaunch: true },
trash: { days: 10 },
user: { deleteDelay: 15 },
});
await expect(sut.getConfig()).resolves.toEqual(updatedConfig);
});
it('should load the config from a json file', async () => {
process.env.IMMICH_CONFIG_FILE = 'immich-config.json';
const partialConfig = {
ffmpeg: { crf: 30 },
oauth: { autoLaunch: true },
trash: { days: 10 },
user: { deleteDelay: 15 },
};
configMock.readFile.mockResolvedValue(JSON.stringify(partialConfig));
systemMock.readFile.mockResolvedValue(JSON.stringify(partialConfig));
await expect(sut.getConfig()).resolves.toEqual(updatedConfig);
expect(configMock.readFile).toHaveBeenCalledWith('immich-config.json');
expect(systemMock.readFile).toHaveBeenCalledWith('immich-config.json');
});
it('should load the config from a yaml file', async () => {
@ -242,26 +238,26 @@ describe(SystemConfigService.name, () => {
user:
deleteDelay: 15
`;
configMock.readFile.mockResolvedValue(partialConfig);
systemMock.readFile.mockResolvedValue(partialConfig);
await expect(sut.getConfig()).resolves.toEqual(updatedConfig);
expect(configMock.readFile).toHaveBeenCalledWith('immich-config.yaml');
expect(systemMock.readFile).toHaveBeenCalledWith('immich-config.yaml');
});
it('should accept an empty configuration file', async () => {
process.env.IMMICH_CONFIG_FILE = 'immich-config.json';
configMock.readFile.mockResolvedValue(JSON.stringify({}));
systemMock.readFile.mockResolvedValue(JSON.stringify({}));
await expect(sut.getConfig()).resolves.toEqual(defaults);
expect(configMock.readFile).toHaveBeenCalledWith('immich-config.json');
expect(systemMock.readFile).toHaveBeenCalledWith('immich-config.json');
});
it('should allow underscores in the machine learning url', async () => {
process.env.IMMICH_CONFIG_FILE = 'immich-config.json';
const partialConfig = { machineLearning: { url: 'immich_machine_learning' } };
configMock.readFile.mockResolvedValue(JSON.stringify(partialConfig));
systemMock.readFile.mockResolvedValue(JSON.stringify(partialConfig));
const config = await sut.getConfig();
expect(config.machineLearning.url).toEqual('immich_machine_learning');
@ -272,7 +268,7 @@ describe(SystemConfigService.name, () => {
const partialConfig = `
unknownOption: true
`;
configMock.readFile.mockResolvedValue(partialConfig);
systemMock.readFile.mockResolvedValue(partialConfig);
await sut.getConfig();
expect(loggerMock.warn).toHaveBeenCalled();
@ -290,7 +286,7 @@ describe(SystemConfigService.name, () => {
for (const test of tests) {
it(`should ${test.should}`, async () => {
process.env.IMMICH_CONFIG_FILE = 'immich-config.json';
configMock.readFile.mockResolvedValue(JSON.stringify(test.config));
systemMock.readFile.mockResolvedValue(JSON.stringify(test.config));
if (test.warn) {
await sut.getConfig();
@ -338,20 +334,20 @@ describe(SystemConfigService.name, () => {
describe('updateConfig', () => {
it('should update the config and emit client and server events', async () => {
configMock.load.mockResolvedValue(updates);
systemMock.get.mockResolvedValue(partialConfig);
await expect(sut.updateConfig(updatedConfig)).resolves.toEqual(updatedConfig);
expect(eventMock.clientBroadcast).toHaveBeenCalled();
expect(eventMock.serverSend).toHaveBeenCalledWith(ServerEvent.CONFIG_UPDATE, null);
expect(configMock.saveAll).toHaveBeenCalledWith(updates);
expect(systemMock.set).toHaveBeenCalledWith(SystemMetadataKey.SYSTEM_CONFIG, partialConfig);
});
it('should throw an error if a config file is in use', async () => {
process.env.IMMICH_CONFIG_FILE = 'immich-config.json';
configMock.readFile.mockResolvedValue(JSON.stringify({}));
systemMock.readFile.mockResolvedValue(JSON.stringify({}));
await expect(sut.updateConfig(defaults)).rejects.toBeInstanceOf(BadRequestException);
expect(configMock.saveAll).not.toHaveBeenCalled();
expect(systemMock.set).not.toHaveBeenCalled();
});
});