@evanp/activitypub-bot 0.8.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.
Files changed (67) hide show
  1. package/.github/dependabot.yml +11 -0
  2. package/.github/workflows/main-docker.yml +45 -0
  3. package/.github/workflows/tag-docker.yml +54 -0
  4. package/Dockerfile +23 -0
  5. package/LICENSE +661 -0
  6. package/README.md +82 -0
  7. package/bots/index.js +7 -0
  8. package/docs/activitypub.bot.drawio +110 -0
  9. package/index.js +23 -0
  10. package/lib/activitydistributor.js +263 -0
  11. package/lib/activityhandler.js +999 -0
  12. package/lib/activitypubclient.js +126 -0
  13. package/lib/activitystreams.js +41 -0
  14. package/lib/actorstorage.js +300 -0
  15. package/lib/app.js +173 -0
  16. package/lib/authorizer.js +133 -0
  17. package/lib/bot.js +44 -0
  18. package/lib/botcontext.js +520 -0
  19. package/lib/botdatastorage.js +87 -0
  20. package/lib/bots/donothing.js +11 -0
  21. package/lib/bots/ok.js +41 -0
  22. package/lib/digester.js +23 -0
  23. package/lib/httpsignature.js +195 -0
  24. package/lib/httpsignatureauthenticator.js +81 -0
  25. package/lib/keystorage.js +113 -0
  26. package/lib/microsyntax.js +140 -0
  27. package/lib/objectcache.js +48 -0
  28. package/lib/objectstorage.js +319 -0
  29. package/lib/remotekeystorage.js +116 -0
  30. package/lib/routes/collection.js +92 -0
  31. package/lib/routes/health.js +24 -0
  32. package/lib/routes/inbox.js +83 -0
  33. package/lib/routes/object.js +69 -0
  34. package/lib/routes/server.js +47 -0
  35. package/lib/routes/user.js +63 -0
  36. package/lib/routes/webfinger.js +36 -0
  37. package/lib/urlformatter.js +97 -0
  38. package/package.json +51 -0
  39. package/tests/activitydistributor.test.js +606 -0
  40. package/tests/activityhandler.test.js +2185 -0
  41. package/tests/activitypubclient.test.js +225 -0
  42. package/tests/actorstorage.test.js +261 -0
  43. package/tests/app.test.js +17 -0
  44. package/tests/authorizer.test.js +306 -0
  45. package/tests/bot.donothing.test.js +30 -0
  46. package/tests/bot.ok.test.js +101 -0
  47. package/tests/botcontext.test.js +674 -0
  48. package/tests/botdatastorage.test.js +87 -0
  49. package/tests/digester.test.js +56 -0
  50. package/tests/fixtures/bots.js +15 -0
  51. package/tests/httpsignature.test.js +200 -0
  52. package/tests/httpsignatureauthenticator.test.js +463 -0
  53. package/tests/keystorage.test.js +89 -0
  54. package/tests/microsyntax.test.js +122 -0
  55. package/tests/objectcache.test.js +133 -0
  56. package/tests/objectstorage.test.js +148 -0
  57. package/tests/remotekeystorage.test.js +76 -0
  58. package/tests/routes.actor.test.js +207 -0
  59. package/tests/routes.collection.test.js +434 -0
  60. package/tests/routes.health.test.js +41 -0
  61. package/tests/routes.inbox.test.js +135 -0
  62. package/tests/routes.object.test.js +519 -0
  63. package/tests/routes.server.test.js +69 -0
  64. package/tests/routes.webfinger.test.js +41 -0
  65. package/tests/urlformatter.test.js +164 -0
  66. package/tests/utils/digest.js +7 -0
  67. package/tests/utils/nock.js +276 -0
