@prism-engineer/router
Version:
Type-safe Express.js router with automatic client generation
570 lines • 28.9 kB
JavaScript
"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