/** * Unit tests for the Joi validation middleware + schema definitions. * Verifies that valid inputs pass through and invalid inputs throw * ValidationError with descriptive messages. */ const { validateBody, schemas } = require('../../src/utilities/validate'); function mockReq(body) { return { body }; } describe('validateBody middleware', () => { test('passes valid body through and strips unknown keys', () => { const schema = schemas.assetUpload; const req = mockReq({ filename: 'logo.png', data: 'data:image/png;base64,abc', extra: true }); const next = jest.fn(); validateBody(schema)(req, {}, next); expect(next).toHaveBeenCalled(); expect(req.body).toHaveProperty('filename', 'logo.png'); expect(req.body).not.toHaveProperty('extra'); }); test('throws ValidationError on missing required field', () => { const req = mockReq({}); expect(() => validateBody(schemas.assetUpload)(req, {}, jest.fn())).toThrow(/filename/); }); }); describe('schemas.backupConfigUpdate', () => { test('accepts valid patch', () => { const { error, value } = schemas.backupConfigUpdate.validate({ backups: { app1: { enabled: true, schedule: 'daily' } }, defaultRetention: { keep: 7 }, }); expect(error).toBeUndefined(); expect(value).toHaveProperty('backups'); }); test('strips unknown top-level keys', () => { const { error, value } = schemas.backupConfigUpdate.validate({ backups: {}, malicious: true, }, { stripUnknown: true }); expect(error).toBeUndefined(); expect(value).not.toHaveProperty('malicious'); }); test('rejects unknown keys inside per-app backup config', () => { const { error } = schemas.backupConfigUpdate.validate({ backups: { app1: { enabled: true, schedule: 'daily', rce: 'yes' } }, }, { stripUnknown: false }); expect(error).toBeDefined(); }); test('rejects invalid retention.keep', () => { const { error } = schemas.backupConfigUpdate.validate({ defaultRetention: { keep: 0 }, }); expect(error).toBeDefined(); expect(error.details[0].message).toMatch(/keep/); }); }); describe('schemas.backupScheduleCreate', () => { test('requires appId', () => { const { error } = schemas.backupScheduleCreate.validate({}); expect(error).toBeDefined(); }); test('accepts full valid body', () => { const { error, value } = schemas.backupScheduleCreate.validate({ appId: 'plex', enabled: true, schedule: 'daily', retention: { keep: 7 }, destination: 'local', maxStorageBytes: '10GB', }); expect(error).toBeUndefined(); expect(value.appId).toBe('plex'); }); test('rejects invalid destination', () => { const { error } = schemas.backupScheduleCreate.validate({ appId: 'plex', destination: 'malicious-cloud', }); expect(error).toBeDefined(); }); test('accepts numeric custom schedule (e.g. "30m", "6h")', () => { const { error: e1 } = schemas.backupScheduleCreate.validate({ appId: 'plex', schedule: '30m' }); expect(e1).toBeUndefined(); const { error: e2 } = schemas.backupScheduleCreate.validate({ appId: 'plex', schedule: '6h' }); expect(e2).toBeUndefined(); }); test('rejects garbage schedule string', () => { const { error } = schemas.backupScheduleCreate.validate({ appId: 'plex', schedule: 'abc' }); expect(error).toBeDefined(); }); }); describe('schemas.appDeploy', () => { test('requires appId + config.subdomain', () => { const { error } = schemas.appDeploy.validate({}); expect(error).toBeDefined(); }); test('accepts minimal valid deploy', () => { const { error, value } = schemas.appDeploy.validate({ appId: 'plex', config: { subdomain: 'plex' }, }); expect(error).toBeUndefined(); expect(value.config.subdomain).toBe('plex'); }); test('rejects port out of range', () => { const { error } = schemas.appDeploy.validate({ appId: 'plex', config: { subdomain: 'plex', port: 99999 }, }); expect(error).toBeDefined(); }); test('accepts valid IP in allowedIPs', () => { const { error } = schemas.appDeploy.validate({ appId: 'plex', config: { subdomain: 'plex', allowedIPs: ['192.168.1.1', '10.0.0.0/24'] }, }); expect(error).toBeUndefined(); }); test('rejects malformed CIDR in allowedIPs', () => { const { error } = schemas.appDeploy.validate({ appId: 'plex', config: { subdomain: 'plex', allowedIPs: ['999.999.999.999/99'] }, }); expect(error).toBeDefined(); }); test('accepts valid customVolumes objects', () => { const { error } = schemas.appDeploy.validate({ appId: 'plex', config: { subdomain: 'plex', customVolumes: [{ hostPath: '/data/movies', containerPath: '/movies' }], }, }); expect(error).toBeUndefined(); }); test('strips unknown keys in config', () => { const { error, value } = schemas.appDeploy.validate({ appId: 'plex', config: { subdomain: 'plex', isAdmin: true }, }, { stripUnknown: true }); expect(error).toBeUndefined(); expect(value.config).not.toHaveProperty('isAdmin'); }); }); describe('schemas.assetUpload', () => { test('requires both fields', () => { const { error: e1 } = schemas.assetUpload.validate({ filename: 'logo.png' }); expect(e1).toBeDefined(); const { error: e2 } = schemas.assetUpload.validate({ data: 'abc' }); expect(e2).toBeDefined(); }); test('accepts valid upload', () => { const { error } = schemas.assetUpload.validate({ filename: 'logo.png', data: 'data:image/png;base64,iVBORw0KGgo=', }); expect(error).toBeUndefined(); }); }); describe('schemas.backupRestoreFile', () => { test('accepts empty body', () => { const { error } = schemas.backupRestoreFile.validate({}); expect(error).toBeUndefined(); }); test('accepts optional fields', () => { const { error } = schemas.backupRestoreFile.validate({ encryptionKey: 'secret', restartContainers: true, }); expect(error).toBeUndefined(); }); test('strips unknown keys', () => { const { error, value } = schemas.backupRestoreFile.validate({ encryptionKey: 'secret', malicious: 'yes', }, { stripUnknown: true }); expect(error).toBeUndefined(); expect(value).not.toHaveProperty('malicious'); }); });