@facetlayer/prism-framework 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +176 -8
- package/dist/Errors.d.ts +38 -0
- package/dist/Errors.d.ts.map +1 -0
- package/dist/Metrics.d.ts +5 -0
- package/dist/Metrics.d.ts.map +1 -0
- package/dist/RequestContext.d.ts +17 -0
- package/dist/RequestContext.d.ts.map +1 -0
- package/dist/ServiceDefinition.d.ts +16 -0
- package/dist/ServiceDefinition.d.ts.map +1 -0
- package/dist/app/PrismApp.d.ts +31 -0
- package/dist/app/PrismApp.d.ts.map +1 -0
- package/dist/app/callEndpoint.d.ts +13 -0
- package/dist/app/callEndpoint.d.ts.map +1 -0
- package/dist/app/validateApp.d.ts +20 -0
- package/dist/app/validateApp.d.ts.map +1 -0
- package/dist/authorization/AuthSource.d.ts +8 -0
- package/dist/authorization/AuthSource.d.ts.map +1 -0
- package/dist/authorization/Authorization.d.ts +24 -0
- package/dist/authorization/Authorization.d.ts.map +1 -0
- package/dist/authorization/Resource.d.ts +5 -0
- package/dist/authorization/Resource.d.ts.map +1 -0
- package/dist/authorization/index.d.ts +5 -0
- package/dist/authorization/index.d.ts.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/databases/DatabaseInitializationOptions.d.ts +9 -0
- package/dist/databases/DatabaseInitializationOptions.d.ts.map +1 -0
- package/dist/databases/DatabaseSetup.d.ts +3 -0
- package/dist/databases/DatabaseSetup.d.ts.map +1 -0
- package/dist/endpoints/createEndpoint.d.ts +4 -0
- package/dist/endpoints/createEndpoint.d.ts.map +1 -0
- package/dist/endpoints/getEffectiveOperationId.d.ts +19 -0
- package/dist/endpoints/getEffectiveOperationId.d.ts.map +1 -0
- package/dist/env/Env.d.ts +2 -0
- package/dist/env/Env.d.ts.map +1 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1364 -0
- package/dist/launch/launchConfig.d.ts +18 -0
- package/dist/launch/launchConfig.d.ts.map +1 -0
- package/dist/logging/index.d.ts +9 -0
- package/dist/logging/index.d.ts.map +1 -0
- package/dist/sse/ConnectionManager.d.ts +23 -0
- package/dist/sse/ConnectionManager.d.ts.map +1 -0
- package/dist/stdin/StdinServer.d.ts +38 -0
- package/dist/stdin/StdinServer.d.ts.map +1 -0
- package/dist/web/EndpointListing.d.ts +3 -0
- package/dist/web/EndpointListing.d.ts.map +1 -0
- package/dist/web/ExpressAppSetup.d.ts +18 -0
- package/dist/web/ExpressAppSetup.d.ts.map +1 -0
- package/dist/web/ExpressEndpointSetup.d.ts +31 -0
- package/dist/web/ExpressEndpointSetup.d.ts.map +1 -0
- package/dist/web/SseResponse.d.ts +15 -0
- package/dist/web/SseResponse.d.ts.map +1 -0
- package/dist/web/ViteIntegration.d.ts +19 -0
- package/dist/web/ViteIntegration.d.ts.map +1 -0
- package/dist/web/corsMiddleware.d.ts +14 -0
- package/dist/web/corsMiddleware.d.ts.map +1 -0
- package/dist/web/localhostOnlyMiddleware.d.ts +3 -0
- package/dist/web/localhostOnlyMiddleware.d.ts.map +1 -0
- package/dist/web/openapi/OpenAPI.d.ts +37 -0
- package/dist/web/openapi/OpenAPI.d.ts.map +1 -0
- package/dist/web/openapi/validateServicesForOpenapi.d.ts +32 -0
- package/dist/web/openapi/validateServicesForOpenapi.d.ts.map +1 -0
- package/dist/web/requestContextMiddleware.d.ts +3 -0
- package/dist/web/requestContextMiddleware.d.ts.map +1 -0
- package/docs/authorization.md +281 -0
- package/docs/cors-setup.md +172 -0
- package/docs/creating-services.md +220 -0
- package/docs/database-setup.md +134 -0
- package/docs/endpoint-tools.md +1 -11
- package/docs/env-files.md +12 -1
- package/docs/error-handling.md +70 -0
- package/docs/getting-started.md +22 -12
- package/docs/launch-configuration.md +223 -0
- package/docs/overview.md +62 -0
- package/docs/server-setup.md +144 -0
- package/docs/source-directory-organization.md +115 -0
- package/docs/stdin-protocol.md +176 -0
- package/package.json +42 -9
- package/src/Errors.ts +120 -0
- package/src/Metrics.ts +53 -0
- package/src/RequestContext.ts +36 -0
- package/src/ServiceDefinition.ts +35 -0
- package/src/__tests__/Authorization.test.ts +350 -0
- package/src/__tests__/Errors.test.ts +378 -0
- package/src/__tests__/ListEndpoints.test.ts +98 -0
- package/src/__tests__/PrismApp.test.ts +274 -0
- package/src/__tests__/RequestContext.test.ts +295 -0
- package/src/__tests__/SseResponse.test.ts +189 -0
- package/src/__tests__/StdinServer.test.ts +304 -0
- package/src/__tests__/corsMiddleware.test.ts +293 -0
- package/src/__tests__/createEndpoint.test.ts +412 -0
- package/src/__tests__/validateApp.test.ts +206 -0
- package/src/app/PrismApp.ts +117 -0
- package/src/app/callEndpoint.ts +55 -0
- package/src/app/validateApp.ts +78 -0
- package/src/authorization/AuthSource.ts +14 -0
- package/src/authorization/Authorization.ts +78 -0
- package/src/authorization/Resource.ts +8 -0
- package/src/authorization/index.ts +4 -0
- package/src/databases/DatabaseInitializationOptions.ts +9 -0
- package/src/databases/DatabaseSetup.ts +19 -0
- package/src/endpoints/createEndpoint.ts +39 -0
- package/src/endpoints/getEffectiveOperationId.ts +90 -0
- package/src/env/Env.ts +23 -0
- package/src/index.ts +78 -0
- package/src/launch/launchConfig.ts +59 -0
- package/src/list-endpoints-command.ts +1 -1
- package/src/logging/index.ts +25 -0
- package/src/sse/ConnectionManager.ts +79 -0
- package/src/stdin/StdinServer.ts +129 -0
- package/src/web/EndpointListing.ts +166 -0
- package/src/web/ExpressAppSetup.ts +125 -0
- package/src/web/ExpressEndpointSetup.ts +178 -0
- package/src/web/SseResponse.ts +78 -0
- package/src/web/ViteIntegration.ts +72 -0
- package/src/web/__tests__/OpenAPI.invalidZodSchemas.test.ts +250 -0
- package/src/web/corsMiddleware.ts +63 -0
- package/src/web/localhostOnlyMiddleware.ts +19 -0
- package/src/web/openapi/OpenAPI.ts +248 -0
- package/src/web/openapi/validateServicesForOpenapi.ts +76 -0
- package/src/web/requestContextMiddleware.ts +25 -0
- package/.claude/settings.local.json +0 -20
- package/CHANGELOG +0 -28
- package/CLAUDE.md +0 -44
- package/build.mts +0 -8
- package/test/call-command.test.ts +0 -96
- package/test/generate-api-clients.test.ts +0 -33
- package/test/generate-api-clients.test.ts.disabled +0 -75
- package/tsconfig.json +0 -21
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { SseResponse } from '../web/SseResponse.ts';
|
|
2
|
+
import { Response } from 'express';
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
|
|
5
|
+
describe('SseResponse', () => {
|
|
6
|
+
let mockResponse: Response;
|
|
7
|
+
let sseResponse: SseResponse;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
mockResponse = {
|
|
11
|
+
writeHead: vi.fn(),
|
|
12
|
+
write: vi.fn(),
|
|
13
|
+
end: vi.fn(),
|
|
14
|
+
on: vi.fn(),
|
|
15
|
+
} as any;
|
|
16
|
+
|
|
17
|
+
sseResponse = new SseResponse(mockResponse);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('constructor', () => {
|
|
21
|
+
it('should set up SSE headers correctly', () => {
|
|
22
|
+
expect(mockResponse.writeHead).toHaveBeenCalledWith(200, {
|
|
23
|
+
'Content-Type': 'text/event-stream',
|
|
24
|
+
'Cache-Control': 'no-cache',
|
|
25
|
+
Connection: 'keep-alive',
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should set up close event handlers', () => {
|
|
30
|
+
expect(mockResponse.on).toHaveBeenCalledWith('close', expect.any(Function));
|
|
31
|
+
expect(mockResponse.on).toHaveBeenCalledWith('finish', expect.any(Function));
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('send', () => {
|
|
36
|
+
it('should send JSON data in SSE format', () => {
|
|
37
|
+
const testData = { type: 'test', message: 'hello' };
|
|
38
|
+
|
|
39
|
+
sseResponse.send(testData);
|
|
40
|
+
|
|
41
|
+
expect(mockResponse.write).toHaveBeenCalledWith(
|
|
42
|
+
`event: item\ndata: ${JSON.stringify(testData)}\n\n`
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should not send data when response is closed', () => {
|
|
47
|
+
// Simulate close event
|
|
48
|
+
const closeCallback = (mockResponse.on as any).mock.calls.find(
|
|
49
|
+
(call: any) => call[0] === 'close'
|
|
50
|
+
)?.[1];
|
|
51
|
+
closeCallback?.();
|
|
52
|
+
|
|
53
|
+
const testData = { type: 'test', message: 'hello' };
|
|
54
|
+
sseResponse.send(testData);
|
|
55
|
+
|
|
56
|
+
expect(mockResponse.write).toHaveBeenCalledTimes(0);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should handle complex objects correctly', () => {
|
|
60
|
+
const complexData = {
|
|
61
|
+
type: 'complex',
|
|
62
|
+
data: {
|
|
63
|
+
nested: { value: 123 },
|
|
64
|
+
array: [1, 2, 3],
|
|
65
|
+
nullValue: null,
|
|
66
|
+
boolValue: true,
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
sseResponse.send(complexData);
|
|
71
|
+
|
|
72
|
+
expect(mockResponse.write).toHaveBeenCalledWith(
|
|
73
|
+
`event: item\ndata: ${JSON.stringify(complexData)}\n\n`
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('isOpen', () => {
|
|
79
|
+
it('should return true initially', () => {
|
|
80
|
+
expect(sseResponse.isOpen()).toBe(true);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should return false after close event', () => {
|
|
84
|
+
// Simulate close event
|
|
85
|
+
const closeCallback = (mockResponse.on as any).mock.calls.find(
|
|
86
|
+
(call: any) => call[0] === 'close'
|
|
87
|
+
)?.[1];
|
|
88
|
+
closeCallback?.();
|
|
89
|
+
|
|
90
|
+
expect(sseResponse.isOpen()).toBe(false);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should return false after finish event', () => {
|
|
94
|
+
// Simulate finish event
|
|
95
|
+
const finishCallback = (mockResponse.on as any).mock.calls.find(
|
|
96
|
+
(call: any) => call[0] === 'finish'
|
|
97
|
+
)?.[1];
|
|
98
|
+
finishCallback?.();
|
|
99
|
+
|
|
100
|
+
expect(sseResponse.isOpen()).toBe(false);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('close', () => {
|
|
105
|
+
it('should send done event and end the response', () => {
|
|
106
|
+
sseResponse.close();
|
|
107
|
+
|
|
108
|
+
expect(mockResponse.write).toHaveBeenCalledWith('event: done\n\n');
|
|
109
|
+
expect(mockResponse.end).toHaveBeenCalled();
|
|
110
|
+
expect(sseResponse.isOpen()).toBe(false);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should not send done event or call end twice if already closed', () => {
|
|
114
|
+
sseResponse.close();
|
|
115
|
+
|
|
116
|
+
// Clear mocks
|
|
117
|
+
(mockResponse.write as any).mockClear();
|
|
118
|
+
(mockResponse.end as any).mockClear();
|
|
119
|
+
|
|
120
|
+
sseResponse.close();
|
|
121
|
+
|
|
122
|
+
expect(mockResponse.write).not.toHaveBeenCalled();
|
|
123
|
+
expect(mockResponse.end).not.toHaveBeenCalled();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should not send done event or call end if response was already closed by client', () => {
|
|
127
|
+
// Simulate close event
|
|
128
|
+
const closeCallback = (mockResponse.on as any).mock.calls.find(
|
|
129
|
+
(call: any) => call[0] === 'close'
|
|
130
|
+
)?.[1];
|
|
131
|
+
closeCallback?.();
|
|
132
|
+
|
|
133
|
+
// Clear mocks from setup
|
|
134
|
+
(mockResponse.write as any).mockClear();
|
|
135
|
+
(mockResponse.end as any).mockClear();
|
|
136
|
+
|
|
137
|
+
sseResponse.close();
|
|
138
|
+
|
|
139
|
+
expect(mockResponse.write).not.toHaveBeenCalled();
|
|
140
|
+
expect(mockResponse.end).not.toHaveBeenCalled();
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe('onClose', () => {
|
|
145
|
+
it('should call the onClose callback when response closes', () => {
|
|
146
|
+
const onCloseCallback = vi.fn();
|
|
147
|
+
sseResponse.onClose(onCloseCallback);
|
|
148
|
+
|
|
149
|
+
// Simulate close event
|
|
150
|
+
const closeCallback = (mockResponse.on as any).mock.calls.find(
|
|
151
|
+
(call: any) => call[0] === 'close'
|
|
152
|
+
)?.[1];
|
|
153
|
+
closeCallback?.();
|
|
154
|
+
|
|
155
|
+
expect(onCloseCallback).toHaveBeenCalledTimes(1);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('should call the onClose callback when close() is called', () => {
|
|
159
|
+
const onCloseCallback = vi.fn();
|
|
160
|
+
sseResponse.onClose(onCloseCallback);
|
|
161
|
+
|
|
162
|
+
sseResponse.close();
|
|
163
|
+
|
|
164
|
+
expect(onCloseCallback).toHaveBeenCalledTimes(1);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('should throw error if onClose is set twice', () => {
|
|
168
|
+
sseResponse.onClose(() => {});
|
|
169
|
+
|
|
170
|
+
expect(() => {
|
|
171
|
+
sseResponse.onClose(() => {});
|
|
172
|
+
}).toThrow('usage error: alrady have onClose callback');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('should not call onClose callback twice', () => {
|
|
176
|
+
const onCloseCallback = vi.fn();
|
|
177
|
+
sseResponse.onClose(onCloseCallback);
|
|
178
|
+
|
|
179
|
+
// Trigger close multiple times
|
|
180
|
+
const closeCallback = (mockResponse.on as any).mock.calls.find(
|
|
181
|
+
(call: any) => call[0] === 'close'
|
|
182
|
+
)?.[1];
|
|
183
|
+
closeCallback?.();
|
|
184
|
+
closeCallback?.();
|
|
185
|
+
|
|
186
|
+
expect(onCloseCallback).toHaveBeenCalledTimes(1);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
});
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { PassThrough } from 'stream';
|
|
3
|
+
import { createEndpoint } from '../endpoints/createEndpoint.ts';
|
|
4
|
+
import { PrismApp } from '../app/PrismApp.ts';
|
|
5
|
+
import { NotFoundError, BadRequestError } from '../Errors.ts';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { startStdinServer, type StdinResponse } from '../stdin/StdinServer.ts';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Create a test app with sample endpoints for testing the stdin protocol.
|
|
11
|
+
*/
|
|
12
|
+
function createTestApp(): PrismApp {
|
|
13
|
+
const items = [
|
|
14
|
+
{ id: '1', title: 'First item', done: false },
|
|
15
|
+
{ id: '2', title: 'Second item', done: true },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const listItems = createEndpoint({
|
|
19
|
+
method: 'GET',
|
|
20
|
+
path: '/items',
|
|
21
|
+
description: 'List all items',
|
|
22
|
+
responseSchema: z.array(z.object({ id: z.string(), title: z.string(), done: z.boolean() })),
|
|
23
|
+
handler: async () => items,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const getItem = createEndpoint({
|
|
27
|
+
method: 'GET',
|
|
28
|
+
path: '/items/:id',
|
|
29
|
+
description: 'Get an item by ID',
|
|
30
|
+
requestSchema: z.object({ id: z.string() }),
|
|
31
|
+
responseSchema: z.object({ id: z.string(), title: z.string(), done: z.boolean() }),
|
|
32
|
+
handler: async (input) => {
|
|
33
|
+
const item = items.find(i => i.id === input.id);
|
|
34
|
+
if (!item) throw new NotFoundError('Item not found');
|
|
35
|
+
return item;
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const createItem = createEndpoint({
|
|
40
|
+
method: 'POST',
|
|
41
|
+
path: '/items',
|
|
42
|
+
description: 'Create a new item',
|
|
43
|
+
requestSchema: z.object({ title: z.string() }),
|
|
44
|
+
responseSchema: z.object({ id: z.string(), title: z.string(), done: z.boolean() }),
|
|
45
|
+
handler: async (input) => {
|
|
46
|
+
if (!input.title) throw new BadRequestError('Title is required');
|
|
47
|
+
const newItem = { id: String(items.length + 1), title: input.title, done: false };
|
|
48
|
+
items.push(newItem);
|
|
49
|
+
return newItem;
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return new PrismApp({
|
|
54
|
+
services: [{ name: 'items', endpoints: [listItems, getItem, createItem] }],
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Helper to set up a stdin server with mocked stdin/stdout streams.
|
|
60
|
+
*/
|
|
61
|
+
function setupStdinServer() {
|
|
62
|
+
const fakeStdin = new PassThrough();
|
|
63
|
+
const fakeStdout = new PassThrough();
|
|
64
|
+
|
|
65
|
+
// Replace process.stdin and process.stdout temporarily
|
|
66
|
+
const origStdin = process.stdin;
|
|
67
|
+
const origStdout = process.stdout;
|
|
68
|
+
const origExit = process.exit;
|
|
69
|
+
|
|
70
|
+
// Monkey-patch process.stdin/stdout
|
|
71
|
+
Object.defineProperty(process, 'stdin', { value: fakeStdin, writable: true, configurable: true });
|
|
72
|
+
|
|
73
|
+
const writtenData: string[] = [];
|
|
74
|
+
const origWrite = process.stdout.write;
|
|
75
|
+
// Override stdout.write to capture output
|
|
76
|
+
const stdoutWrite = vi.fn((chunk: any, ...args: any[]) => {
|
|
77
|
+
const str = typeof chunk === 'string' ? chunk : chunk.toString();
|
|
78
|
+
writtenData.push(str);
|
|
79
|
+
fakeStdout.write(chunk);
|
|
80
|
+
return true;
|
|
81
|
+
});
|
|
82
|
+
(process.stdout as any).write = stdoutWrite;
|
|
83
|
+
|
|
84
|
+
// Prevent process.exit from actually exiting
|
|
85
|
+
let exitCalled = false;
|
|
86
|
+
(process as any).exit = vi.fn((code?: number) => {
|
|
87
|
+
exitCalled = true;
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const app = createTestApp();
|
|
91
|
+
startStdinServer({ app });
|
|
92
|
+
|
|
93
|
+
const responses: StdinResponse[] = [];
|
|
94
|
+
const waiters = new Map<string, (resp: StdinResponse) => void>();
|
|
95
|
+
|
|
96
|
+
// Collect all written data and parse responses
|
|
97
|
+
function processWrittenData() {
|
|
98
|
+
for (const data of writtenData) {
|
|
99
|
+
const lines = data.split('\n');
|
|
100
|
+
for (const line of lines) {
|
|
101
|
+
if (!line.trim()) continue;
|
|
102
|
+
try {
|
|
103
|
+
const resp = JSON.parse(line) as StdinResponse;
|
|
104
|
+
responses.push(resp);
|
|
105
|
+
const waiter = waiters.get(resp.id);
|
|
106
|
+
if (waiter) {
|
|
107
|
+
waiters.delete(resp.id);
|
|
108
|
+
waiter(resp);
|
|
109
|
+
}
|
|
110
|
+
} catch {
|
|
111
|
+
// ignore
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
writtenData.length = 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Process data on each write
|
|
119
|
+
const origStdoutWriteFn = stdoutWrite;
|
|
120
|
+
(process.stdout as any).write = vi.fn((chunk: any, ...args: any[]) => {
|
|
121
|
+
const str = typeof chunk === 'string' ? chunk : chunk.toString();
|
|
122
|
+
writtenData.push(str);
|
|
123
|
+
fakeStdout.write(chunk);
|
|
124
|
+
processWrittenData();
|
|
125
|
+
return true;
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
function send(msg: any) {
|
|
129
|
+
fakeStdin.write(JSON.stringify(msg) + '\n');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function waitForResponse(id: string, timeoutMs = 5000): Promise<StdinResponse> {
|
|
133
|
+
processWrittenData();
|
|
134
|
+
const existing = responses.find(r => r.id === id);
|
|
135
|
+
if (existing) return Promise.resolve(existing);
|
|
136
|
+
|
|
137
|
+
return new Promise((resolve, reject) => {
|
|
138
|
+
const timer = setTimeout(() => {
|
|
139
|
+
waiters.delete(id);
|
|
140
|
+
reject(new Error(`Timeout waiting for response id="${id}"`));
|
|
141
|
+
}, timeoutMs);
|
|
142
|
+
|
|
143
|
+
waiters.set(id, (resp) => {
|
|
144
|
+
clearTimeout(timer);
|
|
145
|
+
resolve(resp);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function cleanup() {
|
|
151
|
+
Object.defineProperty(process, 'stdin', { value: origStdin, writable: true, configurable: true });
|
|
152
|
+
(process.stdout as any).write = origWrite;
|
|
153
|
+
(process as any).exit = origExit;
|
|
154
|
+
fakeStdin.destroy();
|
|
155
|
+
fakeStdout.destroy();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return { send, waitForResponse, cleanup, responses, fakeStdin };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
describe('StdinServer', () => {
|
|
162
|
+
it('should send a ready message on startup', async () => {
|
|
163
|
+
const ctx = setupStdinServer();
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
const ready = await ctx.waitForResponse('_ready');
|
|
167
|
+
expect(ready.status).toBe(200);
|
|
168
|
+
expect(ready.body.message).toBe('stdin server ready');
|
|
169
|
+
} finally {
|
|
170
|
+
ctx.cleanup();
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('should handle GET requests', async () => {
|
|
175
|
+
const ctx = setupStdinServer();
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
await ctx.waitForResponse('_ready');
|
|
179
|
+
|
|
180
|
+
ctx.send({ id: 'req-1', method: 'GET', path: '/items' });
|
|
181
|
+
const resp = await ctx.waitForResponse('req-1');
|
|
182
|
+
|
|
183
|
+
expect(resp.status).toBe(200);
|
|
184
|
+
expect(resp.body).toBeInstanceOf(Array);
|
|
185
|
+
expect(resp.body.length).toBe(2);
|
|
186
|
+
expect(resp.body[0]).toHaveProperty('id');
|
|
187
|
+
expect(resp.body[0]).toHaveProperty('title');
|
|
188
|
+
} finally {
|
|
189
|
+
ctx.cleanup();
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('should handle GET with path parameters', async () => {
|
|
194
|
+
const ctx = setupStdinServer();
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
await ctx.waitForResponse('_ready');
|
|
198
|
+
|
|
199
|
+
ctx.send({ id: 'req-2', method: 'GET', path: '/items/1' });
|
|
200
|
+
const resp = await ctx.waitForResponse('req-2');
|
|
201
|
+
|
|
202
|
+
expect(resp.status).toBe(200);
|
|
203
|
+
expect(resp.body.id).toBe('1');
|
|
204
|
+
expect(resp.body.title).toBe('First item');
|
|
205
|
+
} finally {
|
|
206
|
+
ctx.cleanup();
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('should handle POST requests with body', async () => {
|
|
211
|
+
const ctx = setupStdinServer();
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
await ctx.waitForResponse('_ready');
|
|
215
|
+
|
|
216
|
+
ctx.send({ id: 'req-3', method: 'POST', path: '/items', body: { title: 'New item' } });
|
|
217
|
+
const resp = await ctx.waitForResponse('req-3');
|
|
218
|
+
|
|
219
|
+
expect(resp.status).toBe(200);
|
|
220
|
+
expect(resp.body.title).toBe('New item');
|
|
221
|
+
expect(resp.body.done).toBe(false);
|
|
222
|
+
expect(resp.body.id).toBeDefined();
|
|
223
|
+
} finally {
|
|
224
|
+
ctx.cleanup();
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('should return error for non-existent endpoints', async () => {
|
|
229
|
+
const ctx = setupStdinServer();
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
await ctx.waitForResponse('_ready');
|
|
233
|
+
|
|
234
|
+
ctx.send({ id: 'req-4', method: 'GET', path: '/nonexistent' });
|
|
235
|
+
const resp = await ctx.waitForResponse('req-4');
|
|
236
|
+
|
|
237
|
+
expect(resp.status).toBe(500);
|
|
238
|
+
expect(resp.body.message).toContain('not found');
|
|
239
|
+
} finally {
|
|
240
|
+
ctx.cleanup();
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('should return 404 HttpError for non-existent item', async () => {
|
|
245
|
+
const ctx = setupStdinServer();
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
await ctx.waitForResponse('_ready');
|
|
249
|
+
|
|
250
|
+
ctx.send({ id: 'req-5', method: 'GET', path: '/items/999' });
|
|
251
|
+
const resp = await ctx.waitForResponse('req-5');
|
|
252
|
+
|
|
253
|
+
expect(resp.status).toBe(404);
|
|
254
|
+
expect(resp.body.message).toBe('Item not found');
|
|
255
|
+
} finally {
|
|
256
|
+
ctx.cleanup();
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('should handle multiple concurrent requests', async () => {
|
|
261
|
+
const ctx = setupStdinServer();
|
|
262
|
+
|
|
263
|
+
try {
|
|
264
|
+
await ctx.waitForResponse('_ready');
|
|
265
|
+
|
|
266
|
+
ctx.send({ id: 'c-1', method: 'GET', path: '/items' });
|
|
267
|
+
ctx.send({ id: 'c-2', method: 'GET', path: '/items/1' });
|
|
268
|
+
ctx.send({ id: 'c-3', method: 'GET', path: '/items/2' });
|
|
269
|
+
|
|
270
|
+
const [r1, r2, r3] = await Promise.all([
|
|
271
|
+
ctx.waitForResponse('c-1'),
|
|
272
|
+
ctx.waitForResponse('c-2'),
|
|
273
|
+
ctx.waitForResponse('c-3'),
|
|
274
|
+
]);
|
|
275
|
+
|
|
276
|
+
expect(r1.status).toBe(200);
|
|
277
|
+
expect(r1.body).toBeInstanceOf(Array);
|
|
278
|
+
|
|
279
|
+
expect(r2.status).toBe(200);
|
|
280
|
+
expect(r2.body.id).toBe('1');
|
|
281
|
+
|
|
282
|
+
expect(r3.status).toBe(200);
|
|
283
|
+
expect(r3.body.id).toBe('2');
|
|
284
|
+
} finally {
|
|
285
|
+
ctx.cleanup();
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('should handle missing required fields', async () => {
|
|
290
|
+
const ctx = setupStdinServer();
|
|
291
|
+
|
|
292
|
+
try {
|
|
293
|
+
await ctx.waitForResponse('_ready');
|
|
294
|
+
|
|
295
|
+
ctx.send({ id: 'bad-req', path: '/items' });
|
|
296
|
+
const resp = await ctx.waitForResponse('bad-req');
|
|
297
|
+
|
|
298
|
+
expect(resp.status).toBe(400);
|
|
299
|
+
expect(resp.body.message).toContain('Missing required fields');
|
|
300
|
+
} finally {
|
|
301
|
+
ctx.cleanup();
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
});
|