update tests to account for zod error messages

This commit is contained in:
Ben McCann
2026-07-11 07:53:45 -07:00
parent c06776d85d
commit aaf300a059
4 changed files with 47 additions and 61 deletions
@@ -18,6 +18,12 @@ const makeAssetData = (overrides?: Partial<any>): string => {
});
};
const expectValidationIssue = (body: unknown, path: string[], message: string) => {
expect(body).toEqual(
expect.objectContaining({ errors: expect.arrayContaining([expect.objectContaining({ path, message })]) }),
);
};
describe(AssetUploadController.name, () => {
let ctx: ControllerContext;
let buffer: Buffer;
@@ -70,11 +76,7 @@ describe(AssetUploadController.name, () => {
.send(buffer);
expect(status).toBe(400);
expect(body).toEqual(
expect.objectContaining({
message: expect.arrayContaining(['version must not be less than 3']),
}),
);
expectValidationIssue(body, ['upload-draft-interop-version'], 'Too small: expected number to be >=3');
});
it('should require X-Immich-Asset-Data header', async () => {
@@ -87,7 +89,7 @@ describe(AssetUploadController.name, () => {
.send(buffer);
expect(status).toBe(400);
expect(body).toEqual(expect.objectContaining({ message: 'x-immich-asset-data header is required' }));
expectValidationIssue(body, ['x-immich-asset-data'], 'Invalid input: expected string, received undefined');
});
it('should require Repr-Digest header', async () => {
@@ -100,7 +102,7 @@ describe(AssetUploadController.name, () => {
.send(buffer);
expect(status).toBe(400);
expect(body).toEqual(expect.objectContaining({ message: 'Missing repr-digest header' }));
expectValidationIssue(body, ['repr-digest'], 'Invalid input: expected string, received undefined');
});
it('should allow conventional upload without Upload-Complete header', async () => {
@@ -124,7 +126,7 @@ describe(AssetUploadController.name, () => {
.send(buffer);
expect(status).toBe(400);
expect(body).toEqual(expect.objectContaining({ message: 'Missing upload-length header' }));
expectValidationIssue(body, ['upload-length'], 'Missing upload-length header');
});
it('should infer upload length from content length if complete upload', async () => {
@@ -150,7 +152,7 @@ describe(AssetUploadController.name, () => {
.send(buffer);
expect(status).toBe(400);
expect(body).toEqual(expect.objectContaining({ message: 'Invalid repr-digest header' }));
expectValidationIssue(body, ['repr-digest'], 'Invalid repr-digest header');
});
it('should validate device-asset-id is required in asset data', async () => {
@@ -171,10 +173,10 @@ describe(AssetUploadController.name, () => {
.send(buffer);
expect(status).toBe(400);
expect(body).toEqual(
expect.objectContaining({
message: expect.arrayContaining([expect.stringContaining('deviceAssetId')]),
}),
expectValidationIssue(
body,
['x-immich-asset-data', 'deviceAssetId'],
'Invalid input: expected string, received undefined',
);
});
@@ -196,10 +198,10 @@ describe(AssetUploadController.name, () => {
.send(buffer);
expect(status).toBe(400);
expect(body).toEqual(
expect.objectContaining({
message: expect.arrayContaining([expect.stringContaining('deviceId')]),
}),
expectValidationIssue(
body,
['x-immich-asset-data', 'deviceId'],
'Invalid input: expected string, received undefined',
);
});
@@ -221,10 +223,10 @@ describe(AssetUploadController.name, () => {
.send(buffer);
expect(status).toBe(400);
expect(body).toEqual(
expect.objectContaining({
message: expect.arrayContaining([expect.stringContaining('filename')]),
}),
expectValidationIssue(
body,
['x-immich-asset-data', 'filename'],
'Invalid input: expected string, received undefined',
);
});
@@ -254,7 +256,7 @@ describe(AssetUploadController.name, () => {
.send(buffer);
expect(status).toBe(400);
expect(body).toEqual(expect.objectContaining({ message: 'upload-complete must be a structured boolean value' }));
expectValidationIssue(body, ['upload-complete'], 'upload-complete must be a structured boolean value');
});
it('should validate Upload-Length is a positive integer', async () => {
@@ -268,11 +270,7 @@ describe(AssetUploadController.name, () => {
.send(buffer);
expect(status).toBe(400);
expect(body).toEqual(
expect.objectContaining({
message: expect.arrayContaining(['uploadLength must not be less than 1']),
}),
);
expectValidationIssue(body, ['upload-length'], 'Too small: expected number to be >=1');
});
});
@@ -292,11 +290,7 @@ describe(AssetUploadController.name, () => {
.send(Buffer.from('test'));
expect(status).toBe(400);
expect(body).toEqual(
expect.objectContaining({
message: expect.arrayContaining(['version must be an integer number', 'version must not be less than 3']),
}),
);
expectValidationIssue(body, ['upload-draft-interop-version'], 'Invalid input: expected number, received NaN');
});
it('should require Upload-Offset header', async () => {
@@ -307,14 +301,7 @@ describe(AssetUploadController.name, () => {
.send(Buffer.from('test'));
expect(status).toBe(400);
expect(body).toEqual(
expect.objectContaining({
message: expect.arrayContaining([
'uploadOffset must be an integer number',
'uploadOffset must not be less than 0',
]),
}),
);
expectValidationIssue(body, ['upload-offset'], 'Invalid input: expected number, received NaN');
});
it('should require Upload-Complete header', async () => {
@@ -326,7 +313,7 @@ describe(AssetUploadController.name, () => {
.send(Buffer.from('test'));
expect(status).toBe(400);
expect(body).toEqual(expect.objectContaining({ message: ['uploadComplete must be a boolean value'] }));
expectValidationIssue(body, ['upload-complete'], 'is required');
});
it('should validate UUID parameter', async () => {
@@ -338,7 +325,11 @@ describe(AssetUploadController.name, () => {
.send(Buffer.from('test'));
expect(status).toBe(400);
expect(body).toEqual(expect.objectContaining({ message: ['id must be a UUID'] }));
expect(body).toEqual(
expect.objectContaining({
errors: expect.arrayContaining([expect.objectContaining({ message: 'Invalid UUID', path: ['id'] })]),
}),
);
});
it('should validate Upload-Offset is a non-negative integer', async () => {
@@ -350,11 +341,7 @@ describe(AssetUploadController.name, () => {
.send(Buffer.from('test'));
expect(status).toBe(400);
expect(body).toEqual(
expect.objectContaining({
message: expect.arrayContaining(['uploadOffset must not be less than 0']),
}),
);
expectValidationIssue(body, ['upload-offset'], 'Too small: expected number to be >=0');
});
it('should require Content-Type: application/partial-upload for version >= 6', async () => {
@@ -367,11 +354,7 @@ describe(AssetUploadController.name, () => {
.send(Buffer.from('test'));
expect(status).toBe(400);
expect(body).toEqual(
expect.objectContaining({
message: ['contentType must be equal to application/partial-upload'],
}),
);
expectValidationIssue(body, ['content-type'], 'must be equal to application/partial-upload');
});
it('should allow other Content-Type for version < 6', async () => {
@@ -416,7 +399,11 @@ describe(AssetUploadController.name, () => {
const { status, body } = await request(ctx.getHttpServer()).delete('/upload/invalid-uuid');
expect(status).toBe(400);
expect(body).toEqual(expect.objectContaining({ message: ['id must be a UUID'] }));
expect(body).toEqual(
expect.objectContaining({
errors: expect.arrayContaining([expect.objectContaining({ message: 'Invalid UUID', path: ['id'] })]),
}),
);
});
});
+2 -2
View File
@@ -88,7 +88,7 @@ const StartUploadSchema = BaseUploadHeadersInputSchema.extend({
const complete = parseUploadComplete(headers, ctx);
const length = headers[Header.UploadLength] ?? (complete === false ? undefined : headers[Header.ContentLength]);
if (!length) {
ctx.addIssue({ code: 'custom', message: `Missing ${Header.UploadLength} header` });
ctx.addIssue({ code: 'custom', path: [Header.UploadLength], message: `Missing ${Header.UploadLength} header` });
return z.NEVER;
}
return {
@@ -185,7 +185,7 @@ function parseUploadComplete(
return !incomplete;
}
if (required && headers[Header.UploadComplete] === undefined && headers[Header.UploadIncomplete] === undefined) {
ctx.addIssue({ code: 'custom', message: `${Header.UploadComplete} is required` });
ctx.addIssue({ code: 'custom', path: [Header.UploadComplete], message: 'is required' });
return z.NEVER;
}
}
@@ -12,6 +12,7 @@ describe(AssetUploadService.name, () => {
beforeEach(() => {
({ sut, mocks } = newTestService(AssetUploadService));
mocks.asset.setComplete.mockResolvedValue({ id: factory.uuid() } as any);
});
describe('onStart', () => {
@@ -51,8 +52,6 @@ describe(AssetUploadService.name, () => {
id: assetId,
ownerId: authStub.user1.user.id,
checksum: mockDto.checksum,
deviceAssetId: mockDto.assetData.deviceAssetId,
deviceId: mockDto.assetData.deviceId,
fileCreatedAt: mockDto.assetData.fileCreatedAt,
fileModifiedAt: mockDto.assetData.fileModifiedAt,
type: AssetType.Image,
@@ -225,7 +224,7 @@ describe(AssetUploadService.name, () => {
it('should mark asset as complete and queue metadata extraction job', async () => {
await sut.onComplete({ id: assetId, path, fileModifiedAt });
expect(mocks.asset.setComplete).toHaveBeenCalledWith(assetId);
expect(mocks.asset.setComplete).toHaveBeenCalledWith(assetId, undefined);
expect(mocks.job.queue).toHaveBeenCalledWith({
name: JobName.AssetExtractMetadata,
data: { id: assetId, source: 'upload' },
@@ -378,7 +377,7 @@ describe(AssetUploadService.name, () => {
const result = await sut.removeStaleUpload({ id: assetId });
expect(result).toBe(JobStatus.Success);
expect(mocks.asset.setComplete).toHaveBeenCalledWith(assetId);
expect(mocks.asset.setComplete).toHaveBeenCalledWith(assetId, undefined);
expect(mocks.storage.unlink).not.toHaveBeenCalled();
});
+2 -2
View File
@@ -1,4 +1,4 @@
import { BadRequestException } from '@nestjs/common';
import { ZodValidationException } from 'nestjs-zod';
import { IncomingHttpHeaders } from 'node:http';
import { UAParser } from 'ua-parser-js';
import z from 'zod';
@@ -31,5 +31,5 @@ export function validateSyncOrReject<T>(cls: { schema: z.ZodType<T> }, obj: unkn
if (result.success) {
return result.data;
}
throw new BadRequestException(result.error.issues.map(({ message }) => message));
throw new ZodValidationException(result.error);
}