A library for handling DID identifiers used in Bluesky AT Protocol
1describe DIDKit::Document do
2 subject { described_class }
3
4 let(:did) { DID.new('did:plc:yk4dd2qkboz2yv6tpubpc6co') }
5 let(:base_json) { load_did_json('dholms.json') }
6
7 describe '#initialize' do
8 context 'with valid input' do
9 let(:json) { base_json }
10
11 it 'should return a Document object' do
12 doc = subject.new(did, json)
13
14 doc.should be_a(DIDKit::Document)
15 doc.did.should == did
16 doc.json.should == json
17 end
18
19 it 'should parse services from the JSON' do
20 doc = subject.new(did, json)
21
22 doc.services.should be_an(Array)
23 doc.services.length.should == 1
24
25 doc.services[0].should be_a(DIDKit::ServiceRecord)
26 doc.services[0].key.should == 'atproto_pds'
27 doc.services[0].type.should == 'AtprotoPersonalDataServer'
28 doc.services[0].endpoint.should == 'https://pds.dholms.xyz'
29 end
30
31 it 'should parse handles from the JSON' do
32 doc = subject.new(did, json)
33
34 doc.handles.should == ['dholms.xyz']
35 end
36 end
37
38 context 'when id is missing' do
39 let(:json) { base_json.dup.tap { |h| h.delete('id') }}
40
41 it 'should raise a format error' do
42 expect { subject.new(did, json) }.to raise_error(DIDKit::FormatError)
43 end
44 end
45
46 context 'when id is not a string' do
47 let(:json) { base_json.merge('id' => 123) }
48
49 it 'should raise a format error' do
50 expect { subject.new(did, json) }.to raise_error(DIDKit::FormatError)
51 end
52 end
53
54 context 'when id does not match the DID' do
55 let(:json) { base_json.merge('id' => 'did:plc:notmatching') }
56
57 it 'should raise a format error' do
58 expect { subject.new(did, json) }.to raise_error(DIDKit::FormatError)
59 end
60 end
61
62 context 'when alsoKnownAs is not an array' do
63 let(:json) { base_json.merge('alsoKnownAs' => 'at://dholms.xyz') }
64
65 it 'should raise a format error' do
66 expect { subject.new(did, json) }.to raise_error(DIDKit::FormatError)
67 end
68 end
69
70 context 'when alsoKnownAs elements are not strings' do
71 let(:json) { base_json.merge('alsoKnownAs' => [666]) }
72
73 it 'should raise a format error' do
74 expect { subject.new(did, json) }.to raise_error(DIDKit::FormatError)
75 end
76 end
77
78 context 'when alsoKnownAs contains multiple handles' do
79 let(:json) {
80 base_json.merge('alsoKnownAs' => [
81 'at://dholms.xyz',
82 'https://example.com',
83 'at://other.handle'
84 ])
85 }
86
87 it 'should pick those starting with at:// and remove the prefixes' do
88 doc = subject.new(did, json)
89 doc.handles.should == ['dholms.xyz', 'other.handle']
90 end
91
92 it 'should return all entries in #also_known_as' do
93 op = subject.new(did, json)
94 op.also_known_as.should == ['at://dholms.xyz', 'https://example.com', 'at://other.handle']
95 end
96 end
97
98 context 'when service is not an array' do
99 let(:json) { base_json.merge('service' => 'not-an-array') }
100
101 it 'should raise a format error' do
102 expect { subject.new(did, json) }.to raise_error(DIDKit::FormatError)
103 end
104 end
105
106 context 'when service entries are not hashes' do
107 let(:json) { base_json.merge('service' => ['invalid']) }
108
109 it 'should raise a format error' do
110 expect { subject.new(did, json) }.to raise_error(DIDKit::FormatError)
111 end
112 end
113
114 context 'when service entry id is missing' do
115 let(:json) { base_json.merge('service' => [service]) }
116 let(:service) {{ 'type' => 'AtprotoPersonalDataServer', 'serviceEndpoint' => 'https://pds.dholms.xyz' }}
117
118 it 'should raise a format error' do
119 expect { subject.new(did, json) }.to raise_error(DIDKit::FormatError)
120 end
121 end
122
123 context 'when service entry id is not a string' do
124 let(:json) { base_json.merge('service' => [service]) }
125 let(:service) {{ 'id' => 5, 'type' => 'AtprotoPersonalDataServer', 'serviceEndpoint' => 'https://pds.dholms.xyz' }}
126
127 it 'should raise a format error' do
128 expect { subject.new(did, json) }.to raise_error(DIDKit::FormatError)
129 end
130 end
131
132 context 'when service entry id does not start with a #' do
133 let(:json) { base_json.merge('service' => [service]) }
134 let(:service) {{ 'id' => 'atproto_pds', 'type' => 'AtprotoPersonalDataServer', 'serviceEndpoint' => 'https://pds.dholms.xyz' }}
135
136 it 'should *not* raise a format error' do
137 expect { subject.new(did, json) }.to_not raise_error
138 end
139 end
140
141 context 'when service entry type is missing' do
142 let(:json) { base_json.merge('service' => [service]) }
143 let(:service) {{ 'id' => '#atproto_pds', 'serviceEndpoint' => 'https://pds.dholms.xyz' }}
144
145 it 'should raise a format error' do
146 expect { subject.new(did, json) }.to raise_error(DIDKit::FormatError)
147 end
148 end
149
150 context 'when service entry type is not a string' do
151 let(:json) { base_json.merge('service' => [service]) }
152 let(:service) {{ 'id' => '#atproto_pds', 'type' => 7, 'serviceEndpoint' => 'https://pds.dholms.xyz' }}
153
154 it 'should raise a format error' do
155 expect { subject.new(did, json) }.to raise_error(DIDKit::FormatError)
156 end
157 end
158
159 context 'when service entry endpoint is missing' do
160 let(:json) { base_json.merge('service' => [service]) }
161 let(:service) {{ 'id' => '#atproto_pds', 'type' => 'AtprotoPersonalDataServer' }}
162
163 it 'should raise a format error' do
164 expect { subject.new(did, json) }.to raise_error(DIDKit::FormatError)
165 end
166 end
167
168 context 'when service entry endpoint is not a string' do
169 let(:json) { base_json.merge('service' => [service]) }
170 let(:service) {{ 'id' => '#atproto_pds', 'type' => 'AtprotoPersonalDataServer', 'serviceEndpoint' => :somewhere }}
171
172 it 'should raise a format error' do
173 expect { subject.new(did, json) }.to raise_error(DIDKit::FormatError)
174 end
175 end
176
177 context 'when service entry endpoint is not an URI' do
178 let(:json) { base_json.merge('service' => [service]) }
179 let(:service) {{ 'id' => '#atproto_pds', 'type' => 'AtprotoPersonalDataServer', 'serviceEndpoint' => '<script>alert()</script>' }}
180
181 it 'should *not* raise a format error' do
182 expect { subject.new(did, json) }.to_not raise_error
183 end
184 end
185
186 context 'when service entries are partially valid' do
187 let(:services) {
188 [
189 { 'id' => '#atproto_pds', 'type' => 'AtprotoPersonalDataServer', 'serviceEndpoint' => 'https://pds.dholms.xyz' },
190 { 'id' => 'missing_hash', 'type' => 'AtprotoPersonalDataServer', 'serviceEndpoint' => 'https://pds.dholms.xyz' },
191 { 'id' => '#wrong_endpoint', 'type' => 'AtprotoPersonalDataServer', 'serviceEndpoint' => 'this is not a url' },
192 { 'id' => '#lycan', 'type' => 'LycanService', 'serviceEndpoint' => 'https://lycan.feeds.blue' }
193 ]
194 }
195
196 let(:json) { base_json.merge('service' => services) }
197
198 it 'should only keep the valid records' do
199 doc = subject.new(did, json)
200
201 doc.services.length.should == 2
202 doc.services.map(&:key).should == ['atproto_pds', 'lycan']
203 doc.services.map(&:type).should == ['AtprotoPersonalDataServer', 'LycanService']
204 doc.services.map(&:endpoint).should == ['https://pds.dholms.xyz', 'https://lycan.feeds.blue']
205 end
206 end
207 end
208
209 describe 'service helpers' do
210 let(:service_json) {
211 base_json.merge('service' => [
212 { 'id' => '#atproto_pds', 'type' => 'AtprotoPersonalDataServer', 'serviceEndpoint' => 'https://pds.dholms.xyz' },
213 { 'id' => '#atproto_labeler', 'type' => 'AtprotoLabeler', 'serviceEndpoint' => 'https://labels.dholms.xyz' },
214 { 'id' => '#lycan', 'type' => 'LycanService', 'serviceEndpoint' => 'https://lycan.feeds.blue' }
215 ])
216 }
217
218 describe '#pds_endpoint' do
219 it 'should return the endpoint of #atproto_pds' do
220 doc = subject.new(did, service_json)
221 doc.pds_endpoint.should == 'https://pds.dholms.xyz'
222 end
223 end
224
225 describe '#pds_host' do
226 it 'should return the host part of #atproto_pds endpoint' do
227 doc = subject.new(did, service_json)
228 doc.pds_host.should == 'pds.dholms.xyz'
229 end
230 end
231
232 describe '#labeler_endpoint' do
233 it 'should return the endpoint of #atproto_labeler' do
234 doc = subject.new(did, service_json)
235 doc.labeler_endpoint.should == 'https://labels.dholms.xyz'
236 end
237 end
238
239 describe '#labeler_host' do
240 it 'should return the host part of #atproto_labeler endpoint' do
241 doc = subject.new(did, service_json)
242 doc.labeler_host.should == 'labels.dholms.xyz'
243 end
244 end
245
246 describe '#get_service' do
247 it 'should fetch a service by key and type' do
248 doc = subject.new(did, service_json)
249
250 lycan = doc.get_service('lycan', 'LycanService')
251 lycan.should_not be_nil
252 lycan.endpoint.should == 'https://lycan.feeds.blue'
253 end
254
255 it 'should return nil if none of the services match' do
256 doc = subject.new(did, service_json)
257
258 result = doc.get_service('lycan', 'AtprotoLabeler')
259 result.should be_nil
260
261 result = doc.get_service('atproto_pds', 'PDS')
262 result.should be_nil
263
264 result = doc.get_service('unknown', 'Test')
265 result.should be_nil
266 end
267 end
268
269 it 'should expose the "labeller" aliases for endpoint and host' do
270 doc = subject.new(did, service_json)
271
272 doc.labeller_endpoint.should == 'https://labels.dholms.xyz'
273 doc.labeller_host.should == 'labels.dholms.xyz'
274 end
275
276 describe 'if there is no matching service' do
277 let(:service_json) {
278 base_json.merge('service' => [
279 { 'id' => '#lycan', 'type' => 'LycanService', 'serviceEndpoint' => 'https://lycan.feeds.blue' }
280 ])
281 }
282
283 it 'should return nil from the relevant methods' do
284 doc = subject.new(did, service_json)
285
286 doc.pds_endpoint.should be_nil
287 doc.pds_host.should be_nil
288 doc.labeller_endpoint.should be_nil
289 doc.labeller_host.should be_nil
290 doc.labeler_endpoint.should be_nil
291 doc.labeler_host.should be_nil
292 end
293 end
294 end
295end