@dbos-inc/koa-serve 3.5.44-preview.gc094fdab44 → 3.6.3-preview
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/dist/src/dboshttp.d.ts +15 -0
- package/dist/src/dboshttp.d.ts.map +1 -1
- package/dist/src/dboshttp.js +38 -3
- package/dist/src/dboshttp.js.map +1 -1
- package/dist/src/dboskoa.js +1 -1
- package/dist/src/dboskoa.js.map +1 -1
- package/dist/src/index.d.ts +8 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +17 -3
- package/dist/src/index.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/dboshttp.ts +61 -4
- package/src/dboskoa.ts +1 -1
- package/src/index.ts +16 -0
- package/tests/argsource.test.ts +151 -0
- package/tests/auth.test.ts +42 -7
- package/tests/endpoints.test.ts +86 -34
- package/tests/steps.test.ts +5 -0
- package/tests/transactions.test.ts +5 -0
- package/tests/validation.test.ts +531 -0
package/tests/endpoints.test.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import Koa from 'koa';
|
|
3
3
|
import Router from '@koa/router';
|
|
4
4
|
|
|
5
|
-
import { DBOS, Error as DBOSErrors, StatusString } from '@dbos-inc/dbos-sdk';
|
|
5
|
+
import { DBOS, DBOSResponseError, Error as DBOSErrors, StatusString } from '@dbos-inc/dbos-sdk';
|
|
6
6
|
|
|
7
7
|
import { DBOSKoa, DBOSKoaAuthContext, RequestIDHeader, WorkflowIDHeader } from '../src';
|
|
8
8
|
|
|
@@ -10,6 +10,11 @@ import request from 'supertest';
|
|
|
10
10
|
|
|
11
11
|
const dhttp = new DBOSKoa();
|
|
12
12
|
|
|
13
|
+
interface TestKvTable {
|
|
14
|
+
id?: number;
|
|
15
|
+
value?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
13
18
|
import { randomUUID } from 'node:crypto';
|
|
14
19
|
import { IncomingMessage } from 'http';
|
|
15
20
|
import { bodyParser } from '@koa/bodyparser';
|
|
@@ -24,9 +29,12 @@ describe('httpserver-tests', () => {
|
|
|
24
29
|
let app: Koa;
|
|
25
30
|
let appRouter: Router;
|
|
26
31
|
|
|
32
|
+
const testTableName = 'dbos_test_kv';
|
|
33
|
+
|
|
27
34
|
beforeAll(async () => {
|
|
28
35
|
DBOS.setConfig({
|
|
29
36
|
name: 'dbos-koa-test',
|
|
37
|
+
userDatabaseClient: 'pg-node',
|
|
30
38
|
});
|
|
31
39
|
return Promise.resolve();
|
|
32
40
|
});
|
|
@@ -34,6 +42,8 @@ describe('httpserver-tests', () => {
|
|
|
34
42
|
beforeEach(async () => {
|
|
35
43
|
const _classes = [TestEndpoints];
|
|
36
44
|
await DBOS.launch();
|
|
45
|
+
await DBOS.queryUserDB(`DROP TABLE IF EXISTS ${testTableName};`);
|
|
46
|
+
await DBOS.queryUserDB(`CREATE TABLE IF NOT EXISTS ${testTableName} (id INT PRIMARY KEY, value TEXT);`);
|
|
37
47
|
app = new Koa();
|
|
38
48
|
appRouter = new Router();
|
|
39
49
|
dhttp.registerWithApp(app, appRouter);
|
|
@@ -97,12 +107,17 @@ describe('httpserver-tests', () => {
|
|
|
97
107
|
});
|
|
98
108
|
|
|
99
109
|
test('post-test-custom-body', async () => {
|
|
100
|
-
|
|
110
|
+
let response = await request(app.callback())
|
|
101
111
|
.post('/testpost')
|
|
102
112
|
.set('Content-Type', 'application/custom-content-type')
|
|
103
113
|
.send(JSON.stringify({ name: 'alice' }));
|
|
104
114
|
expect(response.statusCode).toBe(200);
|
|
105
115
|
expect(response.text).toBe('hello alice');
|
|
116
|
+
response = await request(app.callback())
|
|
117
|
+
.post('/testpost')
|
|
118
|
+
.set('Content-Type', 'application/rejected-custom-content-type')
|
|
119
|
+
.send(JSON.stringify({ name: 'alice' }));
|
|
120
|
+
expect(response.statusCode).toBe(400);
|
|
106
121
|
});
|
|
107
122
|
|
|
108
123
|
test('put-test', async () => {
|
|
@@ -112,12 +127,17 @@ describe('httpserver-tests', () => {
|
|
|
112
127
|
});
|
|
113
128
|
|
|
114
129
|
test('put-test-custom-body', async () => {
|
|
115
|
-
|
|
130
|
+
let response = await request(app.callback())
|
|
116
131
|
.put('/testput')
|
|
117
132
|
.set('Content-Type', 'application/custom-content-type')
|
|
118
133
|
.send(JSON.stringify({ name: 'alice' }));
|
|
119
134
|
expect(response.statusCode).toBe(200);
|
|
120
135
|
expect(response.text).toBe('hello alice');
|
|
136
|
+
response = await request(app.callback())
|
|
137
|
+
.put('/testput')
|
|
138
|
+
.set('Content-Type', 'application/rejected-custom-content-type')
|
|
139
|
+
.send(JSON.stringify({ name: 'alice' }));
|
|
140
|
+
expect(response.statusCode).toBe(400);
|
|
121
141
|
});
|
|
122
142
|
|
|
123
143
|
test('patch-test', async () => {
|
|
@@ -127,12 +147,23 @@ describe('httpserver-tests', () => {
|
|
|
127
147
|
});
|
|
128
148
|
|
|
129
149
|
test('patch-test-custom-body', async () => {
|
|
130
|
-
|
|
150
|
+
let response = await request(app.callback())
|
|
131
151
|
.patch('/testpatch')
|
|
132
152
|
.set('Content-Type', 'application/custom-content-type')
|
|
133
153
|
.send(JSON.stringify({ name: 'alice' }));
|
|
134
154
|
expect(response.statusCode).toBe(200);
|
|
135
155
|
expect(response.text).toBe('hello alice');
|
|
156
|
+
response = await request(app.callback())
|
|
157
|
+
.patch('/testpatch')
|
|
158
|
+
.set('Content-Type', 'application/rejected-custom-content-type')
|
|
159
|
+
.send(JSON.stringify({ name: 'alice' }));
|
|
160
|
+
expect(response.statusCode).toBe(400);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('endpoint-transaction', async () => {
|
|
164
|
+
const response = await request(app.callback()).post('/transaction/alice');
|
|
165
|
+
expect(response.statusCode).toBe(200);
|
|
166
|
+
expect(response.text).toBe('hello 1');
|
|
136
167
|
});
|
|
137
168
|
|
|
138
169
|
test('endpoint-step', async () => {
|
|
@@ -144,30 +175,31 @@ describe('httpserver-tests', () => {
|
|
|
144
175
|
test('endpoint-workflow', async () => {
|
|
145
176
|
const response = await request(app.callback()).post('/workflow?name=alice');
|
|
146
177
|
expect(response.statusCode).toBe(200);
|
|
147
|
-
expect(response.text).toBe('
|
|
178
|
+
expect(response.text).toBe('hello 1');
|
|
148
179
|
});
|
|
149
180
|
|
|
150
181
|
test('endpoint-error', async () => {
|
|
151
182
|
const response = await request(app.callback()).post('/error').send({ name: 'alice' });
|
|
152
183
|
expect(response.statusCode).toBe(500);
|
|
184
|
+
expect(response.body.details.code).toBe('23505'); // Should be the expected error.
|
|
153
185
|
});
|
|
154
186
|
|
|
155
187
|
test('endpoint-handler', async () => {
|
|
156
188
|
const response = await request(app.callback()).get('/handler/alice');
|
|
157
189
|
expect(response.statusCode).toBe(200);
|
|
158
|
-
expect(response.text).toBe('
|
|
190
|
+
expect(response.text).toBe('hello 1');
|
|
159
191
|
});
|
|
160
192
|
|
|
161
193
|
test('endpoint-testStartWorkflow', async () => {
|
|
162
194
|
const response = await request(app.callback()).get('/testStartWorkflow/alice');
|
|
163
195
|
expect(response.statusCode).toBe(200);
|
|
164
|
-
expect(response.text).toBe('
|
|
196
|
+
expect(response.text).toBe('hello 1');
|
|
165
197
|
});
|
|
166
198
|
|
|
167
199
|
test('endpoint-testInvokeWorkflow', async () => {
|
|
168
200
|
const response = await request(app.callback()).get('/testInvokeWorkflow/alice');
|
|
169
201
|
expect(response.statusCode).toBe(200);
|
|
170
|
-
expect(response.text).toBe('
|
|
202
|
+
expect(response.text).toBe('hello 1');
|
|
171
203
|
});
|
|
172
204
|
|
|
173
205
|
// This feels unclean, but supertest doesn't expose the error message the people we want. See:
|
|
@@ -178,11 +210,17 @@ describe('httpserver-tests', () => {
|
|
|
178
210
|
|
|
179
211
|
test('response-error', async () => {
|
|
180
212
|
const response = await request(app.callback()).get('/dbos-error');
|
|
181
|
-
expect(response.statusCode).toBe(
|
|
213
|
+
expect(response.statusCode).toBe(503);
|
|
182
214
|
expect((response as unknown as Res).res.statusMessage).toBe('customize error');
|
|
183
215
|
expect(response.body.message).toBe('customize error');
|
|
184
216
|
});
|
|
185
217
|
|
|
218
|
+
test('datavalidation-error', async () => {
|
|
219
|
+
const response = await request(app.callback()).get('/query');
|
|
220
|
+
expect(response.statusCode).toBe(400);
|
|
221
|
+
expect(response.body.details.dbosErrorCode).toBe(9);
|
|
222
|
+
});
|
|
223
|
+
|
|
186
224
|
test('dbos-redirect', async () => {
|
|
187
225
|
const response = await request(app.callback()).get('/redirect');
|
|
188
226
|
expect(response.statusCode).toBe(302);
|
|
@@ -210,17 +248,17 @@ describe('httpserver-tests', () => {
|
|
|
210
248
|
|
|
211
249
|
test('not-authenticated', async () => {
|
|
212
250
|
const response = await request(app.callback()).get('/requireduser?name=alice');
|
|
213
|
-
expect(response.statusCode).toBe(
|
|
251
|
+
expect(response.statusCode).toBe(401);
|
|
214
252
|
});
|
|
215
253
|
|
|
216
254
|
test('not-you', async () => {
|
|
217
255
|
const response = await request(app.callback()).get('/requireduser?name=alice&userid=go_away');
|
|
218
|
-
expect(response.statusCode).toBe(
|
|
256
|
+
expect(response.statusCode).toBe(401);
|
|
219
257
|
});
|
|
220
258
|
|
|
221
259
|
test('not-authorized', async () => {
|
|
222
260
|
const response = await request(app.callback()).get('/requireduser?name=alice&userid=bob');
|
|
223
|
-
expect(response.statusCode).toBe(
|
|
261
|
+
expect(response.statusCode).toBe(403);
|
|
224
262
|
});
|
|
225
263
|
|
|
226
264
|
test('authorized', async () => {
|
|
@@ -230,17 +268,17 @@ describe('httpserver-tests', () => {
|
|
|
230
268
|
|
|
231
269
|
test('not-authenticated2', async () => {
|
|
232
270
|
const response = await request(app.callback()).get('/requireduser2?name=alice');
|
|
233
|
-
expect(response.statusCode).toBe(
|
|
271
|
+
expect(response.statusCode).toBe(401);
|
|
234
272
|
});
|
|
235
273
|
|
|
236
274
|
test('not-you2', async () => {
|
|
237
275
|
const response = await request(app.callback()).get('/requireduser2?name=alice&userid=go_away');
|
|
238
|
-
expect(response.statusCode).toBe(
|
|
276
|
+
expect(response.statusCode).toBe(401);
|
|
239
277
|
});
|
|
240
278
|
|
|
241
279
|
test('not-authorized2', async () => {
|
|
242
280
|
const response = await request(app.callback()).get('/requireduser2?name=alice&userid=bob');
|
|
243
|
-
expect(response.statusCode).toBe(
|
|
281
|
+
expect(response.statusCode).toBe(403);
|
|
244
282
|
});
|
|
245
283
|
|
|
246
284
|
test('authorized2', async () => {
|
|
@@ -254,12 +292,12 @@ describe('httpserver-tests', () => {
|
|
|
254
292
|
.post('/workflow?name=bob')
|
|
255
293
|
.set({ 'dbos-idempotency-key': workflowID });
|
|
256
294
|
expect(response.statusCode).toBe(200);
|
|
257
|
-
expect(response.text).toBe('
|
|
295
|
+
expect(response.text).toBe('hello 1');
|
|
258
296
|
|
|
259
297
|
// Retrieve the workflow with WFID.
|
|
260
298
|
const retrievedHandle = DBOS.retrieveWorkflow(workflowID);
|
|
261
299
|
expect(retrievedHandle).not.toBeNull();
|
|
262
|
-
await expect(retrievedHandle.getResult()).resolves.toBe('
|
|
300
|
+
await expect(retrievedHandle.getResult()).resolves.toBe('hello 1');
|
|
263
301
|
await expect(retrievedHandle.getStatus()).resolves.toMatchObject({
|
|
264
302
|
status: StatusString.SUCCESS,
|
|
265
303
|
});
|
|
@@ -269,12 +307,12 @@ describe('httpserver-tests', () => {
|
|
|
269
307
|
const workflowID = randomUUID();
|
|
270
308
|
const response = await request(app.callback()).get('/handler/bob').set({ 'dbos-idempotency-key': workflowID });
|
|
271
309
|
expect(response.statusCode).toBe(200);
|
|
272
|
-
expect(response.text).toBe('
|
|
310
|
+
expect(response.text).toBe('hello 1');
|
|
273
311
|
|
|
274
312
|
// Retrieve the workflow with WFID.
|
|
275
313
|
const retrievedHandle = DBOS.retrieveWorkflow(workflowID);
|
|
276
314
|
expect(retrievedHandle).not.toBeNull();
|
|
277
|
-
await expect(retrievedHandle.getResult()).resolves.toBe('
|
|
315
|
+
await expect(retrievedHandle.getResult()).resolves.toBe('hello 1');
|
|
278
316
|
await expect(retrievedHandle.getStatus()).resolves.toMatchObject({
|
|
279
317
|
status: StatusString.SUCCESS,
|
|
280
318
|
});
|
|
@@ -311,6 +349,7 @@ describe('httpserver-tests', () => {
|
|
|
311
349
|
parsedMethods: ['POST', 'PUT', 'PATCH', 'GET', 'DELETE'],
|
|
312
350
|
}),
|
|
313
351
|
)
|
|
352
|
+
@DBOSKoa.defaultArgRequired
|
|
314
353
|
class TestEndpoints {
|
|
315
354
|
@dhttp.getApi('/hello')
|
|
316
355
|
static async hello() {
|
|
@@ -339,12 +378,6 @@ describe('httpserver-tests', () => {
|
|
|
339
378
|
return Promise.resolve(url);
|
|
340
379
|
}
|
|
341
380
|
|
|
342
|
-
@dhttp.getApi('/dbos-error')
|
|
343
|
-
@DBOS.workflow()
|
|
344
|
-
static async dbosErr() {
|
|
345
|
-
return Promise.reject(new Error('customize error'));
|
|
346
|
-
}
|
|
347
|
-
|
|
348
381
|
@dhttp.getApi('/query')
|
|
349
382
|
static async helloQuery(name: string) {
|
|
350
383
|
DBOS.logger.info(`query with name ${name}`); // Test logging.
|
|
@@ -390,6 +423,12 @@ describe('httpserver-tests', () => {
|
|
|
390
423
|
return Promise.resolve(`hello ${name}`);
|
|
391
424
|
}
|
|
392
425
|
|
|
426
|
+
@dhttp.getApi('/dbos-error')
|
|
427
|
+
@DBOS.transaction()
|
|
428
|
+
static async dbosErr() {
|
|
429
|
+
return Promise.reject(new DBOSResponseError('customize error', 503));
|
|
430
|
+
}
|
|
431
|
+
|
|
393
432
|
@dhttp.getApi('/handler/:name')
|
|
394
433
|
static async testHandler(name: string) {
|
|
395
434
|
const workflowID: string = DBOSKoa.koaContext.get(WorkflowIDHeader);
|
|
@@ -411,6 +450,16 @@ describe('httpserver-tests', () => {
|
|
|
411
450
|
return await TestEndpoints.testWorkflow(name);
|
|
412
451
|
}
|
|
413
452
|
|
|
453
|
+
@dhttp.postApi('/transaction/:name')
|
|
454
|
+
@DBOS.transaction()
|
|
455
|
+
static async testTransaction(name: string) {
|
|
456
|
+
const { rows } = await DBOS.pgClient.query<TestKvTable>(
|
|
457
|
+
`INSERT INTO ${testTableName}(id, value) VALUES (1, $1) RETURNING id`,
|
|
458
|
+
[name],
|
|
459
|
+
);
|
|
460
|
+
return `hello ${rows[0].id}`;
|
|
461
|
+
}
|
|
462
|
+
|
|
414
463
|
@dhttp.getApi('/step/:input')
|
|
415
464
|
@DBOS.step()
|
|
416
465
|
static async testStep(input: string) {
|
|
@@ -420,27 +469,30 @@ describe('httpserver-tests', () => {
|
|
|
420
469
|
@dhttp.postApi('/workflow')
|
|
421
470
|
@DBOS.workflow()
|
|
422
471
|
static async testWorkflow(name: string) {
|
|
423
|
-
|
|
472
|
+
const res = await TestEndpoints.testTransaction(name);
|
|
473
|
+
return TestEndpoints.testStep(res);
|
|
424
474
|
}
|
|
425
475
|
|
|
426
476
|
@dhttp.postApi('/error')
|
|
427
477
|
@DBOS.workflow()
|
|
428
478
|
static async testWorkflowError(name: string) {
|
|
429
|
-
|
|
430
|
-
|
|
479
|
+
// This workflow should encounter duplicate primary key error.
|
|
480
|
+
let res = await TestEndpoints.testTransaction(name);
|
|
481
|
+
res = await TestEndpoints.testTransaction(name);
|
|
482
|
+
return res;
|
|
431
483
|
}
|
|
432
484
|
|
|
433
485
|
@dhttp.getApi('/requireduser')
|
|
434
486
|
@DBOS.requiredRole(['user'])
|
|
435
487
|
static async testAuth(name: string) {
|
|
436
488
|
if (DBOS.authenticatedUser !== 'a_real_user') {
|
|
437
|
-
throw new
|
|
489
|
+
throw new DBOSResponseError('uid not a real user!', 400);
|
|
438
490
|
}
|
|
439
491
|
if (!DBOS.authenticatedRoles.includes('user')) {
|
|
440
|
-
throw new
|
|
492
|
+
throw new DBOSResponseError("roles don't include user!", 400);
|
|
441
493
|
}
|
|
442
494
|
if (DBOS.assumedRole !== 'user') {
|
|
443
|
-
throw new
|
|
495
|
+
throw new DBOSResponseError('Should never happen! Not assumed to be user', 400);
|
|
444
496
|
}
|
|
445
497
|
return Promise.resolve(`Please say hello to ${name}`);
|
|
446
498
|
}
|
|
@@ -449,13 +501,13 @@ describe('httpserver-tests', () => {
|
|
|
449
501
|
@DBOS.requiredRole(['user'])
|
|
450
502
|
static async testAuth2(name: string) {
|
|
451
503
|
if (DBOS.authenticatedUser !== 'a_real_user') {
|
|
452
|
-
throw new
|
|
504
|
+
throw new DBOSResponseError('uid not a real user!', 400);
|
|
453
505
|
}
|
|
454
506
|
if (!DBOS.authenticatedRoles.includes('user')) {
|
|
455
|
-
throw new
|
|
507
|
+
throw new DBOSResponseError("roles don't include user!", 400);
|
|
456
508
|
}
|
|
457
509
|
if (DBOS.assumedRole !== 'user') {
|
|
458
|
-
throw new
|
|
510
|
+
throw new DBOSResponseError('Should never happen! Not assumed to be user', 400);
|
|
459
511
|
}
|
|
460
512
|
return Promise.resolve(`Please say hello to ${name}`);
|
|
461
513
|
}
|
package/tests/steps.test.ts
CHANGED
|
@@ -56,9 +56,14 @@ describe('registerstep', () => {
|
|
|
56
56
|
expect(response1.statusCode).toBe(200);
|
|
57
57
|
const response2 = await request(app.callback()).get('/api/i2?user=jeremy');
|
|
58
58
|
expect(response2.statusCode).toBe(200);
|
|
59
|
+
const response3 = await request(app.callback()).get('/api/i1');
|
|
60
|
+
expect(response3.statusCode).toBe(400);
|
|
61
|
+
const response4 = await request(app.callback()).get('/api/i2');
|
|
62
|
+
expect(response4.statusCode).toBe(400);
|
|
59
63
|
});
|
|
60
64
|
});
|
|
61
65
|
|
|
66
|
+
@DBOSKoa.defaultArgValidate
|
|
62
67
|
class KnexKoa {
|
|
63
68
|
@customstep()
|
|
64
69
|
@dhttp.getApi('/api/i2')
|
|
@@ -77,9 +77,14 @@ describe('KnexDataSource', () => {
|
|
|
77
77
|
expect(response1.statusCode).toBe(200);
|
|
78
78
|
const response2 = await request(app.callback()).get('/api/i2?user=jeremy');
|
|
79
79
|
expect(response2.statusCode).toBe(200);
|
|
80
|
+
const response3 = await request(app.callback()).get('/api/i1');
|
|
81
|
+
expect(response3.statusCode).toBe(400);
|
|
82
|
+
const response4 = await request(app.callback()).get('/api/i2');
|
|
83
|
+
expect(response4.statusCode).toBe(400);
|
|
80
84
|
});
|
|
81
85
|
});
|
|
82
86
|
|
|
87
|
+
@DBOSKoa.defaultArgValidate
|
|
83
88
|
class KnexKoa {
|
|
84
89
|
@knexds.transaction()
|
|
85
90
|
@dhttp.getApi('/api/i2')
|