@objectstack/nextjs 2.0.1 → 2.0.2
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/.turbo/turbo-build.log +6 -6
- package/CHANGELOG.md +6 -0
- package/package.json +9 -6
- package/src/__mocks__/runtime.ts +4 -0
- package/src/nextjs.test.ts +390 -0
- package/vitest.config.ts +16 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @objectstack/nextjs@2.0.
|
|
2
|
+
> @objectstack/nextjs@2.0.2 build /home/runner/work/spec/spec/packages/adapters/nextjs
|
|
3
3
|
> tsup --config ../../../tsup.config.ts
|
|
4
4
|
|
|
5
5
|
[34mCLI[39m Building entry: src/index.ts
|
|
@@ -10,13 +10,13 @@
|
|
|
10
10
|
[34mCLI[39m Cleaning output folder
|
|
11
11
|
[34mESM[39m Build start
|
|
12
12
|
[34mCJS[39m Build start
|
|
13
|
-
[32mESM[39m [1mdist/index.mjs [22m[32m3.71 KB[39m
|
|
14
|
-
[32mESM[39m [1mdist/index.mjs.map [22m[32m7.89 KB[39m
|
|
15
|
-
[32mESM[39m ⚡️ Build success in 35ms
|
|
16
13
|
[32mCJS[39m [1mdist/index.js [22m[32m4.87 KB[39m
|
|
17
14
|
[32mCJS[39m [1mdist/index.js.map [22m[32m7.94 KB[39m
|
|
18
|
-
[32mCJS[39m ⚡️ Build success in
|
|
15
|
+
[32mCJS[39m ⚡️ Build success in 49ms
|
|
16
|
+
[32mESM[39m [1mdist/index.mjs [22m[32m3.71 KB[39m
|
|
17
|
+
[32mESM[39m [1mdist/index.mjs.map [22m[32m7.89 KB[39m
|
|
18
|
+
[32mESM[39m ⚡️ Build success in 55ms
|
|
19
19
|
[34mDTS[39m Build start
|
|
20
|
-
[32mDTS[39m ⚡️ Build success in
|
|
20
|
+
[32mDTS[39m ⚡️ Build success in 6612ms
|
|
21
21
|
[32mDTS[39m [1mdist/index.d.mts [22m[32m764.00 B[39m
|
|
22
22
|
[32mDTS[39m [1mdist/index.d.ts [22m[32m764.00 B[39m
|
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
|
@@ -1,23 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@objectstack/nextjs",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.2",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"peerDependencies": {
|
|
8
|
-
"next": "^
|
|
8
|
+
"next": "^16.1.6",
|
|
9
9
|
"react": "^19.2.4",
|
|
10
10
|
"react-dom": "^19.2.4",
|
|
11
|
-
"@objectstack/runtime": "2.0.
|
|
11
|
+
"@objectstack/runtime": "2.0.2"
|
|
12
12
|
},
|
|
13
13
|
"devDependencies": {
|
|
14
|
-
"next": "^
|
|
14
|
+
"next": "^16.1.6",
|
|
15
15
|
"react": "^19.2.4",
|
|
16
16
|
"react-dom": "^19.2.4",
|
|
17
17
|
"typescript": "^5.0.0",
|
|
18
|
-
"
|
|
18
|
+
"vitest": "^4.0.18",
|
|
19
|
+
"@objectstack/runtime": "2.0.2"
|
|
19
20
|
},
|
|
20
21
|
"scripts": {
|
|
21
|
-
"build": "tsup --config ../../../tsup.config.ts"
|
|
22
|
+
"build": "tsup --config ../../../tsup.config.ts",
|
|
23
|
+
"test": "vitest run",
|
|
24
|
+
"test:watch": "vitest"
|
|
22
25
|
}
|
|
23
26
|
}
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
+
|
|
3
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
4
|
+
|
|
5
|
+
// Mock dispatcher instance
|
|
6
|
+
const mockDispatcher = {
|
|
7
|
+
getDiscoveryInfo: vi.fn().mockReturnValue({ version: '1.0', endpoints: [] }),
|
|
8
|
+
handleAuth: vi.fn().mockResolvedValue({ handled: true, response: { body: { ok: true }, status: 200 } }),
|
|
9
|
+
handleGraphQL: vi.fn().mockResolvedValue({ data: {} }),
|
|
10
|
+
handleMetadata: vi.fn().mockResolvedValue({ handled: true, response: { body: { objects: [] }, status: 200 } }),
|
|
11
|
+
handleData: vi.fn().mockResolvedValue({ handled: true, response: { body: { records: [] }, status: 200 } }),
|
|
12
|
+
handleStorage: vi.fn().mockResolvedValue({ handled: true, response: { body: {}, status: 200 } }),
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
vi.mock('@objectstack/runtime', () => {
|
|
16
|
+
return {
|
|
17
|
+
HttpDispatcher: function HttpDispatcher() {
|
|
18
|
+
return mockDispatcher;
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
vi.mock('next/server', () => {
|
|
24
|
+
class MockNextRequest {
|
|
25
|
+
url: string;
|
|
26
|
+
method: string;
|
|
27
|
+
private _body: any;
|
|
28
|
+
|
|
29
|
+
constructor(url: string, init?: any) {
|
|
30
|
+
this.url = url;
|
|
31
|
+
this.method = init?.method || 'GET';
|
|
32
|
+
this._body = init?.body;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async json() {
|
|
36
|
+
return this._body ? JSON.parse(this._body) : {};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async formData() {
|
|
40
|
+
const map = new Map();
|
|
41
|
+
map.set('file', { name: 'test.txt', type: 'text/plain' });
|
|
42
|
+
// Return a FormData-like object with get()
|
|
43
|
+
return { get: (key: string) => map.get(key) };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
class MockNextResponse {
|
|
48
|
+
body: any;
|
|
49
|
+
status: number;
|
|
50
|
+
headers: Record<string, string>;
|
|
51
|
+
|
|
52
|
+
constructor(body?: any, init?: any) {
|
|
53
|
+
this.body = body;
|
|
54
|
+
this.status = init?.status || 200;
|
|
55
|
+
this.headers = init?.headers || {};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async json() {
|
|
59
|
+
return typeof this.body === 'string' ? JSON.parse(this.body) : this.body;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
static json(body: any, init?: any) {
|
|
63
|
+
const res = new MockNextResponse(body, init);
|
|
64
|
+
return res;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
static redirect(url: string | URL) {
|
|
68
|
+
const res = new MockNextResponse(null, { status: 307 });
|
|
69
|
+
(res as any).redirectUrl = typeof url === 'string' ? url : url.toString();
|
|
70
|
+
return res;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return { NextRequest: MockNextRequest, NextResponse: MockNextResponse };
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
import { NextRequest } from 'next/server';
|
|
78
|
+
import { createRouteHandler, createDiscoveryHandler } from './index';
|
|
79
|
+
|
|
80
|
+
const mockKernel = { name: 'test-kernel' } as any;
|
|
81
|
+
|
|
82
|
+
function makeReq(url: string, method = 'GET', body?: any) {
|
|
83
|
+
const init: any = { method };
|
|
84
|
+
if (body) init.body = JSON.stringify(body);
|
|
85
|
+
return new (NextRequest as any)(url, init);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
describe('createRouteHandler', () => {
|
|
89
|
+
beforeEach(() => {
|
|
90
|
+
vi.clearAllMocks();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe('Discovery Endpoint', () => {
|
|
94
|
+
it('GET with empty segments returns discovery info', async () => {
|
|
95
|
+
const handler = createRouteHandler({ kernel: mockKernel });
|
|
96
|
+
const req = makeReq('http://localhost/api');
|
|
97
|
+
const res = await handler(req, { params: { objectstack: [] } });
|
|
98
|
+
expect(res.status).toBe(200);
|
|
99
|
+
expect(res.body).toEqual({ data: { version: '1.0', endpoints: [] } });
|
|
100
|
+
expect(mockDispatcher.getDiscoveryInfo).toHaveBeenCalledWith('/api');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('uses custom prefix for discovery', async () => {
|
|
104
|
+
const handler = createRouteHandler({ kernel: mockKernel, prefix: '/v2' });
|
|
105
|
+
const req = makeReq('http://localhost/v2');
|
|
106
|
+
const res = await handler(req, { params: { objectstack: [] } });
|
|
107
|
+
expect(res.status).toBe(200);
|
|
108
|
+
expect(mockDispatcher.getDiscoveryInfo).toHaveBeenCalledWith('/v2');
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe('Auth Endpoint', () => {
|
|
113
|
+
it('POST auth/login calls handleAuth', async () => {
|
|
114
|
+
const handler = createRouteHandler({ kernel: mockKernel });
|
|
115
|
+
const req = makeReq('http://localhost/api/auth/login', 'POST', { email: 'a@b.com' });
|
|
116
|
+
const res = await handler(req, { params: { objectstack: ['auth', 'login'] } });
|
|
117
|
+
expect(res.status).toBe(200);
|
|
118
|
+
expect(res.body).toEqual({ ok: true });
|
|
119
|
+
expect(mockDispatcher.handleAuth).toHaveBeenCalledWith(
|
|
120
|
+
'login',
|
|
121
|
+
'POST',
|
|
122
|
+
expect.any(Object),
|
|
123
|
+
expect.objectContaining({ request: expect.anything() }),
|
|
124
|
+
);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('GET auth/callback calls handleAuth with empty body', async () => {
|
|
128
|
+
const handler = createRouteHandler({ kernel: mockKernel });
|
|
129
|
+
const req = makeReq('http://localhost/api/auth/callback', 'GET');
|
|
130
|
+
const res = await handler(req, { params: { objectstack: ['auth', 'callback'] } });
|
|
131
|
+
expect(res.status).toBe(200);
|
|
132
|
+
expect(mockDispatcher.handleAuth).toHaveBeenCalledWith(
|
|
133
|
+
'callback',
|
|
134
|
+
'GET',
|
|
135
|
+
{},
|
|
136
|
+
expect.objectContaining({ request: expect.anything() }),
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('returns error on handleAuth exception', async () => {
|
|
141
|
+
mockDispatcher.handleAuth.mockRejectedValueOnce(
|
|
142
|
+
Object.assign(new Error('Unauthorized'), { statusCode: 401 }),
|
|
143
|
+
);
|
|
144
|
+
const handler = createRouteHandler({ kernel: mockKernel });
|
|
145
|
+
const req = makeReq('http://localhost/api/auth/login', 'POST');
|
|
146
|
+
const res = await handler(req, { params: { objectstack: ['auth', 'login'] } });
|
|
147
|
+
expect(res.status).toBe(401);
|
|
148
|
+
expect(res.body.success).toBe(false);
|
|
149
|
+
expect(res.body.error.message).toBe('Unauthorized');
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe('GraphQL Endpoint', () => {
|
|
154
|
+
it('POST graphql calls handleGraphQL', async () => {
|
|
155
|
+
const handler = createRouteHandler({ kernel: mockKernel });
|
|
156
|
+
const body = { query: '{ objects { name } }' };
|
|
157
|
+
const req = makeReq('http://localhost/api/graphql', 'POST', body);
|
|
158
|
+
const res = await handler(req, { params: { objectstack: ['graphql'] } });
|
|
159
|
+
expect(res.status).toBe(200);
|
|
160
|
+
expect(res.body).toEqual({ data: {} });
|
|
161
|
+
expect(mockDispatcher.handleGraphQL).toHaveBeenCalledWith(
|
|
162
|
+
body,
|
|
163
|
+
expect.objectContaining({ request: expect.anything() }),
|
|
164
|
+
);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('returns error on handleGraphQL exception', async () => {
|
|
168
|
+
mockDispatcher.handleGraphQL.mockRejectedValueOnce(new Error('Parse error'));
|
|
169
|
+
const handler = createRouteHandler({ kernel: mockKernel });
|
|
170
|
+
const req = makeReq('http://localhost/api/graphql', 'POST', { query: 'bad' });
|
|
171
|
+
const res = await handler(req, { params: { objectstack: ['graphql'] } });
|
|
172
|
+
expect(res.status).toBe(500);
|
|
173
|
+
expect(res.body.success).toBe(false);
|
|
174
|
+
expect(res.body.error.message).toBe('Parse error');
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe('Metadata Endpoint', () => {
|
|
179
|
+
it('GET meta/objects calls handleMetadata', async () => {
|
|
180
|
+
const handler = createRouteHandler({ kernel: mockKernel });
|
|
181
|
+
const req = makeReq('http://localhost/api/meta/objects', 'GET');
|
|
182
|
+
const res = await handler(req, { params: { objectstack: ['meta', 'objects'] } });
|
|
183
|
+
expect(res.status).toBe(200);
|
|
184
|
+
expect(res.body).toEqual({ objects: [] });
|
|
185
|
+
expect(mockDispatcher.handleMetadata).toHaveBeenCalledWith(
|
|
186
|
+
'objects',
|
|
187
|
+
expect.objectContaining({ request: expect.anything() }),
|
|
188
|
+
'GET',
|
|
189
|
+
undefined,
|
|
190
|
+
);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('PUT meta/objects parses JSON body', async () => {
|
|
194
|
+
const handler = createRouteHandler({ kernel: mockKernel });
|
|
195
|
+
const body = { name: 'test_object' };
|
|
196
|
+
const req = makeReq('http://localhost/api/meta/objects', 'PUT', body);
|
|
197
|
+
const res = await handler(req, { params: { objectstack: ['meta', 'objects'] } });
|
|
198
|
+
expect(res.status).toBe(200);
|
|
199
|
+
expect(mockDispatcher.handleMetadata).toHaveBeenCalledWith(
|
|
200
|
+
'objects',
|
|
201
|
+
expect.objectContaining({ request: expect.anything() }),
|
|
202
|
+
'PUT',
|
|
203
|
+
body,
|
|
204
|
+
);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('POST meta/objects parses JSON body', async () => {
|
|
208
|
+
const handler = createRouteHandler({ kernel: mockKernel });
|
|
209
|
+
const body = { label: 'My Object' };
|
|
210
|
+
const req = makeReq('http://localhost/api/meta/objects', 'POST', body);
|
|
211
|
+
const res = await handler(req, { params: { objectstack: ['meta', 'objects'] } });
|
|
212
|
+
expect(res.status).toBe(200);
|
|
213
|
+
expect(mockDispatcher.handleMetadata).toHaveBeenCalledWith(
|
|
214
|
+
'objects',
|
|
215
|
+
expect.objectContaining({ request: expect.anything() }),
|
|
216
|
+
'POST',
|
|
217
|
+
body,
|
|
218
|
+
);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
describe('Data Endpoint', () => {
|
|
223
|
+
it('GET data/account calls handleData', async () => {
|
|
224
|
+
const handler = createRouteHandler({ kernel: mockKernel });
|
|
225
|
+
const req = makeReq('http://localhost/api/data/account', 'GET');
|
|
226
|
+
const res = await handler(req, { params: { objectstack: ['data', 'account'] } });
|
|
227
|
+
expect(res.status).toBe(200);
|
|
228
|
+
expect(res.body).toEqual({ records: [] });
|
|
229
|
+
expect(mockDispatcher.handleData).toHaveBeenCalledWith(
|
|
230
|
+
'account',
|
|
231
|
+
'GET',
|
|
232
|
+
{},
|
|
233
|
+
expect.any(Object),
|
|
234
|
+
expect.objectContaining({ request: expect.anything() }),
|
|
235
|
+
);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('POST data/account parses JSON body', async () => {
|
|
239
|
+
const handler = createRouteHandler({ kernel: mockKernel });
|
|
240
|
+
const body = { name: 'Acme' };
|
|
241
|
+
const req = makeReq('http://localhost/api/data/account', 'POST', body);
|
|
242
|
+
const res = await handler(req, { params: { objectstack: ['data', 'account'] } });
|
|
243
|
+
expect(res.status).toBe(200);
|
|
244
|
+
expect(mockDispatcher.handleData).toHaveBeenCalledWith(
|
|
245
|
+
'account',
|
|
246
|
+
'POST',
|
|
247
|
+
body,
|
|
248
|
+
expect.any(Object),
|
|
249
|
+
expect.objectContaining({ request: expect.anything() }),
|
|
250
|
+
);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('PATCH data/account parses JSON body', async () => {
|
|
254
|
+
const handler = createRouteHandler({ kernel: mockKernel });
|
|
255
|
+
const body = { name: 'Updated' };
|
|
256
|
+
const req = makeReq('http://localhost/api/data/account', 'PATCH', body);
|
|
257
|
+
const res = await handler(req, { params: { objectstack: ['data', 'account'] } });
|
|
258
|
+
expect(res.status).toBe(200);
|
|
259
|
+
expect(mockDispatcher.handleData).toHaveBeenCalledWith(
|
|
260
|
+
'account',
|
|
261
|
+
'PATCH',
|
|
262
|
+
body,
|
|
263
|
+
expect.any(Object),
|
|
264
|
+
expect.objectContaining({ request: expect.anything() }),
|
|
265
|
+
);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('returns 404 when result is not handled', async () => {
|
|
269
|
+
mockDispatcher.handleData.mockResolvedValueOnce({ handled: false });
|
|
270
|
+
const handler = createRouteHandler({ kernel: mockKernel });
|
|
271
|
+
const req = makeReq('http://localhost/api/data/missing', 'GET');
|
|
272
|
+
const res = await handler(req, { params: { objectstack: ['data', 'missing'] } });
|
|
273
|
+
expect(res.status).toBe(404);
|
|
274
|
+
expect(res.body.success).toBe(false);
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
describe('Storage Endpoint', () => {
|
|
279
|
+
it('GET storage/files calls handleStorage', async () => {
|
|
280
|
+
const handler = createRouteHandler({ kernel: mockKernel });
|
|
281
|
+
const req = makeReq('http://localhost/api/storage/files', 'GET');
|
|
282
|
+
const res = await handler(req, { params: { objectstack: ['storage', 'files'] } });
|
|
283
|
+
expect(res.status).toBe(200);
|
|
284
|
+
expect(mockDispatcher.handleStorage).toHaveBeenCalledWith(
|
|
285
|
+
'files',
|
|
286
|
+
'GET',
|
|
287
|
+
undefined,
|
|
288
|
+
expect.objectContaining({ request: expect.anything() }),
|
|
289
|
+
);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('POST storage/upload calls handleStorage with file', async () => {
|
|
293
|
+
const handler = createRouteHandler({ kernel: mockKernel });
|
|
294
|
+
const req = makeReq('http://localhost/api/storage/upload', 'POST');
|
|
295
|
+
const res = await handler(req, { params: { objectstack: ['storage', 'upload'] } });
|
|
296
|
+
expect(res.status).toBe(200);
|
|
297
|
+
expect(mockDispatcher.handleStorage).toHaveBeenCalledWith(
|
|
298
|
+
'upload',
|
|
299
|
+
'POST',
|
|
300
|
+
expect.objectContaining({ name: 'test.txt' }),
|
|
301
|
+
expect.objectContaining({ request: expect.anything() }),
|
|
302
|
+
);
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
describe('Error Handling', () => {
|
|
307
|
+
it('returns 404 for unknown route segments', async () => {
|
|
308
|
+
const handler = createRouteHandler({ kernel: mockKernel });
|
|
309
|
+
const req = makeReq('http://localhost/api/unknown/path', 'GET');
|
|
310
|
+
const res = await handler(req, { params: { objectstack: ['unknown', 'path'] } });
|
|
311
|
+
expect(res.status).toBe(404);
|
|
312
|
+
expect(res.body.success).toBe(false);
|
|
313
|
+
expect(res.body.error.message).toBe('Not Found');
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it('returns 500 with default message on generic error', async () => {
|
|
317
|
+
mockDispatcher.handleData.mockRejectedValueOnce(new Error());
|
|
318
|
+
const handler = createRouteHandler({ kernel: mockKernel });
|
|
319
|
+
const req = makeReq('http://localhost/api/data/account', 'GET');
|
|
320
|
+
const res = await handler(req, { params: { objectstack: ['data', 'account'] } });
|
|
321
|
+
expect(res.status).toBe(500);
|
|
322
|
+
expect(res.body.error.message).toBe('Internal Server Error');
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('uses custom statusCode from error', async () => {
|
|
326
|
+
mockDispatcher.handleData.mockRejectedValueOnce(
|
|
327
|
+
Object.assign(new Error('Forbidden'), { statusCode: 403 }),
|
|
328
|
+
);
|
|
329
|
+
const handler = createRouteHandler({ kernel: mockKernel });
|
|
330
|
+
const req = makeReq('http://localhost/api/data/account', 'GET');
|
|
331
|
+
const res = await handler(req, { params: { objectstack: ['data', 'account'] } });
|
|
332
|
+
expect(res.status).toBe(403);
|
|
333
|
+
expect(res.body.error.message).toBe('Forbidden');
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
describe('toResponse', () => {
|
|
338
|
+
it('handles redirect result', async () => {
|
|
339
|
+
mockDispatcher.handleData.mockResolvedValueOnce({
|
|
340
|
+
handled: true,
|
|
341
|
+
result: { type: 'redirect', url: 'https://example.com' },
|
|
342
|
+
});
|
|
343
|
+
const handler = createRouteHandler({ kernel: mockKernel });
|
|
344
|
+
const req = makeReq('http://localhost/api/data/redir', 'GET');
|
|
345
|
+
const res = await handler(req, { params: { objectstack: ['data', 'redir'] } });
|
|
346
|
+
expect((res as any).redirectUrl).toBe('https://example.com');
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it('handles stream result', async () => {
|
|
350
|
+
const stream = 'mock-stream';
|
|
351
|
+
mockDispatcher.handleData.mockResolvedValueOnce({
|
|
352
|
+
handled: true,
|
|
353
|
+
result: { type: 'stream', stream, headers: { 'Content-Type': 'text/plain' } },
|
|
354
|
+
});
|
|
355
|
+
const handler = createRouteHandler({ kernel: mockKernel });
|
|
356
|
+
const req = makeReq('http://localhost/api/data/stream', 'GET');
|
|
357
|
+
const res = await handler(req, { params: { objectstack: ['data', 'stream'] } });
|
|
358
|
+
expect(res.body).toBe('mock-stream');
|
|
359
|
+
expect(res.status).toBe(200);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it('returns raw result when handled but no response/redirect/stream', async () => {
|
|
363
|
+
const rawResult = { type: 'custom', data: 'test' };
|
|
364
|
+
mockDispatcher.handleData.mockResolvedValueOnce({
|
|
365
|
+
handled: true,
|
|
366
|
+
result: rawResult,
|
|
367
|
+
});
|
|
368
|
+
const handler = createRouteHandler({ kernel: mockKernel });
|
|
369
|
+
const req = makeReq('http://localhost/api/data/custom', 'GET');
|
|
370
|
+
const res = await handler(req, { params: { objectstack: ['data', 'custom'] } });
|
|
371
|
+
expect(res).toBe(rawResult);
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
describe('createDiscoveryHandler', () => {
|
|
377
|
+
it('redirects to default /api path', async () => {
|
|
378
|
+
const handler = createDiscoveryHandler({ kernel: mockKernel });
|
|
379
|
+
const req = makeReq('http://localhost/.well-known/objectstack', 'GET');
|
|
380
|
+
const res = await handler(req);
|
|
381
|
+
expect((res as any).redirectUrl).toBe('http://localhost/api');
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it('redirects to custom prefix', async () => {
|
|
385
|
+
const handler = createDiscoveryHandler({ kernel: mockKernel, prefix: '/v2/api' });
|
|
386
|
+
const req = makeReq('http://localhost/.well-known/objectstack', 'GET');
|
|
387
|
+
const res = await handler(req);
|
|
388
|
+
expect((res as any).redirectUrl).toBe('http://localhost/v2/api');
|
|
389
|
+
});
|
|
390
|
+
});
|
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
+
|
|
3
|
+
import { defineConfig } from 'vitest/config';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
export default defineConfig({
|
|
7
|
+
test: {
|
|
8
|
+
globals: true,
|
|
9
|
+
environment: 'node',
|
|
10
|
+
},
|
|
11
|
+
resolve: {
|
|
12
|
+
alias: {
|
|
13
|
+
'@objectstack/runtime': path.resolve(__dirname, 'src/__mocks__/runtime.ts'),
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
});
|