@@ -0,0 +1,434 @@
1
+ import { describe, it } from 'node:test'
2
+ import assert from 'node:assert'
3
+ import { makeApp } from '../lib/app.js'
4
+ import request from 'supertest'
5
+ import bots from './fixtures/bots.js'
6
+ import as2 from '../lib/activitystreams.js'
7
+ import { nanoid } from 'nanoid'
8
+
9
+ describe('actor collection routes', async () => {
10
+ const databaseUrl = 'sqlite::memory:'
11
+ const origin = 'https://activitypubbot.test'
12
+ const app = await makeApp(databaseUrl, origin, bots, 'silent')
13
+
14
+ for (const coll of ['outbox', 'liked', 'followers', 'following']) {
15
+ describe(`${coll} collection`, async () => {
16
+ describe(`GET /user/{botid}/${coll}`, async () => {
17
+ let response = null
18
+ it('should work without an error', async () => {
19
+ response = await request(app).get(`/user/ok/${coll}`)
20
+ })
21
+ it('should return 200 OK', async () => {
22
+ assert.strictEqual(response.status, 200)
23
+ })
24
+ it('should return AS2', async () => {
25
+ assert.strictEqual(response.type, 'application/activity+json')
26
+ })
27
+ it('should return an object', async () => {
28
+ assert.strictEqual(typeof response.body, 'object')
29
+ console.dir(response.body)
30
+ })
31
+ it('should return an object with an id', async () => {
32
+ assert.strictEqual(typeof response.body.id, 'string')
33
+ })
34
+ it('should return an object with an id matching the request', async () => {
35
+ assert.strictEqual(response.body.id, origin + `/user/ok/${coll}`)
36
+ })
37
+ it('should return an object with a type', async () => {
38
+ assert.strictEqual(typeof response.body.type, 'string')
39
+ })
40
+ it('should return an object with a type matching the request', async () => {
41
+ assert.strictEqual(response.body.type, 'OrderedCollection')
42
+ })
43
+ it('should return an object with a totalItems', async () => {
44
+ assert.strictEqual(typeof response.body.totalItems, 'number')
45
+ })
46
+ it('should return an object with attributedTo', async () => {
47
+ assert.strictEqual(typeof response.body.attributedTo, 'string')
48
+ })
49
+ it('should return an object with attributedTo matching the bot', async () => {
50
+ assert.strictEqual(response.body.attributedTo, origin + '/user/ok')
51
+ })
52
+ it('should return an object with a to', async () => {
53
+ assert.strictEqual(typeof response.body.to, 'string')
54
+ })
55
+ it('should return an object with a to for the public', async () => {
56
+ assert.strictEqual(response.body.to, 'as:Public')
57
+ })
58
+ it('should return an object with a summary', async () => {
59
+ assert.strictEqual(typeof response.body.summaryMap, 'object')
60
+ assert.strictEqual(typeof response.body.summaryMap.en, 'string')
61
+ })
62
+ it('should return an object with a first', async () => {
63
+ assert.strictEqual(typeof response.body.first, 'string')
64
+ })
65
+ it('should return an object with a last', async () => {
66
+ assert.strictEqual(typeof response.body.last, 'string')
67
+ })
68
+ it(`should return an object with a ${coll}Of to the actor`, async () => {
69
+ assert.strictEqual(typeof response.body[coll + 'Of'], 'string')
70
+ assert.strictEqual(response.body[coll + 'Of'], origin + '/user/ok')
71
+ })
72
+ })
73
+ describe('GET collection for non-existent user', async () => {
74
+ let response = null
75
+ it('should work without an error', async () => {
76
+ response = await request(app).get('/user/dne/' + coll)
77
+ })
78
+ it('should return 404 Not Found', async () => {
79
+ assert.strictEqual(response.status, 404)
80
+ })
81
+ it('should return Problem Details JSON', async () => {
82
+ assert.strictEqual(response.type, 'application/problem+json')
83
+ })
84
+ it('should return an object', async () => {
85
+ assert.strictEqual(typeof response.body, 'object')
86
+ })
87
+ it('should return an object with a type', async () => {
88
+ assert.strictEqual(typeof response.body.type, 'string')
89
+ })
90
+ it('should return an object with an type matching the request', async () => {
91
+ assert.strictEqual(response.body.type, 'about:blank')
92
+ })
93
+ it('should return an object with a title', async () => {
94
+ assert.strictEqual(typeof response.body.title, 'string')
95
+ })
96
+ it('should return an object with a title matching the request', async () => {
97
+ assert.strictEqual(response.body.title, 'Not Found')
98
+ })
99
+ it('should return an object with a status', async () => {
100
+ assert.strictEqual(typeof response.body.status, 'number')
101
+ })
102
+ it('should return an object with a status matching the request', async () => {
103
+ assert.strictEqual(response.body.status, 404)
104
+ })
105
+ it('should return an object with a detail', async () => {
106
+ assert.strictEqual(typeof response.body.detail, 'string')
107
+ })
108
+ it('should return an object with a detail matching the request', async () => {
109
+ assert.strictEqual(response.body.detail, 'User dne not found')
110
+ })
111
+ })
112
+ describe(`GET /user/{botid}/${coll}/1`, async () => {
113
+ let response = null
114
+ it('should work without an error', async () => {
115
+ response = await request(app).get(`/user/ok/${coll}/1`)
116
+ })
117
+ it('should return 200 OK', async () => {
118
+ assert.strictEqual(response.status, 200)
119
+ })
120
+ it('should return AS2', async () => {
121
+ assert.strictEqual(response.type, 'application/activity+json')
122
+ })
123
+ it('should return an object', async () => {
124
+ assert.strictEqual(typeof response.body, 'object')
125
+ })
126
+ it('should return an object with an id', async () => {
127
+ assert.strictEqual(typeof response.body.id, 'string')
128
+ })
129
+ it('should return an object with an id matching the request', async () => {
130
+ assert.strictEqual(response.body.id, origin + `/user/ok/${coll}/1`)
131
+ })
132
+ it('should return an object with a type', async () => {
133
+ assert.strictEqual(typeof response.body.type, 'string')
134
+ })
135
+ it('should return an object with a type matching the request', async () => {
136
+ assert.strictEqual(response.body.type, 'OrderedCollectionPage')
137
+ })
138
+ it('should return an object with attributedTo', async () => {
139
+ assert.strictEqual(typeof response.body.attributedTo, 'string')
140
+ })
141
+ it('should return an object with attributedTo matching the bot', async () => {
142
+ assert.strictEqual(response.body.attributedTo, origin + '/user/ok')
143
+ })
144
+ it('should return an object with a to', async () => {
145
+ assert.strictEqual(typeof response.body.to, 'string')
146
+ })
147
+ it('should return an object with a to for the public', async () => {
148
+ assert.strictEqual(response.body.to, 'as:Public')
149
+ })
150
+ it('should return an object with a summary', async () => {
151
+ assert.strictEqual(typeof response.body.summaryMap, 'object')
152
+ assert.strictEqual(typeof response.body.summaryMap.en, 'string')
153
+ })
154
+ it('should return an object with a partOf', async () => {
155
+ assert.strictEqual(typeof response.body.partOf, 'string')
156
+ })
157
+ it('should return an object with a partOf matching the collection', async () => {
158
+ assert.strictEqual(response.body.partOf, origin + `/user/ok/${coll}`)
159
+ })
160
+ })
161
+ describe('GET collection page for non-existent user', async () => {
162
+ let response = null
163
+ it('should work without an error', async () => {
164
+ response = await request(app).get('/user/dne/' + coll + '/1')
165
+ })
166
+ it('should return 404 Not Found', async () => {
167
+ assert.strictEqual(response.status, 404)
168
+ })
169
+ it('should return Problem Details JSON', async () => {
170
+ assert.strictEqual(response.type, 'application/problem+json')
171
+ })
172
+ it('should return an object', async () => {
173
+ assert.strictEqual(typeof response.body, 'object')
174
+ })
175
+ it('should return an object with a type', async () => {
176
+ assert.strictEqual(typeof response.body.type, 'string')
177
+ })
178
+ it('should return an object with an type matching the request', async () => {
179
+ assert.strictEqual(response.body.type, 'about:blank')
180
+ })
181
+ it('should return an object with a title', async () => {
182
+ assert.strictEqual(typeof response.body.title, 'string')
183
+ })
184
+ it('should return an object with a title matching the request', async () => {
185
+ assert.strictEqual(response.body.title, 'Not Found')
186
+ })
187
+ it('should return an object with a status', async () => {
188
+ assert.strictEqual(typeof response.body.status, 'number')
189
+ })
190
+ it('should return an object with a status matching the request', async () => {
191
+ assert.strictEqual(response.body.status, 404)
192
+ })
193
+ it('should return an object with a detail', async () => {
194
+ assert.strictEqual(typeof response.body.detail, 'string')
195
+ })
196
+ it('should return an object with a detail matching the request', async () => {
197
+ assert.strictEqual(response.body.detail, 'User dne not found')
198
+ })
199
+ })
200
+ describe('GET non-existent page for existent collection and existent user', async () => {
201
+ let response = null
202
+ it('should work without an error', async () => {
203
+ response = await request(app).get('/user/ok/' + coll + '/99999999')
204
+ })
205
+ it('should return 404 Not Found', async () => {
206
+ assert.strictEqual(response.status, 404)
207
+ })
208
+ it('should return Problem Details JSON', async () => {
209
+ assert.strictEqual(response.type, 'application/problem+json')
210
+ })
211
+ it('should return an object', async () => {
212
+ assert.strictEqual(typeof response.body, 'object')
213
+ })
214
+ it('should return an object with a type', async () => {
215
+ assert.strictEqual(typeof response.body.type, 'string')
216
+ })
217
+ it('should return an object with an type matching the request', async () => {
218
+ assert.strictEqual(response.body.type, 'about:blank')
219
+ })
220
+ it('should return an object with a title', async () => {
221
+ assert.strictEqual(typeof response.body.title, 'string')
222
+ })
223
+ it('should return an object with a title matching the request', async () => {
224
+ assert.strictEqual(response.body.title, 'Not Found')
225
+ })
226
+ it('should return an object with a status', async () => {
227
+ assert.strictEqual(typeof response.body.status, 'number')
228
+ })
229
+ it('should return an object with a status matching the request', async () => {
230
+ assert.strictEqual(response.body.status, 404)
231
+ })
232
+ it('should return an object with a detail', async () => {
233
+ assert.strictEqual(typeof response.body.detail, 'string')
234
+ })
235
+ it('should return an object with a detail matching the request', async () => {
236
+ assert.strictEqual(response.body.detail, 'No such page 99999999 for collection ' + coll + ' for user ok')
237
+ })
238
+ })
239
+ })
240
+ }
241
+
242
+ describe('GET non-existent collection for existent user', async () => {
243
+ let response = null
244
+ it('should work without an error', async () => {
245
+ response = await request(app).get('/user/ok/dne')
246
+ })
247
+ it('should return 404 Not Found', async () => {
248
+ assert.strictEqual(response.status, 404)
249
+ })
250
+ it('should return Problem Details JSON', async () => {
251
+ assert.strictEqual(response.type, 'application/problem+json')
252
+ })
253
+ it('should return an object', async () => {
254
+ assert.strictEqual(typeof response.body, 'object')
255
+ })
256
+ it('should return an object with a type', async () => {
257
+ assert.strictEqual(typeof response.body.type, 'string')
258
+ })
259
+ it('should return an object with an type matching the request', async () => {
260
+ assert.strictEqual(response.body.type, 'about:blank')
261
+ })
262
+ it('should return an object with a title', async () => {
263
+ assert.strictEqual(typeof response.body.title, 'string')
264
+ })
265
+ it('should return an object with a title matching the request', async () => {
266
+ assert.strictEqual(response.body.title, 'Not Found')
267
+ })
268
+ it('should return an object with a status', async () => {
269
+ assert.strictEqual(typeof response.body.status, 'number')
270
+ })
271
+ it('should return an object with a status matching the request', async () => {
272
+ assert.strictEqual(response.body.status, 404)
273
+ })
274
+ it('should return an object with a detail', async () => {
275
+ assert.strictEqual(typeof response.body.detail, 'string')
276
+ })
277
+ it('should return an object with a detail matching the request', async () => {
278
+ assert.strictEqual(response.body.detail, 'No such collection dne for user ok')
279
+ })
280
+ })
281
+
282
+ describe('GET page for non-existent collection for existent user', async () => {
283
+ let response = null
284
+ it('should work without an error', async () => {
285
+ response = await request(app).get('/user/ok/dne/1')
286
+ })
287
+ it('should return 404 Not Found', async () => {
288
+ assert.strictEqual(response.status, 404)
289
+ })
290
+ it('should return Problem Details JSON', async () => {
291
+ assert.strictEqual(response.type, 'application/problem+json')
292
+ })
293
+ it('should return an object', async () => {
294
+ assert.strictEqual(typeof response.body, 'object')
295
+ })
296
+ it('should return an object with a type', async () => {
297
+ assert.strictEqual(typeof response.body.type, 'string')
298
+ })
299
+ it('should return an object with an type matching the request', async () => {
300
+ assert.strictEqual(response.body.type, 'about:blank')
301
+ })
302
+ it('should return an object with a title', async () => {
303
+ assert.strictEqual(typeof response.body.title, 'string')
304
+ })
305
+ it('should return an object with a title matching the request', async () => {
306
+ assert.strictEqual(response.body.title, 'Not Found')
307
+ })
308
+ it('should return an object with a status', async () => {
309
+ assert.strictEqual(typeof response.body.status, 'number')
310
+ })
311
+ it('should return an object with a status matching the request', async () => {
312
+ assert.strictEqual(response.body.status, 404)
313
+ })
314
+ it('should return an object with a detail', async () => {
315
+ assert.strictEqual(typeof response.body.detail, 'string')
316
+ })
317
+ it('should return an object with a detail matching the request', async () => {
318
+ assert.strictEqual(response.body.detail, 'No such collection dne for user ok')
319
+ })
320
+ })
321
+
322
+ describe('GET /user/{botid}/inbox', async () => {
323
+ let response = null
324
+ it('should work without an error', async () => {
325
+ response = await request(app).get('/user/ok/inbox')
326
+ })
327
+ it('should return 403 Forbidden', async () => {
328
+ assert.strictEqual(response.status, 403)
329
+ })
330
+ it('should return Problem Details JSON', async () => {
331
+ assert.strictEqual(response.type, 'application/problem+json')
332
+ })
333
+ it('should return an object', async () => {
334
+ assert.strictEqual(typeof response.body, 'object')
335
+ })
336
+ it('should return an object with a type', async () => {
337
+ assert.strictEqual(typeof response.body.type, 'string')
338
+ })
339
+ it('should return an object with an type matching the request', async () => {
340
+ assert.strictEqual(response.body.type, 'about:blank')
341
+ })
342
+ it('should return an object with a title', async () => {
343
+ assert.strictEqual(typeof response.body.title, 'string')
344
+ })
345
+ it('should return an object with a title matching the request', async () => {
346
+ assert.strictEqual(response.body.title, 'Forbidden')
347
+ })
348
+ it('should return an object with a status', async () => {
349
+ assert.strictEqual(typeof response.body.status, 'number')
350
+ })
351
+ it('should return an object with a status matching the request', async () => {
352
+ assert.strictEqual(response.body.status, 403)
353
+ })
354
+ it('should return an object with a detail', async () => {
355
+ assert.strictEqual(typeof response.body.detail, 'string')
356
+ })
357
+ it('should return an object with a detail matching the request', async () => {
358
+ assert.strictEqual(response.body.detail, 'No access to inbox collection')
359
+ })
360
+ })
361
+
362
+ describe('GET /user/{botid}/inbox/1', async () => {
363
+ let response = null
364
+ it('should work without an error', async () => {
365
+ response = await request(app).get('/user/ok/inbox/1')
366
+ })
367
+ it('should return 403 Forbidden', async () => {
368
+ assert.strictEqual(response.status, 403)
369
+ })
370
+ it('should return Problem Details JSON', async () => {
371
+ assert.strictEqual(response.type, 'application/problem+json')
372
+ })
373
+ it('should return an object', async () => {
374
+ assert.strictEqual(typeof response.body, 'object')
375
+ })
376
+ it('should return an object with a type', async () => {
377
+ assert.strictEqual(typeof response.body.type, 'string')
378
+ })
379
+ it('should return an object with an type matching the request', async () => {
380
+ assert.strictEqual(response.body.type, 'about:blank')
381
+ })
382
+ it('should return an object with a title', async () => {
383
+ assert.strictEqual(typeof response.body.title, 'string')
384
+ })
385
+ it('should return an object with a title matching the request', async () => {
386
+ assert.strictEqual(response.body.title, 'Forbidden')
387
+ })
388
+ it('should return an object with a status', async () => {
389
+ assert.strictEqual(typeof response.body.status, 'number')
390
+ })
391
+ it('should return an object with a status matching the request', async () => {
392
+ assert.strictEqual(response.body.status, 403)
393
+ })
394
+ it('should return an object with a detail', async () => {
395
+ assert.strictEqual(typeof response.body.detail, 'string')
396
+ })
397
+ it('should return an object with a detail matching the request', async () => {
398
+ assert.strictEqual(response.body.detail, 'No access to inbox collection')
399
+ })
400
+ })
401
+
402
+ describe('GET /user/{botid}/outbox/1 with contents', async () => {
403
+ let response = null
404
+ const username = 'test0'
405
+ const actorStorage = app.locals.actorStorage
406
+ const objectStorage = app.locals.objectStorage
407
+ const formatter = app.locals.formatter
408
+
409
+ for (let i = 0; i < 20; i++) {
410
+ const activity = await as2.import({
411
+ '@context': 'https://www.w3.org/ns/activitystreams',
412
+ to: 'as:Public',
413
+ actor: formatter.format({ username }),
414
+ type: 'IntransitiveActivity',
415
+ id: formatter.format({
416
+ username,
417
+ type: 'intransitiveactivity',
418
+ nanoid: nanoid()
419
+ }),
420
+ summary: 'An intransitive activity by the test0 bot',
421
+ published: (new Date()).toISOString()
422
+ })
423
+ await objectStorage.create(activity)
424
+ await actorStorage.addToCollection(username, 'outbox', activity)
425
+ }
426
+
427
+ it('should work without an error', async () => {
428
+ response = await request(app).get(`/user/${username}/outbox/1`)
429
+ })
430
+ it('should return 200 OK', async () => {
431
+ assert.strictEqual(response.status, 200)
432
+ })
433
+ })
434
+ })
@@ -0,0 +1,41 @@
1
+ import { describe, it } from 'node:test'
2
+ import assert from 'node:assert'
3
+ import { makeApp } from '../lib/app.js'
4
+ import request from 'supertest'
5
+ import bots from './fixtures/bots.js'
6
+
7
+ describe('health check routes', async () => {
8
+ const databaseUrl = 'sqlite::memory:'
9
+ const origin = 'https://activitypubbot.test'
10
+ const app = await makeApp(databaseUrl, origin, bots, 'silent')
11
+ describe('GET /livez', async () => {
12
+ let response = null
13
+ it('should work without an error', async () => {
14
+ response = await request(app).get('/livez')
15
+ })
16
+ it('should return 200 OK', async () => {
17
+ assert.strictEqual(response.status, 200)
18
+ })
19
+ it('should return plain text', async () => {
20
+ assert.strictEqual(response.type, 'text/plain')
21
+ })
22
+ it('should return an OK flag', async () => {
23
+ assert.strictEqual(response.text, 'OK')
24
+ })
25
+ })
26
+ describe('GET /readyz', async () => {
27
+ let response = null
28
+ it('should work without an error', async () => {
29
+ response = await request(app).get('/readyz')
30
+ })
31
+ it('should return 200 OK', async () => {
32
+ assert.strictEqual(response.status, 200)
33
+ })
34
+ it('should return plain text', async () => {
35
+ assert.strictEqual(response.type, 'text/plain')
36
+ })
37
+ it('should return an OK flag', async () => {
38
+ assert.strictEqual(response.text, 'OK')
39
+ })
40
+ })
41
+ })
@@ -0,0 +1,135 @@
1
+ import { describe, it, before } from 'node:test'
2
+ import assert from 'node:assert'
3
+ import as2 from '../lib/activitystreams.js'
4
+ import request from 'supertest'
5
+
6
+ import { makeApp } from '../lib/app.js'
7
+
8
+ import { nockSetup, nockSignature, nockFormat } from './utils/nock.js'
9
+ import { makeDigest } from './utils/digest.js'
10
+ import bots from './fixtures/bots.js'
11
+
12
+ describe('routes.inbox', async () => {
13
+ const host = 'activitypubbot.test'
14
+ const origin = `https://${host}`
15
+ const databaseUrl = 'sqlite::memory:'
16
+ let app = null
17
+
18
+ before(async () => {
19
+ nockSetup('social.example')
20
+ app = await makeApp(databaseUrl, origin, bots, 'silent')
21
+ })
22
+
23
+ describe('can handle an incoming activity', async () => {
24
+ const username = 'actor1'
25
+ const botName = 'test0'
26
+ const path = `/user/${botName}/inbox`
27
+ const url = `${origin}${path}`
28
+ const date = new Date().toUTCString()
29
+ const activity = await as2.import({
30
+ type: 'Activity',
31
+ actor: nockFormat({ username }),
32
+ id: nockFormat({ username, type: 'activity', num: 1 })
33
+ })
34
+ const body = await activity.write()
35
+ const digest = makeDigest(body)
36
+ const signature = await nockSignature({
37
+ method: 'POST',
38
+ username,
39
+ url,
40
+ digest,
41
+ date
42
+ })
43
+ let response = null
44
+ it('should work without an error', async () => {
45
+ response = await request(app)
46
+ .post(path)
47
+ .send(body)
48
+ .set('Signature', signature)
49
+ .set('Date', date)
50
+ .set('Host', host)
51
+ .set('Digest', digest)
52
+ .set('Content-Type', 'application/activity+json')
53
+ assert.ok(response)
54
+ await app.onIdle()
55
+ })
56
+ it('should return a 200 status', async () => {
57
+ assert.strictEqual(response.status, 200)
58
+ })
59
+ it('should appear in the inbox', async () => {
60
+ const { actorStorage } = app.locals
61
+ assert.strictEqual(
62
+ true,
63
+ await actorStorage.isInCollection(
64
+ botName,
65
+ 'inbox',
66
+ activity
67
+ )
68
+ )
69
+ })
70
+ })
71
+ describe('can handle a duplicate incoming activity', async () => {
72
+ const username = 'actor2'
73
+ const botName = 'test1'
74
+ const path = `/user/${botName}/inbox`
75
+ const url = `${origin}${path}`
76
+ const date = new Date().toUTCString()
77
+ const activity = await as2.import({
78
+ type: 'Activity',
79
+ actor: nockFormat({ username }),
80
+ id: nockFormat({ username, type: 'activity', num: 2 }),
81
+ to: 'as:Public'
82
+ })
83
+ const body = await activity.write()
84
+ const digest = makeDigest(body)
85
+ const signature = await nockSignature({
86
+ method: 'POST',
87
+ username,
88
+ url,
89
+ digest,
90
+ date
91
+ })
92
+ let response = null
93
+ it('should work without an error', async () => {
94
+ response = await request(app)
95
+ .post(path)
96
+ .send(body)
97
+ .set('Signature', signature)
98
+ .set('Date', date)
99
+ .set('Host', host)
100
+ .set('Digest', digest)
101
+ .set('Content-Type', 'application/activity+json')
102
+ assert.ok(response)
103
+ await app.onIdle()
104
+ })
105
+ it('should return a 200 status', async () => {
106
+ assert.strictEqual(response.status, 200)
107
+ })
108
+ it('should appear in the inbox', async () => {
109
+ const { actorStorage } = app.locals
110
+ assert.strictEqual(
111
+ true,
112
+ await actorStorage.isInCollection(
113
+ botName,
114
+ 'inbox',
115
+ activity
116
+ )
117
+ )
118
+ })
119
+ it('should fail the second time', async () => {
120
+ response = await request(app)
121
+ .post(path)
122
+ .send(body)
123
+ .set('Signature', signature)
124
+ .set('Date', date)
125
+ .set('Host', host)
126
+ .set('Digest', digest)
127
+ .set('Content-Type', 'application/activity+json')
128
+ assert.ok(response)
129
+ await app.onIdle()
130
+ })
131
+ it('should return a 400 status', async () => {
132
+ assert.strictEqual(response.status, 400)
133
+ })
134
+ })
135
+ })