@ossy/users 1.8.0 → 1.9.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ossy/users",
3
3
  "description": "User domain — aggregate, events, and validators for the Ossy user model",
4
- "version": "1.8.0",
4
+ "version": "1.9.1",
5
5
  "private": false,
6
6
  "type": "module",
7
7
  "main": "./src/index.js",
@@ -9,6 +9,9 @@
9
9
  "exports": {
10
10
  ".": "./src/index.js"
11
11
  },
12
+ "scripts": {
13
+ "test": "NODE_OPTIONS=--experimental-vm-modules jest --verbose"
14
+ },
12
15
  "author": "Ossy <yourfriends@ossy.se> (https://ossy.se)",
13
16
  "license": "MIT",
14
17
  "ossy": {
@@ -19,12 +22,18 @@
19
22
  "registry": "https://registry.npmjs.org"
20
23
  },
21
24
  "dependencies": {
22
- "@ossy/observability": "^1.3.0",
25
+ "@ossy/observability": "^1.4.1",
23
26
  "nanoid": "^5.1.11"
24
27
  },
28
+ "devDependencies": {
29
+ "@jest/globals": "^30.2.0",
30
+ "@ossy/platform": "^1.35.1",
31
+ "casual": "^1.6.2",
32
+ "jest": "^30.2.0"
33
+ },
25
34
  "files": [
26
35
  "/src",
27
36
  "README.md"
28
37
  ],
29
- "gitHead": "c6697078268867d88553ca0bac08faad5cea1546"
38
+ "gitHead": "a144d7767264d96bc6ae095784d4f884f03a89a0"
30
39
  }
package/src/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from './user.aggregate.js'
2
2
  export * from './users.events.js'
3
3
  export * from './users.validators.js'
