@evanp/activitypub-bot 0.9.0 → 0.12.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/.github/workflows/main.yml +34 -0
- package/.github/workflows/{tag-docker.yml → tag.yml} +57 -5
- package/.nvmrc +1 -0
- package/Dockerfile +10 -16
- package/README.md +87 -13
- package/bin/activitypub-bot.js +68 -0
- package/lib/activitydeliverer.js +260 -0
- package/lib/activityhandler.js +14 -0
- package/lib/activitypubclient.js +52 -1
- package/lib/activitystreams.js +31 -0
- package/lib/actorstorage.js +18 -28
- package/lib/app.js +18 -7
- package/lib/bot.js +7 -0
- package/lib/botcontext.js +62 -0
- package/lib/botdatastorage.js +0 -13
- package/lib/botfactory.js +24 -0
- package/lib/botmaker.js +23 -0
- package/lib/index.js +3 -0
- package/lib/keystorage.js +7 -24
- package/lib/migrations/001-initial.js +107 -0
- package/lib/migrations/index.js +28 -0
- package/lib/objectcache.js +4 -1
- package/lib/objectstorage.js +0 -36
- package/lib/remotekeystorage.js +0 -24
- package/lib/routes/collection.js +6 -2
- package/lib/routes/inbox.js +7 -20
- package/lib/routes/sharedinbox.js +54 -0
- package/lib/routes/user.js +11 -5
- package/lib/routes/webfinger.js +11 -2
- package/lib/urlformatter.js +8 -0
- package/package.json +14 -7
- package/tests/activitydistributor.test.js +3 -3
- package/tests/activityhandler.test.js +96 -5
- package/tests/activitypubclient.test.js +115 -130
- package/tests/actorstorage.test.js +26 -4
- package/tests/authorizer.test.js +3 -8
- package/tests/botcontext.test.js +109 -63
- package/tests/botdatastorage.test.js +3 -2
- package/tests/botfactory.provincebotfactory.test.js +430 -0
- package/tests/fixtures/bots.js +13 -1
- package/tests/fixtures/eventloggingbot.js +57 -0
- package/tests/fixtures/provincebotfactory.js +53 -0
- package/tests/httpsignature.test.js +3 -4
- package/tests/httpsignatureauthenticator.test.js +3 -3
- package/tests/index.test.js +10 -0
- package/tests/keystorage.test.js +37 -2
- package/tests/microsyntax.test.js +3 -2
- package/tests/objectstorage.test.js +4 -3
- package/tests/remotekeystorage.test.js +10 -8
- package/tests/routes.actor.test.js +7 -0
- package/tests/routes.collection.test.js +0 -1
- package/tests/routes.inbox.test.js +1 -0
- package/tests/routes.object.test.js +44 -38
- package/tests/routes.sharedinbox.test.js +473 -0
- package/tests/routes.webfinger.test.js +27 -0
- package/tests/utils/nock.js +250 -27
- package/.github/workflows/main-docker.yml +0 -45
- package/index.js +0 -23
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
import { describe, it, before } from 'node:test'
|
|
2
|
+
import assert from 'node:assert'
|
|
3
|
+
import request from 'supertest'
|
|
4
|
+
import as2 from '../lib/activitystreams.js'
|
|
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
|
+
|
|
11
|
+
import bots from './fixtures/bots.js'
|
|
12
|
+
|
|
13
|
+
describe('ProvinceBotFactory', async () => {
|
|
14
|
+
const host = 'activitypubbot.example'
|
|
15
|
+
const origin = `https://${host}`
|
|
16
|
+
const databaseUrl = 'sqlite::memory:'
|
|
17
|
+
let app = null
|
|
18
|
+
|
|
19
|
+
before(async () => {
|
|
20
|
+
nockSetup('social.example')
|
|
21
|
+
app = await makeApp(databaseUrl, origin, bots, 'silent')
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
describe('Webfinger discovery for province', async () => {
|
|
25
|
+
let response = null
|
|
26
|
+
it('should work without an error', async () => {
|
|
27
|
+
response = await request(app).get('/.well-known/webfinger?resource=acct%3Aqc%40activitypubbot.example')
|
|
28
|
+
})
|
|
29
|
+
it('should return 200 OK', async () => {
|
|
30
|
+
assert.strictEqual(response.status, 200)
|
|
31
|
+
})
|
|
32
|
+
it('should return JRD', async () => {
|
|
33
|
+
assert.strictEqual(response.type, 'application/jrd+json')
|
|
34
|
+
})
|
|
35
|
+
it('should return an object with a subject', async () => {
|
|
36
|
+
assert.strictEqual(typeof response.body.subject, 'string')
|
|
37
|
+
})
|
|
38
|
+
it('should return an object with an subject matching the request', async () => {
|
|
39
|
+
assert.strictEqual(response.body.subject, 'acct:qc@activitypubbot.example')
|
|
40
|
+
})
|
|
41
|
+
it('should return an object with a links array', async () => {
|
|
42
|
+
assert.strictEqual(Array.isArray(response.body.links), true)
|
|
43
|
+
})
|
|
44
|
+
it('should return an object with a links array containing the actor id', async () => {
|
|
45
|
+
assert.strictEqual(response.body.links.length, 1)
|
|
46
|
+
assert.strictEqual(typeof response.body.links[0].rel, 'string')
|
|
47
|
+
assert.strictEqual(response.body.links[0].rel, 'self')
|
|
48
|
+
assert.strictEqual(typeof response.body.links[0].type, 'string')
|
|
49
|
+
assert.strictEqual(response.body.links[0].type, 'application/activity+json')
|
|
50
|
+
assert.strictEqual(typeof response.body.links[0].href, 'string')
|
|
51
|
+
assert.strictEqual(response.body.links[0].href, 'https://activitypubbot.example/user/qc')
|
|
52
|
+
})
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
describe('Actor for province', async () => {
|
|
56
|
+
let response = null
|
|
57
|
+
it('should work without an error', async () => {
|
|
58
|
+
response = await request(app).get('/user/qc')
|
|
59
|
+
})
|
|
60
|
+
it('should return 200 OK', async () => {
|
|
61
|
+
assert.strictEqual(response.status, 200)
|
|
62
|
+
})
|
|
63
|
+
it('should return AS2', async () => {
|
|
64
|
+
assert.strictEqual(response.type, 'application/activity+json')
|
|
65
|
+
})
|
|
66
|
+
it('should return an object', async () => {
|
|
67
|
+
assert.strictEqual(typeof response.body, 'object')
|
|
68
|
+
})
|
|
69
|
+
it('should return an object with an id', async () => {
|
|
70
|
+
assert.strictEqual(typeof response.body.id, 'string')
|
|
71
|
+
})
|
|
72
|
+
it('should return an object with an id matching the request', async () => {
|
|
73
|
+
assert.strictEqual(response.body.id, origin + '/user/qc')
|
|
74
|
+
})
|
|
75
|
+
it('should return an object with a type', async () => {
|
|
76
|
+
assert.strictEqual(typeof response.body.type, 'string')
|
|
77
|
+
})
|
|
78
|
+
it('should return an object with a type matching the request', async () => {
|
|
79
|
+
assert.strictEqual(response.body.type, 'Service')
|
|
80
|
+
})
|
|
81
|
+
it('should return an object with a preferredUsername', async () => {
|
|
82
|
+
assert.strictEqual(typeof response.body.preferredUsername, 'string')
|
|
83
|
+
})
|
|
84
|
+
it('should return an object with a preferredUsername matching the request', async () => {
|
|
85
|
+
assert.strictEqual(response.body.preferredUsername, 'qc')
|
|
86
|
+
})
|
|
87
|
+
it('should return an object with an inbox', async () => {
|
|
88
|
+
assert.strictEqual(typeof response.body.inbox, 'string')
|
|
89
|
+
})
|
|
90
|
+
it('should return an object with an outbox', async () => {
|
|
91
|
+
assert.strictEqual(typeof response.body.outbox, 'string')
|
|
92
|
+
})
|
|
93
|
+
it('should return an object with a followers', async () => {
|
|
94
|
+
assert.strictEqual(typeof response.body.followers, 'string')
|
|
95
|
+
})
|
|
96
|
+
it('should return an object with a following', async () => {
|
|
97
|
+
assert.strictEqual(typeof response.body.following, 'string')
|
|
98
|
+
})
|
|
99
|
+
it('should return an object with a liked', async () => {
|
|
100
|
+
assert.strictEqual(typeof response.body.liked, 'string')
|
|
101
|
+
})
|
|
102
|
+
it('should return an object with a to', async () => {
|
|
103
|
+
assert.strictEqual(typeof response.body.to, 'string')
|
|
104
|
+
})
|
|
105
|
+
it('should return an object with a to matching the request', async () => {
|
|
106
|
+
assert.strictEqual(response.body.to, 'as:Public')
|
|
107
|
+
})
|
|
108
|
+
it('should return an object with a summary', async () => {
|
|
109
|
+
assert.strictEqual(typeof response.body.summary, 'string')
|
|
110
|
+
})
|
|
111
|
+
it('should return an object with a summary matching the request', async () => {
|
|
112
|
+
assert.strictEqual(response.body.summary, 'The province of Quebec')
|
|
113
|
+
})
|
|
114
|
+
it('should return an object with a name', async () => {
|
|
115
|
+
assert.strictEqual(typeof response.body.name, 'string')
|
|
116
|
+
})
|
|
117
|
+
it('should return an object with a name matching the request', async () => {
|
|
118
|
+
assert.strictEqual(response.body.name, 'Quebec')
|
|
119
|
+
})
|
|
120
|
+
it('should return an object with a publicKey', async () => {
|
|
121
|
+
assert.strictEqual(typeof response.body.publicKey, 'object')
|
|
122
|
+
assert.ok(response.body.publicKey)
|
|
123
|
+
})
|
|
124
|
+
it('should return an object with a publicKey matching the request', async () => {
|
|
125
|
+
assert.strictEqual(response.body.publicKey.id, origin + '/user/qc/publickey')
|
|
126
|
+
})
|
|
127
|
+
it('should return an object with a publicKey with an owner matching the request', async () => {
|
|
128
|
+
assert.strictEqual(response.body.publicKey.owner, origin + '/user/qc')
|
|
129
|
+
})
|
|
130
|
+
it('should return an object with a publicKey with a type', async () => {
|
|
131
|
+
assert.strictEqual(response.body.publicKey.type, 'CryptographicKey')
|
|
132
|
+
})
|
|
133
|
+
it('should return an object with a publicKey with a to', async () => {
|
|
134
|
+
assert.strictEqual(response.body.publicKey.to, 'as:Public')
|
|
135
|
+
})
|
|
136
|
+
it('should return an object with a publicKey with a publicKeyPem', async () => {
|
|
137
|
+
assert.strictEqual(typeof response.body.publicKey.publicKeyPem, 'string')
|
|
138
|
+
})
|
|
139
|
+
it('publicKeyPem should be an RSA PKCS-8 key', async () => {
|
|
140
|
+
assert.match(response.body.publicKey.publicKeyPem, /^-----BEGIN PUBLIC KEY-----\n/)
|
|
141
|
+
assert.match(response.body.publicKey.publicKeyPem, /\n-----END PUBLIC KEY-----\n$/)
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
describe('Public key for province', async () => {
|
|
146
|
+
let response = null
|
|
147
|
+
it('should work without an error', async () => {
|
|
148
|
+
response = await request(app).get('/user/qc/publickey')
|
|
149
|
+
})
|
|
150
|
+
it('should return 200 OK', async () => {
|
|
151
|
+
assert.strictEqual(response.status, 200)
|
|
152
|
+
})
|
|
153
|
+
it('should return AS2', async () => {
|
|
154
|
+
assert.strictEqual(response.type, 'application/activity+json')
|
|
155
|
+
})
|
|
156
|
+
it('should return an object', async () => {
|
|
157
|
+
assert.strictEqual(typeof response.body, 'object')
|
|
158
|
+
})
|
|
159
|
+
it('should return an object with an id', async () => {
|
|
160
|
+
assert.strictEqual(typeof response.body.id, 'string')
|
|
161
|
+
})
|
|
162
|
+
it('should return an object with the requested public key id', async () => {
|
|
163
|
+
assert.strictEqual(response.body.id, origin + '/user/qc/publickey')
|
|
164
|
+
})
|
|
165
|
+
it('should return an object with an owner', async () => {
|
|
166
|
+
assert.strictEqual(typeof response.body.owner, 'string')
|
|
167
|
+
})
|
|
168
|
+
it('should return an object with the bot as owner', async () => {
|
|
169
|
+
assert.strictEqual(response.body.owner, origin + '/user/qc')
|
|
170
|
+
})
|
|
171
|
+
it('should return an object with a publicKeyPem', async () => {
|
|
172
|
+
assert.strictEqual(typeof response.body.publicKeyPem, 'string')
|
|
173
|
+
})
|
|
174
|
+
it('publicKeyPem should be an RSA PKCS-8 key', async () => {
|
|
175
|
+
assert.match(response.body.publicKeyPem, /^-----BEGIN PUBLIC KEY-----\n/)
|
|
176
|
+
assert.match(response.body.publicKeyPem, /\n-----END PUBLIC KEY-----\n$/)
|
|
177
|
+
})
|
|
178
|
+
it('should return an object with a type', async () => {
|
|
179
|
+
assert.strictEqual(typeof response.body.type, 'string')
|
|
180
|
+
})
|
|
181
|
+
it('should return an object with a type matching the request', async () => {
|
|
182
|
+
assert.strictEqual(response.body.type, 'CryptographicKey')
|
|
183
|
+
})
|
|
184
|
+
it('should return an object with a to', async () => {
|
|
185
|
+
assert.strictEqual(typeof response.body.to, 'string')
|
|
186
|
+
})
|
|
187
|
+
it('should return an object with a to matching the request', async () => {
|
|
188
|
+
assert.strictEqual(response.body.to, 'as:Public')
|
|
189
|
+
})
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
for (const coll of ['outbox', 'liked', 'followers', 'following']) {
|
|
193
|
+
describe(`Province ${coll} collection`, async () => {
|
|
194
|
+
describe(`GET /user/{botid}/${coll}`, async () => {
|
|
195
|
+
let response = null
|
|
196
|
+
it('should work without an error', async () => {
|
|
197
|
+
response = await request(app).get(`/user/qc/${coll}`)
|
|
198
|
+
})
|
|
199
|
+
it('should return 200 OK', async () => {
|
|
200
|
+
assert.strictEqual(response.status, 200)
|
|
201
|
+
})
|
|
202
|
+
it('should return AS2', async () => {
|
|
203
|
+
assert.strictEqual(response.type, 'application/activity+json')
|
|
204
|
+
})
|
|
205
|
+
it('should return an object', async () => {
|
|
206
|
+
assert.strictEqual(typeof response.body, 'object')
|
|
207
|
+
})
|
|
208
|
+
it('should return an object with an id', async () => {
|
|
209
|
+
assert.strictEqual(typeof response.body.id, 'string')
|
|
210
|
+
})
|
|
211
|
+
it('should return an object with an id matching the request', async () => {
|
|
212
|
+
assert.strictEqual(response.body.id, origin + `/user/qc/${coll}`)
|
|
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 a type matching the request', async () => {
|
|
218
|
+
assert.strictEqual(response.body.type, 'OrderedCollection')
|
|
219
|
+
})
|
|
220
|
+
it('should return an object with a totalItems', async () => {
|
|
221
|
+
assert.strictEqual(typeof response.body.totalItems, 'number')
|
|
222
|
+
})
|
|
223
|
+
it('should return an object with attributedTo', async () => {
|
|
224
|
+
assert.strictEqual(typeof response.body.attributedTo, 'string')
|
|
225
|
+
})
|
|
226
|
+
it('should return an object with attributedTo matching the bot', async () => {
|
|
227
|
+
assert.strictEqual(response.body.attributedTo, origin + '/user/qc')
|
|
228
|
+
})
|
|
229
|
+
it('should return an object with a to', async () => {
|
|
230
|
+
assert.strictEqual(typeof response.body.to, 'string')
|
|
231
|
+
})
|
|
232
|
+
it('should return an object with a to for the public', async () => {
|
|
233
|
+
assert.strictEqual(response.body.to, 'as:Public')
|
|
234
|
+
})
|
|
235
|
+
it('should return an object with a summary', async () => {
|
|
236
|
+
assert.strictEqual(typeof response.body.summaryMap, 'object')
|
|
237
|
+
assert.strictEqual(typeof response.body.summaryMap.en, 'string')
|
|
238
|
+
})
|
|
239
|
+
it('should return an object with a first', async () => {
|
|
240
|
+
assert.strictEqual(typeof response.body.first, 'string')
|
|
241
|
+
})
|
|
242
|
+
it('should return an object with a last', async () => {
|
|
243
|
+
assert.strictEqual(typeof response.body.last, 'string')
|
|
244
|
+
})
|
|
245
|
+
it(`should return an object with a ${coll}Of to the actor`, async () => {
|
|
246
|
+
assert.strictEqual(typeof response.body[coll + 'Of'], 'string')
|
|
247
|
+
assert.strictEqual(response.body[coll + 'Of'], origin + '/user/qc')
|
|
248
|
+
})
|
|
249
|
+
})
|
|
250
|
+
describe(`GET /user/{botid}/${coll}/1`, async () => {
|
|
251
|
+
let response = null
|
|
252
|
+
it('should work without an error', async () => {
|
|
253
|
+
response = await request(app).get(`/user/qc/${coll}/1`)
|
|
254
|
+
})
|
|
255
|
+
it('should return 200 OK', async () => {
|
|
256
|
+
assert.strictEqual(response.status, 200)
|
|
257
|
+
})
|
|
258
|
+
it('should return AS2', async () => {
|
|
259
|
+
assert.strictEqual(response.type, 'application/activity+json')
|
|
260
|
+
})
|
|
261
|
+
it('should return an object', async () => {
|
|
262
|
+
assert.strictEqual(typeof response.body, 'object')
|
|
263
|
+
})
|
|
264
|
+
it('should return an object with an id', async () => {
|
|
265
|
+
assert.strictEqual(typeof response.body.id, 'string')
|
|
266
|
+
})
|
|
267
|
+
it('should return an object with an id matching the request', async () => {
|
|
268
|
+
assert.strictEqual(response.body.id, origin + `/user/qc/${coll}/1`)
|
|
269
|
+
})
|
|
270
|
+
it('should return an object with a type', async () => {
|
|
271
|
+
assert.strictEqual(typeof response.body.type, 'string')
|
|
272
|
+
})
|
|
273
|
+
it('should return an object with a type matching the request', async () => {
|
|
274
|
+
assert.strictEqual(response.body.type, 'OrderedCollectionPage')
|
|
275
|
+
})
|
|
276
|
+
it('should return an object with attributedTo', async () => {
|
|
277
|
+
assert.strictEqual(typeof response.body.attributedTo, 'string')
|
|
278
|
+
})
|
|
279
|
+
it('should return an object with attributedTo matching the bot', async () => {
|
|
280
|
+
assert.strictEqual(response.body.attributedTo, origin + '/user/qc')
|
|
281
|
+
})
|
|
282
|
+
it('should return an object with a to', async () => {
|
|
283
|
+
assert.strictEqual(typeof response.body.to, 'string')
|
|
284
|
+
})
|
|
285
|
+
it('should return an object with a to for the public', async () => {
|
|
286
|
+
assert.strictEqual(response.body.to, 'as:Public')
|
|
287
|
+
})
|
|
288
|
+
it('should return an object with a summary', async () => {
|
|
289
|
+
assert.strictEqual(typeof response.body.summaryMap, 'object')
|
|
290
|
+
assert.strictEqual(typeof response.body.summaryMap.en, 'string')
|
|
291
|
+
})
|
|
292
|
+
it('should return an object with a partOf', async () => {
|
|
293
|
+
assert.strictEqual(typeof response.body.partOf, 'string')
|
|
294
|
+
})
|
|
295
|
+
it('should return an object with a partOf matching the collection', async () => {
|
|
296
|
+
assert.strictEqual(response.body.partOf, origin + `/user/qc/${coll}`)
|
|
297
|
+
})
|
|
298
|
+
})
|
|
299
|
+
})
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
describe('Province inbox collection', async () => {
|
|
303
|
+
let response = null
|
|
304
|
+
it('should work without an error', async () => {
|
|
305
|
+
response = await request(app).get('/user/qc/inbox')
|
|
306
|
+
})
|
|
307
|
+
it('should return 403 Forbidden', async () => {
|
|
308
|
+
assert.strictEqual(response.status, 403)
|
|
309
|
+
})
|
|
310
|
+
it('should return Problem Details JSON', async () => {
|
|
311
|
+
assert.strictEqual(response.type, 'application/problem+json')
|
|
312
|
+
})
|
|
313
|
+
it('should return an object', async () => {
|
|
314
|
+
assert.strictEqual(typeof response.body, 'object')
|
|
315
|
+
})
|
|
316
|
+
it('should return an object with a type', async () => {
|
|
317
|
+
assert.strictEqual(typeof response.body.type, 'string')
|
|
318
|
+
})
|
|
319
|
+
it('should return an object with an type matching the request', async () => {
|
|
320
|
+
assert.strictEqual(response.body.type, 'about:blank')
|
|
321
|
+
})
|
|
322
|
+
it('should return an object with a title', async () => {
|
|
323
|
+
assert.strictEqual(typeof response.body.title, 'string')
|
|
324
|
+
})
|
|
325
|
+
it('should return an object with a title matching the request', async () => {
|
|
326
|
+
assert.strictEqual(response.body.title, 'Forbidden')
|
|
327
|
+
})
|
|
328
|
+
it('should return an object with a status', async () => {
|
|
329
|
+
assert.strictEqual(typeof response.body.status, 'number')
|
|
330
|
+
})
|
|
331
|
+
it('should return an object with a status matching the request', async () => {
|
|
332
|
+
assert.strictEqual(response.body.status, 403)
|
|
333
|
+
})
|
|
334
|
+
it('should return an object with a detail', async () => {
|
|
335
|
+
assert.strictEqual(typeof response.body.detail, 'string')
|
|
336
|
+
})
|
|
337
|
+
it('should return an object with a detail matching the request', async () => {
|
|
338
|
+
assert.strictEqual(response.body.detail, 'No access to inbox collection')
|
|
339
|
+
})
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
describe('Province inbox page', async () => {
|
|
343
|
+
let response = null
|
|
344
|
+
it('should work without an error', async () => {
|
|
345
|
+
response = await request(app).get('/user/qc/inbox/1')
|
|
346
|
+
})
|
|
347
|
+
it('should return 403 Forbidden', async () => {
|
|
348
|
+
assert.strictEqual(response.status, 403)
|
|
349
|
+
})
|
|
350
|
+
it('should return Problem Details JSON', async () => {
|
|
351
|
+
assert.strictEqual(response.type, 'application/problem+json')
|
|
352
|
+
})
|
|
353
|
+
it('should return an object', async () => {
|
|
354
|
+
assert.strictEqual(typeof response.body, 'object')
|
|
355
|
+
})
|
|
356
|
+
it('should return an object with a type', async () => {
|
|
357
|
+
assert.strictEqual(typeof response.body.type, 'string')
|
|
358
|
+
})
|
|
359
|
+
it('should return an object with an type matching the request', async () => {
|
|
360
|
+
assert.strictEqual(response.body.type, 'about:blank')
|
|
361
|
+
})
|
|
362
|
+
it('should return an object with a title', async () => {
|
|
363
|
+
assert.strictEqual(typeof response.body.title, 'string')
|
|
364
|
+
})
|
|
365
|
+
it('should return an object with a title matching the request', async () => {
|
|
366
|
+
assert.strictEqual(response.body.title, 'Forbidden')
|
|
367
|
+
})
|
|
368
|
+
it('should return an object with a status', async () => {
|
|
369
|
+
assert.strictEqual(typeof response.body.status, 'number')
|
|
370
|
+
})
|
|
371
|
+
it('should return an object with a status matching the request', async () => {
|
|
372
|
+
assert.strictEqual(response.body.status, 403)
|
|
373
|
+
})
|
|
374
|
+
it('should return an object with a detail', async () => {
|
|
375
|
+
assert.strictEqual(typeof response.body.detail, 'string')
|
|
376
|
+
})
|
|
377
|
+
it('should return an object with a detail matching the request', async () => {
|
|
378
|
+
assert.strictEqual(response.body.detail, 'No access to inbox collection')
|
|
379
|
+
})
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
describe('Province inbox incoming activity', async () => {
|
|
383
|
+
const username = 'actor1'
|
|
384
|
+
const botName = 'qc'
|
|
385
|
+
const path = `/user/${botName}/inbox`
|
|
386
|
+
const url = `${origin}${path}`
|
|
387
|
+
const date = new Date().toUTCString()
|
|
388
|
+
const activity = await as2.import({
|
|
389
|
+
type: 'Activity',
|
|
390
|
+
actor: nockFormat({ username }),
|
|
391
|
+
id: nockFormat({ username, type: 'activity', num: 1 })
|
|
392
|
+
})
|
|
393
|
+
const body = await activity.write()
|
|
394
|
+
const digest = makeDigest(body)
|
|
395
|
+
const signature = await nockSignature({
|
|
396
|
+
method: 'POST',
|
|
397
|
+
username,
|
|
398
|
+
url,
|
|
399
|
+
digest,
|
|
400
|
+
date
|
|
401
|
+
})
|
|
402
|
+
let response = null
|
|
403
|
+
it('should work without an error', async () => {
|
|
404
|
+
response = await request(app)
|
|
405
|
+
.post(path)
|
|
406
|
+
.send(body)
|
|
407
|
+
.set('Signature', signature)
|
|
408
|
+
.set('Date', date)
|
|
409
|
+
.set('Host', host)
|
|
410
|
+
.set('Digest', digest)
|
|
411
|
+
.set('Content-Type', 'application/activity+json')
|
|
412
|
+
assert.ok(response)
|
|
413
|
+
await app.onIdle()
|
|
414
|
+
})
|
|
415
|
+
it('should return a 200 status', async () => {
|
|
416
|
+
assert.strictEqual(response.status, 200)
|
|
417
|
+
})
|
|
418
|
+
it('should appear in the inbox', async () => {
|
|
419
|
+
const { actorStorage } = app.locals
|
|
420
|
+
assert.strictEqual(
|
|
421
|
+
true,
|
|
422
|
+
await actorStorage.isInCollection(
|
|
423
|
+
botName,
|
|
424
|
+
'inbox',
|
|
425
|
+
activity
|
|
426
|
+
)
|
|
427
|
+
)
|
|
428
|
+
})
|
|
429
|
+
})
|
|
430
|
+
})
|
package/tests/fixtures/bots.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import DoNothingBot from '../../lib/bots/donothing.js'
|
|
2
2
|
import OKBot from '../../lib/bots/ok.js'
|
|
3
|
+
import EventLoggingBot from './eventloggingbot.js'
|
|
4
|
+
import ProvinceBotFactory from './provincebotfactory.js'
|
|
3
5
|
|
|
4
6
|
export default {
|
|
5
7
|
ok: new OKBot('ok'),
|
|
@@ -11,5 +13,15 @@ export default {
|
|
|
11
13
|
test4: new DoNothingBot('test4'),
|
|
12
14
|
test5: new DoNothingBot('test5'),
|
|
13
15
|
test6: new DoNothingBot('test6'),
|
|
14
|
-
test7: new DoNothingBot('test7')
|
|
16
|
+
test7: new DoNothingBot('test7'),
|
|
17
|
+
test8: new DoNothingBot('test8'),
|
|
18
|
+
test9: new DoNothingBot('test9'),
|
|
19
|
+
test10: new DoNothingBot('test10'),
|
|
20
|
+
test11: new DoNothingBot('test11'),
|
|
21
|
+
test12: new DoNothingBot('test12'),
|
|
22
|
+
test13: new DoNothingBot('test13'),
|
|
23
|
+
test14: new DoNothingBot('test14'),
|
|
24
|
+
test15: new DoNothingBot('test15'),
|
|
25
|
+
logging: new EventLoggingBot('logging'),
|
|
26
|
+
'*': new ProvinceBotFactory()
|
|
15
27
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import Bot from '../../lib/bot.js'
|
|
2
|
+
|
|
3
|
+
export default class EventLoggingBot extends Bot {
|
|
4
|
+
#follows = new Map()
|
|
5
|
+
#mentions = new Map()
|
|
6
|
+
#likes = new Map()
|
|
7
|
+
#publics = new Map()
|
|
8
|
+
#shares = new Map()
|
|
9
|
+
|
|
10
|
+
get fullname () {
|
|
11
|
+
return 'Event-logging bot'
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
get description () {
|
|
15
|
+
return 'A bot that logs events that happen to it'
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async onMention (object, activity) {
|
|
19
|
+
this.#mentions.set(activity.id, activity)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async onFollow (actor, activity) {
|
|
23
|
+
this.#follows.set(activity.id, activity)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async onLike (object, activity) {
|
|
27
|
+
this.#likes.set(activity.id, activity)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async onPublic (activity) {
|
|
31
|
+
this.#publics.set(activity.id, activity)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async onAnnounce (object, activity) {
|
|
35
|
+
this.#shares.set(activity.id, activity)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
get follows () {
|
|
39
|
+
return this.#follows
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
get mentions () {
|
|
43
|
+
return this.#mentions
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
get likes () {
|
|
47
|
+
return this.#likes
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
get publics () {
|
|
51
|
+
return this.#publics
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
get shares () {
|
|
55
|
+
return this.#shares
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import BotFactory from '../../lib/botfactory.js'
|
|
2
|
+
import Bot from '../../lib/bot.js'
|
|
3
|
+
|
|
4
|
+
class ProvinceBot extends Bot {
|
|
5
|
+
#name
|
|
6
|
+
#type
|
|
7
|
+
|
|
8
|
+
constructor (username, name, type) {
|
|
9
|
+
super(username)
|
|
10
|
+
this.#name = name
|
|
11
|
+
this.#type = type
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
get fullname () {
|
|
15
|
+
return this.#name
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
get description () {
|
|
19
|
+
return `The ${this.#type} of ${this.#name}`
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default class ProvinceBotFactory extends BotFactory {
|
|
24
|
+
static #provinces = {
|
|
25
|
+
ab: ['Alberta', 'province'],
|
|
26
|
+
on: ['Ontario', 'province'],
|
|
27
|
+
qc: ['Quebec', 'province'],
|
|
28
|
+
bc: ['British Columbia', 'province'],
|
|
29
|
+
mb: ['Manitoba', 'province'],
|
|
30
|
+
sk: ['Saskatchewan', 'province'],
|
|
31
|
+
nb: ['New Brunswick', 'province'],
|
|
32
|
+
ns: ['Nova Scotia', 'province'],
|
|
33
|
+
pe: ['Prince Edward Island', 'province'],
|
|
34
|
+
nl: ['Newfoundland and Labrador', 'province'],
|
|
35
|
+
nu: ['Nunavut', 'territory'],
|
|
36
|
+
nt: ['Northwest Territories', 'territory'],
|
|
37
|
+
yt: ['Yukon', 'territory']
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async canCreate (username) {
|
|
41
|
+
return (username in ProvinceBotFactory.#provinces)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async create (username) {
|
|
45
|
+
if (!(username in ProvinceBotFactory.#provinces)) {
|
|
46
|
+
throw new Error(`cannot create a bot with username ${username}`)
|
|
47
|
+
}
|
|
48
|
+
const [name, type] = ProvinceBotFactory.#provinces[username]
|
|
49
|
+
const bot = new ProvinceBot(username, name, type)
|
|
50
|
+
await bot.initialize(await this._context.duplicate(username))
|
|
51
|
+
return bot
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { describe, before, after, it } from 'node:test'
|
|
2
2
|
import assert from 'node:assert'
|
|
3
3
|
import { Sequelize } from 'sequelize'
|
|
4
|
-
import { KeyStorage } from '../lib/keystorage.js'
|
|
5
4
|
import { nockSetup, nockSignature, nockKeyRotate, getPublicKey, getPrivateKey, nockFormat } from './utils/nock.js'
|
|
6
5
|
import { HTTPSignature } from '../lib/httpsignature.js'
|
|
7
6
|
import Logger from 'pino'
|
|
8
7
|
import { Digester } from '../lib/digester.js'
|
|
8
|
+
import { runMigrations } from '../lib/migrations/index.js'
|
|
9
9
|
|
|
10
10
|
describe('HTTPSignature', async () => {
|
|
11
11
|
const domain = 'activitypubbot.example'
|
|
@@ -18,10 +18,9 @@ describe('HTTPSignature', async () => {
|
|
|
18
18
|
logger = Logger({
|
|
19
19
|
level: 'silent'
|
|
20
20
|
})
|
|
21
|
-
connection = new Sequelize('sqlite
|
|
21
|
+
connection = new Sequelize({ dialect: 'sqlite', storage: ':memory:', logging: false })
|
|
22
22
|
await connection.authenticate()
|
|
23
|
-
|
|
24
|
-
await keyStorage.initialize()
|
|
23
|
+
await runMigrations(connection)
|
|
25
24
|
nockSetup('social.example')
|
|
26
25
|
digester = new Digester(logger)
|
|
27
26
|
})
|
|
@@ -11,6 +11,7 @@ import { RemoteKeyStorage } from '../lib/remotekeystorage.js'
|
|
|
11
11
|
import { ActivityPubClient } from '../lib/activitypubclient.js'
|
|
12
12
|
import { UrlFormatter } from '../lib/urlformatter.js'
|
|
13
13
|
import as2 from '../lib/activitystreams.js'
|
|
14
|
+
import { runMigrations } from '../lib/migrations/index.js'
|
|
14
15
|
|
|
15
16
|
describe('HTTPSignatureAuthenticator', async () => {
|
|
16
17
|
const domain = 'activitypubbot.example'
|
|
@@ -39,16 +40,15 @@ describe('HTTPSignatureAuthenticator', async () => {
|
|
|
39
40
|
logger = Logger({
|
|
40
41
|
level: 'silent'
|
|
41
42
|
})
|
|
42
|
-
connection = new Sequelize('sqlite
|
|
43
|
+
connection = new Sequelize({ dialect: 'sqlite', storage: ':memory:', logging: false })
|
|
43
44
|
await connection.authenticate()
|
|
45
|
+
await runMigrations(connection)
|
|
44
46
|
signer = new HTTPSignature(logger)
|
|
45
47
|
digester = new Digester(logger)
|
|
46
48
|
const formatter = new UrlFormatter(origin)
|
|
47
49
|
const keyStorage = new KeyStorage(connection, logger)
|
|
48
|
-
await keyStorage.initialize()
|
|
49
50
|
const client = new ActivityPubClient(keyStorage, formatter, signer, digester, logger)
|
|
50
51
|
remoteKeyStorage = new RemoteKeyStorage(client, connection, logger)
|
|
51
|
-
await remoteKeyStorage.initialize()
|
|
52
52
|
nockSetup('social.example')
|
|
53
53
|
})
|
|
54
54
|
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import assert from 'node:assert'
|
|
2
|
+
import { describe, it } from 'node:test'
|
|
3
|
+
describe('package exports', () => {
|
|
4
|
+
it('exposes Bot, BotFactory, and makeApp', async () => {
|
|
5
|
+
const { Bot, BotFactory, makeApp } = await import('../lib/index.js')
|
|
6
|
+
assert.equal(typeof Bot, 'function')
|
|
7
|
+
assert.equal(typeof BotFactory, 'function')
|
|
8
|
+
assert.equal(typeof makeApp, 'function')
|
|
9
|
+
})
|
|
10
|
+
})
|