Skip to content

Commit

Permalink
refactor(server): cron validation (#13990)
Browse files Browse the repository at this point in the history
  • Loading branch information
jrasm91 authored Nov 7, 2024
1 parent dc2de47 commit e84ad08
Show file tree
Hide file tree
Showing 7 changed files with 40 additions and 78 deletions.
16 changes: 3 additions & 13 deletions server/src/dtos/system-config.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,8 @@ import {
IsUrl,
Max,
Min,
Validate,
ValidateIf,
ValidateNested,
ValidatorConstraint,
ValidatorConstraintInterface,
} from 'class-validator';
import { SystemConfig } from 'src/config';
import { CLIPConfig, DuplicateDetectionConfig, FacialRecognitionConfig } from 'src/dtos/model-config.dto';
Expand All @@ -33,14 +30,7 @@ import {
VideoContainer,
} from 'src/enum';
import { ConcurrentQueueName, QueueName } from 'src/interfaces/job.interface';
import { ValidateBoolean, validateCronExpression } from 'src/validation';

@ValidatorConstraint({ name: 'cronValidator' })
class CronValidator implements ValidatorConstraintInterface {
validate(expression: string): boolean {
return validateCronExpression(expression);
}
}
import { IsCronExpression, ValidateBoolean } from 'src/validation';

const isLibraryScanEnabled = (config: SystemConfigLibraryScanDto) => config.enabled;
const isOAuthEnabled = (config: SystemConfigOAuthDto) => config.enabled;
Expand All @@ -54,7 +44,7 @@ export class DatabaseBackupConfig {

@ValidateIf(isDatabaseBackupEnabled)
@IsNotEmpty()
@Validate(CronValidator, { message: 'Invalid cron expression' })
@IsCronExpression()
@IsString()
cronExpression!: string;

Expand Down Expand Up @@ -244,7 +234,7 @@ class SystemConfigLibraryScanDto {

@ValidateIf(isLibraryScanEnabled)
@IsNotEmpty()
@Validate(CronValidator, { message: 'Invalid cron expression' })
@IsCronExpression()
@IsString()
cronExpression!: string;
}
Expand Down
20 changes: 0 additions & 20 deletions server/src/services/backup.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,26 +88,6 @@ describe(BackupService.name, () => {
});
});

describe('onConfigValidateEvent', () => {
it('should allow a valid cron expression', () => {
expect(() =>
sut.onConfigValidate({
newConfig: { backup: { database: { cronExpression: '0 0 * * *' } } } as SystemConfig,
oldConfig: {} as SystemConfig,
}),
).not.toThrow(expect.stringContaining('Invalid cron expression'));
});

it('should fail for an invalid cron expression', () => {
expect(() =>
sut.onConfigValidate({
newConfig: { backup: { database: { cronExpression: 'foo' } } } as SystemConfig,
oldConfig: {} as SystemConfig,
}),
).toThrow(/Invalid cron expression.*/);
});
});

