@ossy/users 1.8.0 → 1.9.0
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 +12 -3
- package/src/index.js +1 -0
- package/src/users.queries.js +52 -0
- package/src/users.spec.js +681 -0
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.
|
|
4
|
+
"version": "1.9.0",
|
|
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.
|
|
25
|
+
"@ossy/observability": "^1.4.0",
|
|
23
26
|
"nanoid": "^5.1.11"
|
|
24
27
|
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@jest/globals": "^30.2.0",
|
|
30
|
+
"@ossy/platform": "^1.35.0",
|
|
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": "
|
|
38
|
+
"gitHead": "24df2bde0d5d8794c5a82b9e802a2594ca73a5d9"
|
|
30
39
|
}
|
package/src/index.js
CHANGED
|
@@ -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
|
+
})
|