Skip to content

Commit

Permalink
Make ResponseValidationError more useful (#91)
Browse files Browse the repository at this point in the history
  • Loading branch information
kibertoad authored Jun 25, 2024
1 parent c1fa086 commit e0e5c0b
Show file tree
Hide file tree
Showing 12 changed files with 114 additions and 73 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
node-version: [14, 16, 18, 20, 22]
node-version: [16, 18, 20, 22]
os: [ubuntu-latest, windows-latest]

steps:
Expand Down
20 changes: 0 additions & 20 deletions jest.config.json

This file was deleted.

9 changes: 4 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
],
"scripts": {
"build": "tsc",
"test": "npm run build && npm run typescript && jest",
"test:coverage": "jest --coverage",
"test": "npm run build && npm run typescript && vitest",
"test:coverage": "vitest --coverage",
"lint": "eslint .",
"lint:fix": "eslint --fix . && prettier --write .",
"typescript": "tsd",
Expand Down Expand Up @@ -43,20 +43,19 @@
"devDependencies": {
"@fastify/swagger": "^8.14.0",
"@fastify/swagger-ui": "^3.0.0",
"@types/jest": "^29.5.12",
"@types/node": "^20.12.12",
"@typescript-eslint/eslint-plugin": "^7.9.0",
"@typescript-eslint/parser": "^7.9.0",
"@vitest/coverage-v8": "^1.6.0",
"eslint": "^8.57.0",
"eslint-plugin-import": "^2.29.1",
"fastify": "^4.27.0",
"fastify-plugin": "^4.5.1",
"jest": "^29.7.0",
"oas-validator": "^5.0.8",
"prettier": "^3.2.5",
"ts-jest": "^29.1.2",
"tsd": "^0.31.0",
"typescript": "^5.4.5",
"vitest": "^1.6.0",
"zod": "^3.23.8"
},
"tsd": {
Expand Down
25 changes: 25 additions & 0 deletions src/ResponseValidationError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { z, ZodIssue } from 'zod';

export type ResponseValidationErrorDetails = {
error: ZodIssue[];
method: string;
url: string;
};

export class ResponseValidationError extends Error {
public details: ResponseValidationErrorDetails;

constructor(
validationResult: z.SafeParseReturnType<unknown, unknown>,
method: string,
url: string,
) {
super("Response doesn't match the schema");
this.name = 'ResponseValidationError';
this.details = {
error: validationResult.error?.issues ?? [],
method,
url,
};
}
}
16 changes: 4 additions & 12 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import type { FastifySerializerCompiler } from 'fastify/types/schema';
import type { ZodAny, ZodTypeAny, z } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema';

import { ResponseValidationError } from './ResponseValidationError';

// eslint-disable-next-line @typescript-eslint/no-explicit-any
type FreeformRecord = Record<string, any>;

Expand Down Expand Up @@ -123,18 +125,8 @@ function resolveSchema(maybeSchema: ZodAny | { properties: ZodAny }): Pick<ZodAn
throw new Error(`Invalid schema passed: ${JSON.stringify(maybeSchema)}`);
}

export class ResponseValidationError extends Error {
public details: FreeformRecord;

constructor(validationResult: FreeformRecord) {
super("Response doesn't match the schema");
this.name = 'ResponseValidationError';
this.details = validationResult.error;
}
}

export const serializerCompiler: FastifySerializerCompiler<ZodAny | { properties: ZodAny }> =
({ schema: maybeSchema }) =>
({ schema: maybeSchema, method, url }) =>
(data) => {
const schema: Pick<ZodAny, 'safeParse'> = resolveSchema(maybeSchema);

Expand All @@ -143,7 +135,7 @@ export const serializerCompiler: FastifySerializerCompiler<ZodAny | { properties
return JSON.stringify(result.data);
}

throw new ResponseValidationError(result);
throw new ResponseValidationError(result, method, url);
};

/**
Expand Down
6 changes: 3 additions & 3 deletions test/__snapshots__/fastify-swagger.spec.ts.snap
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`transformer generates types for fastify-swagger correctly 1`] = `
exports[`transformer > generates types for fastify-swagger correctly 1`] = `
{
"components": {
"schemas": {},
Expand Down Expand Up @@ -123,7 +123,7 @@ exports[`transformer generates types for fastify-swagger correctly 1`] = `
}
`;

exports[`transformer should not generate ref 1`] = `
exports[`transformer > should not generate ref 1`] = `
{
"components": {
"schemas": {},
Expand Down
20 changes: 0 additions & 20 deletions test/__snapshots__/request-schema.spec.ts.snap

This file was deleted.

3 changes: 0 additions & 3 deletions test/__snapshots__/response-schema.spec.ts.snap

This file was deleted.

19 changes: 18 additions & 1 deletion test/request-schema.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,23 @@ describe('response schema', () => {
const response = await app.inject().get('/');

expect(response.statusCode).toBe(400);
expect(response.json()).toMatchSnapshot();
expect(response.json()).toMatchInlineSnapshot(`
{
"code": "FST_ERR_VALIDATION",
"error": "Bad Request",
"message": "[
{
"code": "invalid_type",
"expected": "string",
"received": "undefined",
"path": [
"name"
],
"message": "Required"
}
]",
"statusCode": 400,
}
`);
});
});
41 changes: 34 additions & 7 deletions test/response-schema.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { z } from 'zod';

import type { ZodTypeProvider } from '../src';
import { serializerCompiler, validatorCompiler } from '../src';
import type { ResponseValidationError } from '../src/ResponseValidationError';

describe('response schema', () => {
describe('does not fail on empty response schema (204)', () => {
Expand Down Expand Up @@ -37,10 +38,19 @@ describe('response schema', () => {
},
},
handler: (req, res) => {
// @ts-expect-error
res.status(204).send({ id: 1 });
},
});
});
app.setErrorHandler((err, req, reply) => {
return reply.code(500).send({
error: 'Internal Server Error',
message: "Response doesn't match the schema",
details: (err as unknown as ResponseValidationError).details,
statusCode: 500,
});
});
await app.ready();
});

Expand All @@ -59,11 +69,26 @@ describe('response schema', () => {
const response = await app.inject().get('/incorrect');

expect(response.statusCode).toBe(500);
expect(response.json()).toEqual({
error: 'Internal Server Error',
message: "Response doesn't match the schema",
statusCode: 500,
});
expect(response.json()).toMatchInlineSnapshot(`
{
"details": {
"error": [
{
"code": "invalid_type",
"expected": "undefined",
"message": "Expected undefined, received object",
"path": [],
"received": "object",
},
],
"method": "GET",
"url": "/incorrect",
},
"error": "Internal Server Error",
"message": "Response doesn't match the schema",
"statusCode": 500,
}
`);
});
});

Expand Down Expand Up @@ -123,7 +148,9 @@ describe('response schema', () => {
const response = await app.inject().get('/incorrect');

expect(response.statusCode).toBe(500);
expect(response.body).toMatchSnapshot();
expect(response.body).toMatchInlineSnapshot(
`"{"statusCode":500,"error":"Internal Server Error","message":"Response doesn't match the schema"}"`,
);
});
});

Expand Down Expand Up @@ -189,7 +216,7 @@ describe('response schema', () => {
const response = await app.inject().get('/incorrect');

expect(response.statusCode).toBe(500);
expect(response.json()).toMatchSnapshot();
expect(response.json()).toMatchInlineSnapshot();
});
});
});
3 changes: 2 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
"rootDir": "src",
"outDir": "dist",
"esModuleInterop": true,
"skipLibCheck": true,
"moduleResolution": "Node",
"module": "CommonJS",
"target": "ES2020"
},
"exclude": ["./node_modules", "types/**/*.test-d.ts", "test", "dist"]
"exclude": ["./node_modules", "types/**/*.test-d.ts", "test", "dist", "vitest.config.mts"]
}
23 changes: 23 additions & 0 deletions vitest.config.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { defineConfig } from 'vitest/config';

export default defineConfig({
test: {
globals: true,
watch: false,
environment: 'node',
reporters: ['verbose'],
coverage: {
provider: 'v8',
include: ['src/**/*.ts'],
exclude: ['test/**/*.ts'],
reporter: ['text', 'lcov'],
all: true,
thresholds: {
statements: 95,
branches: 90,
functions: 100,
lines: 95,
},
},
},
});

0 comments on commit e0e5c0b

Please sign in to comment.