Fork of github.com/did-method-plc/did-method-plc
1import { P256Keypair } from '@atproto/crypto'
2import * as plc from '@did-plc/lib'
3import { CloseFn, runTestServer } from './_util'
4import { check } from '@atproto/common'
5import { Database } from '../src'
6import { didForCreateOp, PlcClientError } from '@did-plc/lib'
7
8describe('PLC server', () => {
9 let handle = 'at://alice.example.com'
10 let atpPds = 'https://example.com'
11
12 let close: CloseFn
13 let db: Database
14 let client: plc.Client
15
16 let signingKey: P256Keypair
17 let rotationKey1: P256Keypair
18 let rotationKey2: P256Keypair
19
20 let did: string
21
22 beforeAll(async () => {
23 const server = await runTestServer({
24 dbSchema: 'server',
25 })
26
27 db = server.db
28 close = server.close
29 client = new plc.Client(server.url)
30 signingKey = await P256Keypair.create()
31 rotationKey1 = await P256Keypair.create()
32 rotationKey2 = await P256Keypair.create()
33 })
34
35 afterAll(async () => {
36 if (close) {
37 await close()
38 }
39 })
40
41 const verifyDoc = (doc: plc.DocumentData | null) => {
42 if (!doc) {
43 throw new Error('expected doc')
44 }
45 expect(doc.did).toEqual(did)
46 expect(doc.verificationMethods).toEqual({ atproto: signingKey.did() })
47 expect(doc.rotationKeys).toEqual([rotationKey1.did(), rotationKey2.did()])
48 expect(doc.alsoKnownAs).toEqual([handle])
49 expect(doc.services).toEqual({
50 atproto_pds: {
51 type: 'AtprotoPersonalDataServer',
52 endpoint: atpPds,
53 },
54 })
55 }
56
57 it('registers a did', async () => {
58 did = await client.createDid({
59 signingKey: signingKey.did(),
60 rotationKeys: [rotationKey1.did(), rotationKey2.did()],
61 handle,
62 pds: atpPds,
63 signer: rotationKey1,
64 })
65 })
66
67 it('retrieves did doc data', async () => {
68 const doc = await client.getDocumentData(did)
69 verifyDoc(doc)
70 })
71
72 it('can perform some updates', async () => {
73 const newRotationKey = await P256Keypair.create()
74 signingKey = await P256Keypair.create()
75 handle = 'at://ali.example2.com'
76 atpPds = 'https://example2.com'
77
78 await client.updateAtprotoKey(did, rotationKey1, signingKey.did())
79 await client.updateRotationKeys(did, rotationKey1, [
80 newRotationKey.did(),
81 rotationKey2.did(),
82 ])
83 rotationKey1 = newRotationKey
84
85 await client.updateHandle(did, rotationKey1, handle)
86 await client.updatePds(did, rotationKey1, atpPds)
87
88 const doc = await client.getDocumentData(did)
89 verifyDoc(doc)
90 })
91
92 it('does not allow key types that we do not support', async () => {
93 // an ed25519 key which we don't yet support
94 const newSigningKey =
95 'did:key:z6MkjwbBXZnFqL8su24wGL2Fdjti6GSLv9SWdYGswfazUPm9'
96
97 const promise = client.updateAtprotoKey(did, rotationKey1, newSigningKey)
98 await expect(promise).rejects.toThrow(PlcClientError)
99
100 const promise2 = client.updateRotationKeys(did, rotationKey1, [
101 newSigningKey,
102 ])
103 await expect(promise2).rejects.toThrow(PlcClientError)
104 })
105
106 it('retrieves the operation log', async () => {
107 const doc = await client.getDocumentData(did)
108 const ops = await client.getOperationLog(did)
109 const computedDoc = await plc.validateOperationLog(did, ops)
110 expect(computedDoc).toEqual(doc)
111 })
112
113 it('rejects on bad updates', async () => {
114 const newKey = await P256Keypair.create()
115 const operation = client.updateAtprotoKey(did, newKey, newKey.did())
116 await expect(operation).rejects.toThrow()
117 })
118
119 it('allows for recovery through a forked history', async () => {
120 const attackerKey = await P256Keypair.create()
121 await client.updateRotationKeys(did, rotationKey2, [attackerKey.did()])
122
123 const newKey = await P256Keypair.create()
124 const ops = await client.getOperationLog(did)
125 const forkPoint = ops.at(-2)
126 if (!check.is(forkPoint, plc.def.operation)) {
127 throw new Error('Could not find fork point')
128 }
129 const op = await plc.updateRotationKeysOp(forkPoint, rotationKey1, [
130 rotationKey1.did(),
131 newKey.did(),
132 ])
133 await client.sendOperation(did, op)
134
135 rotationKey2 = newKey
136
137 const doc = await client.getDocumentData(did)
138 verifyDoc(doc)
139 })
140
141 it('retrieves the auditable operation log', async () => {
142 const log = await client.getOperationLog(did)
143 const auditable = await client.getAuditableLog(did)
144 // has one nullifed op
145 expect(auditable.length).toBe(log.length + 1)
146 expect(auditable.filter((op) => op.nullified).length).toBe(1)
147 expect(auditable.at(-2)?.nullified).toBe(true)
148 expect(
149 auditable.every((op) => check.is(op, plc.def.exportedOp)),
150 ).toBeTruthy()
151 })
152
153 it('retrieves the did doc', async () => {
154 const data = await client.getDocumentData(did)
155 const doc = await client.getDocument(did)
156 expect(doc).toEqual(plc.formatDidDoc(data))
157 })
158
159 it('handles concurrent requests to many docs', async () => {
160 const COUNT = 20
161 const keys: P256Keypair[] = []
162 for (let i = 0; i < COUNT; i++) {
163 keys.push(await P256Keypair.create())
164 }
165 await Promise.all(
166 keys.map(async (key, index) => {
167 await client.createDid({
168 signingKey: key.did(),
169 rotationKeys: [key.did()],
170 handle: `user${index}`,
171 pds: `example.com`,
172 signer: key,
173 })
174 }),
175 )
176 })
177
178 it('resolves races into a coherent history with no forks', async () => {
179 const COUNT = 20
180 const keys: P256Keypair[] = []
181 for (let i = 0; i < COUNT; i++) {
182 keys.push(await P256Keypair.create())
183 }
184 // const prev = await client.getPrev(did)
185
186 let successes = 0
187 let failures = 0
188 await Promise.all(
189 keys.map(async (key) => {
190 try {
191 await client.updateAtprotoKey(did, rotationKey1, key.did())
192 successes++
193 } catch (err) {
194 failures++
195 }
196 }),
197 )
198 expect(successes).toBe(1)
199 expect(failures).toBe(19)
200
201 const ops = await client.getOperationLog(did)
202 await plc.validateOperationLog(did, ops)
203 })
204
205 it('tombstones the did', async () => {
206 await client.tombstone(did, rotationKey1)
207
208 const promise = client.getDocument(did)
209 await expect(promise).rejects.toThrow(PlcClientError)
210 const promise2 = client.getDocumentData(did)
211 await expect(promise2).rejects.toThrow(PlcClientError)
212 })
213
214 it('exports the data set', async () => {
215 const data = await client.export()
216 expect(data.every((row) => check.is(row, plc.def.exportedOp))).toBeTruthy()
217 expect(data.length).toBe(29)
218 for (let i = 1; i < data.length; i++) {
219 expect(data[i].createdAt >= data[i - 1].createdAt).toBeTruthy()
220 }
221 })
222
223 it('disallows create v1s', async () => {
224 const createV1 = await plc.deprecatedSignCreate(
225 {
226 type: 'create',
227 signingKey: signingKey.did(),
228 recoveryKey: rotationKey1.did(),
229 handle,
230 service: atpPds,
231 prev: null,
232 },
233 signingKey,
234 )
235 const did = await didForCreateOp(createV1)
236 const attempt = client.sendOperation(did, createV1 as any)
237 await expect(attempt).rejects.toThrow()
238 })
239
240 it('healthcheck succeeds when database is available.', async () => {
241 const res = await client.health()
242 expect(res).toEqual({ version: '0.0.0' })
243 })
244
245 it('healthcheck fails when database is unavailable.', async () => {
246 await db.db.destroy()
247 let error: PlcClientError
248 try {
249 await client.health()
250 throw new Error('Healthcheck should have failed')
251 } catch (err) {
252 if (err instanceof PlcClientError) {
253 error = err
254 } else {
255 throw err
256 }
257 }
258 expect(error.status).toEqual(503)
259 expect(error.data).toEqual({
260 version: '0.0.0',
261 error: 'Service Unavailable',
262 })
263 })
264})