4
+ export * from './users.queries.js'
@@ -0,0 +1,52 @@
1
+ import { EventStore, Aggregate } from '@ossy/event-store'
2
+ import { createLogger } from '@ossy/observability'
3
+
4
+ const log = createLogger('users')
5
+
6
+ /**
7
+ * Database queries related to user aggregates.
8
+ * @class
9
+ */
10
+ export class UsersQueries {
11
+
12
+ static getByEmail(email) {
13
+ log.debug(`[UsersQueries][getByEmail()] Fetch user for ${email}`)
14
+
15
+ return Aggregate.Collection.findOne({
16
+ type: 'User',
17
+ 'state.email': email
18
+ }).then(aggregate => aggregate?.state)
19
+ .then(user => {
20
+ !!user
21
+ ? log.debug('[UsersQueries][getByEmail()] Found user', { user })
22
+ : log.debug('[UsersQueries][getByEmail()] No user found')
23
+
24
+ return user
25
+ })
26
+ }
27
+
28
+ static getIdByVerificationToken(token) {
29
+ log.info('[UsersQueries] Using verificationToken to fetch userId')
30
+
31
+ return EventStore.FindEvent({
32
+ aggregateType: 'User',
33
+ type: { $in: [ 'SignedUp', 'SignInRequested' ] },
34
+ 'payload.verificationToken': token
35
+ }).then(event => event.aggregateId)
36
+ }
37
+
38
+ static getIdByAuthToken(token) {
39
+ log.info('[UsersQueries] Using authToken to fetch userId')
40
+ log.debug('[UsersQueries] token', { token })
41
+
42
+ return EventStore.FindEvent({
43
+ aggregateType: 'User',
44
+ type: { $in: [ 'SignInVerified' ] },
45
+ 'payload.token': token
46
+ }).then(event => {
47
+ log.debug('[UsersQueries] Found', { event })
48
+ return event.aggregateId
49
+ })
50
+ }
51
+
52
+ }
@@ -0,0 +1,681 @@
1
+ import casual from 'casual'
2
+ import { TestUtil, getApiTestBaseUrl, getSetCookieHeader } from '@ossy/platform/test'
3
+
4
+ const isUUID = x =>
5
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
6
+ .test(x)
7
+
8
+ const invalidEmails = [
9
+ undefined,
10
+ '',
11
+ 2,
12
+ 'notanemail',
13
+ 'not@anemail',
14
+ '3',
15
+ false,
16
+ true,
17
+ { email: casual.email }
18
+ ]
19
+
20
+ const invalidVerifySignInCases = [
21
+ { label: 'notatoken', query: 'token=notatoken', status: 401, body: '' },
22
+ { label: 'numeric string', query: 'token=2', status: 401, body: '' },
23
+ ]
24
+
25
+ function invalidSignUpBody(invalidEmail) {
26
+ const o = { firstName: 'Test', lastName: 'User' }
27
+ if (invalidEmail !== undefined) o.email = invalidEmail
28
+ return JSON.stringify(o)
29
+ }
30
+
31
+ describe('[/users/sign-up][POST]', () => {
32
+
33
+ describe('given an email that is not already in use', () => {
34
+
35
+ it('must return OK 200', () => TestUtil.AssertResponse({
36
+ endpoint: '/users/sign-up',
37
+ method: 'POST',
38
+ headers: { 'Content-Type': 'application/json'},
39
+ body: TestUtil.signUpBody({ email: casual.email }),
40
+ expectedResponseStatus: 200,
41
+ expectedResponseBody: ''
42
+ }))
43
+
44
+ it('must produce a SignedUp event', async () => {
45
+
46
+ const email = casual.email
47
+
48
+ await TestUtil.AssertResponse({
49
+ endpoint: '/users/sign-up',
50
+ method: 'POST',
51
+ headers: { 'Content-Type': 'application/json'},
52
+ body: TestUtil.signUpBody({ email }),
53
+ expectedResponseStatus: 200,
54
+ expectedResponseBody: ''
55
+ })
56
+
57
+ await TestUtil.AssertEventExist({
58
+ aggregateType: 'User',
59
+ type: { $in: [ 'SignedUp' ] },
60
+ 'payload.email': email
61
+ })
62
+
63
+ })
64
+
65
+ })
66
+
67
+ describe('given an email that is already in use', () => {
68
+
69
+ it('must for sequerity reasons return OK 200', async () => {
70
+ const email = casual.email
71
+
72
+ const signUpTestRequest = {
73
+ endpoint: '/users/sign-up',
74
+ method: 'POST',
75
+ headers: { 'Content-Type': 'application/json'},
76
+ body: TestUtil.signUpBody({ email }),
77
+ expectedResponseStatus: 200,
78
+ expectedResponseBody: ''
79
+ }
80
+
81
+ await TestUtil.AssertResponse(signUpTestRequest)
82
+ await TestUtil.AssertResponse(signUpTestRequest)
83
+ })
84
+
85
+ it('must not produce another SignUp event', async () => {
86
+ const email = `dup-${Date.now()}-${casual.email}`
87
+
88
+ const signUpTestRequest = {
89
+ endpoint: '/users/sign-up',
90
+ method: 'POST',
91
+ headers: { 'Content-Type': 'application/json'},
92
+ body: TestUtil.signUpBody({ email }),
93
+ expectedResponseStatus: 200,
94
+ expectedResponseBody: ''
95
+ }
96
+
97
+ await TestUtil.AssertResponse(signUpTestRequest)
98
+ await TestUtil.AssertResponse(signUpTestRequest)
99
+
100
+ const signUpEvents = await TestUtil.GetEvents({
101
+ aggregateType: 'User',
102
+ type: { $in: [ 'SignedUp' ] },
103
+ 'payload.email': email
104
+ })
105
+
106
+ expect(signUpEvents.length).toEqual(1)
107
+ })
108
+
109
+ })
110
+
111
+ describe('given an invalid email', () => {
112
+
113
+ invalidEmails.forEach(invalidEmail => {
114
+ it(
115
+ `must return BAD REQUEST 400 for invalid email: ${invalidEmail}`,
116
+ () => TestUtil.AssertResponse({
117
+ endpoint: '/users/sign-up',
118
+ method: 'POST',
119
+ headers: { 'Content-Type': 'application/json'},
120
+ body: invalidSignUpBody(invalidEmail),
121
+ expectedResponseStatus: 400,
122
+ expectedResponseBody: 'Invalid email'
123
+ })
124
+ )
125
+ })
126
+ })
127
+
128
+ })
129
+
130
+ describe('[/users/sign-in][POST]', () => {
131
+
132
+ describe('given an email that is in use', () => {
133
+
134
+ it('must return an OK 200 response', async () => {
135
+ const email = casual.email
136
+
137
+ await TestUtil.AssertResponse({
138
+ endpoint: '/users/sign-up',
139
+ description: '',
140
+ method: 'POST',
141
+ headers: { 'Content-Type': 'application/json'},
142
+ body: TestUtil.signUpBody({ email }),
143
+ expectedResponseStatus: 200,
144
+ expectedResponseBody: ''
145
+ })
146
+
147
+ await TestUtil.AssertResponse({
148
+ endpoint: '/users/sign-in',
149
+ description: '',
150
+ method: 'POST',
151
+ headers: { 'Content-Type': 'application/json'},
152
+ body: JSON.stringify({ email }),
153
+ expectedResponseStatus: 200,
154
+ expectedResponseBody: ''
155
+ })
156
+
157
+ })
158
+
159
+ it('must create a Verification token for sign-in (Token aggregate)', async () => {
160
+
161
+ const email = casual.email
162
+
163
+ await TestUtil.AssertResponse({
164
+ endpoint: '/users/sign-up',
165
+ description: '',
166
+ method: 'POST',
167
+ headers: { 'Content-Type': 'application/json'},
168
+ body: TestUtil.signUpBody({ email }),
169
+ expectedResponseStatus: 200,
170
+ expectedResponseBody: ''
171
+ })
172
+
173
+ await TestUtil.AssertResponse({
174
+ endpoint: '/users/sign-in',
175
+ description: '',
176
+ method: 'POST',
177
+ headers: { 'Content-Type': 'application/json'},
178
+ body: JSON.stringify({ email }),
179
+ expectedResponseStatus: 200,
180
+ expectedResponseBody: ''
181
+ })
182
+
183
+ const signedUpEvent = await TestUtil.GetEvent({
184
+ aggregateType: 'User',
185
+ type: { $in: [ 'SignedUp' ] },
186
+ 'payload.email': email
187
+ })
188
+
189
+ const jwt = await TestUtil.getLatestVerificationJwtForSubject(signedUpEvent.aggregateId)
190
+ expect(typeof jwt).toBe('string')
191
+ })
192
+
193
+ })
194
+
195
+ describe('given an email that is not in use', () => {
196
+
197
+ it('must, for security reasons, return an OK 200 response', async () => {
198
+ await TestUtil.AssertResponse({
199
+ endpoint: '/users/sign-in',
200
+ description: '',
201
+ method: 'POST',
202
+ headers: { 'Content-Type': 'application/json'},
203
+ body: JSON.stringify({ email: casual.email }),
204
+ expectedResponseStatus: 200,
205
+ expectedResponseBody: ''
206
+ })
207
+ })
208
+
209
+ it('must not create a Verification token for an unknown user', async () => {
210
+
211
+ const verificationTokensBefore = await TestUtil.countVerificationTokenEvents()
212
+
213
+ await TestUtil.AssertResponse({
214
+ endpoint: '/users/sign-in',
215
+ description: '',
216
+ method: 'POST',
217
+ headers: { 'Content-Type': 'application/json'},
218
+ body: JSON.stringify({ email: casual.email }),
219
+ expectedResponseStatus: 200,
220
+ expectedResponseBody: ''
221
+ })
222
+
223
+ const verificationTokensAfter = await TestUtil.countVerificationTokenEvents()
224
+
225
+ expect(verificationTokensAfter).toEqual(verificationTokensBefore)
226
+
227
+ })
228
+
229
+ })
230
+
231
+ describe('given an invalid email', () => {
232
+
233
+ invalidEmails.forEach(invalidEmail => {
234
+ it(
235
+ `must return BAD REQUEST 400 for invalid email: ${invalidEmail}`,
236
+ () => TestUtil.AssertResponse({
237
+ endpoint: '/users/sign-in',
238
+ method: 'POST',
239
+ headers: { 'Content-Type': 'application/json'},
240
+ body: JSON.stringify({ email: invalidEmail }),
241
+ expectedResponseStatus: 400,
242
+ expectedResponseBody: { message: 'No email provided' }
243
+ })
244
+ )
245
+ })
246
+ })
247
+
248
+ })
249
+
250
+ describe('[/users/verify-sign-in][GET]', () => {
251
+
252
+ describe('given a valid token created from a SignedUp event', () => {
253
+ it('must return OK 200 and an auth cookie and must produce a SignInVerified event', async () => {
254
+ const email = casual.email
255
+
256
+ await TestUtil.AssertResponse({
257
+ endpoint: '/users/sign-up',
258
+ method: 'POST',
259
+ headers: { 'Content-Type': 'application/json'},
260
+ body: TestUtil.signUpBody({ email }),
261
+ expectedResponseStatus: 200,
262
+ expectedResponseBody: ''
263
+ })
264
+
265
+ const signedUpEvent = await TestUtil.GetEvent({
266
+ aggregateType: 'User',
267
+ type: { $in: [ 'SignedUp' ] },
268
+ 'payload.email': email
269
+ })
270
+
271
+ const verificationJwt = await TestUtil.getLatestVerificationJwtForSubject(signedUpEvent.aggregateId)
272
+
273
+ const response = await fetch(
274
+ `${getApiTestBaseUrl()}/users/verify-sign-in?token=${verificationJwt}`,
275
+ { method: 'GET' }
276
+ )
277
+
278
+ const body = await response.json()
279
+
280
+ const signInVerifiedEvent = await TestUtil.GetEvent({
281
+ aggregateType: 'User',
282
+ aggregateId: signedUpEvent.aggregateId,
283
+ type: { $in: [ 'SignInVerified' ] },
284
+ })
285
+
286
+ expect(body).toBe('')
287
+ expect(!!signInVerifiedEvent).toBe(true)
288
+ expect(response.status).toBe(200)
289
+ expect(getSetCookieHeader(response).includes(signInVerifiedEvent.payload.token)).toEqual(true)
290
+ })
291
+ })
292
+
293
+ describe('given a valid token created from a SignInRequested event', () => {
294
+ it('must return OK 200 and an auth cookie and must produce a SignInVerified event', async () => {
295
+ const email = casual.email
296
+
297
+ await TestUtil.AssertResponse({
298
+ endpoint: '/users/sign-up',
299
+ method: 'POST',
300
+ headers: { 'Content-Type': 'application/json'},
301
+ body: TestUtil.signUpBody({ email }),
302
+ expectedResponseStatus: 200,
303
+ expectedResponseBody: ''
304
+ })
305
+
306
+ await TestUtil.AssertResponse({
307
+ endpoint: '/users/sign-in',
308
+ method: 'POST',
309
+ headers: { 'Content-Type': 'application/json'},
310
+ body: JSON.stringify({ email }),
311
+ expectedResponseStatus: 200,
312
+ expectedResponseBody: ''
313
+ })
314
+
315
+ const signedUpEvent = await TestUtil.GetEvent({
316
+ aggregateType: 'User',
317
+ type: { $in: [ 'SignedUp' ] },
318
+ 'payload.email': email
319
+ })
320
+
321
+ const verificationJwt = await TestUtil.getLatestVerificationJwtForSubject(signedUpEvent.aggregateId)
322
+
323
+ const response = await fetch(
324
+ `${getApiTestBaseUrl()}/users/verify-sign-in?token=${verificationJwt}`,
325
+ { method: 'GET' }
326
+ )
327
+
328
+ const body = await response.json()
329
+
330
+ const signInVerifiedEvent = await TestUtil.GetEvent({
331
+ aggregateType: 'User',
332
+ aggregateId: signedUpEvent.aggregateId,
333
+ type: { $in: [ 'SignInVerified' ] },
334
+ })
335
+
336
+ expect(body).toBe('')
337
+ expect(!!signInVerifiedEvent).toBe(true)
338
+ expect(response.status).toBe(200)
339
+ expect(getSetCookieHeader(response).includes(signInVerifiedEvent.payload.token)).toEqual(true)
340
+ })
341
+ })
342
+
343
+ describe('given an invalid token', () => {
344
+
345
+ it('must return 400 when token query param is missing', () => TestUtil.AssertResponse({
346
+ endpoint: '/users/verify-sign-in',
347
+ method: 'GET',
348
+ expectedResponseStatus: 400,
349
+ expectedResponseBody: { message: 'No token provided' },
350
+ }))
351
+
352
+ it('must return 400 when token is empty string', () => TestUtil.AssertResponse({
353
+ endpoint: '/users/verify-sign-in?token=',
354
+ method: 'GET',
355
+ expectedResponseStatus: 400,
356
+ expectedResponseBody: { message: 'No token provided' },
357
+ }))
358
+
359
+ invalidVerifySignInCases.forEach(({ label, query, status, body }) => {
360
+ it(`must return ${status} for invalid token: ${label}`, () => TestUtil.AssertResponse({
361
+ endpoint: `/users/verify-sign-in?${query}`,
362
+ method: 'GET',
363
+ expectedResponseStatus: status,
364
+ expectedResponseBody: body,
365
+ }))
366
+ })
367
+
368
+ })
369
+
370
+ })
371
+
372
+ describe('[/users/me][GET]', () => {
373
+
374
+ TestUtil.AssertAuthenticationNeeded({
375
+ endpoint: '/users/me',
376
+ method: 'GET',
377
+ headers: { 'Content-Type': 'application/json' },
378
+ })
379
+
380
+ describe('given an auth token', () => {
381
+ it('must return OK 200 with a view of the current user state', async () => {
382
+ const user = await TestUtil.GetAuthenticatedTestUser()
383
+
384
+ const response = await TestUtil.MakeRequest({
385
+ endpoint: '/users/me',
386
+ method: 'GET',
387
+ headers: { 'Content-Type': 'application/json', 'Authorization': user.token }
388
+ })
389
+
390
+ const body = await response.json()
391
+
392
+ expect(response.status).toEqual(200)
393
+ expect(response.headers.get('Content-Type').includes('application/json')).toEqual(true)
394
+
395
+ expect(body).toEqual(
396
+ expect.objectContaining({
397
+ id: expect.stringMatching(/.../),
398
+ email: user.email,
399
+ created: expect.any(Number)
400
+ })
401
+ )
402
+
403
+ })
404
+ })
405
+ })
406
+
407
+ describe('[/users/me/history][GET]', () => {
408
+
409
+ TestUtil.AssertAuthenticationNeeded({
410
+ endpoint: '/users/me/history',
411
+ method: 'GET',
412
+ headers: { 'Content-Type': 'application/json' },
413
+ })
414
+
415
+ describe('given an auth token', () => {
416
+ it('must return OK 200 with a view of the history of the current user', async () => {
417
+ const user = await TestUtil.GetAuthenticatedTestUser()
418
+
419
+ const response = await TestUtil.MakeRequest({
420
+ endpoint: '/users/me/history',
421
+ method: 'GET',
422
+ headers: { 'Content-Type': 'application/json', 'Authorization': user.token }
423
+ })
424
+
425
+ const body = await response.json()
426
+
427
+ expect(response.status).toEqual(200)
428
+ expect(response.headers.get('Content-Type').includes('application/json')).toEqual(true)
429
+
430
+ expect(body.map(x => x.type)).toEqual([
431
+ 'SignInVerified',
432
+ 'SignedUp'
433
+ ])
434
+
435
+ })
436
+ })
437
+ })
438
+
439
+ describe('[/users/me/tokens][POST]', () => {
440
+
441
+ TestUtil.AssertAuthenticationNeeded({
442
+ endpoint: '/users/me/tokens',
443
+ method: 'POST',
444
+ headers: { 'Content-Type': 'application/json' },
445
+ body: JSON.stringify({ name: 't', description: 'd' })
446
+ })
447
+
448
+ describe('given a valid auth token', () => {
449
+ it('must return OK 200 with an API token and produce an ApiTokenCreated event', async () => {
450
+ const user = await TestUtil.GetAuthenticatedTestUser()
451
+
452
+ const response = await TestUtil.MakeRequest({
453
+ endpoint: '/users/me/tokens',
454
+ method: 'POST',
455
+ headers: { 'Content-Type': 'application/json', 'Authorization': user.token },
456
+ body: JSON.stringify({ name: 'cli', description: casual.sentence })
457
+ })
458
+
459
+ const responseBody = await response.json()
460
+
461
+ const event = await TestUtil.getLatestApiTokenCreatedEventForSubject(user.id)
462
+
463
+ expect(response.status).toEqual(200)
464
+ expect(response.headers.get('Content-Type').includes('application/json')).toEqual(true)
465
+ expect(event.createdBy).toEqual(user.id)
466
+ expect(responseBody).toEqual(
467
+ expect.objectContaining({
468
+ description: event.payload.description,
469
+ token: event.payload.token,
470
+ created: event.created,
471
+ id: event.aggregateId,
472
+ type: 'Api',
473
+ name: 'cli',
474
+ })
475
+ )
476
+
477
+ })
478
+ })
479
+
480
+ describe('given an invalid description', () => {
481
+ ['', {}, [], undefined].forEach(invalidDescription => {
482
+ it(`must return 400 and produce no event for invalid description: ${invalidDescription}`, async () => {
483
+ const user = await TestUtil.GetAuthenticatedTestUser()
484
+
485
+ let payload
486
+ if (invalidDescription === undefined) payload = { name: 'token' }
487
+ else if (invalidDescription === '') payload = { name: 'token', description: '' }
488
+ else payload = invalidDescription
489
+
490
+ const response = await TestUtil.MakeRequest({
491
+ endpoint: '/users/me/tokens',
492
+ method: 'POST',
493
+ headers: { 'Content-Type': 'application/json', 'Authorization': user.token },
494
+ body: JSON.stringify(payload)
495
+ })
496
+
497
+ const responseBody = await response.json()
498
+
499
+ const event = TestUtil.GetEvent({
500
+ aggregateId: user.id,
501
+ aggregateType: 'User',
502
+ type: 'ApiTokenCreated'
503
+ })
504
+
505
+ expect(response.status).toEqual(400)
506
+ expect(response.headers.get('Content-Type').includes('application/json')).toEqual(true)
507
+ expect(responseBody).toEqual('')
508
+ await expect(event).rejects.toEqual(undefined)
509
+ })
510
+ })
511
+ })
512
+
513
+ })
514
+
515
+ describe('[/users/me/tokens][GET]', () => {
516
+
517
+ TestUtil.AssertAuthenticationNeeded({
518
+ endpoint: '/users/me/tokens',
519
+ method: 'GET',
520
+ headers: { 'Content-Type': 'application/json' },
521
+ })
522
+
523
+ describe('given authentication token is provided', () => {
524
+ it('must return OK 200 and with a list of token meta data (not actual tokens)', async () => {
525
+
526
+ const user = await TestUtil.GetAuthenticatedTestUser()
527
+
528
+ const token = await TestUtil.MakeRequest({
529
+ endpoint: '/users/me/tokens',
530
+ method: 'POST',
531
+ headers: { 'Content-Type': 'application/json', 'Authorization': user.token },
532
+ body: JSON.stringify({ name: 'list-test', description: casual.sentence })
533
+ }).then(response => response.json())
534
+
535
+ const response = await TestUtil.MakeRequest({
536
+ endpoint: '/users/me/tokens',
537
+ method: 'GET',
538
+ headers: { 'Content-Type': 'application/json', 'Authorization': user.token }
539
+ })
540
+
541
+ const responseBody = await response.json()
542
+
543
+ const expectedToken = {
544
+ description: token.description,
545
+ id: token.id,
546
+ created: token.created,
547
+ type: 'Api',
548
+ }
549
+
550
+ expect(response.status).toEqual(200)
551
+ expect(response.headers.get('Content-Type').includes('application/json')).toEqual(true)
552
+ expect(responseBody.length).toBeGreaterThanOrEqual(1)
553
+ expect(responseBody.every(t => t.token === undefined)).toBe(true)
554
+ expect(responseBody).toEqual(
555
+ expect.arrayContaining([expect.objectContaining(expectedToken)])
556
+ )
557
+ })
558
+ })
559
+
560
+ })
561
+
562
+ const createApiToken = async (user) => {
563
+ const token = await TestUtil.MakeRequest({
564
+ endpoint: '/users/me/tokens',
565
+ method: 'POST',
566
+ headers: { 'Content-Type': 'application/json', 'Authorization': user.token },
567
+ body: JSON.stringify({ name: 'delete-test', description: casual.sentence })
568
+ }).then(response => response.json())
569
+
570
+ return token
571
+ }
572
+
573
+ describe('[/users/me/tokens/:tokenId][DELETE]', () => {
574
+
575
+ TestUtil.AssertAuthenticationNeeded({
576
+ endpoint: `/users/me/tokens/123`,
577
+ method: 'DELETE',
578
+ headers: { 'Content-Type': 'application/json' },
579
+ })
580
+
581
+ describe('authentication with user token', () => {
582
+
583
+ it('must return OK 200 and produce an ApiTokenInvalidated event', async () => {
584
+ const user = await TestUtil.GetAuthenticatedTestUser()
585
+ const token = await createApiToken(user)
586
+
587
+ const response = await TestUtil.MakeRequest({
588
+ endpoint: `/users/me/tokens/${token.id}`,
589
+ method: 'DELETE',
590
+ headers: { 'Content-Type': 'application/json', 'Authorization': user.token }
591
+ })
592
+
593
+ const responseBody = await response.json()
594
+
595
+ const event = await TestUtil.GetEvent({
596
+ aggregateId: token.id,
597
+ aggregateType: 'Token',
598
+ type: 'Revoked',
599
+ })
600
+
601
+ expect(response.status).toEqual(200)
602
+ expect(response.headers.get('Content-Type').includes('application/json')).toEqual(true)
603
+ expect(responseBody).toEqual('')
604
+ expect(!!event).toBe(true)
605
+ })
606
+
607
+ it('must not show up en subsequent [GET] requests', async () => {
608
+ const user = await TestUtil.GetAuthenticatedTestUser()
609
+ const token = await createApiToken(user)
610
+
611
+ await TestUtil.MakeRequest({
612
+ endpoint: `/users/me/tokens/${token.id}`,
613
+ method: 'DELETE',
614
+ headers: { 'Content-Type': 'application/json', 'Authorization': user.token }
615
+ })
616
+
617
+ const response = await TestUtil.MakeRequest({
618
+ endpoint: '/users/me/tokens',
619
+ method: 'GET',
620
+ headers: { 'Content-Type': 'application/json', 'Authorization': user.token }
621
+ })
622
+
623
+ const responseBody = await response.json()
624
+
625
+ expect(response.status).toEqual(200)
626
+ expect(responseBody.map(t => t.id)).not.toContain(token.id)
627
+ })
628
+
629
+ })
630
+
631
+ describe('authentication with API token', () => {
632
+
633
+ it('must return OK 200 and produce an ApiTokenInvalidated event', async () => {
634
+ const user = await TestUtil.GetAuthenticatedTestUser()
635
+ const token = await createApiToken(user)
636
+
637
+ const response = await TestUtil.MakeRequest({
638
+ endpoint: `/users/me/tokens/${token.id}`,
639
+ method: 'DELETE',
640
+ headers: { 'Content-Type': 'application/json', 'Authorization': token.token }
641
+ })
642
+
643
+ const responseBody = await response.json()
644
+
645
+ const event = await TestUtil.GetEvent({
646
+ aggregateId: token.id,
647
+ aggregateType: 'Token',
648
+ type: 'Revoked',
649
+ })
650
+
651
+ expect(response.status).toEqual(200)
652
+ expect(response.headers.get('Content-Type').includes('application/json')).toEqual(true)
653
+ expect(responseBody).toEqual('')
654
+ expect(!!event).toBe(true)
655
+ })
656
+
657
+ it('must not be usable in subsequent API calls', async () => {
658
+ const user = await TestUtil.GetAuthenticatedTestUser()
659
+ const token = await createApiToken(user)
660
+
661
+ await TestUtil.MakeRequest({
662
+ endpoint: `/users/me/tokens/${token.id}`,
663
+ method: 'DELETE',
664
+ headers: { 'Content-Type': 'application/json', 'Authorization': token.token }
665
+ })
666
+
667
+ const response = await TestUtil.MakeRequest({
668
+ endpoint: '/users/me/tokens',
669
+ method: 'GET',
670
+ headers: { 'Content-Type': 'application/json', 'Authorization': token.token }
671
+ })
672
+
673
+ const responseBody = await response.json()
674
+
675
+ expect(response.status).toEqual(401)
676
+ expect(responseBody).toEqual('')
677
+ })
678
+
679
+ })
680
+
681
+ })