mirror of
https://github.com/immich-app/immich.git
synced 2026-05-22 06:52:33 -04:00
refactor(server)!: structured validation error responses (#28204)
* refactor(server)!: structured validation error responses * refactor(server): clarify comment on removing duplicate HTTP response fields * enhance validation error tests * make path and message required * fmt * fix e2e test * fmt * feat: enhance error handling in getServerErrorMessage function
This commit is contained in:
@@ -28,14 +28,16 @@ describe(ActivityController.name, () => {
|
||||
const { status, body } = await request(ctx.getHttpServer()).get('/activities');
|
||||
expect(status).toEqual(400);
|
||||
expect(body).toEqual(
|
||||
factory.responses.badRequest(['[albumId] Invalid input: expected string, received undefined']),
|
||||
factory.responses.validationError([
|
||||
{ path: ['albumId'], message: 'Invalid input: expected string, received undefined' },
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject an invalid albumId', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer()).get('/activities').query({ albumId: '123' });
|
||||
expect(status).toEqual(400);
|
||||
expect(body).toEqual(factory.responses.badRequest(['[albumId] Invalid UUID']));
|
||||
expect(body).toEqual(factory.responses.validationError([{ path: ['albumId'], message: 'Invalid UUID' }]));
|
||||
});
|
||||
|
||||
it('should reject an invalid assetId', async () => {
|
||||
@@ -43,7 +45,7 @@ describe(ActivityController.name, () => {
|
||||
.get('/activities')
|
||||
.query({ albumId: factory.uuid(), assetId: '123' });
|
||||
expect(status).toEqual(400);
|
||||
expect(body).toEqual(factory.responses.badRequest(['[assetId] Invalid UUID']));
|
||||
expect(body).toEqual(factory.responses.validationError([{ path: ['assetId'], message: 'Invalid UUID' }]));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -58,7 +60,7 @@ describe(ActivityController.name, () => {
|
||||
.post('/activities')
|
||||
.send({ albumId: '123', type: 'like' });
|
||||
expect(status).toEqual(400);
|
||||
expect(body).toEqual(factory.responses.badRequest(['[albumId] Invalid UUID']));
|
||||
expect(body).toEqual(factory.responses.validationError([{ path: ['albumId'], message: 'Invalid UUID' }]));
|
||||
});
|
||||
|
||||
it('should require a comment when type is comment', async () => {
|
||||
@@ -66,7 +68,11 @@ describe(ActivityController.name, () => {
|
||||
.post('/activities')
|
||||
.send({ albumId: factory.uuid(), type: 'comment', comment: null });
|
||||
expect(status).toEqual(400);
|
||||
expect(body).toEqual(factory.responses.badRequest(['[comment] Invalid input: expected string, received null']));
|
||||
expect(body).toEqual(
|
||||
factory.responses.validationError([
|
||||
{ path: ['comment'], message: 'Invalid input: expected string, received null' },
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -79,7 +85,7 @@ describe(ActivityController.name, () => {
|
||||
it('should require a valid uuid', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer()).delete(`/activities/123`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(factory.responses.badRequest(['[id] Invalid UUID']));
|
||||
expect(body).toEqual(factory.responses.validationError([{ path: ['id'], message: 'Invalid UUID' }]));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -27,13 +27,17 @@ describe(AlbumController.name, () => {
|
||||
it('should reject an invalid shared param', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer()).get('/albums?shared=invalid');
|
||||
expect(status).toEqual(400);
|
||||
expect(body).toEqual(factory.responses.badRequest(['[shared] Invalid option: expected one of "true"|"false"']));
|
||||
expect(body).toEqual(
|
||||
factory.responses.validationError([
|
||||
{ path: ['shared'], message: 'Invalid option: expected one of "true"|"false"' },
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject an invalid assetId param', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer()).get('/albums?assetId=invalid');
|
||||
expect(status).toEqual(400);
|
||||
expect(body).toEqual(factory.responses.badRequest(['[assetId] Invalid UUID']));
|
||||
expect(body).toEqual(factory.responses.validationError([{ path: ['assetId'], message: 'Invalid UUID' }]));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ describe(ApiKeyController.name, () => {
|
||||
it('should require a valid uuid', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer()).get(`/api-keys/123`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(factory.responses.badRequest(['[id] Invalid UUID']));
|
||||
expect(body).toEqual(factory.responses.validationError([{ path: ['id'], message: 'Invalid UUID' }]));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -64,7 +64,7 @@ describe(ApiKeyController.name, () => {
|
||||
.put(`/api-keys/123`)
|
||||
.send({ name: 'new name', permissions: [Permission.All] });
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(factory.responses.badRequest(['[id] Invalid UUID']));
|
||||
expect(body).toEqual(factory.responses.validationError([{ path: ['id'], message: 'Invalid UUID' }]));
|
||||
});
|
||||
|
||||
it('should allow updating just the name', async () => {
|
||||
@@ -84,7 +84,7 @@ describe(ApiKeyController.name, () => {
|
||||
it('should require a valid uuid', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer()).delete(`/api-keys/123`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(factory.responses.badRequest(['[id] Invalid UUID']));
|
||||
expect(body).toEqual(factory.responses.validationError([{ path: ['id'], message: 'Invalid UUID' }]));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -80,7 +80,9 @@ describe(AssetMediaController.name, () => {
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
factory.responses.badRequest(['[metadata] Invalid input: expected JSON string, received string']),
|
||||
factory.responses.validationError([
|
||||
{ path: ['metadata'], message: 'Invalid input: expected JSON string, received string' },
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -91,8 +93,8 @@ describe(AssetMediaController.name, () => {
|
||||
.field({ ...makeUploadDto({ omit: 'fileCreatedAt' }) });
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
factory.responses.badRequest([
|
||||
'[fileCreatedAt] Invalid input: expected ISO 8601 datetime string, received undefined',
|
||||
factory.responses.validationError([
|
||||
{ path: ['fileCreatedAt'], message: 'Invalid input: expected ISO 8601 datetime string, received undefined' },
|
||||
]),
|
||||
);
|
||||
});
|
||||
@@ -104,8 +106,8 @@ describe(AssetMediaController.name, () => {
|
||||
.field(makeUploadDto({ omit: 'fileModifiedAt' }));
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
factory.responses.badRequest([
|
||||
'[fileModifiedAt] Invalid input: expected ISO 8601 datetime string, received undefined',
|
||||
factory.responses.validationError([
|
||||
{ path: ['fileModifiedAt'], message: 'Invalid input: expected ISO 8601 datetime string, received undefined' },
|
||||
]),
|
||||
);
|
||||
});
|
||||
@@ -117,7 +119,9 @@ describe(AssetMediaController.name, () => {
|
||||
.field({ ...makeUploadDto(), isFavorite: 'not-a-boolean' });
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
factory.responses.badRequest(['[isFavorite] Invalid option: expected one of "true"|"false"']),
|
||||
factory.responses.validationError([
|
||||
{ path: ['isFavorite'], message: 'Invalid option: expected one of "true"|"false"' },
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -128,7 +132,9 @@ describe(AssetMediaController.name, () => {
|
||||
.field({ ...makeUploadDto(), visibility: 'not-an-option' });
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
factory.responses.badRequest([expect.stringContaining('[visibility] Invalid option: expected one of')]),
|
||||
factory.responses.validationError([
|
||||
{ path: ['visibility'], message: expect.stringContaining('Invalid option: expected one of') },
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ describe(AssetController.name, () => {
|
||||
.send({ ids: ['123'] });
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(factory.responses.badRequest(['[ids.0] Invalid UUID']));
|
||||
expect(body).toEqual(factory.responses.validationError([{ path: ['ids', 0], message: 'Invalid UUID' }]));
|
||||
});
|
||||
|
||||
it('should require duplicateId to be a string', async () => {
|
||||
@@ -42,7 +42,9 @@ describe(AssetController.name, () => {
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
factory.responses.badRequest(['[duplicateId] Invalid input: expected string, received boolean']),
|
||||
factory.responses.validationError([
|
||||
{ path: ['duplicateId'], message: 'Invalid input: expected string, received boolean' },
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -70,7 +72,7 @@ describe(AssetController.name, () => {
|
||||
.send({ ids: ['123'] });
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(factory.responses.badRequest(['[ids.0] Invalid UUID']));
|
||||
expect(body).toEqual(factory.responses.validationError([{ path: ['ids', 0], message: 'Invalid UUID' }]));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -83,7 +85,7 @@ describe(AssetController.name, () => {
|
||||
it('should require a valid id', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer()).get(`/assets/123`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(factory.responses.badRequest(['[id] Invalid UUID']));
|
||||
expect(body).toEqual(factory.responses.validationError([{ path: ['id'], message: 'Invalid UUID' }]));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -97,12 +99,10 @@ describe(AssetController.name, () => {
|
||||
const { status, body } = await request(ctx.getHttpServer()).put('/assets/copy').send({});
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
factory.responses.badRequest(
|
||||
expect.arrayContaining([
|
||||
'[sourceId] Invalid input: expected string, received undefined',
|
||||
'[targetId] Invalid input: expected string, received undefined',
|
||||
]),
|
||||
),
|
||||
factory.responses.validationError([
|
||||
{ path: ['sourceId'], message: 'Invalid input: expected string, received undefined' },
|
||||
{ path: ['targetId'], message: 'Invalid input: expected string, received undefined' },
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -125,7 +125,9 @@ describe(AssetController.name, () => {
|
||||
.put('/assets/metadata')
|
||||
.send({ items: [{ assetId: '123', key: 'test', value: {} }] });
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['[items.0.assetId] Invalid UUID'])));
|
||||
expect(body).toEqual(
|
||||
factory.responses.validationError([{ path: ['items', 0, 'assetId'], message: 'Invalid UUID' }]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should require a key', async () => {
|
||||
@@ -134,9 +136,9 @@ describe(AssetController.name, () => {
|
||||
.send({ items: [{ assetId: factory.uuid(), value: {} }] });
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
factory.responses.badRequest(
|
||||
expect.arrayContaining(['[items.0.key] Invalid input: expected string, received undefined']),
|
||||
),
|
||||
factory.responses.validationError([
|
||||
{ path: ['items', 0, 'key'], message: 'Invalid input: expected string, received undefined' },
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -159,7 +161,9 @@ describe(AssetController.name, () => {
|
||||
.delete('/assets/metadata')
|
||||
.send({ items: [{ assetId: '123', key: 'test' }] });
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['[items.0.assetId] Invalid UUID'])));
|
||||
expect(body).toEqual(
|
||||
factory.responses.validationError([{ path: ['items', 0, 'assetId'], message: 'Invalid UUID' }]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should require a key', async () => {
|
||||
@@ -168,9 +172,9 @@ describe(AssetController.name, () => {
|
||||
.send({ items: [{ assetId: factory.uuid() }] });
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
factory.responses.badRequest(
|
||||
expect.arrayContaining(['[items.0.key] Invalid input: expected string, received undefined']),
|
||||
),
|
||||
factory.responses.validationError([
|
||||
{ path: ['items', 0, 'key'], message: 'Invalid input: expected string, received undefined' },
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -191,33 +195,56 @@ describe(AssetController.name, () => {
|
||||
it('should require a valid id', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer()).put(`/assets/123`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(factory.responses.badRequest(['Invalid input: expected object, received undefined']));
|
||||
expect(body).toEqual(
|
||||
factory.responses.validationError([
|
||||
{ path: [], message: 'Invalid input: expected object, received undefined' },
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject invalid gps coordinates', async () => {
|
||||
for (const test of [
|
||||
{ latitude: 12 },
|
||||
{ longitude: 12 },
|
||||
{ latitude: 12, longitude: 'abc' },
|
||||
{ latitude: 'abc', longitude: 12 },
|
||||
{ latitude: null, longitude: 12 },
|
||||
{ latitude: 12, longitude: null },
|
||||
{ latitude: 91, longitude: 12 },
|
||||
{ latitude: -91, longitude: 12 },
|
||||
{ latitude: 12, longitude: -181 },
|
||||
{ latitude: 12, longitude: 181 },
|
||||
]) {
|
||||
for (const [test, errors] of [
|
||||
[{ latitude: 12 }, [{ path: [], message: 'Latitude and longitude must be provided together' }]],
|
||||
[{ longitude: 12 }, [{ path: [], message: 'Latitude and longitude must be provided together' }]],
|
||||
[
|
||||
{ latitude: 12, longitude: 'abc' },
|
||||
[{ path: ['longitude'], message: 'Invalid input: expected number, received string' }],
|
||||
],
|
||||
[
|
||||
{ latitude: 'abc', longitude: 12 },
|
||||
[{ path: ['latitude'], message: 'Invalid input: expected number, received string' }],
|
||||
],
|
||||
[
|
||||
{ latitude: null, longitude: 12 },
|
||||
[{ path: ['latitude'], message: 'Invalid input: expected number, received null' }],
|
||||
],
|
||||
[
|
||||
{ latitude: 12, longitude: null },
|
||||
[{ path: ['longitude'], message: 'Invalid input: expected number, received null' }],
|
||||
],
|
||||
[{ latitude: 91, longitude: 12 }, [{ path: ['latitude'], message: 'Too big: expected number to be <=90' }]],
|
||||
[{ latitude: -91, longitude: 12 }, [{ path: ['latitude'], message: 'Too small: expected number to be >=-90' }]],
|
||||
[
|
||||
{ latitude: 12, longitude: -181 },
|
||||
[{ path: ['longitude'], message: 'Too small: expected number to be >=-180' }],
|
||||
],
|
||||
[{ latitude: 12, longitude: 181 }, [{ path: ['longitude'], message: 'Too big: expected number to be <=180' }]],
|
||||
] as const) {
|
||||
const { status, body } = await request(ctx.getHttpServer()).put(`/assets/${factory.uuid()}`).send(test);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(factory.responses.badRequest());
|
||||
expect(body).toEqual(factory.responses.validationError(errors));
|
||||
}
|
||||
});
|
||||
|
||||
it('should reject invalid rating', async () => {
|
||||
for (const test of [{ rating: 7 }, { rating: 3.5 }, { rating: -2 }]) {
|
||||
for (const [test, errors] of [
|
||||
[{ rating: 7 }, [{ path: ['rating'], message: 'Too big: expected number to be <=5' }]],
|
||||
[{ rating: 3.5 }, [{ path: ['rating'], message: 'Invalid input: expected int, received number' }]],
|
||||
[{ rating: -2 }, [{ path: ['rating'], message: 'Too small: expected number to be >=-1' }]],
|
||||
] as const) {
|
||||
const { status, body } = await request(ctx.getHttpServer()).put(`/assets/${factory.uuid()}`).send(test);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(factory.responses.badRequest());
|
||||
expect(body).toEqual(factory.responses.validationError(errors));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -261,13 +288,17 @@ describe(AssetController.name, () => {
|
||||
it('should require a valid id', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer()).put(`/assets/123/metadata`).send({ items: [] });
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['[id] Invalid UUID'])));
|
||||
expect(body).toEqual(factory.responses.validationError([{ path: ['id'], message: 'Invalid UUID' }]));
|
||||
});
|
||||
|
||||
it('should require items to be an array', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer()).put(`/assets/${factory.uuid()}/metadata`).send({});
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(factory.responses.badRequest(['[items] Invalid input: expected array, received undefined']));
|
||||
expect(body).toEqual(
|
||||
factory.responses.validationError([
|
||||
{ path: ['items'], message: 'Invalid input: expected array, received undefined' },
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should require each item to have a valid key', async () => {
|
||||
@@ -276,7 +307,9 @@ describe(AssetController.name, () => {
|
||||
.send({ items: [{ value: { some: 'value' } }] });
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
factory.responses.badRequest(['[items.0.key] Invalid input: expected string, received undefined']),
|
||||
factory.responses.validationError([
|
||||
{ path: ['items', 0, 'key'], message: 'Invalid input: expected string, received undefined' },
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -286,9 +319,9 @@ describe(AssetController.name, () => {
|
||||
.send({ items: [{ key: 'mobile-app', value: null }] });
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
factory.responses.badRequest(
|
||||
expect.arrayContaining(['[items.0.value] Invalid input: expected record, received null']),
|
||||
),
|
||||
factory.responses.validationError([
|
||||
{ path: ['items', 0, 'value'], message: 'Invalid input: expected record, received null' },
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -326,7 +359,7 @@ describe(AssetController.name, () => {
|
||||
it('should require a valid id', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer()).get(`/assets/123/metadata/mobile-app`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['[id] Invalid UUID'])));
|
||||
expect(body).toEqual(factory.responses.validationError([{ path: ['id'], message: 'Invalid UUID' }]));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -376,7 +409,7 @@ describe(AssetController.name, () => {
|
||||
});
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['[id] Invalid UUID'])));
|
||||
expect(body).toEqual(factory.responses.validationError([{ path: ['id'], message: 'Invalid UUID' }]));
|
||||
});
|
||||
|
||||
it('should check the action and parameters discriminator', async () => {
|
||||
@@ -398,13 +431,12 @@ describe(AssetController.name, () => {
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
factory.responses.badRequest(
|
||||
expect.arrayContaining([
|
||||
expect.stringContaining(
|
||||
"[edits.0.parameters] Invalid parameters for action 'rotate', expecting keys: angle",
|
||||
),
|
||||
]),
|
||||
),
|
||||
factory.responses.validationError([
|
||||
{
|
||||
path: ['edits', 0, 'parameters'],
|
||||
message: expect.stringContaining("Invalid parameters for action 'rotate', expecting keys: angle"),
|
||||
},
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -413,7 +445,11 @@ describe(AssetController.name, () => {
|
||||
.put(`/assets/${factory.uuid()}/edits`)
|
||||
.send({ edits: [] });
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(factory.responses.badRequest(['[edits] Too small: expected array to have >=1 items']));
|
||||
expect(body).toEqual(
|
||||
factory.responses.validationError([
|
||||
{ path: ['edits'], message: 'Too small: expected array to have >=1 items' },
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -426,7 +462,7 @@ describe(AssetController.name, () => {
|
||||
it('should require a valid id', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer()).delete(`/assets/123/metadata/mobile-app`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(factory.responses.badRequest(['[id] Invalid UUID']));
|
||||
expect(body).toEqual(factory.responses.validationError([{ path: ['id'], message: 'Invalid UUID' }]));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,19 +28,27 @@ describe(AuthController.name, () => {
|
||||
it('should require an email address', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer()).post('/auth/admin-sign-up').send({ name, password });
|
||||
expect(status).toEqual(400);
|
||||
expect(body).toEqual(errorDto.badRequest());
|
||||
expect(body).toEqual(
|
||||
errorDto.validationError([{ path: ['email'], message: 'Invalid input: expected email, received undefined' }]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should require a password', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer()).post('/auth/admin-sign-up').send({ name, email });
|
||||
expect(status).toEqual(400);
|
||||
expect(body).toEqual(errorDto.badRequest());
|
||||
expect(body).toEqual(
|
||||
errorDto.validationError([
|
||||
{ path: ['password'], message: 'Invalid input: expected string, received undefined' },
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should require a name', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer()).post('/auth/admin-sign-up').send({ email, password });
|
||||
expect(status).toEqual(400);
|
||||
expect(body).toEqual(errorDto.badRequest());
|
||||
expect(body).toEqual(
|
||||
errorDto.validationError([{ path: ['name'], message: 'Invalid input: expected string, received undefined' }]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should require a valid email', async () => {
|
||||
@@ -48,7 +56,9 @@ describe(AuthController.name, () => {
|
||||
.post('/auth/admin-sign-up')
|
||||
.send({ name, email: 'immich', password });
|
||||
expect(status).toEqual(400);
|
||||
expect(body).toEqual(errorDto.badRequest());
|
||||
expect(body).toEqual(
|
||||
errorDto.validationError([{ path: ['email'], message: 'Invalid input: expected email, received string' }]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should transform email to lower case', async () => {
|
||||
@@ -73,9 +83,9 @@ describe(AuthController.name, () => {
|
||||
const { status, body } = await request(ctx.getHttpServer()).post('/auth/login').send({ name: 'admin' });
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
errorDto.badRequest([
|
||||
'[email] Invalid input: expected email, received undefined',
|
||||
'[password] Invalid input: expected string, received undefined',
|
||||
errorDto.validationError([
|
||||
{ path: ['email'], message: 'Invalid input: expected email, received undefined' },
|
||||
{ path: ['password'], message: 'Invalid input: expected string, received undefined' },
|
||||
]),
|
||||
);
|
||||
});
|
||||
@@ -85,7 +95,9 @@ describe(AuthController.name, () => {
|
||||
.post('/auth/login')
|
||||
.send({ name: 'admin', email: null, password: 'password' });
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['[email] Invalid input: expected email, received object']));
|
||||
expect(body).toEqual(
|
||||
errorDto.validationError([{ path: ['email'], message: 'Invalid input: expected email, received object' }]),
|
||||
);
|
||||
});
|
||||
|
||||
it(`should not allow null password`, async () => {
|
||||
@@ -93,7 +105,9 @@ describe(AuthController.name, () => {
|
||||
.post('/auth/login')
|
||||
.send({ name: 'admin', email: 'admin@immich.cloud', password: null });
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['[password] Invalid input: expected string, received null']));
|
||||
expect(body).toEqual(
|
||||
errorDto.validationError([{ path: ['password'], message: 'Invalid input: expected string, received null' }]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject an invalid email', async () => {
|
||||
@@ -104,7 +118,9 @@ describe(AuthController.name, () => {
|
||||
.send({ name: 'admin', email: [], password: 'password' });
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['[email] Invalid input: expected email, received object']));
|
||||
expect(body).toEqual(
|
||||
errorDto.validationError([{ path: ['email'], message: 'Invalid input: expected email, received object' }]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should transform the email to all lowercase', async () => {
|
||||
@@ -195,19 +211,31 @@ describe(AuthController.name, () => {
|
||||
it('should reject 5 digits', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer()).post('/auth/pin-code').send({ pinCode: '12345' });
|
||||
expect(status).toEqual(400);
|
||||
expect(body).toEqual(errorDto.badRequest([String.raw`[pinCode] Invalid string: must match pattern /^\d{6}$/`]));
|
||||
expect(body).toEqual(
|
||||
errorDto.validationError([
|
||||
{ path: ['pinCode'], message: String.raw`Invalid string: must match pattern /^\d{6}$/` },
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject 7 digits', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer()).post('/auth/pin-code').send({ pinCode: '1234567' });
|
||||
expect(status).toEqual(400);
|
||||
expect(body).toEqual(errorDto.badRequest([String.raw`[pinCode] Invalid string: must match pattern /^\d{6}$/`]));
|
||||
expect(body).toEqual(
|
||||
errorDto.validationError([
|
||||
{ path: ['pinCode'], message: String.raw`Invalid string: must match pattern /^\d{6}$/` },
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject non-numbers', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer()).post('/auth/pin-code').send({ pinCode: 'A12345' });
|
||||
expect(status).toEqual(400);
|
||||
expect(body).toEqual(errorDto.badRequest([String.raw`[pinCode] Invalid string: must match pattern /^\d{6}$/`]));
|
||||
expect(body).toEqual(
|
||||
errorDto.validationError([
|
||||
{ path: ['pinCode'], message: String.raw`Invalid string: must match pattern /^\d{6}$/` },
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ describe(DuplicateController.name, () => {
|
||||
it('should require a valid uuid', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer()).delete(`/duplicates/123`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(factory.responses.badRequest(['[id] Invalid UUID']));
|
||||
expect(body).toEqual(factory.responses.validationError([{ path: ['id'], message: 'Invalid UUID' }]));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -31,7 +31,9 @@ describe(MaintenanceController.name, () => {
|
||||
});
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
errorDto.badRequest(['[restoreBackupFilename] Backup filename is required when action is restore_database']),
|
||||
errorDto.validationError([
|
||||
{ path: ['restoreBackupFilename'], message: 'Backup filename is required when action is restore_database' },
|
||||
]),
|
||||
);
|
||||
expect(ctx.authenticate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -47,7 +47,11 @@ describe(MemoryController.name, () => {
|
||||
});
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['[data.year] Invalid input: expected number, received undefined']));
|
||||
expect(body).toEqual(
|
||||
errorDto.validationError([
|
||||
{ path: ['data', 'year'], message: 'Invalid input: expected number, received undefined' },
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should accept showAt and hideAt', async () => {
|
||||
@@ -81,7 +85,7 @@ describe(MemoryController.name, () => {
|
||||
it('should require a valid id', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer()).get(`/memories/invalid`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['[id] Invalid UUID']));
|
||||
expect(body).toEqual(errorDto.validationError([{ path: ['id'], message: 'Invalid UUID' }]));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -94,13 +98,15 @@ describe(MemoryController.name, () => {
|
||||
it('should require a valid id', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer()).put(`/memories/invalid`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['Invalid input: expected object, received undefined']));
|
||||
expect(body).toEqual(
|
||||
errorDto.validationError([{ path: [], message: 'Invalid input: expected object, received undefined' }]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should require at least one field', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer()).put(`/memories/${factory.uuid()}`).send({});
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['At least one field must be provided']));
|
||||
expect(body).toEqual(errorDto.validationError([{ path: [], message: 'At least one field must be provided' }]));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -120,7 +126,7 @@ describe(MemoryController.name, () => {
|
||||
it('should require a valid id', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer()).put(`/memories/invalid/assets`).send({ ids: [] });
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['[id] Invalid UUID']));
|
||||
expect(body).toEqual(errorDto.validationError([{ path: ['id'], message: 'Invalid UUID' }]));
|
||||
});
|
||||
|
||||
it('should require a valid asset id', async () => {
|
||||
@@ -128,7 +134,7 @@ describe(MemoryController.name, () => {
|
||||
.put(`/memories/${factory.uuid()}/assets`)
|
||||
.send({ ids: ['invalid'] });
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['[ids.0] Invalid UUID']));
|
||||
expect(body).toEqual(errorDto.validationError([{ path: ['ids', 0], message: 'Invalid UUID' }]));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -141,7 +147,7 @@ describe(MemoryController.name, () => {
|
||||
it('should require a valid id', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer()).delete(`/memories/invalid/assets`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['[id] Invalid UUID']));
|
||||
expect(body).toEqual(errorDto.validationError([{ path: ['id'], message: 'Invalid UUID' }]));
|
||||
});
|
||||
|
||||
it('should require a valid asset id', async () => {
|
||||
@@ -149,7 +155,7 @@ describe(MemoryController.name, () => {
|
||||
.delete(`/memories/${factory.uuid()}/assets`)
|
||||
.send({ ids: ['invalid'] });
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['[ids.0] Invalid UUID']));
|
||||
expect(body).toEqual(errorDto.validationError([{ path: ['ids', 0], message: 'Invalid UUID' }]));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -31,7 +31,11 @@ describe(NotificationController.name, () => {
|
||||
.query({ level: 'invalid' })
|
||||
.set('Authorization', `Bearer token`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest([expect.stringContaining('[level] Invalid option: expected one of')]));
|
||||
expect(body).toEqual(
|
||||
errorDto.validationError([
|
||||
{ path: ['level'], message: expect.stringContaining('Invalid option: expected one of') },
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -45,7 +49,9 @@ describe(NotificationController.name, () => {
|
||||
it('should require a list', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer()).put(`/notifications`).send({ ids: true });
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['[ids] Invalid input: expected array, received boolean']));
|
||||
expect(body).toEqual(
|
||||
errorDto.validationError([{ path: ['ids'], message: 'Invalid input: expected array, received boolean' }]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should require uuids', async () => {
|
||||
@@ -53,7 +59,9 @@ describe(NotificationController.name, () => {
|
||||
.put(`/notifications`)
|
||||
.send({ ids: [true] });
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['[ids.0] Invalid input: expected string, received boolean']));
|
||||
expect(body).toEqual(
|
||||
errorDto.validationError([{ path: ['ids', 0], message: 'Invalid input: expected string, received boolean' }]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should accept valid uuids', async () => {
|
||||
@@ -75,7 +83,7 @@ describe(NotificationController.name, () => {
|
||||
it('should require a valid uuid', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer()).get(`/notifications/123`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['[id] Invalid UUID']));
|
||||
expect(body).toEqual(errorDto.validationError([{ path: ['id'], message: 'Invalid UUID' }]));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -33,7 +33,9 @@ describe(PartnerController.name, () => {
|
||||
const { status, body } = await request(ctx.getHttpServer()).get(`/partners`).set('Authorization', `Bearer token`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
errorDto.badRequest([expect.stringContaining('[direction] Invalid option: expected one of')]),
|
||||
errorDto.validationError([
|
||||
{ path: ['direction'], message: expect.stringContaining('Invalid option: expected one of') },
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -44,7 +46,9 @@ describe(PartnerController.name, () => {
|
||||
.set('Authorization', `Bearer token`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
errorDto.badRequest([expect.stringContaining('[direction] Invalid option: expected one of')]),
|
||||
errorDto.validationError([
|
||||
{ path: ['direction'], message: expect.stringContaining('Invalid option: expected one of') },
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -61,7 +65,7 @@ describe(PartnerController.name, () => {
|
||||
.send({ sharedWithId: 'invalid' })
|
||||
.set('Authorization', `Bearer token`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['[sharedWithId] Invalid UUID']));
|
||||
expect(body).toEqual(errorDto.validationError([{ path: ['sharedWithId'], message: 'Invalid UUID' }]));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -77,7 +81,7 @@ describe(PartnerController.name, () => {
|
||||
.send({ inTimeline: true })
|
||||
.set('Authorization', `Bearer token`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['[id] Invalid UUID']));
|
||||
expect(body).toEqual(errorDto.validationError([{ path: ['id'], message: 'Invalid UUID' }]));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -92,7 +96,7 @@ describe(PartnerController.name, () => {
|
||||
.delete(`/partners/invalid`)
|
||||
.set('Authorization', `Bearer token`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['[id] Invalid UUID']));
|
||||
expect(body).toEqual(errorDto.validationError([{ path: ['id'], message: 'Invalid UUID' }]));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -35,7 +35,7 @@ describe(PersonController.name, () => {
|
||||
.query({ closestPersonId: 'invalid' })
|
||||
.set('Authorization', `Bearer token`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['[closestPersonId] Invalid UUID']));
|
||||
expect(body).toEqual(errorDto.validationError([{ path: ['closestPersonId'], message: 'Invalid UUID' }]));
|
||||
});
|
||||
|
||||
it(`should require closestAssetId to be a uuid`, async () => {
|
||||
@@ -44,7 +44,7 @@ describe(PersonController.name, () => {
|
||||
.query({ closestAssetId: 'invalid' })
|
||||
.set('Authorization', `Bearer token`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['[closestAssetId] Invalid UUID']));
|
||||
expect(body).toEqual(errorDto.validationError([{ path: ['closestAssetId'], message: 'Invalid UUID' }]));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -76,7 +76,7 @@ describe(PersonController.name, () => {
|
||||
.delete('/people')
|
||||
.send({ ids: ['invalid'] });
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['[ids.0] Invalid UUID']));
|
||||
expect(body).toEqual(errorDto.validationError([{ path: ['ids', 0], message: 'Invalid UUID' }]));
|
||||
});
|
||||
|
||||
it('should respond with 204', async () => {
|
||||
@@ -104,7 +104,9 @@ describe(PersonController.name, () => {
|
||||
it('should require a valid uuid', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer()).put(`/people/123`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['Invalid input: expected object, received undefined']));
|
||||
expect(body).toEqual(
|
||||
errorDto.validationError([{ path: [], message: 'Invalid input: expected object, received undefined' }]),
|
||||
);
|
||||
});
|
||||
|
||||
it(`should not allow a null name`, async () => {
|
||||
@@ -113,7 +115,9 @@ describe(PersonController.name, () => {
|
||||
.send({ name: null })
|
||||
.set('Authorization', `Bearer token`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['[name] Invalid input: expected string, received null']));
|
||||
expect(body).toEqual(
|
||||
errorDto.validationError([{ path: ['name'], message: 'Invalid input: expected string, received null' }]),
|
||||
);
|
||||
});
|
||||
|
||||
it(`should require featureFaceAssetId to be a uuid`, async () => {
|
||||
@@ -122,7 +126,7 @@ describe(PersonController.name, () => {
|
||||
.send({ featureFaceAssetId: 'invalid' })
|
||||
.set('Authorization', `Bearer token`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['[featureFaceAssetId] Invalid UUID']));
|
||||
expect(body).toEqual(errorDto.validationError([{ path: ['featureFaceAssetId'], message: 'Invalid UUID' }]));
|
||||
});
|
||||
|
||||
it(`should require isFavorite to be a boolean`, async () => {
|
||||
@@ -131,7 +135,11 @@ describe(PersonController.name, () => {
|
||||
.send({ isFavorite: 'invalid' })
|
||||
.set('Authorization', `Bearer token`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['[isFavorite] Invalid input: expected boolean, received string']));
|
||||
expect(body).toEqual(
|
||||
errorDto.validationError([
|
||||
{ path: ['isFavorite'], message: 'Invalid input: expected boolean, received string' },
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it(`should require isHidden to be a boolean`, async () => {
|
||||
@@ -140,7 +148,9 @@ describe(PersonController.name, () => {
|
||||
.send({ isHidden: 'invalid' })
|
||||
.set('Authorization', `Bearer token`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['[isHidden] Invalid input: expected boolean, received string']));
|
||||
expect(body).toEqual(
|
||||
errorDto.validationError([{ path: ['isHidden'], message: 'Invalid input: expected boolean, received string' }]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should map an empty birthDate to null', async () => {
|
||||
@@ -154,7 +164,11 @@ describe(PersonController.name, () => {
|
||||
.put(`/people/${factory.uuid()}`)
|
||||
.send({ birthDate: false });
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['[birthDate] Invalid input: expected string, received boolean']));
|
||||
expect(body).toEqual(
|
||||
errorDto.validationError([
|
||||
{ path: ['birthDate'], message: 'Invalid input: expected string, received boolean' },
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should not accept an invalid birth date (number)', async () => {
|
||||
@@ -162,7 +176,9 @@ describe(PersonController.name, () => {
|
||||
.put(`/people/${factory.uuid()}`)
|
||||
.send({ birthDate: 123_456 });
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['[birthDate] Invalid input: expected string, received number']));
|
||||
expect(body).toEqual(
|
||||
errorDto.validationError([{ path: ['birthDate'], message: 'Invalid input: expected string, received number' }]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should not accept a birth date in the future)', async () => {
|
||||
@@ -170,7 +186,9 @@ describe(PersonController.name, () => {
|
||||
.put(`/people/${factory.uuid()}`)
|
||||
.send({ birthDate: '9999-01-01' });
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['[birthDate] Birth date cannot be in the future']));
|
||||
expect(body).toEqual(
|
||||
errorDto.validationError([{ path: ['birthDate'], message: 'Birth date cannot be in the future' }]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -183,7 +201,7 @@ describe(PersonController.name, () => {
|
||||
it('should require a valid uuid', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer()).delete(`/people/invalid`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['[id] Invalid UUID']));
|
||||
expect(body).toEqual(errorDto.validationError([{ path: ['id'], message: 'Invalid UUID' }]));
|
||||
});
|
||||
|
||||
it('should respond with 204', async () => {
|
||||
|
||||
@@ -27,31 +27,41 @@ describe(SearchController.name, () => {
|
||||
it('should reject page as a string', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer()).post('/search/metadata').send({ page: 'abc' });
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['[page] Invalid input: expected number, received string']));
|
||||
expect(body).toEqual(
|
||||
errorDto.validationError([{ path: ['page'], message: 'Invalid input: expected number, received string' }]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject page as a negative number', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer()).post('/search/metadata').send({ page: -10 });
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['[page] Too small: expected number to be >=1']));
|
||||
expect(body).toEqual(
|
||||
errorDto.validationError([{ path: ['page'], message: 'Too small: expected number to be >=1' }]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject page as 0', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer()).post('/search/metadata').send({ page: 0 });
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['[page] Too small: expected number to be >=1']));
|
||||
expect(body).toEqual(
|
||||
errorDto.validationError([{ path: ['page'], message: 'Too small: expected number to be >=1' }]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject size as a string', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer()).post('/search/metadata').send({ size: 'abc' });
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['[size] Invalid input: expected number, received string']));
|
||||
expect(body).toEqual(
|
||||
errorDto.validationError([{ path: ['size'], message: 'Invalid input: expected number, received string' }]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject an invalid size', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer()).post('/search/metadata').send({ size: -1 });
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['[size] Too small: expected number to be >=1']));
|
||||
expect(body).toEqual(
|
||||
errorDto.validationError([{ path: ['size'], message: 'Too small: expected number to be >=1' }]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject an visibility as not an enum', async () => {
|
||||
@@ -60,7 +70,9 @@ describe(SearchController.name, () => {
|
||||
.send({ visibility: 'immich' });
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
errorDto.badRequest([expect.stringContaining('[visibility] Invalid option: expected one of')]),
|
||||
errorDto.validationError([
|
||||
{ path: ['visibility'], message: expect.stringContaining('Invalid option: expected one of') },
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -69,7 +81,11 @@ describe(SearchController.name, () => {
|
||||
.post('/search/metadata')
|
||||
.send({ isFavorite: 'immich' });
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['[isFavorite] Invalid input: expected boolean, received string']));
|
||||
expect(body).toEqual(
|
||||
errorDto.validationError([
|
||||
{ path: ['isFavorite'], message: 'Invalid input: expected boolean, received string' },
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject an isEncoded as not a boolean', async () => {
|
||||
@@ -77,7 +93,11 @@ describe(SearchController.name, () => {
|
||||
.post('/search/metadata')
|
||||
.send({ isEncoded: 'immich' });
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['[isEncoded] Invalid input: expected boolean, received string']));
|
||||
expect(body).toEqual(
|
||||
errorDto.validationError([
|
||||
{ path: ['isEncoded'], message: 'Invalid input: expected boolean, received string' },
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject an isOffline as not a boolean', async () => {
|
||||
@@ -85,13 +105,19 @@ describe(SearchController.name, () => {
|
||||
.post('/search/metadata')
|
||||
.send({ isOffline: 'immich' });
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['[isOffline] Invalid input: expected boolean, received string']));
|
||||
expect(body).toEqual(
|
||||
errorDto.validationError([
|
||||
{ path: ['isOffline'], message: 'Invalid input: expected boolean, received string' },
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject an isMotion as not a boolean', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer()).post('/search/metadata').send({ isMotion: 'immich' });
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['[isMotion] Invalid input: expected boolean, received string']));
|
||||
expect(body).toEqual(
|
||||
errorDto.validationError([{ path: ['isMotion'], message: 'Invalid input: expected boolean, received string' }]),
|
||||
);
|
||||
});
|
||||
|
||||
describe('POST /search/random', () => {
|
||||
@@ -105,7 +131,11 @@ describe(SearchController.name, () => {
|
||||
.post('/search/random')
|
||||
.send({ withStacked: 'immich' });
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['[withStacked] Invalid input: expected boolean, received string']));
|
||||
expect(body).toEqual(
|
||||
errorDto.validationError([
|
||||
{ path: ['withStacked'], message: 'Invalid input: expected boolean, received string' },
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject if withPeople is not a boolean', async () => {
|
||||
@@ -113,7 +143,11 @@ describe(SearchController.name, () => {
|
||||
.post('/search/random')
|
||||
.send({ withPeople: 'immich' });
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['[withPeople] Invalid input: expected boolean, received string']));
|
||||
expect(body).toEqual(
|
||||
errorDto.validationError([
|
||||
{ path: ['withPeople'], message: 'Invalid input: expected boolean, received string' },
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -140,7 +174,9 @@ describe(SearchController.name, () => {
|
||||
it('should require a name', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer()).get('/search/person').send({});
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['[name] Invalid input: expected string, received undefined']));
|
||||
expect(body).toEqual(
|
||||
errorDto.validationError([{ path: ['name'], message: 'Invalid input: expected string, received undefined' }]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -153,7 +189,9 @@ describe(SearchController.name, () => {
|
||||
it('should require a name', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer()).get('/search/places').send({});
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['[name] Invalid input: expected string, received undefined']));
|
||||
expect(body).toEqual(
|
||||
errorDto.validationError([{ path: ['name'], message: 'Invalid input: expected string, received undefined' }]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -173,7 +211,11 @@ describe(SearchController.name, () => {
|
||||
it('should require a type', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer()).get('/search/suggestions').send({});
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest([expect.stringContaining('[type] Invalid option: expected one of')]));
|
||||
expect(body).toEqual(
|
||||
errorDto.validationError([
|
||||
{ path: ['type'], message: expect.stringContaining('Invalid option: expected one of') },
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -35,7 +35,11 @@ describe(SyncController.name, () => {
|
||||
.post('/sync/stream')
|
||||
.send({ types: ['invalid'] });
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest([expect.stringContaining('[types.0] Invalid option: expected one of')]));
|
||||
expect(body).toEqual(
|
||||
errorDto.validationError([
|
||||
{ path: ['types', 0], message: expect.stringContaining('Invalid option: expected one of') },
|
||||
]),
|
||||
);
|
||||
expect(ctx.authenticate).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -57,7 +61,9 @@ describe(SyncController.name, () => {
|
||||
const acks = Array.from({ length: 1001 }, (_, i) => `ack-${i}`);
|
||||
const { status, body } = await request(ctx.getHttpServer()).post('/sync/ack').send({ acks });
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['[acks] Too big: expected array to have <=1000 items']));
|
||||
expect(body).toEqual(
|
||||
errorDto.validationError([{ path: ['acks'], message: 'Too big: expected array to have <=1000 items' }]),
|
||||
);
|
||||
expect(ctx.authenticate).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -73,7 +79,11 @@ describe(SyncController.name, () => {
|
||||
.delete('/sync/ack')
|
||||
.send({ types: ['invalid'] });
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest([expect.stringContaining('[types.0] Invalid option: expected one of')]));
|
||||
expect(body).toEqual(
|
||||
errorDto.validationError([
|
||||
{ path: ['types', 0], message: expect.stringContaining('Invalid option: expected one of') },
|
||||
]),
|
||||
);
|
||||
expect(ctx.authenticate).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -67,8 +67,11 @@ describe(SystemConfigController.name, () => {
|
||||
const { status, body } = await request(ctx.getHttpServer()).put('/system-config').send(config);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
errorDto.badRequest([
|
||||
'[nightlyTasks.startTime] Invalid input: expected string in HH:mm format, received string',
|
||||
errorDto.validationError([
|
||||
{
|
||||
path: ['nightlyTasks', 'startTime'],
|
||||
message: 'Invalid input: expected string in HH:mm format, received string',
|
||||
},
|
||||
]),
|
||||
);
|
||||
});
|
||||
@@ -86,7 +89,9 @@ describe(SystemConfigController.name, () => {
|
||||
const { status, body } = await request(ctx.getHttpServer()).put('/system-config').send(config);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
errorDto.badRequest(['[nightlyTasks.databaseCleanup] Invalid input: expected boolean, received string']),
|
||||
errorDto.validationError([
|
||||
{ path: ['nightlyTasks', 'databaseCleanup'], message: 'Invalid input: expected boolean, received string' },
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -116,7 +121,12 @@ describe(SystemConfigController.name, () => {
|
||||
const { status, body } = await request(ctx.getHttpServer()).put('/system-config').send(config);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
errorDto.badRequest(['[image.thumbnail.progressive] Invalid input: expected boolean, received string']),
|
||||
errorDto.validationError([
|
||||
{
|
||||
path: ['image', 'thumbnail', 'progressive'],
|
||||
message: 'Invalid input: expected boolean, received string',
|
||||
},
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -54,7 +54,7 @@ describe(TagController.name, () => {
|
||||
it('should require a valid uuid', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer()).get(`/tags/123`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['[id] Invalid UUID']));
|
||||
expect(body).toEqual(errorDto.validationError([{ path: ['id'], message: 'Invalid UUID' }]));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -42,7 +42,9 @@ describe(TimelineController.name, () => {
|
||||
const { status, body } = await request(ctx.getHttpServer()).get('/timeline/buckets').query({ bbox: '1,2,3' });
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
errorDto.badRequest(['[bbox] bbox must have 4 comma-separated numbers: west,south,east,north'] as any),
|
||||
errorDto.validationError([
|
||||
{ path: ['bbox'], message: 'bbox must have 4 comma-separated numbers: west,south,east,north' },
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -51,7 +53,7 @@ describe(TimelineController.name, () => {
|
||||
.get('/timeline/buckets')
|
||||
.query({ bbox: '1,2,3,invalid' });
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['[bbox] bbox parts must be valid numbers'] as any));
|
||||
expect(body).toEqual(errorDto.validationError([{ path: ['bbox'], message: 'bbox parts must be valid numbers' }]));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -78,9 +78,9 @@ describe(UserAdminController.name, () => {
|
||||
.send(dto);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
errorDto.badRequest(
|
||||
expect.arrayContaining(['[quotaSizeInBytes] Invalid input: expected int, received number']),
|
||||
),
|
||||
errorDto.validationError([
|
||||
{ path: ['quotaSizeInBytes'], message: 'Invalid input: expected int, received number' },
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -98,9 +98,9 @@ describe(UserAdminController.name, () => {
|
||||
.send(dto);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
errorDto.badRequest(
|
||||
expect.arrayContaining(['[quotaSizeInBytes] Invalid input: expected int, received number']),
|
||||
),
|
||||
errorDto.validationError([
|
||||
{ path: ['quotaSizeInBytes'], message: 'Invalid input: expected int, received number' },
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -125,9 +125,9 @@ describe(UserAdminController.name, () => {
|
||||
.send({ quotaSizeInBytes: 1.2 });
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
errorDto.badRequest(
|
||||
expect.arrayContaining(['[quotaSizeInBytes] Invalid input: expected int, received number']),
|
||||
),
|
||||
errorDto.validationError([
|
||||
{ path: ['quotaSizeInBytes'], message: 'Invalid input: expected int, received number' },
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -43,15 +43,17 @@ describe(UserController.name, () => {
|
||||
expect(ctx.authenticate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
for (const key of ['email', 'name']) {
|
||||
for (const [key, message] of [
|
||||
['email', 'Invalid input: expected email, received object'],
|
||||
['name', 'Invalid input: expected string, received null'],
|
||||
] as const) {
|
||||
it(`should not allow null ${key}`, async () => {
|
||||
const dto = { [key]: null };
|
||||
const { status, body } = await request(ctx.getHttpServer())
|
||||
.put(`/users/me`)
|
||||
.set('Authorization', `Bearer token`)
|
||||
.send(dto);
|
||||
.send({ [key]: null });
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest());
|
||||
expect(body).toEqual(errorDto.validationError([{ path: [key], message }]));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -40,16 +40,16 @@ export class GlobalExceptionFilter implements ExceptionFilter<Error> {
|
||||
if (error instanceof ZodValidationException || error instanceof ZodSerializationException) {
|
||||
const zodError = error.getZodError();
|
||||
if (zodError instanceof ZodError && zodError.issues.length > 0) {
|
||||
body['message'] = zodError.issues.map((issue) =>
|
||||
issue.path.length > 0 ? `[${issue.path.join('.')}] ${issue.message}` : issue.message,
|
||||
);
|
||||
return {
|
||||
status,
|
||||
body: { message: 'Validation failed', errors: zodError.issues },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// remove fields that duplicate the HTTP response line or will be reformatted in a later step
|
||||
// remove fields injected by NestJS that duplicate the HTTP response line
|
||||
delete body['error'];
|
||||
delete body['statusCode'];
|
||||
delete body['errors'];
|
||||
return { status, body };
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user