@prism-engineer/router
Version:
Type-safe Express.js router with automatic client generation
504 lines β’ 24.2 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 - 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