@dbos-inc/koa-serve 3.5.44-preview.gc094fdab44 → 3.6.5-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.
@@ -0,0 +1,150 @@
1
+ import Koa from 'koa';
2
+ import Router from '@koa/router';
3
+
4
+ import { DBOS } from '@dbos-inc/dbos-sdk';
5
+
6
+ import { ArgSources, DBOSKoa } from '../src';
7
+
8
+ import request from 'supertest';
9
+ import bodyParser from '@koa/bodyparser';
10
+
11
+ const dhttp = new DBOSKoa();
12
+
13
+ describe('httpserver-argsource-tests', () => {
14
+ let app: Koa;
15
+ let appRouter: Router;
16
+
17
+ beforeAll(async () => {
18
+ DBOS.setConfig({
19
+ name: 'dbos-koa-test',
20
+ });
21
+ return Promise.resolve();
22
+ });
23
+
24
+ beforeEach(async () => {
25
+ const _classes = [ArgTestEndpoints];
26
+ await DBOS.launch();
27
+ app = new Koa();
28
+ appRouter = new Router();
29
+ dhttp.registerWithApp(app, appRouter);
30
+ });
31
+
32
+ afterEach(async () => {
33
+ await DBOS.shutdown();
34
+ });
35
+
36
+ test('get-query', async () => {
37
+ const response1 = await request(app.callback()).get('/getquery?name=alice');
38
+ expect(response1.statusCode).toBe(200);
39
+ expect(response1.text).toBe('hello alice');
40
+ const response2 = await request(app.callback()).get('/getquery').send({ name: 'alice' });
41
+ expect(response2.statusCode).toBe(400);
42
+ });
43
+
44
+ test('get-body', async () => {
45
+ const response1 = await request(app.callback()).get('/getbody?name=alice');
46
+ expect(response1.statusCode).toBe(400);
47
+ const response2 = await request(app.callback()).get('/getbody').send({ name: 'alice' });
48
+ expect(response2.statusCode).toBe(200);
49
+ expect(response2.text).toBe('hello alice');
50
+ });
51
+
52
+ test('get-default', async () => {
53
+ const response1 = await request(app.callback()).get('/getdefault?name=alice');
54
+ expect(response1.statusCode).toBe(200);
55
+ expect(response1.text).toBe('hello alice');
56
+ const response2 = await request(app.callback()).get('/getdefault').send({ name: 'alice' });
57
+ expect(response2.statusCode).toBe(400);
58
+ });
59
+
60
+ test('get-auto', async () => {
61
+ const response1 = await request(app.callback()).get('/getauto?name=alice');
62
+ expect(response1.statusCode).toBe(200);
63
+ expect(response1.text).toBe('hello alice');
64
+ const response2 = await request(app.callback()).get('/getauto').send({ name: 'alice' });
65
+ expect(response2.statusCode).toBe(200);
66
+ expect(response2.text).toBe('hello alice');
67
+ });
68
+
69
+ test('post-query', async () => {
70
+ const response1 = await request(app.callback()).post('/postquery?name=alice');
71
+ expect(response1.statusCode).toBe(200);
72
+ expect(response1.text).toBe('hello alice');
73
+ const response2 = await request(app.callback()).post('/postquery').send({ name: 'alice' });
74
+ expect(response2.statusCode).toBe(400);
75
+ });
76
+
77
+ test('post-body', async () => {
78
+ const response1 = await request(app.callback()).post('/postbody?name=alice');
79
+ expect(response1.statusCode).toBe(400);
80
+ const response2 = await request(app.callback()).post('/postbody').send({ name: 'alice' });
81
+ expect(response2.statusCode).toBe(200);
82
+ expect(response2.text).toBe('hello alice');
83
+ });
84
+
85
+ test('post-default', async () => {
86
+ const response1 = await request(app.callback()).post('/postdefault?name=alice');
87
+ expect(response1.statusCode).toBe(400);
88
+ const response2 = await request(app.callback()).post('/postdefault').send({ name: 'alice' });
89
+ expect(response2.statusCode).toBe(200);
90
+ expect(response2.text).toBe('hello alice');
91
+ });
92
+
93
+ test('post-auto', async () => {
94
+ const response1 = await request(app.callback()).post('/postauto?name=alice');
95
+ expect(response1.statusCode).toBe(200);
96
+ expect(response1.text).toBe('hello alice');
97
+ const response2 = await request(app.callback()).post('/postauto').send({ name: 'alice' });
98
+ expect(response2.statusCode).toBe(200);
99
+ expect(response2.text).toBe('hello alice');
100
+ });
101
+
102
+ @dhttp.koaBodyParser(
103
+ bodyParser({
104
+ enableTypes: ['json'],
105
+ parsedMethods: ['GET', 'POST'],
106
+ }),
107
+ )
108
+ @DBOSKoa.defaultArgRequired
109
+ class ArgTestEndpoints {
110
+ @dhttp.getApi('/getquery')
111
+ static async getQuery(@DBOSKoa.argSource(ArgSources.QUERY) name: string) {
112
+ return Promise.resolve(`hello ${name}`);
113
+ }
114
+
115
+ @dhttp.getApi('/getbody')
116
+ static async getBody(@DBOSKoa.argSource(ArgSources.BODY) name: string) {
117
+ return Promise.resolve(`hello ${name}`);
118
+ }
119
+
120
+ @dhttp.getApi('/getdefault')
121
+ static async getDefault(@DBOSKoa.argSource(ArgSources.DEFAULT) name: string) {
122
+ return Promise.resolve(`hello ${name}`);
123
+ }
124
+
125
+ @dhttp.getApi('/getauto')
126
+ static async getAuto(@DBOSKoa.argSource(ArgSources.AUTO) name: string) {
127
+ return Promise.resolve(`hello ${name}`);
128
+ }
129
+
130
+ @dhttp.postApi('/postquery')
131
+ static async postQuery(@DBOSKoa.argSource(ArgSources.QUERY) name: string) {
132
+ return Promise.resolve(`hello ${name}`);
133
+ }
134
+
135
+ @dhttp.postApi('/postbody')
136
+ static async postBody(@DBOSKoa.argSource(ArgSources.BODY) name: string) {
137
+ return Promise.resolve(`hello ${name}`);
138
+ }
139
+
140
+ @dhttp.postApi('/postdefault')
141
+ static async postDefault(@DBOSKoa.argSource(ArgSources.DEFAULT) name: string) {
142
+ return Promise.resolve(`hello ${name}`);
143
+ }
144
+
145
+ @dhttp.postApi('/postauto')
146
+ static async postAuto(@DBOSKoa.argSource(ArgSources.AUTO) name: string) {
147
+ return Promise.resolve(`hello ${name}`);
148
+ }
149
+ }
150
+ });
@@ -61,17 +61,17 @@ describe('httpserver-defsec-tests', () => {
61
61
 
62
62
  test('not-authenticated', async () => {
63
63
  const response = await request(app.callback()).get('/requireduser?name=alice');
64
- expect(response.statusCode).toBe(500);
64
+ expect(response.statusCode).toBe(401);
65
65
  });
66
66
 
67
67
  test('not-you', async () => {
68
68
  const response = await request(app.callback()).get('/requireduser?name=alice&userid=go_away');
69
- expect(response.statusCode).toBe(500);
69
+ expect(response.statusCode).toBe(401);
70
70
  });
71
71
 
72
72
  test('not-authorized', async () => {
73
73
  const response = await request(app.callback()).get('/requireduser?name=alice&userid=bob');
74
- expect(response.statusCode).toBe(500);
74
+ expect(response.statusCode).toBe(403);
75
75
  });
76
76
 
77
77
  test('authorized', async () => {
@@ -83,6 +83,22 @@ describe('httpserver-defsec-tests', () => {
83
83
  test('cascade-authorized', async () => {
84
84
  const response = await request(app.callback()).get('/workflow?name=alice&userid=a_real_user');
85
85
  expect(response.statusCode).toBe(200);
86
+
87
+ const txnResponse = await request(app.callback()).get('/transaction?name=alice&userid=a_real_user');
88
+ expect(txnResponse.statusCode).toBe(200);
89
+ });
90
+
91
+ // We can directly test a transaction with passed in authorizedRoles.
92
+ test('direct-transaction-test', async () => {
93
+ await DBOS.withAuthedContext('user', ['user'], async () => {
94
+ const res = await TestEndpointDefSec.testStep('alice');
95
+ expect(res).toBe('hello 1');
96
+ });
97
+
98
+ // Unauthorized.
99
+ await expect(TestEndpointDefSec.testStep('alice')).rejects.toThrow(
100
+ new DBOSError.DBOSNotAuthorizedError('User does not have a role with permission to call testStep', 403),
101
+ );
86
102
  });
87
103
 
88
104
  async function authTestMiddleware(ctx: DBOSKoaAuthContext) {
@@ -147,7 +163,8 @@ describe('httpserver-defsec-tests', () => {
147
163
 
148
164
  @DBOS.step()
149
165
  static async testStep(name: string) {
150
- return Promise.resolve(`hello ${name}`);
166
+ void name;
167
+ return Promise.resolve(`hello 1`);
151
168
  }
152
169
 
153
170
  @DBOS.workflow()
@@ -160,6 +177,11 @@ describe('httpserver-defsec-tests', () => {
160
177
  static async testWfEndpoint(name: string) {
161
178
  return await TestEndpointDefSec.testWorkflow(name);
162
179
  }
180
+
181
+ @dhttp.getApi('/transaction')
182
+ static async testTxnEndpoint(name: string) {
183
+ return await TestEndpointDefSec.testStep(name);
184
+ }
163
185
  }
164
186
 
165
187
  class SecondClass {
@@ -4,7 +4,7 @@ import Router from '@koa/router';
4
4
 
5
5
  import { DBOS, Error as DBOSErrors, StatusString } from '@dbos-inc/dbos-sdk';
6
6
 
7
- import { DBOSKoa, DBOSKoaAuthContext, RequestIDHeader, WorkflowIDHeader } from '../src';
7
+ import { DBOSKoa, DBOSKoaAuthContext, RequestIDHeader, WorkflowIDHeader, DBOSResponseError } from '../src';
8
8
 
9
9
  import request from 'supertest';
10
10
 
@@ -97,12 +97,17 @@ describe('httpserver-tests', () => {
97
97
  });
98
98
 
99
99
  test('post-test-custom-body', async () => {
100
- const response = await request(app.callback())
100
+ let response = await request(app.callback())
101
101
  .post('/testpost')
102
102
  .set('Content-Type', 'application/custom-content-type')
103
103
  .send(JSON.stringify({ name: 'alice' }));
104
104
  expect(response.statusCode).toBe(200);
105
105
  expect(response.text).toBe('hello alice');
106
+ response = await request(app.callback())
107
+ .post('/testpost')
108
+ .set('Content-Type', 'application/rejected-custom-content-type')
109
+ .send(JSON.stringify({ name: 'alice' }));
110
+ expect(response.statusCode).toBe(400);
106
111
  });
107
112
 
108
113
  test('put-test', async () => {
@@ -112,12 +117,17 @@ describe('httpserver-tests', () => {
112
117
  });
113
118
 
114
119
  test('put-test-custom-body', async () => {
115
- const response = await request(app.callback())
120
+ let response = await request(app.callback())
116
121
  .put('/testput')
117
122
  .set('Content-Type', 'application/custom-content-type')
118
123
  .send(JSON.stringify({ name: 'alice' }));
119
124
  expect(response.statusCode).toBe(200);
120
125
  expect(response.text).toBe('hello alice');
126
+ response = await request(app.callback())
127
+ .put('/testput')
128
+ .set('Content-Type', 'application/rejected-custom-content-type')
129
+ .send(JSON.stringify({ name: 'alice' }));
130
+ expect(response.statusCode).toBe(400);
121
131
  });
122
132
 
123
133
  test('patch-test', async () => {
@@ -127,12 +137,17 @@ describe('httpserver-tests', () => {
127
137
  });
128
138
 
129
139
  test('patch-test-custom-body', async () => {
130
- const response = await request(app.callback())
140
+ let response = await request(app.callback())
131
141
  .patch('/testpatch')
132
142
  .set('Content-Type', 'application/custom-content-type')
133
143
  .send(JSON.stringify({ name: 'alice' }));
134
144
  expect(response.statusCode).toBe(200);
135
145
  expect(response.text).toBe('hello alice');
146
+ response = await request(app.callback())
147
+ .patch('/testpatch')
148
+ .set('Content-Type', 'application/rejected-custom-content-type')
149
+ .send(JSON.stringify({ name: 'alice' }));
150
+ expect(response.statusCode).toBe(400);
136
151
  });
137
152
 
138
153
  test('endpoint-step', async () => {
@@ -144,7 +159,7 @@ describe('httpserver-tests', () => {
144
159
  test('endpoint-workflow', async () => {
145
160
  const response = await request(app.callback()).post('/workflow?name=alice');
146
161
  expect(response.statusCode).toBe(200);
147
- expect(response.text).toBe('alice');
162
+ expect(response.text).toBe('hello alice');
148
163
  });
149
164
 
150
165
  test('endpoint-error', async () => {
@@ -155,19 +170,19 @@ describe('httpserver-tests', () => {
155
170
  test('endpoint-handler', async () => {
156
171
  const response = await request(app.callback()).get('/handler/alice');
157
172
  expect(response.statusCode).toBe(200);
158
- expect(response.text).toBe('alice');
173
+ expect(response.text).toBe('hello alice');
159
174
  });
160
175
 
161
176
  test('endpoint-testStartWorkflow', async () => {
162
177
  const response = await request(app.callback()).get('/testStartWorkflow/alice');
163
178
  expect(response.statusCode).toBe(200);
164
- expect(response.text).toBe('alice');
179
+ expect(response.text).toBe('hello alice');
165
180
  });
166
181
 
167
182
  test('endpoint-testInvokeWorkflow', async () => {
168
183
  const response = await request(app.callback()).get('/testInvokeWorkflow/alice');
169
184
  expect(response.statusCode).toBe(200);
170
- expect(response.text).toBe('alice');
185
+ expect(response.text).toBe('hello alice');
171
186
  });
172
187
 
173
188
  // This feels unclean, but supertest doesn't expose the error message the people we want. See:
@@ -178,11 +193,17 @@ describe('httpserver-tests', () => {
178
193
 
179
194
  test('response-error', async () => {
180
195
  const response = await request(app.callback()).get('/dbos-error');
181
- expect(response.statusCode).toBe(500);
196
+ expect(response.statusCode).toBe(503);
182
197
  expect((response as unknown as Res).res.statusMessage).toBe('customize error');
183
198
  expect(response.body.message).toBe('customize error');
184
199
  });
185
200
 
201
+ test('datavalidation-error', async () => {
202
+ const response = await request(app.callback()).get('/query');
203
+ expect(response.statusCode).toBe(400);
204
+ expect(response.body.details.dbosErrorCode).toBe(9);
205
+ });
206
+
186
207
  test('dbos-redirect', async () => {
187
208
  const response = await request(app.callback()).get('/redirect');
188
209
  expect(response.statusCode).toBe(302);
@@ -210,17 +231,17 @@ describe('httpserver-tests', () => {
210
231
 
211
232
  test('not-authenticated', async () => {
212
233
  const response = await request(app.callback()).get('/requireduser?name=alice');
213
- expect(response.statusCode).toBe(500);
234
+ expect(response.statusCode).toBe(401);
214
235
  });
215
236
 
216
237
  test('not-you', async () => {
217
238
  const response = await request(app.callback()).get('/requireduser?name=alice&userid=go_away');
218
- expect(response.statusCode).toBe(500);
239
+ expect(response.statusCode).toBe(401);
219
240
  });
220
241
 
221
242
  test('not-authorized', async () => {
222
243
  const response = await request(app.callback()).get('/requireduser?name=alice&userid=bob');
223
- expect(response.statusCode).toBe(500);
244
+ expect(response.statusCode).toBe(403);
224
245
  });
225
246
 
226
247
  test('authorized', async () => {
@@ -230,17 +251,17 @@ describe('httpserver-tests', () => {
230
251
 
231
252
  test('not-authenticated2', async () => {
232
253
  const response = await request(app.callback()).get('/requireduser2?name=alice');
233
- expect(response.statusCode).toBe(500);
254
+ expect(response.statusCode).toBe(401);
234
255
  });
235
256
 
236
257
  test('not-you2', async () => {
237
258
  const response = await request(app.callback()).get('/requireduser2?name=alice&userid=go_away');
238
- expect(response.statusCode).toBe(500);
259
+ expect(response.statusCode).toBe(401);
239
260
  });
240
261
 
241
262
  test('not-authorized2', async () => {
242
263
  const response = await request(app.callback()).get('/requireduser2?name=alice&userid=bob');
243
- expect(response.statusCode).toBe(500);
264
+ expect(response.statusCode).toBe(403);
244
265
  });
245
266
 
246
267
  test('authorized2', async () => {
@@ -254,12 +275,12 @@ describe('httpserver-tests', () => {
254
275
  .post('/workflow?name=bob')
255
276
  .set({ 'dbos-idempotency-key': workflowID });
256
277
  expect(response.statusCode).toBe(200);
257
- expect(response.text).toBe('bob');
278
+ expect(response.text).toBe('hello bob');
258
279
 
259
280
  // Retrieve the workflow with WFID.
260
281
  const retrievedHandle = DBOS.retrieveWorkflow(workflowID);
261
282
  expect(retrievedHandle).not.toBeNull();
262
- await expect(retrievedHandle.getResult()).resolves.toBe('bob');
283
+ await expect(retrievedHandle.getResult()).resolves.toBe('hello bob');
263
284
  await expect(retrievedHandle.getStatus()).resolves.toMatchObject({
264
285
  status: StatusString.SUCCESS,
265
286
  });
@@ -269,12 +290,12 @@ describe('httpserver-tests', () => {
269
290
  const workflowID = randomUUID();
270
291
  const response = await request(app.callback()).get('/handler/bob').set({ 'dbos-idempotency-key': workflowID });
271
292
  expect(response.statusCode).toBe(200);
272
- expect(response.text).toBe('bob');
293
+ expect(response.text).toBe('hello bob');
273
294
 
274
295
  // Retrieve the workflow with WFID.
275
296
  const retrievedHandle = DBOS.retrieveWorkflow(workflowID);
276
297
  expect(retrievedHandle).not.toBeNull();
277
- await expect(retrievedHandle.getResult()).resolves.toBe('bob');
298
+ await expect(retrievedHandle.getResult()).resolves.toBe('hello bob');
278
299
  await expect(retrievedHandle.getStatus()).resolves.toMatchObject({
279
300
  status: StatusString.SUCCESS,
280
301
  });
@@ -311,6 +332,7 @@ describe('httpserver-tests', () => {
311
332
  parsedMethods: ['POST', 'PUT', 'PATCH', 'GET', 'DELETE'],
312
333
  }),
313
334
  )
335
+ @DBOSKoa.defaultArgRequired
314
336
  class TestEndpoints {
315
337
  @dhttp.getApi('/hello')
316
338
  static async hello() {
@@ -339,12 +361,6 @@ describe('httpserver-tests', () => {
339
361
  return Promise.resolve(url);
340
362
  }
341
363
 
342
- @dhttp.getApi('/dbos-error')
343
- @DBOS.workflow()
344
- static async dbosErr() {
345
- return Promise.reject(new Error('customize error'));
346
- }
347
-
348
364
  @dhttp.getApi('/query')
349
365
  static async helloQuery(name: string) {
350
366
  DBOS.logger.info(`query with name ${name}`); // Test logging.
@@ -390,6 +406,12 @@ describe('httpserver-tests', () => {
390
406
  return Promise.resolve(`hello ${name}`);
391
407
  }
392
408
 
409
+ @dhttp.getApi('/dbos-error')
410
+ @DBOS.step()
411
+ static async dbosErr() {
412
+ return Promise.reject(new DBOSResponseError('customize error', 503));
413
+ }
414
+
393
415
  @dhttp.getApi('/handler/:name')
394
416
  static async testHandler(name: string) {
395
417
  const workflowID: string = DBOSKoa.koaContext.get(WorkflowIDHeader);
@@ -420,27 +442,29 @@ describe('httpserver-tests', () => {
420
442
  @dhttp.postApi('/workflow')
421
443
  @DBOS.workflow()
422
444
  static async testWorkflow(name: string) {
423
- return TestEndpoints.testStep(name);
445
+ return TestEndpoints.testStep(`hello ${name}`);
424
446
  }
425
447
 
426
448
  @dhttp.postApi('/error')
427
449
  @DBOS.workflow()
428
450
  static async testWorkflowError(name: string) {
429
- await Promise.resolve();
430
- throw Error(name);
451
+ void name;
452
+ // This workflow should encounter duplicate primary key error.
453
+ throw new Error('fail');
454
+ return Promise.resolve('');
431
455
  }
432
456
 
433
457
  @dhttp.getApi('/requireduser')
434
458
  @DBOS.requiredRole(['user'])
435
459
  static async testAuth(name: string) {
436
460
  if (DBOS.authenticatedUser !== 'a_real_user') {
437
- throw new Error('uid not a real user!');
461
+ throw new DBOSResponseError('uid not a real user!', 400);
438
462
  }
439
463
  if (!DBOS.authenticatedRoles.includes('user')) {
440
- throw new Error("roles don't include user!");
464
+ throw new DBOSResponseError("roles don't include user!", 400);
441
465
  }
442
466
  if (DBOS.assumedRole !== 'user') {
443
- throw new Error('Should never happen! Not assumed to be user');
467
+ throw new DBOSResponseError('Should never happen! Not assumed to be user', 400);
444
468
  }
445
469
  return Promise.resolve(`Please say hello to ${name}`);
446
470
  }
@@ -449,13 +473,13 @@ describe('httpserver-tests', () => {
449
473
  @DBOS.requiredRole(['user'])
450
474
  static async testAuth2(name: string) {
451
475
  if (DBOS.authenticatedUser !== 'a_real_user') {
452
- throw new Error('uid not a real user!');
476
+ throw new DBOSResponseError('uid not a real user!', 400);
453
477
  }
454
478
  if (!DBOS.authenticatedRoles.includes('user')) {
455
- throw new Error("roles don't include user!");
479
+ throw new DBOSResponseError("roles don't include user!", 400);
456
480
  }
457
481
  if (DBOS.assumedRole !== 'user') {
458
- throw new Error('Should never happen! Not assumed to be user');
482
+ throw new DBOSResponseError('Should never happen! Not assumed to be user', 400);
459
483
  }
460
484
  return Promise.resolve(`Please say hello to ${name}`);
461
485
  }
@@ -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')