UNPKG

@prism-engineer/router

Version:

Type-safe Express.js router with automatic client generation

561 lines 22.5 kB
"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