UNPKG

@prism-engineer/router

Version:

Type-safe Express.js router with automatic client generation

570 lines 28.9 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 - Custom Content Types', () => { 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: 'CustomContentClient', baseUrl: 'http://localhost:3000', routes: [{ directory: path_1.default.resolve(__dirname, '../../../dist/tests/router/fixtures/api'), pattern: /.*\.js$/ }] }); // Create a mock client with custom content type handling generatedClient = createMockCustomContentClient(); }); (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 custom content type testing function createMockCustomContentClient() { return { api: { download: { get: vitest_1.vi.fn().mockImplementation(async (options = {}) => { const response = { ok: true, status: 200, headers: new Map([ ['content-type', 'application/octet-stream'], ['content-disposition', 'attachment; filename="file.bin"'] ]), blob: vitest_1.vi.fn().mockResolvedValue(new Blob(['binary data'], { type: 'application/octet-stream' })), arrayBuffer: vitest_1.vi.fn().mockResolvedValue(new ArrayBuffer(8)) }; mockFetch.mockResolvedValueOnce(response); const fetchResponse = await fetch('http://localhost:3000/api/download', { method: 'GET', headers: options.headers || {} }); // For non-JSON content types, return the response object for custom handling if (!fetchResponse.headers.get('content-type')?.includes('json')) { return fetchResponse; } return await fetchResponse.json(); }) }, upload: { 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: 'upload-123', filename: options.filename || 'unknown', size: options.size || 0 }) }; mockFetch.mockResolvedValueOnce(response); const fetchResponse = await fetch('http://localhost:3000/api/upload', { method: 'POST', headers: { 'Content-Type': 'multipart/form-data', ...options.headers }, body: options.body }); return await fetchResponse.json(); }) }, xml: { get: vitest_1.vi.fn().mockImplementation(async (options = {}) => { const xmlData = '<?xml version="1.0" encoding="UTF-8"?><root><item id="1">Test</item></root>'; const response = { ok: true, status: 200, headers: new Map([['content-type', 'application/xml']]), text: vitest_1.vi.fn().mockResolvedValue(xmlData) }; mockFetch.mockResolvedValueOnce(response); const fetchResponse = await fetch('http://localhost:3000/api/xml', { method: 'GET', headers: options.headers || {} }); // For XML content, return response for custom handling return fetchResponse; }) }, csv: { get: vitest_1.vi.fn().mockImplementation(async (options = {}) => { const csvData = 'id,name,email\n1,John Doe,john@example.com\n2,Jane Smith,jane@example.com'; const response = { ok: true, status: 200, headers: new Map([['content-type', 'text/csv']]), text: vitest_1.vi.fn().mockResolvedValue(csvData) }; mockFetch.mockResolvedValueOnce(response); const fetchResponse = await fetch('http://localhost:3000/api/csv', { method: 'GET', headers: options.headers || {} }); return fetchResponse; }) }, image: { get: vitest_1.vi.fn().mockImplementation(async (options = {}) => { // Mock image data as ArrayBuffer const imageBuffer = new ArrayBuffer(1024); const response = { ok: true, status: 200, headers: new Map([ ['content-type', 'image/png'], ['content-length', '1024'] ]), blob: vitest_1.vi.fn().mockResolvedValue(new Blob([imageBuffer], { type: 'image/png' })), arrayBuffer: vitest_1.vi.fn().mockResolvedValue(imageBuffer) }; mockFetch.mockResolvedValueOnce(response); const fetchResponse = await fetch('http://localhost:3000/api/image', { method: 'GET', headers: options.headers || {} }); return fetchResponse; }) }, pdf: { get: vitest_1.vi.fn().mockImplementation(async (options = {}) => { const pdfBuffer = new ArrayBuffer(2048); const response = { ok: true, status: 200, headers: new Map([ ['content-type', 'application/pdf'], ['content-disposition', 'inline; filename="document.pdf"'] ]), blob: vitest_1.vi.fn().mockResolvedValue(new Blob([pdfBuffer], { type: 'application/pdf' })), arrayBuffer: vitest_1.vi.fn().mockResolvedValue(pdfBuffer) }; mockFetch.mockResolvedValueOnce(response); const fetchResponse = await fetch('http://localhost:3000/api/pdf', { method: 'GET', headers: options.headers || {} }); return fetchResponse; }) }, stream: { get: vitest_1.vi.fn().mockImplementation(async (options = {}) => { const response = { ok: true, status: 200, headers: new Map([ ['content-type', 'text/plain'], ['transfer-encoding', 'chunked'] ]), body: { getReader: vitest_1.vi.fn().mockReturnValue({ read: vitest_1.vi.fn() .mockResolvedValueOnce({ value: new TextEncoder().encode('chunk1'), done: false }) .mockResolvedValueOnce({ value: new TextEncoder().encode('chunk2'), done: false }) .mockResolvedValueOnce({ value: undefined, done: true }) }) } }; mockFetch.mockResolvedValueOnce(response); const fetchResponse = await fetch('http://localhost:3000/api/stream', { method: 'GET', headers: options.headers || {} }); return fetchResponse; }) } } }; } (0, vitest_1.it)('should handle binary file downloads', async () => { const response = await generatedClient.api.download.get(); (0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/download', { method: 'GET', headers: {} }); (0, vitest_1.expect)(response.ok).toBe(true); (0, vitest_1.expect)(response.headers.get('content-type')).toBe('application/octet-stream'); (0, vitest_1.expect)(response.headers.get('content-disposition')).toContain('attachment'); // Test binary data handling const blob = await response.blob(); (0, vitest_1.expect)(blob).toBeInstanceOf(Blob); (0, vitest_1.expect)(blob.type).toBe('application/octet-stream'); }); (0, vitest_1.it)('should handle file uploads with multipart/form-data', async () => { const formData = new FormData(); formData.append('file', new Blob(['test data'], { type: 'text/plain' }), 'test.txt'); formData.append('description', 'Test file upload'); const result = await generatedClient.api.upload.post({ body: formData, filename: 'test.txt', size: 9 }); (0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/upload', { method: 'POST', headers: { 'Content-Type': 'multipart/form-data' }, body: formData }); (0, vitest_1.expect)(result).toEqual({ id: 'upload-123', filename: 'test.txt', size: 9 }); }); (0, vitest_1.it)('should handle XML responses', async () => { const response = await generatedClient.api.xml.get(); (0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/xml', { method: 'GET', headers: {} }); (0, vitest_1.expect)(response.ok).toBe(true); (0, vitest_1.expect)(response.headers.get('content-type')).toBe('application/xml'); // Test XML data handling const xmlText = await response.text(); (0, vitest_1.expect)(xmlText).toContain('<?xml version="1.0" encoding="UTF-8"?>'); (0, vitest_1.expect)(xmlText).toContain('<root>'); (0, vitest_1.expect)(xmlText).toContain('<item id="1">Test</item>'); }); (0, vitest_1.it)('should handle CSV responses', async () => { const response = await generatedClient.api.csv.get(); (0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/csv', { method: 'GET', headers: {} }); (0, vitest_1.expect)(response.ok).toBe(true); (0, vitest_1.expect)(response.headers.get('content-type')).toBe('text/csv'); // Test CSV data handling const csvText = await response.text(); (0, vitest_1.expect)(csvText).toContain('id,name,email'); (0, vitest_1.expect)(csvText).toContain('John Doe,john@example.com'); (0, vitest_1.expect)(csvText).toContain('Jane Smith,jane@example.com'); }); (0, vitest_1.it)('should handle image responses', async () => { const response = await generatedClient.api.image.get(); (0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/image', { method: 'GET', headers: {} }); (0, vitest_1.expect)(response.ok).toBe(true); (0, vitest_1.expect)(response.headers.get('content-type')).toBe('image/png'); (0, vitest_1.expect)(response.headers.get('content-length')).toBe('1024'); // Test image data handling const blob = await response.blob(); (0, vitest_1.expect)(blob).toBeInstanceOf(Blob); (0, vitest_1.expect)(blob.type).toBe('image/png'); const arrayBuffer = await response.arrayBuffer(); (0, vitest_1.expect)(arrayBuffer).toBeInstanceOf(ArrayBuffer); (0, vitest_1.expect)(arrayBuffer.byteLength).toBe(1024); }); (0, vitest_1.it)('should handle PDF responses', async () => { const response = await generatedClient.api.pdf.get(); (0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/pdf', { method: 'GET', headers: {} }); (0, vitest_1.expect)(response.ok).toBe(true); (0, vitest_1.expect)(response.headers.get('content-type')).toBe('application/pdf'); (0, vitest_1.expect)(response.headers.get('content-disposition')).toContain('inline'); (0, vitest_1.expect)(response.headers.get('content-disposition')).toContain('document.pdf'); // Test PDF data handling const blob = await response.blob(); (0, vitest_1.expect)(blob).toBeInstanceOf(Blob); (0, vitest_1.expect)(blob.type).toBe('application/pdf'); }); (0, vitest_1.it)('should handle streaming responses', async () => { const response = await generatedClient.api.stream.get(); (0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/stream', { method: 'GET', headers: {} }); (0, vitest_1.expect)(response.ok).toBe(true); (0, vitest_1.expect)(response.headers.get('content-type')).toBe('text/plain'); (0, vitest_1.expect)(response.headers.get('transfer-encoding')).toBe('chunked'); // Test streaming data handling const reader = response.body.getReader(); const chunks = []; let chunk = await reader.read(); while (!chunk.done) { chunks.push(new TextDecoder().decode(chunk.value)); chunk = await reader.read(); } (0, vitest_1.expect)(chunks).toEqual(['chunk1', 'chunk2']); }); (0, vitest_1.it)('should handle custom content types with Accept headers', async () => { const response = await generatedClient.api.xml.get({ headers: { 'Accept': 'application/xml, text/xml' } }); (0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/xml', { method: 'GET', headers: { 'Accept': 'application/xml, text/xml' } }); (0, vitest_1.expect)(response.ok).toBe(true); (0, vitest_1.expect)(response.headers.get('content-type')).toBe('application/xml'); }); (0, vitest_1.it)('should handle content with custom encoding', async () => { const encodedClient = { api: { encoded: { get: vitest_1.vi.fn().mockImplementation(async () => { const response = { ok: true, status: 200, headers: new Map([ ['content-type', 'text/plain; charset=utf-8'], ['content-encoding', 'gzip'] ]), text: vitest_1.vi.fn().mockResolvedValue('Encoded content with special chars: café, naïve, résumé') }; mockFetch.mockResolvedValueOnce(response); const fetchResponse = await fetch('http://localhost:3000/api/encoded'); return fetchResponse; }) } } }; const response = await encodedClient.api.encoded.get(); (0, vitest_1.expect)(response.headers.get('content-encoding')).toBe('gzip'); (0, vitest_1.expect)(response.headers.get('content-type')).toContain('charset=utf-8'); const text = await response.text(); (0, vitest_1.expect)(text).toContain('café, naïve, résumé'); }); (0, vitest_1.it)('should handle different image formats', async () => { const imageFormats = ['image/png', 'image/jpeg', 'image/gif', 'image/webp', 'image/svg+xml']; for (const contentType of imageFormats) { const formatClient = { api: { image: { get: vitest_1.vi.fn().mockImplementation(async () => { const response = { ok: true, status: 200, headers: new Map([['content-type', contentType]]), blob: vitest_1.vi.fn().mockResolvedValue(new Blob([new ArrayBuffer(512)], { type: contentType })) }; mockFetch.mockResolvedValueOnce(response); const fetchResponse = await fetch(`http://localhost:3000/api/image`); return fetchResponse; }) } } }; const response = await formatClient.api.image.get(); (0, vitest_1.expect)(response.headers.get('content-type')).toBe(contentType); const blob = await response.blob(); (0, vitest_1.expect)(blob.type).toBe(contentType); } }); (0, vitest_1.it)('should handle video content', async () => { const videoClient = { api: { video: { get: vitest_1.vi.fn().mockImplementation(async () => { const response = { ok: true, status: 200, headers: new Map([ ['content-type', 'video/mp4'], ['content-length', '10485760'], // 10MB ['accept-ranges', 'bytes'] ]), blob: vitest_1.vi.fn().mockResolvedValue(new Blob([new ArrayBuffer(10485760)], { type: 'video/mp4' })) }; mockFetch.mockResolvedValueOnce(response); const fetchResponse = await fetch('http://localhost:3000/api/video'); return fetchResponse; }) } } }; const response = await videoClient.api.video.get(); (0, vitest_1.expect)(response.headers.get('content-type')).toBe('video/mp4'); (0, vitest_1.expect)(response.headers.get('accept-ranges')).toBe('bytes'); const blob = await response.blob(); (0, vitest_1.expect)(blob.type).toBe('video/mp4'); (0, vitest_1.expect)(blob.size).toBe(10485760); }); (0, vitest_1.it)('should handle audio content', async () => { const audioClient = { api: { audio: { get: vitest_1.vi.fn().mockImplementation(async () => { const response = { ok: true, status: 200, headers: new Map([ ['content-type', 'audio/mpeg'], ['content-disposition', 'inline; filename="song.mp3"'] ]), blob: vitest_1.vi.fn().mockResolvedValue(new Blob([new ArrayBuffer(5242880)], { type: 'audio/mpeg' })) }; mockFetch.mockResolvedValueOnce(response); const fetchResponse = await fetch('http://localhost:3000/api/audio'); return fetchResponse; }) } } }; const response = await audioClient.api.audio.get(); (0, vitest_1.expect)(response.headers.get('content-type')).toBe('audio/mpeg'); (0, vitest_1.expect)(response.headers.get('content-disposition')).toContain('song.mp3'); }); (0, vitest_1.it)('should handle text-based custom formats', async () => { const textFormats = [ { type: 'text/css', data: 'body { margin: 0; padding: 0; }' }, { type: 'text/javascript', data: 'console.log("Hello World");' }, { type: 'text/html', data: '<html><body><h1>Test</h1></body></html>' }, { type: 'application/yaml', data: 'key: value\nlist:\n - item1\n - item2' }, { type: 'application/toml', data: '[section]\nkey = "value"' } ]; for (const format of textFormats) { const textClient = { api: { text: { get: vitest_1.vi.fn().mockImplementation(async () => { const response = { ok: true, status: 200, headers: new Map([['content-type', format.type]]), text: vitest_1.vi.fn().mockResolvedValue(format.data) }; mockFetch.mockResolvedValueOnce(response); const fetchResponse = await fetch('http://localhost:3000/api/text'); return fetchResponse; }) } } }; const response = await textClient.api.text.get(); (0, vitest_1.expect)(response.headers.get('content-type')).toBe(format.type); const text = await response.text(); (0, vitest_1.expect)(text).toBe(format.data); } }); (0, vitest_1.it)('should handle compressed content', async () => { const compressedClient = { api: { compressed: { get: vitest_1.vi.fn().mockImplementation(async () => { const response = { ok: true, status: 200, headers: new Map([ ['content-type', 'application/zip'], ['content-disposition', 'attachment; filename="archive.zip"'] ]), blob: vitest_1.vi.fn().mockResolvedValue(new Blob([new ArrayBuffer(1024)], { type: 'application/zip' })) }; mockFetch.mockResolvedValueOnce(response); const fetchResponse = await fetch('http://localhost:3000/api/compressed'); return fetchResponse; }) } } }; const response = await compressedClient.api.compressed.get(); (0, vitest_1.expect)(response.headers.get('content-type')).toBe('application/zip'); (0, vitest_1.expect)(response.headers.get('content-disposition')).toContain('archive.zip'); const blob = await response.blob(); (0, vitest_1.expect)(blob.type).toBe('application/zip'); }); (0, vitest_1.it)('should handle responses with no content-type header', async () => { const noTypeClient = { api: { notype: { get: vitest_1.vi.fn().mockImplementation(async () => { const response = { ok: true, status: 200, headers: new Map(), // No content-type header text: vitest_1.vi.fn().mockResolvedValue('Plain text content'), blob: vitest_1.vi.fn().mockResolvedValue(new Blob(['Plain text content'])) }; mockFetch.mockResolvedValueOnce(response); const fetchResponse = await fetch('http://localhost:3000/api/notype'); return fetchResponse; }) } } }; const response = await noTypeClient.api.notype.get(); (0, vitest_1.expect)(response.headers.get('content-type')).toBeUndefined(); // Should still be able to handle the response const text = await response.text(); (0, vitest_1.expect)(text).toBe('Plain text content'); }); (0, vitest_1.it)('should handle content type negotiation', async () => { const negotiationClient = { api: { negotiate: { get: vitest_1.vi.fn().mockImplementation(async (options = {}) => { const acceptHeader = options.headers?.['Accept'] || 'application/json'; let contentType = 'application/json'; let responseData = { message: 'JSON response' }; if (acceptHeader.includes('text/xml')) { contentType = 'text/xml'; responseData = { message: 'XML response' }; } else if (acceptHeader.includes('text/csv')) { contentType = 'text/csv'; responseData = { message: 'CSV response' }; } const response = { ok: true, status: 200, headers: new Map([['content-type', contentType]]), json: contentType === 'application/json' ? vitest_1.vi.fn().mockResolvedValue(responseData) : undefined, text: contentType !== 'application/json' ? vitest_1.vi.fn().mockResolvedValue(responseData) : undefined }; mockFetch.mockResolvedValueOnce(response); const fetchResponse = await fetch('http://localhost:3000/api/negotiate', { headers: options.headers || {} }); return fetchResponse; }) } } }; // Test JSON response const jsonResponse = await negotiationClient.api.negotiate.get({ headers: { 'Accept': 'application/json' } }); (0, vitest_1.expect)(jsonResponse.headers.get('content-type')).toBe('application/json'); // Test XML response const xmlResponse = await negotiationClient.api.negotiate.get({ headers: { 'Accept': 'text/xml' } }); (0, vitest_1.expect)(xmlResponse.headers.get('content-type')).toBe('text/xml'); // Test CSV response const csvResponse = await negotiationClient.api.negotiate.get({ headers: { 'Accept': 'text/csv' } }); (0, vitest_1.expect)(csvResponse.headers.get('content-type')).toBe('text/csv'); }); }); //# sourceMappingURL=client-custom-content-types.test.js.map