@prism-engineer/router
Version:
Type-safe Express.js router with automatic client generation
561 lines • 22.5 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const vitest_1 = require("vitest");
const typebox_1 = require("@sinclair/typebox");
const createApiRoute_1 = require("../../../createApiRoute");
const createAuthScheme_1 = require("../../../createAuthScheme");
(0, vitest_1.describe)('createApiRoute - Error Handling', () => {
(0, vitest_1.it)('should handle handler function errors gracefully', () => {
const route = (0, createApiRoute_1.createApiRoute)({
path: '/api/error-test',
method: 'GET',
response: {
200: {
contentType: 'application/json',
body: typebox_1.Type.Object({
success: typebox_1.Type.Boolean()
})
},
500: {
contentType: 'application/json',
body: typebox_1.Type.Object({
error: typebox_1.Type.String()
})
}
},
handler: async (req) => {
// This handler will work fine, errors are tested at runtime
return {
status: 200,
body: { success: true }
};
}
});
(0, vitest_1.expect)(route.handler).toBeInstanceOf(Function);
(0, vitest_1.expect)(route.response?.[200]).toBeDefined();
(0, vitest_1.expect)(route.response?.[500]).toBeDefined();
});
(0, vitest_1.it)('should handle async handler errors', async () => {
const route = (0, createApiRoute_1.createApiRoute)({
path: '/api/async-error',
method: 'GET',
response: {
200: {
contentType: 'application/json',
body: typebox_1.Type.Object({
data: typebox_1.Type.String()
})
},
500: {
contentType: 'application/json',
body: typebox_1.Type.Object({
error: typebox_1.Type.String()
})
}
},
handler: async (req) => {
// Simulate an async operation that might fail
await new Promise(resolve => setTimeout(resolve, 1));
// This would typically throw an error, but we'll return a success response
return {
status: 200,
body: { data: 'success' }
};
}
});
(0, vitest_1.expect)(route.handler).toBeInstanceOf(Function);
(0, vitest_1.expect)(typeof route.handler).toBe('function');
});
(0, vitest_1.it)('should handle invalid response status codes', () => {
// This should compile fine - validation happens at runtime
const route = (0, createApiRoute_1.createApiRoute)({
path: '/api/invalid-status',
method: 'GET',
response: {
200: {
contentType: 'application/json',
body: typebox_1.Type.Object({
message: typebox_1.Type.String()
})
},
404: {
contentType: 'application/json',
body: typebox_1.Type.Object({
error: typebox_1.Type.String()
})
}
},
handler: async (req) => {
// Handler returns valid status codes
return {
status: 200,
body: { message: 'success' }
};
}
});
(0, vitest_1.expect)(route.response?.[200]).toBeDefined();
(0, vitest_1.expect)(route.response?.[404]).toBeDefined();
});
(0, vitest_1.it)('should handle missing required request body', () => {
const route = (0, createApiRoute_1.createApiRoute)({
path: '/api/missing-body',
method: 'POST',
request: {
body: typebox_1.Type.Object({
name: typebox_1.Type.String(),
email: typebox_1.Type.String()
})
},
response: {
200: {
contentType: 'application/json',
body: typebox_1.Type.Object({
success: typebox_1.Type.Boolean()
})
},
400: {
contentType: 'application/json',
body: typebox_1.Type.Object({
error: typebox_1.Type.String(),
missing: typebox_1.Type.Array(typebox_1.Type.String())
})
}
},
handler: async (req) => {
// Handler expects body to be present and valid
const { name, email } = req.body;
return {
status: 200,
body: { success: true }
};
}
});
(0, vitest_1.expect)(route.request?.body).toBeDefined();
(0, vitest_1.expect)(route.response?.[400]).toBeDefined();
});
(0, vitest_1.it)('should handle authentication errors', () => {
const failingAuth = (0, createAuthScheme_1.createAuthScheme)({
name: 'failing-auth',
validate: async (req) => {
// This auth scheme always fails
throw new Error('Authentication failed');
}
});
const route = (0, createApiRoute_1.createApiRoute)({
path: '/api/auth-error',
method: 'GET',
auth: failingAuth,
response: {
200: {
contentType: 'application/json',
body: typebox_1.Type.Object({
message: typebox_1.Type.String()
})
},
401: {
contentType: 'application/json',
body: typebox_1.Type.Object({
error: typebox_1.Type.String()
})
}
},
handler: async (req) => {
// This handler won't be reached if auth fails
return {
status: 200,
body: { message: 'authenticated' }
};
}
});
(0, vitest_1.expect)(route.auth).toBeDefined();
(0, vitest_1.expect)(route.response?.[401]).toBeDefined();
});
(0, vitest_1.it)('should handle multiple auth schemes with all failing', () => {
const failingAuth1 = (0, createAuthScheme_1.createAuthScheme)({
name: 'failing-auth-1',
validate: async (req) => {
throw new Error('First auth failed');
}
});
const failingAuth2 = (0, createAuthScheme_1.createAuthScheme)({
name: 'failing-auth-2',
validate: async (req) => {
throw new Error('Second auth failed');
}
});
const route = (0, createApiRoute_1.createApiRoute)({
path: '/api/multi-auth-error',
method: 'GET',
auth: [failingAuth1, failingAuth2],
response: {
200: {
contentType: 'application/json',
body: typebox_1.Type.Object({
message: typebox_1.Type.String()
})
},
401: {
contentType: 'application/json',
body: typebox_1.Type.Object({
error: typebox_1.Type.String()
})
}
},
handler: async (req) => {
return {
status: 200,
body: { message: 'authenticated' }
};
}
});
(0, vitest_1.expect)(Array.isArray(route.auth)).toBe(true);
(0, vitest_1.expect)(route.auth).toHaveLength(2);
});
(0, vitest_1.it)('should handle validation errors in query parameters', () => {
const route = (0, createApiRoute_1.createApiRoute)({
path: '/api/query-validation',
method: 'GET',
request: {
query: typebox_1.Type.Object({
page: typebox_1.Type.Number({ minimum: 1 }),
limit: typebox_1.Type.Number({ minimum: 1, maximum: 100 }),
sort: typebox_1.Type.Union([typebox_1.Type.Literal('asc'), typebox_1.Type.Literal('desc')])
})
},
response: {
200: {
contentType: 'application/json',
body: typebox_1.Type.Array(typebox_1.Type.Object({
id: typebox_1.Type.Number(),
name: typebox_1.Type.String()
}))
},
400: {
contentType: 'application/json',
body: typebox_1.Type.Object({
error: typebox_1.Type.String(),
validationErrors: typebox_1.Type.Array(typebox_1.Type.String())
})
}
},
handler: async (req) => {
// Handler assumes query params are valid
const { page, limit, sort } = req.query;
return {
status: 200,
body: [{ id: 1, name: 'Test' }]
};
}
});
(0, vitest_1.expect)(route.request?.query).toBeDefined();
(0, vitest_1.expect)(route.response?.[400]).toBeDefined();
});
(0, vitest_1.it)('should handle header validation errors', () => {
const route = (0, createApiRoute_1.createApiRoute)({
path: '/api/header-validation',
method: 'GET',
request: {
headers: typebox_1.Type.Object({
authorization: typebox_1.Type.String({ pattern: '^Bearer [A-Za-z0-9-_]+$' }),
'content-type': typebox_1.Type.Literal('application/json'),
'x-api-version': typebox_1.Type.Union([typebox_1.Type.Literal('v1'), typebox_1.Type.Literal('v2')])
})
},
response: {
200: {
contentType: 'application/json',
body: typebox_1.Type.Object({
message: typebox_1.Type.String()
})
},
400: {
contentType: 'application/json',
body: typebox_1.Type.Object({
error: typebox_1.Type.String(),
invalidHeaders: typebox_1.Type.Array(typebox_1.Type.String())
})
}
},
handler: async (req) => {
// Handler assumes headers are valid
const { authorization, 'content-type': contentType, 'x-api-version': version } = req.headers;
return {
status: 200,
body: { message: 'valid headers' }
};
}
});
(0, vitest_1.expect)(route.request?.headers).toBeDefined();
(0, vitest_1.expect)(route.response?.[400]).toBeDefined();
});
(0, vitest_1.it)('should handle custom response handler errors', () => {
const route = (0, createApiRoute_1.createApiRoute)({
path: '/api/custom-response-error',
method: 'GET',
response: {
200: {
contentType: 'text/plain'
},
500: {
contentType: 'application/json',
body: typebox_1.Type.Object({
error: typebox_1.Type.String()
})
}
},
handler: async (req) => {
return {
status: 200,
custom: (res) => {
// This custom handler could potentially throw errors
res.setHeader('Content-Type', 'text/plain');
res.send('Custom response');
}
};
}
});
(0, vitest_1.expect)(route.response?.[200]?.contentType).toBe('text/plain');
(0, vitest_1.expect)(route.response?.[500]).toBeDefined();
});
(0, vitest_1.it)('should handle path parameter validation errors', () => {
const route = (0, createApiRoute_1.createApiRoute)({
path: '/api/users/{userId}/posts/{postId}',
method: 'GET',
response: {
200: {
contentType: 'application/json',
body: typebox_1.Type.Object({
userId: typebox_1.Type.String(),
postId: typebox_1.Type.String(),
data: typebox_1.Type.Object({
title: typebox_1.Type.String()
})
})
},
400: {
contentType: 'application/json',
body: typebox_1.Type.Object({
error: typebox_1.Type.String(),
invalidParams: typebox_1.Type.Array(typebox_1.Type.String())
})
},
404: {
contentType: 'application/json',
body: typebox_1.Type.Object({
error: typebox_1.Type.String(),
resource: typebox_1.Type.String()
})
}
},
handler: async (req) => {
const { userId, postId } = req.params;
// Simulate validation
if (!userId || !postId) {
return {
status: 400,
body: {
error: 'Invalid parameters',
invalidParams: ['userId', 'postId']
}
};
}
if (userId === '999' || postId === '999') {
return {
status: 404,
body: {
error: 'Resource not found',
resource: userId === '999' ? 'user' : 'post'
}
};
}
return {
status: 200,
body: {
userId,
postId,
data: { title: 'Sample Post' }
}
};
}
});
(0, vitest_1.expect)(route.path).toBe('/api/users/{userId}/posts/{postId}');
(0, vitest_1.expect)(route.response?.[400]).toBeDefined();
(0, vitest_1.expect)(route.response?.[404]).toBeDefined();
});
(0, vitest_1.it)('should handle missing content type for custom responses', () => {
const route = (0, createApiRoute_1.createApiRoute)({
path: '/api/missing-content-type',
method: 'GET',
response: {
200: {
contentType: 'application/octet-stream'
},
500: {
contentType: 'application/json',
body: typebox_1.Type.Object({
error: typebox_1.Type.String()
})
}
},
handler: async (req) => {
return {
status: 200,
custom: (res) => {
// Handler doesn't set content type - should be handled by framework
res.send('Response without explicit content type');
}
};
}
});
(0, vitest_1.expect)(route.response?.[200]?.contentType).toBe('application/octet-stream');
(0, vitest_1.expect)(route.response?.[500]).toBeDefined();
});
(0, vitest_1.it)('should handle response body validation errors', () => {
const route = (0, createApiRoute_1.createApiRoute)({
path: '/api/response-validation',
method: 'GET',
response: {
200: {
contentType: 'application/json',
body: typebox_1.Type.Object({
id: typebox_1.Type.Number(),
name: typebox_1.Type.String(),
email: typebox_1.Type.String({ format: 'email' }),
createdAt: typebox_1.Type.String({ format: 'date-time' })
})
},
500: {
contentType: 'application/json',
body: typebox_1.Type.Object({
error: typebox_1.Type.String()
})
}
},
handler: async (req) => {
// Handler should return data matching the schema
return {
status: 200,
body: {
id: 1,
name: 'John Doe',
email: 'john@example.com',
createdAt: new Date().toISOString()
}
};
}
});
(0, vitest_1.expect)(route.response?.[200]?.body).toBeDefined();
(0, vitest_1.expect)(route.response?.[500]).toBeDefined();
});
(0, vitest_1.it)('should handle mixed success and error responses', () => {
const route = (0, createApiRoute_1.createApiRoute)({
path: '/api/mixed-responses',
method: 'POST',
request: {
body: typebox_1.Type.Object({
operation: typebox_1.Type.Union([
typebox_1.Type.Literal('success'),
typebox_1.Type.Literal('client-error'),
typebox_1.Type.Literal('server-error'),
typebox_1.Type.Literal('not-found')
])
})
},
response: {
200: {
contentType: 'application/json',
body: typebox_1.Type.Object({
success: typebox_1.Type.Boolean(),
data: typebox_1.Type.String()
})
},
400: {
contentType: 'application/json',
body: typebox_1.Type.Object({
error: typebox_1.Type.String(),
code: typebox_1.Type.Literal('CLIENT_ERROR')
})
},
404: {
contentType: 'application/json',
body: typebox_1.Type.Object({
error: typebox_1.Type.String(),
code: typebox_1.Type.Literal('NOT_FOUND')
})
},
500: {
contentType: 'application/json',
body: typebox_1.Type.Object({
error: typebox_1.Type.String(),
code: typebox_1.Type.Literal('SERVER_ERROR')
})
}
},
handler: async (req) => {
const { operation } = req.body;
switch (operation) {
case 'success':
return {
status: 200,
body: { success: true, data: 'Operation completed' }
};
case 'client-error':
return {
status: 400,
body: { error: 'Bad request', code: 'CLIENT_ERROR' }
};
case 'not-found':
return {
status: 404,
body: { error: 'Resource not found', code: 'NOT_FOUND' }
};
case 'server-error':
return {
status: 500,
body: { error: 'Internal server error', code: 'SERVER_ERROR' }
};
default:
return {
status: 400,
body: { error: 'Unknown operation', code: 'CLIENT_ERROR' }
};
}
}
});
(0, vitest_1.expect)(route.request?.body).toBeDefined();
(0, vitest_1.expect)(route.response?.[200]).toBeDefined();
(0, vitest_1.expect)(route.response?.[400]).toBeDefined();
(0, vitest_1.expect)(route.response?.[404]).toBeDefined();
(0, vitest_1.expect)(route.response?.[500]).toBeDefined();
});
(0, vitest_1.it)('should handle undefined or null response bodies', () => {
const route = (0, createApiRoute_1.createApiRoute)({
path: '/api/empty-response',
method: 'DELETE',
response: {
204: {
contentType: 'application/json',
body: typebox_1.Type.Object({
deleted: typebox_1.Type.Boolean()
})
},
500: {
contentType: 'application/json',
body: typebox_1.Type.Object({
error: typebox_1.Type.String()
})
}
},
handler: async (req) => {
// DELETE operations often return 204 No Content
return {
status: 204,
body: { deleted: true }
};
}
});
(0, vitest_1.expect)(route.response?.[204]).toBeDefined();
(0, vitest_1.expect)(route.response?.[500]).toBeDefined();
});
});
//# sourceMappingURL=error-handling.test.js.map