UNPKG

@prism-engineer/router

Version:

Type-safe Express.js router with automatic client generation

504 lines β€’ 24.2 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const vitest_1 = require("vitest"); const router_1 = require("../../router"); const path_1 = __importDefault(require("path")); const promises_1 = __importDefault(require("fs/promises")); (0, vitest_1.describe)('Frontend Client - JSON Response Handling', () => { let tempDir; let generatedClient; let mockFetch; (0, vitest_1.beforeEach)(async () => { vitest_1.vi.clearAllMocks(); // Mock fetch globally mockFetch = vitest_1.vi.fn(); global.fetch = mockFetch; // Create temporary directory for generated client tempDir = path_1.default.join(process.cwd(), 'temp-test-' + crypto.randomUUID()); await promises_1.default.mkdir(tempDir, { recursive: true }); // Generate client for testing await router_1.router.compile({ outputDir: tempDir, name: 'JsonClient', baseUrl: 'http://localhost:3000', routes: [{ directory: path_1.default.resolve(__dirname, '../../../dist/tests/router/fixtures/api'), pattern: /.*\.js$/ }] }); // Create a mock client with JSON response handling generatedClient = createMockJsonClient(); }); (0, vitest_1.afterEach)(async () => { // Cleanup temporary directory try { await promises_1.default.rm(tempDir, { recursive: true, force: true }); } catch { // Ignore cleanup errors } vitest_1.vi.restoreAllMocks(); }); // Mock client factory for JSON response testing function createMockJsonClient() { return { api: { hello: { get: vitest_1.vi.fn().mockImplementation(async (options = {}) => { const response = { ok: true, status: 200, headers: new Map([['content-type', 'application/json']]), json: vitest_1.vi.fn().mockResolvedValue({ message: 'Hello World' }) }; mockFetch.mockResolvedValueOnce(response); const fetchResponse = await fetch('http://localhost:3000/api/hello', { method: 'GET', headers: options.headers || {} }); if (fetchResponse.ok && fetchResponse.headers.get('content-type')?.includes('application/json')) { return await fetchResponse.json(); } throw new Error('Non-JSON response'); }) }, users: { get: vitest_1.vi.fn().mockImplementation(async (options = {}) => { const response = { ok: true, status: 200, headers: new Map([['content-type', 'application/json; charset=utf-8']]), json: vitest_1.vi.fn().mockResolvedValue([ { id: 1, name: 'John Doe', email: 'john@example.com' }, { id: 2, name: 'Jane Smith', email: 'jane@example.com' } ]) }; mockFetch.mockResolvedValueOnce(response); const fetchResponse = await fetch('http://localhost:3000/api/users', { method: 'GET', headers: options.headers || {} }); return await fetchResponse.json(); }), post: vitest_1.vi.fn().mockImplementation(async (options = {}) => { const response = { ok: true, status: 201, headers: new Map([['content-type', 'application/json']]), json: vitest_1.vi.fn().mockResolvedValue({ id: 3, ...options.body, createdAt: '2024-01-01T00:00:00Z' }) }; mockFetch.mockResolvedValueOnce(response); const fetchResponse = await fetch('http://localhost:3000/api/users', { method: 'POST', headers: { 'Content-Type': 'application/json', ...options.headers }, body: JSON.stringify(options.body) }); return await fetchResponse.json(); }) }, complex: { get: vitest_1.vi.fn().mockImplementation(async () => { const response = { ok: true, status: 200, headers: new Map([['content-type', 'application/json']]), json: vitest_1.vi.fn().mockResolvedValue({ data: { users: [ { id: 1, profile: { name: 'John', settings: { theme: 'dark' } } } ], meta: { total: 100, page: 1, hasMore: true } }, timestamp: '2024-01-01T00:00:00Z' }) }; mockFetch.mockResolvedValueOnce(response); const fetchResponse = await fetch('http://localhost:3000/api/complex'); return await fetchResponse.json(); }) } } }; } (0, vitest_1.it)('should handle simple JSON response', async () => { const result = await generatedClient.api.hello.get(); (0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/hello', { method: 'GET', headers: {} }); (0, vitest_1.expect)(result).toEqual({ message: 'Hello World' }); (0, vitest_1.expect)(typeof result).toBe('object'); (0, vitest_1.expect)(result.message).toBe('Hello World'); }); (0, vitest_1.it)('should handle JSON array responses', async () => { const result = await generatedClient.api.users.get(); (0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/users', { method: 'GET', headers: {} }); (0, vitest_1.expect)(Array.isArray(result)).toBe(true); (0, vitest_1.expect)(result).toHaveLength(2); (0, vitest_1.expect)(result[0]).toEqual({ id: 1, name: 'John Doe', email: 'john@example.com' }); (0, vitest_1.expect)(result[1]).toEqual({ id: 2, name: 'Jane Smith', email: 'jane@example.com' }); }); (0, vitest_1.it)('should handle JSON response with POST request', async () => { const userData = { name: 'Alice Brown', email: 'alice@example.com' }; const result = await generatedClient.api.users.post({ body: userData }); (0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(userData) }); (0, vitest_1.expect)(result).toEqual({ id: 3, name: 'Alice Brown', email: 'alice@example.com', createdAt: '2024-01-01T00:00:00Z' }); }); (0, vitest_1.it)('should handle nested JSON object responses', async () => { const result = await generatedClient.api.complex.get(); (0, vitest_1.expect)(result).toEqual({ data: { users: [ { id: 1, profile: { name: 'John', settings: { theme: 'dark' } } } ], meta: { total: 100, page: 1, hasMore: true } }, timestamp: '2024-01-01T00:00:00Z' }); // Test nested access (0, vitest_1.expect)(result.data.users[0].profile.settings.theme).toBe('dark'); (0, vitest_1.expect)(result.data.meta.hasMore).toBe(true); }); (0, vitest_1.it)('should handle JSON response with different content-type variations', async () => { const variations = [ 'application/json', 'application/json; charset=utf-8', 'application/json;charset=UTF-8', 'application/vnd.api+json', 'application/hal+json' ]; for (const contentType of variations) { const mockClient = { test: { get: vitest_1.vi.fn().mockImplementation(async () => { const response = { ok: true, status: 200, headers: new Map([['content-type', contentType]]), json: vitest_1.vi.fn().mockResolvedValue({ type: 'test', contentType }) }; mockFetch.mockResolvedValueOnce(response); const fetchResponse = await fetch('http://localhost:3000/test'); if (fetchResponse.headers.get('content-type')?.includes('json')) { return await fetchResponse.json(); } return null; }) } }; const result = await mockClient.test.get(); (0, vitest_1.expect)(result).toEqual({ type: 'test', contentType }); } }); (0, vitest_1.it)('should handle JSON response parsing errors gracefully', async () => { const errorClient = { api: { invalid: { get: vitest_1.vi.fn().mockImplementation(async () => { const response = { ok: true, status: 200, headers: new Map([['content-type', 'application/json']]), json: vitest_1.vi.fn().mockRejectedValue(new Error('Invalid JSON')) }; mockFetch.mockResolvedValueOnce(response); const fetchResponse = await fetch('http://localhost:3000/api/invalid'); try { return await fetchResponse.json(); } catch (error) { throw new Error(`JSON parsing failed: ${error.message}`); } }) } } }; await (0, vitest_1.expect)(errorClient.api.invalid.get()).rejects.toThrow('JSON parsing failed: Invalid JSON'); }); (0, vitest_1.it)('should handle empty JSON responses', async () => { const emptyClient = { api: { empty: { get: vitest_1.vi.fn().mockImplementation(async () => { const response = { ok: true, status: 200, headers: new Map([['content-type', 'application/json']]), json: vitest_1.vi.fn().mockResolvedValue({}) }; mockFetch.mockResolvedValueOnce(response); const fetchResponse = await fetch('http://localhost:3000/api/empty'); return await fetchResponse.json(); }) } } }; const result = await emptyClient.api.empty.get(); (0, vitest_1.expect)(result).toEqual({}); (0, vitest_1.expect)(typeof result).toBe('object'); }); (0, vitest_1.it)('should handle null JSON responses', async () => { const nullClient = { api: { null: { get: vitest_1.vi.fn().mockImplementation(async () => { const response = { ok: true, status: 200, headers: new Map([['content-type', 'application/json']]), json: vitest_1.vi.fn().mockResolvedValue(null) }; mockFetch.mockResolvedValueOnce(response); const fetchResponse = await fetch('http://localhost:3000/api/null'); return await fetchResponse.json(); }) } } }; const result = await nullClient.api.null.get(); (0, vitest_1.expect)(result).toBeNull(); }); (0, vitest_1.it)('should handle JSON responses with numbers and booleans', async () => { const typesClient = { api: { types: { get: vitest_1.vi.fn().mockImplementation(async () => { const response = { ok: true, status: 200, headers: new Map([['content-type', 'application/json']]), json: vitest_1.vi.fn().mockResolvedValue({ string: 'text', number: 42, float: 3.14, boolean: true, nullValue: null, array: [1, 2, 3], nested: { flag: false, count: 0 } }) }; mockFetch.mockResolvedValueOnce(response); const fetchResponse = await fetch('http://localhost:3000/api/types'); return await fetchResponse.json(); }) } } }; const result = await typesClient.api.types.get(); (0, vitest_1.expect)(typeof result.string).toBe('string'); (0, vitest_1.expect)(typeof result.number).toBe('number'); (0, vitest_1.expect)(typeof result.float).toBe('number'); (0, vitest_1.expect)(typeof result.boolean).toBe('boolean'); (0, vitest_1.expect)(result.nullValue).toBeNull(); (0, vitest_1.expect)(Array.isArray(result.array)).toBe(true); (0, vitest_1.expect)(typeof result.nested).toBe('object'); (0, vitest_1.expect)(result.nested.flag).toBe(false); }); (0, vitest_1.it)('should handle large JSON responses', async () => { const largeClient = { api: { large: { get: vitest_1.vi.fn().mockImplementation(async () => { // Generate large dataset const largeData = { items: Array.from({ length: 1000 }, (_, i) => ({ id: i, name: `Item ${i}`, description: `Description for item ${i}`, metadata: { created: '2024-01-01T00:00:00Z', updated: '2024-01-01T00:00:00Z', tags: [`tag${i}`, `category${i % 10}`] } })), total: 1000, processed: true }; const response = { ok: true, status: 200, headers: new Map([['content-type', 'application/json']]), json: vitest_1.vi.fn().mockResolvedValue(largeData) }; mockFetch.mockResolvedValueOnce(response); const fetchResponse = await fetch('http://localhost:3000/api/large'); return await fetchResponse.json(); }) } } }; const result = await largeClient.api.large.get(); (0, vitest_1.expect)(result.items).toHaveLength(1000); (0, vitest_1.expect)(result.total).toBe(1000); (0, vitest_1.expect)(result.processed).toBe(true); (0, vitest_1.expect)(result.items[0]).toEqual({ id: 0, name: 'Item 0', description: 'Description for item 0', metadata: { created: '2024-01-01T00:00:00Z', updated: '2024-01-01T00:00:00Z', tags: ['tag0', 'category0'] } }); }); (0, vitest_1.it)('should handle JSON responses with special characters', async () => { const specialClient = { api: { special: { get: vitest_1.vi.fn().mockImplementation(async () => { const response = { ok: true, status: 200, headers: new Map([['content-type', 'application/json']]), json: vitest_1.vi.fn().mockResolvedValue({ unicode: 'Hello δΈ–η•Œ 🌍', quotes: 'Text with "quotes" and \'apostrophes\'', backslashes: 'Path\\to\\file', newlines: 'Line 1\nLine 2\nLine 3', tabs: 'Column1\tColumn2\tColumn3', emoji: 'πŸ˜€πŸ˜ƒπŸ˜„πŸ˜πŸ₯³πŸŽ‰βœ¨' }) }; mockFetch.mockResolvedValueOnce(response); const fetchResponse = await fetch('http://localhost:3000/api/special'); return await fetchResponse.json(); }) } } }; const result = await specialClient.api.special.get(); (0, vitest_1.expect)(result.unicode).toBe('Hello δΈ–η•Œ 🌍'); (0, vitest_1.expect)(result.quotes).toBe('Text with "quotes" and \'apostrophes\''); (0, vitest_1.expect)(result.backslashes).toBe('Path\\to\\file'); (0, vitest_1.expect)(result.newlines).toBe('Line 1\nLine 2\nLine 3'); (0, vitest_1.expect)(result.tabs).toBe('Column1\tColumn2\tColumn3'); (0, vitest_1.expect)(result.emoji).toBe('πŸ˜€πŸ˜ƒπŸ˜„πŸ˜πŸ₯³πŸŽ‰βœ¨'); }); (0, vitest_1.it)('should handle concurrent JSON response parsing', async () => { const promises = [ generatedClient.api.hello.get(), generatedClient.api.users.get(), generatedClient.api.complex.get() ]; const results = await Promise.all(promises); (0, vitest_1.expect)(results).toHaveLength(3); (0, vitest_1.expect)(results[0]).toEqual({ message: 'Hello World' }); (0, vitest_1.expect)(Array.isArray(results[1])).toBe(true); (0, vitest_1.expect)(results[2]).toHaveProperty('data'); (0, vitest_1.expect)(results[2]).toHaveProperty('timestamp'); }); (0, vitest_1.it)('should preserve JSON response type information', async () => { const result = await generatedClient.api.users.get(); // Verify type preservation (0, vitest_1.expect)(typeof result[0].id).toBe('number'); (0, vitest_1.expect)(typeof result[0].name).toBe('string'); (0, vitest_1.expect)(typeof result[0].email).toBe('string'); // Verify array methods are available (0, vitest_1.expect)(typeof result.map).toBe('function'); (0, vitest_1.expect)(typeof result.filter).toBe('function'); (0, vitest_1.expect)(typeof result.find).toBe('function'); }); (0, vitest_1.it)('should handle JSON response with circular references safely', async () => { const circularClient = { api: { circular: { get: vitest_1.vi.fn().mockImplementation(async () => { // Create object without circular reference for JSON serialization const data = { id: 1, name: 'Test', parent: null, children: [ { id: 2, name: 'Child 1', parentId: 1 }, { id: 3, name: 'Child 2', parentId: 1 } ] }; const response = { ok: true, status: 200, headers: new Map([['content-type', 'application/json']]), json: vitest_1.vi.fn().mockResolvedValue(data) }; mockFetch.mockResolvedValueOnce(response); const fetchResponse = await fetch('http://localhost:3000/api/circular'); return await fetchResponse.json(); }) } } }; const result = await circularClient.api.circular.get(); (0, vitest_1.expect)(result.id).toBe(1); (0, vitest_1.expect)(result.children).toHaveLength(2); (0, vitest_1.expect)(result.children[0].parentId).toBe(1); }); (0, vitest_1.it)('should handle JSON responses with Date-like strings', async () => { const dateClient = { api: { dates: { get: vitest_1.vi.fn().mockImplementation(async () => { const response = { ok: true, status: 200, headers: new Map([['content-type', 'application/json']]), json: vitest_1.vi.fn().mockResolvedValue({ created: '2024-01-01T00:00:00Z', updated: '2024-01-01T12:30:45.123Z', date: '2024-01-01', time: '12:30:45', timestamp: 1704067200000 }) }; mockFetch.mockResolvedValueOnce(response); const fetchResponse = await fetch('http://localhost:3000/api/dates'); return await fetchResponse.json(); }) } } }; const result = await dateClient.api.dates.get(); // JSON parsing should preserve strings, not convert to Date objects (0, vitest_1.expect)(typeof result.created).toBe('string'); (0, vitest_1.expect)(typeof result.updated).toBe('string'); (0, vitest_1.expect)(typeof result.date).toBe('string'); (0, vitest_1.expect)(typeof result.time).toBe('string'); (0, vitest_1.expect)(typeof result.timestamp).toBe('number'); // Verify date strings are valid (0, vitest_1.expect)(new Date(result.created).getTime()).not.toBeNaN(); (0, vitest_1.expect)(new Date(result.updated).getTime()).not.toBeNaN(); }); }); //# sourceMappingURL=client-json-responses.test.js.map