@atproto/tap 0.0.2
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/CHANGELOG.md +10 -0
- package/LICENSE.txt +7 -0
- package/README.md +221 -0
- package/dist/channel.d.ts +32 -0
- package/dist/channel.d.ts.map +1 -0
- package/dist/channel.js +146 -0
- package/dist/channel.js.map +1 -0
- package/dist/client.d.ts +19 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +104 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/simple-indexer.d.ts +17 -0
- package/dist/simple-indexer.d.ts.map +1 -0
- package/dist/simple-indexer.js +53 -0
- package/dist/simple-indexer.js.map +1 -0
- package/dist/types.d.ts +286 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +75 -0
- package/dist/types.js.map +1 -0
- package/dist/util.d.ts +4 -0
- package/dist/util.d.ts.map +1 -0
- package/dist/util.js +37 -0
- package/dist/util.js.map +1 -0
- package/jest.config.js +10 -0
- package/package.json +43 -0
- package/src/channel.ts +152 -0
- package/src/client.ts +100 -0
- package/src/index.ts +5 -0
- package/src/simple-indexer.ts +47 -0
- package/src/types.ts +109 -0
- package/src/util.ts +33 -0
- package/tests/channel.test.ts +379 -0
- package/tests/client.test.ts +208 -0
- package/tests/simple-indexer.test.ts +188 -0
- package/tests/util.test.ts +88 -0
- package/tsconfig.build.json +9 -0
- package/tsconfig.build.tsbuildinfo +1 -0
- package/tsconfig.json +4 -0
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import getPort from 'get-port'
|
|
2
|
+
import { WebSocketServer } from 'ws'
|
|
3
|
+
import { TapChannel, TapHandler } from '../src/channel'
|
|
4
|
+
import { TapEvent } from '../src/types'
|
|
5
|
+
|
|
6
|
+
const createRecordEvent = (id: number) => ({
|
|
7
|
+
id,
|
|
8
|
+
type: 'record' as const,
|
|
9
|
+
record: {
|
|
10
|
+
did: 'did:example:alice',
|
|
11
|
+
rev: '3abc123',
|
|
12
|
+
collection: 'com.example.post',
|
|
13
|
+
rkey: 'abc123',
|
|
14
|
+
action: 'create' as const,
|
|
15
|
+
record: { text: 'hello' },
|
|
16
|
+
cid: 'bafyabc',
|
|
17
|
+
live: true,
|
|
18
|
+
},
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const createIdentityEvent = (id: number) => ({
|
|
22
|
+
id,
|
|
23
|
+
type: 'identity' as const,
|
|
24
|
+
identity: {
|
|
25
|
+
did: 'did:example:alice',
|
|
26
|
+
handle: 'alice.test',
|
|
27
|
+
is_active: true,
|
|
28
|
+
status: 'active' as const,
|
|
29
|
+
},
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
describe('TapChannel', () => {
|
|
33
|
+
describe('receiving events', () => {
|
|
34
|
+
it('receives and parses record events', async () => {
|
|
35
|
+
const port = await getPort()
|
|
36
|
+
const server = new WebSocketServer({ port })
|
|
37
|
+
|
|
38
|
+
const receivedEvents: TapEvent[] = []
|
|
39
|
+
|
|
40
|
+
server.on('connection', (socket) => {
|
|
41
|
+
socket.send(JSON.stringify(createRecordEvent(1)))
|
|
42
|
+
socket.on('message', (data) => {
|
|
43
|
+
const msg = JSON.parse(data.toString())
|
|
44
|
+
if (msg.type === 'ack') {
|
|
45
|
+
socket.close()
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
const handler: TapHandler = {
|
|
51
|
+
onEvent: async (evt, opts) => {
|
|
52
|
+
receivedEvents.push(evt)
|
|
53
|
+
await opts.ack()
|
|
54
|
+
},
|
|
55
|
+
onError: (err) => {
|
|
56
|
+
throw err
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const channel = new TapChannel(`ws://localhost:${port}`, handler)
|
|
61
|
+
await channel.start()
|
|
62
|
+
|
|
63
|
+
expect(receivedEvents).toHaveLength(1)
|
|
64
|
+
expect(receivedEvents[0].type).toBe('record')
|
|
65
|
+
expect(receivedEvents[0].did).toBe('did:example:alice')
|
|
66
|
+
if (receivedEvents[0].type === 'record') {
|
|
67
|
+
expect(receivedEvents[0].collection).toBe('com.example.post')
|
|
68
|
+
expect(receivedEvents[0].action).toBe('create')
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
server.close()
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('receives and parses identity events', async () => {
|
|
75
|
+
const port = await getPort()
|
|
76
|
+
const server = new WebSocketServer({ port })
|
|
77
|
+
|
|
78
|
+
const receivedEvents: TapEvent[] = []
|
|
79
|
+
|
|
80
|
+
server.on('connection', (socket) => {
|
|
81
|
+
socket.send(JSON.stringify(createIdentityEvent(1)))
|
|
82
|
+
socket.on('message', (data) => {
|
|
83
|
+
const msg = JSON.parse(data.toString())
|
|
84
|
+
if (msg.type === 'ack') {
|
|
85
|
+
socket.close()
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
const handler: TapHandler = {
|
|
91
|
+
onEvent: async (evt, opts) => {
|
|
92
|
+
receivedEvents.push(evt)
|
|
93
|
+
await opts.ack()
|
|
94
|
+
},
|
|
95
|
+
onError: (err) => {
|
|
96
|
+
throw err
|
|
97
|
+
},
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const channel = new TapChannel(`ws://localhost:${port}`, handler)
|
|
101
|
+
await channel.start()
|
|
102
|
+
|
|
103
|
+
expect(receivedEvents).toHaveLength(1)
|
|
104
|
+
expect(receivedEvents[0].type).toBe('identity')
|
|
105
|
+
expect(receivedEvents[0].did).toBe('did:example:alice')
|
|
106
|
+
if (receivedEvents[0].type === 'identity') {
|
|
107
|
+
expect(receivedEvents[0].handle).toBe('alice.test')
|
|
108
|
+
expect(receivedEvents[0].status).toBe('active')
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
server.close()
|
|
112
|
+
})
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
describe('ack behavior', () => {
|
|
116
|
+
it('sends ack when handler calls ack()', async () => {
|
|
117
|
+
const port = await getPort()
|
|
118
|
+
const server = new WebSocketServer({ port })
|
|
119
|
+
|
|
120
|
+
const receivedAcks: number[] = []
|
|
121
|
+
|
|
122
|
+
server.on('connection', (socket) => {
|
|
123
|
+
socket.send(JSON.stringify(createRecordEvent(42)))
|
|
124
|
+
socket.on('message', (data) => {
|
|
125
|
+
const msg = JSON.parse(data.toString())
|
|
126
|
+
if (msg.type === 'ack') {
|
|
127
|
+
receivedAcks.push(msg.id)
|
|
128
|
+
socket.close()
|
|
129
|
+
}
|
|
130
|
+
})
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
const handler: TapHandler = {
|
|
134
|
+
onEvent: async (_evt, opts) => {
|
|
135
|
+
await opts.ack()
|
|
136
|
+
},
|
|
137
|
+
onError: (err) => {
|
|
138
|
+
throw err
|
|
139
|
+
},
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const channel = new TapChannel(`ws://localhost:${port}`, handler)
|
|
143
|
+
await channel.start()
|
|
144
|
+
|
|
145
|
+
expect(receivedAcks).toEqual([42])
|
|
146
|
+
|
|
147
|
+
server.close()
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('does not send ack if handler throws', async () => {
|
|
151
|
+
const port = await getPort()
|
|
152
|
+
const server = new WebSocketServer({ port })
|
|
153
|
+
|
|
154
|
+
const receivedAcks: number[] = []
|
|
155
|
+
const errors: Error[] = []
|
|
156
|
+
|
|
157
|
+
server.on('connection', (socket) => {
|
|
158
|
+
socket.send(JSON.stringify(createRecordEvent(1)))
|
|
159
|
+
socket.on('message', (data) => {
|
|
160
|
+
const msg = JSON.parse(data.toString())
|
|
161
|
+
if (msg.type === 'ack') {
|
|
162
|
+
receivedAcks.push(msg.id)
|
|
163
|
+
}
|
|
164
|
+
})
|
|
165
|
+
// Close after a short delay to let error propagate
|
|
166
|
+
setTimeout(() => socket.close(), 100)
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
const handler: TapHandler = {
|
|
170
|
+
onEvent: async () => {
|
|
171
|
+
throw new Error('Handler failed')
|
|
172
|
+
},
|
|
173
|
+
onError: (err) => {
|
|
174
|
+
errors.push(err)
|
|
175
|
+
},
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const channel = new TapChannel(`ws://localhost:${port}`, handler)
|
|
179
|
+
await channel.start()
|
|
180
|
+
|
|
181
|
+
expect(receivedAcks).toHaveLength(0)
|
|
182
|
+
expect(errors).toHaveLength(1)
|
|
183
|
+
expect(errors[0].message).toContain('Failed to process event')
|
|
184
|
+
|
|
185
|
+
server.close()
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it('does not send ack if handler does not call ack()', async () => {
|
|
189
|
+
const port = await getPort()
|
|
190
|
+
const server = new WebSocketServer({ port })
|
|
191
|
+
|
|
192
|
+
const receivedAcks: number[] = []
|
|
193
|
+
|
|
194
|
+
server.on('connection', (socket) => {
|
|
195
|
+
socket.send(JSON.stringify(createRecordEvent(1)))
|
|
196
|
+
socket.on('message', (data) => {
|
|
197
|
+
const msg = JSON.parse(data.toString())
|
|
198
|
+
if (msg.type === 'ack') {
|
|
199
|
+
receivedAcks.push(msg.id)
|
|
200
|
+
}
|
|
201
|
+
})
|
|
202
|
+
// Close after a short delay
|
|
203
|
+
setTimeout(() => socket.close(), 100)
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
const handler: TapHandler = {
|
|
207
|
+
onEvent: async () => {
|
|
208
|
+
// Don't call ack
|
|
209
|
+
},
|
|
210
|
+
onError: (err) => {
|
|
211
|
+
throw err
|
|
212
|
+
},
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const channel = new TapChannel(`ws://localhost:${port}`, handler)
|
|
216
|
+
await channel.start()
|
|
217
|
+
|
|
218
|
+
expect(receivedAcks).toHaveLength(0)
|
|
219
|
+
|
|
220
|
+
server.close()
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
it('handles reconnection and receives events from new connection', async () => {
|
|
224
|
+
const port = await getPort()
|
|
225
|
+
const server = new WebSocketServer({ port })
|
|
226
|
+
|
|
227
|
+
const receivedEvents: TapEvent[] = []
|
|
228
|
+
const receivedAcks: number[] = []
|
|
229
|
+
let connectionCount = 0
|
|
230
|
+
|
|
231
|
+
server.on('connection', (socket) => {
|
|
232
|
+
connectionCount++
|
|
233
|
+
// Send a different event each connection
|
|
234
|
+
const eventId = connectionCount
|
|
235
|
+
socket.send(JSON.stringify(createRecordEvent(eventId)))
|
|
236
|
+
socket.on('message', (data) => {
|
|
237
|
+
const msg = JSON.parse(data.toString())
|
|
238
|
+
if (msg.type === 'ack') {
|
|
239
|
+
receivedAcks.push(msg.id)
|
|
240
|
+
if (connectionCount === 1) {
|
|
241
|
+
// After first ack, terminate to trigger reconnect
|
|
242
|
+
socket.terminate()
|
|
243
|
+
} else {
|
|
244
|
+
// After second ack, close cleanly
|
|
245
|
+
socket.close()
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
})
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
const handler: TapHandler = {
|
|
252
|
+
onEvent: async (evt, opts) => {
|
|
253
|
+
receivedEvents.push(evt)
|
|
254
|
+
await opts.ack()
|
|
255
|
+
},
|
|
256
|
+
onError: () => {},
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const channel = new TapChannel(`ws://localhost:${port}`, handler, {
|
|
260
|
+
maxReconnectSeconds: 1,
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
await channel.start()
|
|
264
|
+
|
|
265
|
+
// Should have connected twice and received two events
|
|
266
|
+
expect(connectionCount).toBe(2)
|
|
267
|
+
expect(receivedEvents).toHaveLength(2)
|
|
268
|
+
expect(receivedEvents[0].id).toBe(1)
|
|
269
|
+
expect(receivedEvents[1].id).toBe(2)
|
|
270
|
+
expect(receivedAcks).toContain(1)
|
|
271
|
+
expect(receivedAcks).toContain(2)
|
|
272
|
+
|
|
273
|
+
server.close()
|
|
274
|
+
})
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
describe('multiple events', () => {
|
|
278
|
+
it('processes multiple events in sequence', async () => {
|
|
279
|
+
const port = await getPort()
|
|
280
|
+
const server = new WebSocketServer({ port })
|
|
281
|
+
|
|
282
|
+
const receivedEvents: TapEvent[] = []
|
|
283
|
+
const receivedAcks: number[] = []
|
|
284
|
+
|
|
285
|
+
server.on('connection', (socket) => {
|
|
286
|
+
socket.send(JSON.stringify(createRecordEvent(1)))
|
|
287
|
+
socket.send(JSON.stringify(createRecordEvent(2)))
|
|
288
|
+
socket.send(JSON.stringify(createIdentityEvent(3)))
|
|
289
|
+
socket.on('message', (data) => {
|
|
290
|
+
const msg = JSON.parse(data.toString())
|
|
291
|
+
if (msg.type === 'ack') {
|
|
292
|
+
receivedAcks.push(msg.id)
|
|
293
|
+
if (receivedAcks.length === 3) {
|
|
294
|
+
socket.close()
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
})
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
const handler: TapHandler = {
|
|
301
|
+
onEvent: async (evt, opts) => {
|
|
302
|
+
receivedEvents.push(evt)
|
|
303
|
+
await opts.ack()
|
|
304
|
+
},
|
|
305
|
+
onError: (err) => {
|
|
306
|
+
throw err
|
|
307
|
+
},
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const channel = new TapChannel(`ws://localhost:${port}`, handler)
|
|
311
|
+
await channel.start()
|
|
312
|
+
|
|
313
|
+
expect(receivedEvents).toHaveLength(3)
|
|
314
|
+
expect(receivedEvents[0].id).toBe(1)
|
|
315
|
+
expect(receivedEvents[1].id).toBe(2)
|
|
316
|
+
expect(receivedEvents[2].id).toBe(3)
|
|
317
|
+
expect(receivedAcks).toEqual([1, 2, 3])
|
|
318
|
+
|
|
319
|
+
server.close()
|
|
320
|
+
})
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
describe('auth', () => {
|
|
324
|
+
it('includes auth header when adminPassword is provided', async () => {
|
|
325
|
+
const port = await getPort()
|
|
326
|
+
const server = new WebSocketServer({ port })
|
|
327
|
+
|
|
328
|
+
let receivedAuthHeader: string | undefined
|
|
329
|
+
|
|
330
|
+
server.on('connection', (socket, request) => {
|
|
331
|
+
receivedAuthHeader = request.headers.authorization
|
|
332
|
+
socket.close()
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
const handler: TapHandler = {
|
|
336
|
+
onEvent: async () => {},
|
|
337
|
+
onError: () => {},
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const channel = new TapChannel(`ws://localhost:${port}`, handler, {
|
|
341
|
+
adminPassword: 'secret',
|
|
342
|
+
})
|
|
343
|
+
await channel.start()
|
|
344
|
+
|
|
345
|
+
expect(receivedAuthHeader).toBe('Basic YWRtaW46c2VjcmV0')
|
|
346
|
+
|
|
347
|
+
server.close()
|
|
348
|
+
})
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
describe('error handling', () => {
|
|
352
|
+
it('calls onError for malformed messages', async () => {
|
|
353
|
+
const port = await getPort()
|
|
354
|
+
const server = new WebSocketServer({ port })
|
|
355
|
+
|
|
356
|
+
const errors: Error[] = []
|
|
357
|
+
|
|
358
|
+
server.on('connection', (socket) => {
|
|
359
|
+
socket.send('not valid json')
|
|
360
|
+
setTimeout(() => socket.close(), 100)
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
const handler: TapHandler = {
|
|
364
|
+
onEvent: async () => {},
|
|
365
|
+
onError: (err) => {
|
|
366
|
+
errors.push(err)
|
|
367
|
+
},
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const channel = new TapChannel(`ws://localhost:${port}`, handler)
|
|
371
|
+
await channel.start()
|
|
372
|
+
|
|
373
|
+
expect(errors).toHaveLength(1)
|
|
374
|
+
expect(errors[0].message).toBe('Failed to parse message')
|
|
375
|
+
|
|
376
|
+
server.close()
|
|
377
|
+
})
|
|
378
|
+
})
|
|
379
|
+
})
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { once } from 'node:events'
|
|
2
|
+
import * as http from 'node:http'
|
|
3
|
+
import { default as express } from 'express'
|
|
4
|
+
import getPort from 'get-port'
|
|
5
|
+
import { Tap } from '../src/client'
|
|
6
|
+
|
|
7
|
+
describe('Tap client', () => {
|
|
8
|
+
describe('constructor', () => {
|
|
9
|
+
it('accepts http URL', () => {
|
|
10
|
+
const tap = new Tap('http://localhost:8080')
|
|
11
|
+
expect(tap.url).toBe('http://localhost:8080')
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('accepts https URL', () => {
|
|
15
|
+
const tap = new Tap('https://example.com')
|
|
16
|
+
expect(tap.url).toBe('https://example.com')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('throws on invalid URL', () => {
|
|
20
|
+
expect(() => new Tap('ws://localhost:8080')).toThrow(
|
|
21
|
+
'Invalid URL, expected http:// or https://',
|
|
22
|
+
)
|
|
23
|
+
expect(() => new Tap('localhost:8080')).toThrow(
|
|
24
|
+
'Invalid URL, expected http:// or https://',
|
|
25
|
+
)
|
|
26
|
+
})
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
describe('HTTP methods', () => {
|
|
30
|
+
let server: http.Server<
|
|
31
|
+
typeof http.IncomingMessage,
|
|
32
|
+
typeof http.ServerResponse
|
|
33
|
+
>
|
|
34
|
+
let tap: Tap
|
|
35
|
+
let requests: {
|
|
36
|
+
path: string
|
|
37
|
+
method: string
|
|
38
|
+
body?: unknown
|
|
39
|
+
headers: http.IncomingHttpHeaders
|
|
40
|
+
}[]
|
|
41
|
+
|
|
42
|
+
beforeAll(async () => {
|
|
43
|
+
const port = await getPort()
|
|
44
|
+
const app = express()
|
|
45
|
+
app.use(express.json())
|
|
46
|
+
|
|
47
|
+
requests = []
|
|
48
|
+
|
|
49
|
+
app.post('/repos/add', (req, res) => {
|
|
50
|
+
requests.push({
|
|
51
|
+
path: req.path,
|
|
52
|
+
method: req.method,
|
|
53
|
+
body: req.body,
|
|
54
|
+
headers: req.headers,
|
|
55
|
+
})
|
|
56
|
+
res.sendStatus(200)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
app.post('/repos/remove', (req, res) => {
|
|
60
|
+
requests.push({
|
|
61
|
+
path: req.path,
|
|
62
|
+
method: req.method,
|
|
63
|
+
body: req.body,
|
|
64
|
+
headers: req.headers,
|
|
65
|
+
})
|
|
66
|
+
res.sendStatus(200)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
app.get('/resolve/:did', (req, res) => {
|
|
70
|
+
requests.push({
|
|
71
|
+
path: req.path,
|
|
72
|
+
method: req.method,
|
|
73
|
+
headers: req.headers,
|
|
74
|
+
})
|
|
75
|
+
if (req.params.did === 'did:example:notfound') {
|
|
76
|
+
res.sendStatus(404)
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
res.json({
|
|
80
|
+
id: req.params.did,
|
|
81
|
+
alsoKnownAs: ['at://alice.test'],
|
|
82
|
+
verificationMethod: [],
|
|
83
|
+
service: [],
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
app.get('/info/:did', (req, res) => {
|
|
88
|
+
requests.push({
|
|
89
|
+
path: req.path,
|
|
90
|
+
method: req.method,
|
|
91
|
+
headers: req.headers,
|
|
92
|
+
})
|
|
93
|
+
res.json({
|
|
94
|
+
did: req.params.did,
|
|
95
|
+
handle: 'alice.test',
|
|
96
|
+
state: 'active',
|
|
97
|
+
rev: '3abc123',
|
|
98
|
+
records: 42,
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
server = app.listen(port) as http.Server
|
|
103
|
+
await once(server, 'listening')
|
|
104
|
+
tap = new Tap(`http://localhost:${port}`, { adminPassword: 'secret' })
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
afterAll(async () => {
|
|
108
|
+
await new Promise((resolve) => server.close(resolve))
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
beforeEach(() => {
|
|
112
|
+
requests = []
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
describe('addRepos', () => {
|
|
116
|
+
it('sends POST to /repos/add with dids', async () => {
|
|
117
|
+
await tap.addRepos(['did:example:alice', 'did:example:bob'])
|
|
118
|
+
expect(requests).toHaveLength(1)
|
|
119
|
+
expect(requests[0].path).toBe('/repos/add')
|
|
120
|
+
expect(requests[0].method).toBe('POST')
|
|
121
|
+
expect(requests[0].body).toEqual({
|
|
122
|
+
dids: ['did:example:alice', 'did:example:bob'],
|
|
123
|
+
})
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('includes auth header', async () => {
|
|
127
|
+
await tap.addRepos(['did:example:alice'])
|
|
128
|
+
expect(requests[0].headers.authorization).toBe('Basic YWRtaW46c2VjcmV0')
|
|
129
|
+
})
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
describe('removeRepos', () => {
|
|
133
|
+
it('sends POST to /repos/remove with dids', async () => {
|
|
134
|
+
await tap.removeRepos(['did:example:alice'])
|
|
135
|
+
expect(requests).toHaveLength(1)
|
|
136
|
+
expect(requests[0].path).toBe('/repos/remove')
|
|
137
|
+
expect(requests[0].method).toBe('POST')
|
|
138
|
+
expect(requests[0].body).toEqual({ dids: ['did:example:alice'] })
|
|
139
|
+
})
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
describe('resolveDid', () => {
|
|
143
|
+
it('fetches and parses DID document', async () => {
|
|
144
|
+
const doc = await tap.resolveDid('did:example:alice')
|
|
145
|
+
expect(doc).not.toBeNull()
|
|
146
|
+
expect(doc?.id).toBe('did:example:alice')
|
|
147
|
+
expect(doc?.alsoKnownAs).toEqual(['at://alice.test'])
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('returns null for 404', async () => {
|
|
151
|
+
const doc = await tap.resolveDid('did:example:notfound')
|
|
152
|
+
expect(doc).toBeNull()
|
|
153
|
+
})
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
describe('getRepoInfo', () => {
|
|
157
|
+
it('fetches and parses repo info', async () => {
|
|
158
|
+
const info = await tap.getRepoInfo('did:example:alice')
|
|
159
|
+
expect(info.did).toBe('did:example:alice')
|
|
160
|
+
expect(info.handle).toBe('alice.test')
|
|
161
|
+
expect(info.state).toBe('active')
|
|
162
|
+
expect(info.records).toBe(42)
|
|
163
|
+
})
|
|
164
|
+
})
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
describe('HTTP error handling', () => {
|
|
168
|
+
let server: http.Server<
|
|
169
|
+
typeof http.IncomingMessage,
|
|
170
|
+
typeof http.ServerResponse
|
|
171
|
+
>
|
|
172
|
+
let tap: Tap
|
|
173
|
+
|
|
174
|
+
beforeAll(async () => {
|
|
175
|
+
const port = await getPort()
|
|
176
|
+
const app = express()
|
|
177
|
+
app.use(express.json())
|
|
178
|
+
|
|
179
|
+
app.post('/repos/add', (_req, res) => {
|
|
180
|
+
res.status(500).send('Internal Server Error')
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
app.get('/info/:did', (_req, res) => {
|
|
184
|
+
res.status(500).send('Internal Server Error')
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
server = app.listen(port) as http.Server
|
|
188
|
+
await once(server, 'listening')
|
|
189
|
+
tap = new Tap(`http://localhost:${port}`)
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
afterAll(async () => {
|
|
193
|
+
await new Promise((resolve) => server.close(resolve))
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it('throws on addRepos failure', async () => {
|
|
197
|
+
await expect(tap.addRepos(['did:example:alice'])).rejects.toThrow(
|
|
198
|
+
'Failed to add repos',
|
|
199
|
+
)
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
it('throws on getRepoInfo failure', async () => {
|
|
203
|
+
await expect(tap.getRepoInfo('did:example:alice')).rejects.toThrow(
|
|
204
|
+
'Failed to get repo info',
|
|
205
|
+
)
|
|
206
|
+
})
|
|
207
|
+
})
|
|
208
|
+
})
|