@atproto/tap 0.3.3 → 0.3.5
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 +23 -0
- package/package.json +17 -12
- package/src/channel.ts +0 -161
- package/src/client.ts +0 -106
- package/src/index.ts +0 -6
- package/src/lex-indexer.ts +0 -193
- package/src/simple-indexer.ts +0 -52
- package/src/types.ts +0 -113
- package/src/util.ts +0 -42
- package/tests/_util.ts +0 -63
- package/tests/channel.test.ts +0 -371
- package/tests/client.test.ts +0 -207
- package/tests/lex-indexer.test.ts +0 -343
- package/tests/simple-indexer.test.ts +0 -161
- package/tests/util.test.ts +0 -89
- package/tsconfig.build.json +0 -9
- package/tsconfig.build.tsbuildinfo +0 -1
- package/tsconfig.json +0 -7
- package/tsconfig.tests.json +0 -8
- package/vitest.config.ts +0 -5
package/tests/client.test.ts
DELETED
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
import { once } from 'node:events'
|
|
2
|
-
import * as http from 'node:http'
|
|
3
|
-
import { AddressInfo } from 'node:net'
|
|
4
|
-
import { default as express } from 'express'
|
|
5
|
-
// eslint-disable-next-line import/default
|
|
6
|
-
import httpTerminator from 'http-terminator'
|
|
7
|
-
import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest'
|
|
8
|
-
import { Tap } from '../src/client.js'
|
|
9
|
-
|
|
10
|
-
describe('Tap client', () => {
|
|
11
|
-
describe('constructor', () => {
|
|
12
|
-
it('accepts http URL', () => {
|
|
13
|
-
const tap = new Tap('http://localhost:8080')
|
|
14
|
-
expect(tap.url).toBe('http://localhost:8080')
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
it('accepts https URL', () => {
|
|
18
|
-
const tap = new Tap('https://example.com')
|
|
19
|
-
expect(tap.url).toBe('https://example.com')
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
it('throws on invalid URL', () => {
|
|
23
|
-
expect(() => new Tap('ws://localhost:8080')).toThrow(
|
|
24
|
-
'Invalid URL, expected http:// or https://',
|
|
25
|
-
)
|
|
26
|
-
expect(() => new Tap('localhost:8080')).toThrow(
|
|
27
|
-
'Invalid URL, expected http:// or https://',
|
|
28
|
-
)
|
|
29
|
-
})
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
describe('HTTP methods', () => {
|
|
33
|
-
let terminator: httpTerminator.HttpTerminator
|
|
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 app = express()
|
|
44
|
-
app.use(express.json())
|
|
45
|
-
|
|
46
|
-
requests = []
|
|
47
|
-
|
|
48
|
-
app.post('/repos/add', (req, res) => {
|
|
49
|
-
requests.push({
|
|
50
|
-
path: req.path,
|
|
51
|
-
method: req.method,
|
|
52
|
-
body: req.body,
|
|
53
|
-
headers: req.headers,
|
|
54
|
-
})
|
|
55
|
-
res.sendStatus(200)
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
app.post('/repos/remove', (req, res) => {
|
|
59
|
-
requests.push({
|
|
60
|
-
path: req.path,
|
|
61
|
-
method: req.method,
|
|
62
|
-
body: req.body,
|
|
63
|
-
headers: req.headers,
|
|
64
|
-
})
|
|
65
|
-
res.sendStatus(200)
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
app.get('/resolve/:did', (req, res) => {
|
|
69
|
-
requests.push({
|
|
70
|
-
path: req.path,
|
|
71
|
-
method: req.method,
|
|
72
|
-
headers: req.headers,
|
|
73
|
-
})
|
|
74
|
-
if (req.params.did === 'did:example:notfound') {
|
|
75
|
-
res.sendStatus(404)
|
|
76
|
-
return
|
|
77
|
-
}
|
|
78
|
-
res.json({
|
|
79
|
-
id: req.params.did,
|
|
80
|
-
alsoKnownAs: ['at://alice.test'],
|
|
81
|
-
verificationMethod: [],
|
|
82
|
-
service: [],
|
|
83
|
-
})
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
app.get('/info/:did', (req, res) => {
|
|
87
|
-
requests.push({
|
|
88
|
-
path: req.path,
|
|
89
|
-
method: req.method,
|
|
90
|
-
headers: req.headers,
|
|
91
|
-
})
|
|
92
|
-
res.json({
|
|
93
|
-
did: req.params.did,
|
|
94
|
-
handle: 'alice.test',
|
|
95
|
-
state: 'active',
|
|
96
|
-
rev: '3abc123',
|
|
97
|
-
records: 42,
|
|
98
|
-
})
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
const server = app.listen()
|
|
102
|
-
await once(server, 'listening')
|
|
103
|
-
const { port } = server.address() as AddressInfo
|
|
104
|
-
terminator = httpTerminator.createHttpTerminator({ server })
|
|
105
|
-
tap = new Tap(`http://localhost:${port}`, { adminPassword: 'secret' })
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
afterAll(async () => {
|
|
109
|
-
await terminator?.terminate()
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
beforeEach(() => {
|
|
113
|
-
requests = []
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
describe('addRepos', () => {
|
|
117
|
-
it('sends POST to /repos/add with dids', async () => {
|
|
118
|
-
await tap.addRepos(['did:example:alice', 'did:example:bob'])
|
|
119
|
-
expect(requests).toHaveLength(1)
|
|
120
|
-
expect(requests[0].path).toBe('/repos/add')
|
|
121
|
-
expect(requests[0].method).toBe('POST')
|
|
122
|
-
expect(requests[0].body).toEqual({
|
|
123
|
-
dids: ['did:example:alice', 'did:example:bob'],
|
|
124
|
-
})
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
it('includes auth header', async () => {
|
|
128
|
-
await tap.addRepos(['did:example:alice'])
|
|
129
|
-
expect(requests[0].headers.authorization).toBe('Basic YWRtaW46c2VjcmV0')
|
|
130
|
-
})
|
|
131
|
-
})
|
|
132
|
-
|
|
133
|
-
describe('removeRepos', () => {
|
|
134
|
-
it('sends POST to /repos/remove with dids', async () => {
|
|
135
|
-
await tap.removeRepos(['did:example:alice'])
|
|
136
|
-
expect(requests).toHaveLength(1)
|
|
137
|
-
expect(requests[0].path).toBe('/repos/remove')
|
|
138
|
-
expect(requests[0].method).toBe('POST')
|
|
139
|
-
expect(requests[0].body).toEqual({ dids: ['did:example:alice'] })
|
|
140
|
-
})
|
|
141
|
-
})
|
|
142
|
-
|
|
143
|
-
describe('resolveDid', () => {
|
|
144
|
-
it('fetches and parses DID document', async () => {
|
|
145
|
-
const doc = await tap.resolveDid('did:example:alice')
|
|
146
|
-
expect(doc).not.toBeNull()
|
|
147
|
-
expect(doc?.id).toBe('did:example:alice')
|
|
148
|
-
expect(doc?.alsoKnownAs).toEqual(['at://alice.test'])
|
|
149
|
-
})
|
|
150
|
-
|
|
151
|
-
it('returns null for 404', async () => {
|
|
152
|
-
const doc = await tap.resolveDid('did:example:notfound')
|
|
153
|
-
expect(doc).toBeNull()
|
|
154
|
-
})
|
|
155
|
-
})
|
|
156
|
-
|
|
157
|
-
describe('getRepoInfo', () => {
|
|
158
|
-
it('fetches and parses repo info', async () => {
|
|
159
|
-
const info = await tap.getRepoInfo('did:example:alice')
|
|
160
|
-
expect(info.did).toBe('did:example:alice')
|
|
161
|
-
expect(info.handle).toBe('alice.test')
|
|
162
|
-
expect(info.state).toBe('active')
|
|
163
|
-
expect(info.records).toBe(42)
|
|
164
|
-
})
|
|
165
|
-
})
|
|
166
|
-
})
|
|
167
|
-
|
|
168
|
-
describe('HTTP error handling', () => {
|
|
169
|
-
let terminator: httpTerminator.HttpTerminator
|
|
170
|
-
let tap: Tap
|
|
171
|
-
|
|
172
|
-
beforeAll(async () => {
|
|
173
|
-
const app = express()
|
|
174
|
-
app.use(express.json())
|
|
175
|
-
|
|
176
|
-
app.post('/repos/add', (_req, res) => {
|
|
177
|
-
res.status(500).send('Internal Server Error')
|
|
178
|
-
})
|
|
179
|
-
|
|
180
|
-
app.get('/info/:did', (_req, res) => {
|
|
181
|
-
res.status(500).send('Internal Server Error')
|
|
182
|
-
})
|
|
183
|
-
|
|
184
|
-
const server = app.listen(0)
|
|
185
|
-
await once(server, 'listening')
|
|
186
|
-
const { port } = server.address() as AddressInfo
|
|
187
|
-
terminator = httpTerminator.createHttpTerminator({ server })
|
|
188
|
-
tap = new Tap(`http://localhost:${port}`)
|
|
189
|
-
})
|
|
190
|
-
|
|
191
|
-
afterAll(async () => {
|
|
192
|
-
await terminator?.terminate()
|
|
193
|
-
})
|
|
194
|
-
|
|
195
|
-
it('throws on addRepos failure', async () => {
|
|
196
|
-
await expect(tap.addRepos(['did:example:alice'])).rejects.toThrow(
|
|
197
|
-
'Failed to add repos',
|
|
198
|
-
)
|
|
199
|
-
})
|
|
200
|
-
|
|
201
|
-
it('throws on getRepoInfo failure', async () => {
|
|
202
|
-
await expect(tap.getRepoInfo('did:example:alice')).rejects.toThrow(
|
|
203
|
-
'Failed to get repo info',
|
|
204
|
-
)
|
|
205
|
-
})
|
|
206
|
-
})
|
|
207
|
-
})
|
|
@@ -1,343 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import { l } from '@atproto/lex'
|
|
3
|
-
import {
|
|
4
|
-
CreateEvent,
|
|
5
|
-
DeleteEvent,
|
|
6
|
-
LexIndexer,
|
|
7
|
-
UpdateEvent,
|
|
8
|
-
} from '../src/lex-indexer.js'
|
|
9
|
-
import { IdentityEvent, RecordEvent } from '../src/types.js'
|
|
10
|
-
import {
|
|
11
|
-
createIdentityEvent,
|
|
12
|
-
createMockOpts,
|
|
13
|
-
createRecordEvent as baseCreateRecordEvent,
|
|
14
|
-
} from './_util.js'
|
|
15
|
-
|
|
16
|
-
// Test lexicon definitions
|
|
17
|
-
const postNsid = 'com.example.post'
|
|
18
|
-
type Post = {
|
|
19
|
-
$type: 'com.example.post'
|
|
20
|
-
text: string
|
|
21
|
-
}
|
|
22
|
-
const post = {
|
|
23
|
-
main: l.record<'tid', Post>('tid', postNsid, l.object({ text: l.string() })),
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const likeNsid = 'com.example.like'
|
|
27
|
-
type Like = {
|
|
28
|
-
$type: 'com.example.like'
|
|
29
|
-
subject: string
|
|
30
|
-
}
|
|
31
|
-
const like = {
|
|
32
|
-
main: l.record<'tid', Like>(
|
|
33
|
-
'tid',
|
|
34
|
-
likeNsid,
|
|
35
|
-
l.object({ subject: l.string() }),
|
|
36
|
-
),
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const createRecordEvent = (overrides: Partial<RecordEvent> = {}): RecordEvent =>
|
|
40
|
-
baseCreateRecordEvent({
|
|
41
|
-
collection: postNsid,
|
|
42
|
-
record: { $type: postNsid, text: 'hello' },
|
|
43
|
-
...overrides,
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
describe('LexIndexer', () => {
|
|
47
|
-
describe('handler registration', () => {
|
|
48
|
-
it('registers create handler', async () => {
|
|
49
|
-
const indexer = new LexIndexer()
|
|
50
|
-
const received: CreateEvent<Post>[] = []
|
|
51
|
-
|
|
52
|
-
indexer.create(post, async (evt) => {
|
|
53
|
-
received.push(evt)
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
const opts = createMockOpts()
|
|
57
|
-
await indexer.onEvent(createRecordEvent(), opts)
|
|
58
|
-
|
|
59
|
-
expect(received).toHaveLength(1)
|
|
60
|
-
expect(received[0].action).toBe('create')
|
|
61
|
-
expect(received[0].record.text).toBe('hello')
|
|
62
|
-
expect(received[0].cid).toBe(
|
|
63
|
-
'bafyreiclp443lavogvhj3d2ob2cxbfuscni2k5jk7bebjzg7khl3esabwq',
|
|
64
|
-
)
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
it('registers update handler', async () => {
|
|
68
|
-
const indexer = new LexIndexer()
|
|
69
|
-
const received: UpdateEvent<Post>[] = []
|
|
70
|
-
|
|
71
|
-
indexer.update(post, async (evt) => {
|
|
72
|
-
received.push(evt)
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
const opts = createMockOpts()
|
|
76
|
-
await indexer.onEvent(
|
|
77
|
-
createRecordEvent({
|
|
78
|
-
action: 'update',
|
|
79
|
-
record: { $type: postNsid, text: 'updated' },
|
|
80
|
-
}),
|
|
81
|
-
opts,
|
|
82
|
-
)
|
|
83
|
-
|
|
84
|
-
expect(received).toHaveLength(1)
|
|
85
|
-
expect(received[0].action).toBe('update')
|
|
86
|
-
expect(received[0].record.text).toBe('updated')
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
it('registers delete handler', async () => {
|
|
90
|
-
const indexer = new LexIndexer()
|
|
91
|
-
const received: DeleteEvent[] = []
|
|
92
|
-
|
|
93
|
-
indexer.delete(post, async (evt) => {
|
|
94
|
-
received.push(evt)
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
const opts = createMockOpts()
|
|
98
|
-
await indexer.onEvent(
|
|
99
|
-
createRecordEvent({
|
|
100
|
-
action: 'delete',
|
|
101
|
-
record: undefined,
|
|
102
|
-
cid: undefined,
|
|
103
|
-
}),
|
|
104
|
-
opts,
|
|
105
|
-
)
|
|
106
|
-
|
|
107
|
-
expect(received).toHaveLength(1)
|
|
108
|
-
expect(received[0].action).toBe('delete')
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
it('registers put handler for both create and update', async () => {
|
|
112
|
-
const indexer = new LexIndexer()
|
|
113
|
-
const received: Array<{ action: string; text: string }> = []
|
|
114
|
-
|
|
115
|
-
indexer.put(post, async (evt) => {
|
|
116
|
-
received.push({ action: evt.action, text: evt.record.text })
|
|
117
|
-
})
|
|
118
|
-
|
|
119
|
-
const opts1 = createMockOpts()
|
|
120
|
-
await indexer.onEvent(createRecordEvent({ action: 'create' }), opts1)
|
|
121
|
-
|
|
122
|
-
const opts2 = createMockOpts()
|
|
123
|
-
await indexer.onEvent(
|
|
124
|
-
createRecordEvent({
|
|
125
|
-
action: 'update',
|
|
126
|
-
record: { $type: postNsid, text: 'updated' },
|
|
127
|
-
}),
|
|
128
|
-
opts2,
|
|
129
|
-
)
|
|
130
|
-
|
|
131
|
-
expect(received).toHaveLength(2)
|
|
132
|
-
expect(received[0].action).toBe('create')
|
|
133
|
-
expect(received[1].action).toBe('update')
|
|
134
|
-
})
|
|
135
|
-
})
|
|
136
|
-
|
|
137
|
-
describe('handler routing', () => {
|
|
138
|
-
it('routes to correct handler by collection', async () => {
|
|
139
|
-
const indexer = new LexIndexer()
|
|
140
|
-
const postEvents: CreateEvent<Post>[] = []
|
|
141
|
-
const likeEvents: CreateEvent<Like>[] = []
|
|
142
|
-
|
|
143
|
-
indexer.create(post, async (evt) => {
|
|
144
|
-
postEvents.push(evt)
|
|
145
|
-
})
|
|
146
|
-
indexer.create(like, async (evt) => {
|
|
147
|
-
likeEvents.push(evt)
|
|
148
|
-
})
|
|
149
|
-
|
|
150
|
-
const opts1 = createMockOpts()
|
|
151
|
-
await indexer.onEvent(createRecordEvent(), opts1)
|
|
152
|
-
|
|
153
|
-
const opts2 = createMockOpts()
|
|
154
|
-
await indexer.onEvent(
|
|
155
|
-
createRecordEvent({
|
|
156
|
-
collection: likeNsid,
|
|
157
|
-
record: { $type: likeNsid, subject: 'at://did:example:bob/post/123' },
|
|
158
|
-
}),
|
|
159
|
-
opts2,
|
|
160
|
-
)
|
|
161
|
-
|
|
162
|
-
expect(postEvents).toHaveLength(1)
|
|
163
|
-
expect(likeEvents).toHaveLength(1)
|
|
164
|
-
})
|
|
165
|
-
|
|
166
|
-
it('routes to other handler for unregistered collections', async () => {
|
|
167
|
-
const indexer = new LexIndexer()
|
|
168
|
-
const otherEvents: RecordEvent[] = []
|
|
169
|
-
|
|
170
|
-
indexer.create(post, async () => {})
|
|
171
|
-
indexer.other(async (evt) => {
|
|
172
|
-
otherEvents.push(evt)
|
|
173
|
-
})
|
|
174
|
-
|
|
175
|
-
const opts = createMockOpts()
|
|
176
|
-
await indexer.onEvent(
|
|
177
|
-
createRecordEvent({ collection: 'com.example.unknown' }),
|
|
178
|
-
opts,
|
|
179
|
-
)
|
|
180
|
-
|
|
181
|
-
expect(otherEvents).toHaveLength(1)
|
|
182
|
-
expect(otherEvents[0].collection).toBe('com.example.unknown')
|
|
183
|
-
})
|
|
184
|
-
|
|
185
|
-
it('routes to other handler for unregistered actions', async () => {
|
|
186
|
-
const indexer = new LexIndexer()
|
|
187
|
-
const otherEvents: RecordEvent[] = []
|
|
188
|
-
|
|
189
|
-
indexer.create(post, async () => {})
|
|
190
|
-
indexer.other(async (evt) => {
|
|
191
|
-
otherEvents.push(evt)
|
|
192
|
-
})
|
|
193
|
-
|
|
194
|
-
const opts = createMockOpts()
|
|
195
|
-
await indexer.onEvent(createRecordEvent({ action: 'delete' }), opts)
|
|
196
|
-
|
|
197
|
-
expect(otherEvents).toHaveLength(1)
|
|
198
|
-
expect(otherEvents[0].action).toBe('delete')
|
|
199
|
-
})
|
|
200
|
-
|
|
201
|
-
it('routes identity events to identity handler', async () => {
|
|
202
|
-
const indexer = new LexIndexer()
|
|
203
|
-
const received: IdentityEvent[] = []
|
|
204
|
-
|
|
205
|
-
indexer.identity(async (evt) => {
|
|
206
|
-
received.push(evt)
|
|
207
|
-
})
|
|
208
|
-
|
|
209
|
-
const opts = createMockOpts()
|
|
210
|
-
await indexer.onEvent(createIdentityEvent(), opts)
|
|
211
|
-
|
|
212
|
-
expect(received).toHaveLength(1)
|
|
213
|
-
expect(received[0].handle).toBe('alice.test')
|
|
214
|
-
})
|
|
215
|
-
})
|
|
216
|
-
|
|
217
|
-
describe('duplicate registration', () => {
|
|
218
|
-
it('throws on duplicate create handler', () => {
|
|
219
|
-
const indexer = new LexIndexer()
|
|
220
|
-
indexer.create(post, async () => {})
|
|
221
|
-
|
|
222
|
-
expect(() => indexer.create(post, async () => {})).toThrow(
|
|
223
|
-
'Handler already registered',
|
|
224
|
-
)
|
|
225
|
-
})
|
|
226
|
-
|
|
227
|
-
it('throws on duplicate update handler', () => {
|
|
228
|
-
const indexer = new LexIndexer()
|
|
229
|
-
indexer.update(post, async () => {})
|
|
230
|
-
|
|
231
|
-
expect(() => indexer.update(post, async () => {})).toThrow(
|
|
232
|
-
'Handler already registered',
|
|
233
|
-
)
|
|
234
|
-
})
|
|
235
|
-
|
|
236
|
-
it('throws on duplicate delete handler', () => {
|
|
237
|
-
const indexer = new LexIndexer()
|
|
238
|
-
indexer.delete(post, async () => {})
|
|
239
|
-
|
|
240
|
-
expect(() => indexer.delete(post, async () => {})).toThrow(
|
|
241
|
-
'Handler already registered',
|
|
242
|
-
)
|
|
243
|
-
})
|
|
244
|
-
|
|
245
|
-
it('throws when put conflicts with create', () => {
|
|
246
|
-
const indexer = new LexIndexer()
|
|
247
|
-
indexer.create(post, async () => {})
|
|
248
|
-
|
|
249
|
-
expect(() => indexer.put(post, async () => {})).toThrow(
|
|
250
|
-
'Handler already registered',
|
|
251
|
-
)
|
|
252
|
-
})
|
|
253
|
-
|
|
254
|
-
it('throws when create conflicts with put', () => {
|
|
255
|
-
const indexer = new LexIndexer()
|
|
256
|
-
indexer.put(post, async () => {})
|
|
257
|
-
|
|
258
|
-
expect(() => indexer.create(post, async () => {})).toThrow(
|
|
259
|
-
'Handler already registered',
|
|
260
|
-
)
|
|
261
|
-
})
|
|
262
|
-
})
|
|
263
|
-
|
|
264
|
-
describe('schema validation', () => {
|
|
265
|
-
it('validates record on create', async () => {
|
|
266
|
-
const indexer = new LexIndexer()
|
|
267
|
-
indexer.create(post, async () => {})
|
|
268
|
-
|
|
269
|
-
const opts = createMockOpts()
|
|
270
|
-
await expect(
|
|
271
|
-
indexer.onEvent(createRecordEvent({ record: { text: 123 } }), opts),
|
|
272
|
-
).rejects.toThrow('Record validation failed')
|
|
273
|
-
})
|
|
274
|
-
|
|
275
|
-
it('validates record on update', async () => {
|
|
276
|
-
const indexer = new LexIndexer()
|
|
277
|
-
indexer.update(post, async () => {})
|
|
278
|
-
|
|
279
|
-
const opts = createMockOpts()
|
|
280
|
-
await expect(
|
|
281
|
-
indexer.onEvent(
|
|
282
|
-
createRecordEvent({ action: 'update', record: { invalid: true } }),
|
|
283
|
-
opts,
|
|
284
|
-
),
|
|
285
|
-
).rejects.toThrow('Record validation failed')
|
|
286
|
-
})
|
|
287
|
-
})
|
|
288
|
-
|
|
289
|
-
describe('ack behavior', () => {
|
|
290
|
-
it('calls ack after handler completes', async () => {
|
|
291
|
-
const indexer = new LexIndexer()
|
|
292
|
-
indexer.create(post, async () => {})
|
|
293
|
-
|
|
294
|
-
const opts = createMockOpts()
|
|
295
|
-
await indexer.onEvent(createRecordEvent(), opts)
|
|
296
|
-
|
|
297
|
-
expect(opts.acked).toBe(true)
|
|
298
|
-
})
|
|
299
|
-
|
|
300
|
-
it('calls ack when routed to other handler', async () => {
|
|
301
|
-
const indexer = new LexIndexer()
|
|
302
|
-
indexer.other(async () => {})
|
|
303
|
-
|
|
304
|
-
const opts = createMockOpts()
|
|
305
|
-
await indexer.onEvent(createRecordEvent(), opts)
|
|
306
|
-
|
|
307
|
-
expect(opts.acked).toBe(true)
|
|
308
|
-
})
|
|
309
|
-
|
|
310
|
-
it('calls ack even when no handler matches', async () => {
|
|
311
|
-
const indexer = new LexIndexer()
|
|
312
|
-
|
|
313
|
-
const opts = createMockOpts()
|
|
314
|
-
await indexer.onEvent(createRecordEvent(), opts)
|
|
315
|
-
|
|
316
|
-
expect(opts.acked).toBe(true)
|
|
317
|
-
})
|
|
318
|
-
})
|
|
319
|
-
|
|
320
|
-
describe('error handling', () => {
|
|
321
|
-
it('calls error handler when provided', () => {
|
|
322
|
-
const indexer = new LexIndexer()
|
|
323
|
-
const errors: Error[] = []
|
|
324
|
-
|
|
325
|
-
indexer.error((err) => {
|
|
326
|
-
errors.push(err)
|
|
327
|
-
})
|
|
328
|
-
|
|
329
|
-
const testError = new Error('test error')
|
|
330
|
-
indexer.onError(testError)
|
|
331
|
-
|
|
332
|
-
expect(errors).toHaveLength(1)
|
|
333
|
-
expect(errors[0]).toBe(testError)
|
|
334
|
-
})
|
|
335
|
-
|
|
336
|
-
it('throws when no error handler is registered', () => {
|
|
337
|
-
const indexer = new LexIndexer()
|
|
338
|
-
const testError = new Error('test error')
|
|
339
|
-
|
|
340
|
-
expect(() => indexer.onError(testError)).toThrow('test error')
|
|
341
|
-
})
|
|
342
|
-
})
|
|
343
|
-
})
|
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import { HandlerOpts } from '../src/channel.js'
|
|
3
|
-
import { SimpleIndexer } from '../src/simple-indexer.js'
|
|
4
|
-
import { IdentityEvent, RecordEvent } from '../src/types.js'
|
|
5
|
-
import {
|
|
6
|
-
createIdentityEvent,
|
|
7
|
-
createMockOpts,
|
|
8
|
-
createRecordEvent,
|
|
9
|
-
} from './_util.js'
|
|
10
|
-
|
|
11
|
-
describe('SimpleIndexer', () => {
|
|
12
|
-
describe('event routing', () => {
|
|
13
|
-
it('routes record events to record handler', async () => {
|
|
14
|
-
const indexer = new SimpleIndexer()
|
|
15
|
-
const receivedEvents: RecordEvent[] = []
|
|
16
|
-
|
|
17
|
-
indexer.record(async (evt) => {
|
|
18
|
-
receivedEvents.push(evt)
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
const opts = createMockOpts()
|
|
22
|
-
await indexer.onEvent(createRecordEvent(), opts)
|
|
23
|
-
|
|
24
|
-
expect(receivedEvents).toHaveLength(1)
|
|
25
|
-
expect(receivedEvents[0].type).toBe('record')
|
|
26
|
-
expect(receivedEvents[0].collection).toBe('com.example.post')
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
it('routes identity events to identity handler', async () => {
|
|
30
|
-
const indexer = new SimpleIndexer()
|
|
31
|
-
const receivedEvents: IdentityEvent[] = []
|
|
32
|
-
|
|
33
|
-
indexer.identity(async (evt) => {
|
|
34
|
-
receivedEvents.push(evt)
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
const opts = createMockOpts()
|
|
38
|
-
await indexer.onEvent(createIdentityEvent(), opts)
|
|
39
|
-
|
|
40
|
-
expect(receivedEvents).toHaveLength(1)
|
|
41
|
-
expect(receivedEvents[0].type).toBe('identity')
|
|
42
|
-
expect(receivedEvents[0].handle).toBe('alice.test')
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
it('does not call identity handler for record events', async () => {
|
|
46
|
-
const indexer = new SimpleIndexer()
|
|
47
|
-
let identityCalled = false
|
|
48
|
-
let recordCalled = false
|
|
49
|
-
|
|
50
|
-
indexer.identity(async () => {
|
|
51
|
-
identityCalled = true
|
|
52
|
-
})
|
|
53
|
-
indexer.record(async () => {
|
|
54
|
-
recordCalled = true
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
const opts = createMockOpts()
|
|
58
|
-
await indexer.onEvent(createRecordEvent(), opts)
|
|
59
|
-
|
|
60
|
-
expect(recordCalled).toBe(true)
|
|
61
|
-
expect(identityCalled).toBe(false)
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
it('does not call record handler for identity events', async () => {
|
|
65
|
-
const indexer = new SimpleIndexer()
|
|
66
|
-
let identityCalled = false
|
|
67
|
-
let recordCalled = false
|
|
68
|
-
|
|
69
|
-
indexer.identity(async () => {
|
|
70
|
-
identityCalled = true
|
|
71
|
-
})
|
|
72
|
-
indexer.record(async () => {
|
|
73
|
-
recordCalled = true
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
const opts = createMockOpts()
|
|
77
|
-
await indexer.onEvent(createIdentityEvent(), opts)
|
|
78
|
-
|
|
79
|
-
expect(identityCalled).toBe(true)
|
|
80
|
-
expect(recordCalled).toBe(false)
|
|
81
|
-
})
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
describe('ack behavior', () => {
|
|
85
|
-
it('calls ack after handler completes', async () => {
|
|
86
|
-
const indexer = new SimpleIndexer()
|
|
87
|
-
indexer.record(async () => {})
|
|
88
|
-
|
|
89
|
-
const opts = createMockOpts()
|
|
90
|
-
await indexer.onEvent(createRecordEvent(), opts)
|
|
91
|
-
|
|
92
|
-
expect(opts.acked).toBe(true)
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
it('calls ack even when no handler is registered', async () => {
|
|
96
|
-
const indexer = new SimpleIndexer()
|
|
97
|
-
// No handlers registered
|
|
98
|
-
|
|
99
|
-
const opts = createMockOpts()
|
|
100
|
-
await indexer.onEvent(createRecordEvent(), opts)
|
|
101
|
-
|
|
102
|
-
expect(opts.acked).toBe(true)
|
|
103
|
-
})
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
describe('error handling', () => {
|
|
107
|
-
it('calls error handler when provided', () => {
|
|
108
|
-
const indexer = new SimpleIndexer()
|
|
109
|
-
const errors: Error[] = []
|
|
110
|
-
|
|
111
|
-
indexer.error((err) => {
|
|
112
|
-
errors.push(err)
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
const testError = new Error('test error')
|
|
116
|
-
indexer.onError(testError)
|
|
117
|
-
|
|
118
|
-
expect(errors).toHaveLength(1)
|
|
119
|
-
expect(errors[0]).toBe(testError)
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
it('throws when no error handler is registered', () => {
|
|
123
|
-
const indexer = new SimpleIndexer()
|
|
124
|
-
const testError = new Error('test error')
|
|
125
|
-
|
|
126
|
-
expect(() => indexer.onError(testError)).toThrow('test error')
|
|
127
|
-
})
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
describe('handler opts passthrough', () => {
|
|
131
|
-
it('passes opts to record handler', async () => {
|
|
132
|
-
const indexer = new SimpleIndexer()
|
|
133
|
-
let receivedOpts: HandlerOpts | undefined
|
|
134
|
-
|
|
135
|
-
indexer.record(async (_evt, opts) => {
|
|
136
|
-
receivedOpts = opts
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
const opts = createMockOpts()
|
|
140
|
-
await indexer.onEvent(createRecordEvent(), opts)
|
|
141
|
-
|
|
142
|
-
expect(receivedOpts).toBeDefined()
|
|
143
|
-
expect(receivedOpts?.signal).toBe(opts.signal)
|
|
144
|
-
})
|
|
145
|
-
|
|
146
|
-
it('passes opts to identity handler', async () => {
|
|
147
|
-
const indexer = new SimpleIndexer()
|
|
148
|
-
let receivedOpts: HandlerOpts | undefined
|
|
149
|
-
|
|
150
|
-
indexer.identity(async (_evt, opts) => {
|
|
151
|
-
receivedOpts = opts
|
|
152
|
-
})
|
|
153
|
-
|
|
154
|
-
const opts = createMockOpts()
|
|
155
|
-
await indexer.onEvent(createIdentityEvent(), opts)
|
|
156
|
-
|
|
157
|
-
expect(receivedOpts).toBeDefined()
|
|
158
|
-
expect(receivedOpts?.signal).toBe(opts.signal)
|
|
159
|
-
})
|
|
160
|
-
})
|
|
161
|
-
})
|