describe('cleanupDatabaseBackups', () => {
it('should do nothing if not reached keepLastAmount', async () => {
systemMock.get.mockResolvedValue(systemConfigStub.backupEnabled);
Expand Down
9 changes: 0 additions & 9 deletions server/src/services/backup.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { ArgOf } from 'src/interfaces/event.interface';
import { JobName, JobStatus, QueueName } from 'src/interfaces/job.interface';
import { BaseService } from 'src/services/base.service';
import { handlePromiseError } from 'src/utils/misc';
import { validateCronExpression } from 'src/validation';

@Injectable()
export class BackupService extends BaseService {
Expand Down Expand Up @@ -49,14 +48,6 @@ export class BackupService extends BaseService {
});
}

@OnEvent({ name: 'config.validate' })
onConfigValidate({ newConfig }: ArgOf<'config.validate'>) {
const { database } = newConfig.backup;
if (!validateCronExpression(database.cronExpression)) {
throw new Error(`Invalid cron expression ${database.cronExpression}`);
}
}

async cleanupDatabaseBackups() {
this.logger.debug(`Database Backup Cleanup Started`);
const {
Expand Down
20 changes: 0 additions & 20 deletions server/src/services/library.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,26 +177,6 @@ describe(LibraryService.name, () => {
});
});

describe('onConfigValidateEvent', () => {
it('should allow a valid cron expression', () => {
expect(() =>
sut.onConfigValidate({
newConfig: { library: { scan: { cronExpression: '0 0 * * *' } } } as SystemConfig,
oldConfig: {} as SystemConfig,
}),
).not.toThrow(expect.stringContaining('Invalid cron expression'));
});

it('should fail for an invalid cron expression', () => {
expect(() =>
sut.onConfigValidate({
newConfig: { library: { scan: { cronExpression: 'foo' } } } as SystemConfig,
oldConfig: {} as SystemConfig,
}),
).toThrow(/Invalid cron expression.*/);
});
});

describe('handleQueueSyncFiles', () => {
it('should queue refresh of a new asset', async () => {
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
Expand Down
9 changes: 0 additions & 9 deletions server/src/services/library.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import { BaseService } from 'src/services/base.service';
import { mimeTypes } from 'src/utils/mime-types';
import { handlePromiseError } from 'src/utils/misc';
import { usePagination } from 'src/utils/pagination';
import { validateCronExpression } from 'src/validation';

@Injectable()
export class LibraryService extends BaseService {
Expand Down Expand Up @@ -81,14 +80,6 @@ export class LibraryService extends BaseService {
}
}

@OnEvent({ name: 'config.validate' })
onConfigValidate({ newConfig }: ArgOf<'config.validate'>) {
const { scan } = newConfig.library;
if (!validateCronExpression(scan.cronExpression)) {
throw new Error(`Invalid cron expression ${scan.cronExpression}`);
}
}

private async watch(id: string): Promise<boolean> {
if (!this.watchLibraries) {
return false;
Expand Down
23 changes: 23 additions & 0 deletions server/src/services/system-config.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,29 @@ 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 * * *' } } }));

await expect(sut.getSystemConfig()).resolves.toMatchObject({
library: {
scan: {
enabled: true,
cronExpression: '0 0 * * *',
},
},
});
});

it('should reject invalid cron expressions', async () => {
configMock.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
systemMock.readFile.mockResolvedValue(JSON.stringify({ library: { scan: { cronExpression: 'foo' } } }));

await expect(sut.getSystemConfig()).rejects.toThrow(
'library.scan.cronExpression has failed the following constraints: cronValidator',
);
});

it('should log errors with the config file', async () => {
configMock.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));

Expand Down
21 changes: 14 additions & 7 deletions server/src/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,12 @@ import {
IsOptional,
IsString,
IsUUID,
Validate,
ValidateBy,
ValidateIf,
ValidationOptions,
ValidatorConstraint,
ValidatorConstraintInterface,
buildMessage,
isDateString,
} from 'class-validator';
Expand Down Expand Up @@ -156,16 +159,20 @@ export const ValidateBoolean = (options?: BooleanOptions) => {
return applyDecorators(...decorators);
};

export function validateCronExpression(expression: string) {
try {
new CronJob(expression, () => {});
} catch {
return false;
@ValidatorConstraint({ name: 'cronValidator' })
class CronValidator implements ValidatorConstraintInterface {
validate(expression: string): boolean {
try {
new CronJob(expression, () => {});
return true;
} catch {
return false;
}
}

return true;
}

export const IsCronExpression = () => Validate(CronValidator, { message: 'Invalid cron expression' });

type IValue = { value: unknown };

export const toEmail = ({ value }: IValue) => (typeof value === 'string' ? value.toLowerCase() : value);
Expand Down

0 comments on commit e84ad08

Please sign in to comment.