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 {
43 subject.new(did, json)
44 }.to raise_error(DIDKit::FormatError)
45 end
46 end
47
48 context 'when id is not a string' do
49 let(:json) { base_json.merge('id' => 123) }
50
51 it 'should raise a format error' do
52 expect {
53 subject.new(did, json)
54 }.to raise_error(DIDKit::FormatError)
55 end
56 end
57
58 context 'when id does not match the DID' do
59 let(:json) { base_json.merge('id' => 'did:plc:notmatching') }
60
61 it 'should raise a format error' do
62 expect {
63 subject.new(did, json)
64 }.to raise_error(DIDKit::FormatError)
65 end
66 end
67
68 context 'when alsoKnownAs is not an array' do
69 let(:json) { base_json.merge('alsoKnownAs' => 'at://dholms.xyz') }
70
71 it 'should raise an AtHandles format error' do
72 expect {
73 subject.new(did, json)
74 }.to raise_error(DIDKit::FormatError)
75 end
76 end
77
78 context 'when alsoKnownAs elements are not strings' do
79 let(:json) { base_json.merge('alsoKnownAs' => [666]) }
80
81 it 'should raise an AtHandles format error' do
82 expect {
83 subject.new(did, json)
84 }.to raise_error(DIDKit::FormatError)
85 end
86 end
87
88 context 'when alsoKnownAs contains multiple handles' do
89 let(:json) {
90 base_json.merge('alsoKnownAs' => [
91 'at://dholms.xyz',
92 'https://example.com',
93 'at://other.handle'
94 ])
95 }
96
97 it 'should pick those starting with at:// and remove the prefixes' do
98 doc = subject.new(did, json)
99 doc.handles.should == ['dholms.xyz', 'other.handle']
100 end
101 end
102
103 context 'when service is not an array' do
104 let(:json) { base_json.merge('service' => 'not-an-array') }
105
106 it 'should raise a format error' do
107 expect {
108 subject.new(did, json)
109 }.to raise_error(DIDKit::FormatError)
110 end
111 end
112
113 context 'when service entries are not hashes' do
114 let(:json) { base_json.merge('service' => ['invalid']) }
115
116 it 'should raise a format error' do
117 expect {
118 subject.new(did, json)
119 }.to raise_error(DIDKit::FormatError)
120 end
121 end
122
123 context 'when service entries are partially valid' do
124 let(:services) {
125 [
126 { 'id' => '#atproto_pds', 'type' => 'AtprotoPersonalDataServer', 'serviceEndpoint' => 'https://pds.dholms.xyz' },
127 { 'id' => 'not_a_hash', 'type' => 'AtprotoPersonalDataServer', 'serviceEndpoint' => 'https://pds.dholms.xyz' },
128 { 'id' => '#wrong_type', 'type' => 123, 'serviceEndpoint' => 'https://pds.dholms.xyz' },
129 { 'id' => '#wrong_endpoint', 'type' => 'AtprotoPersonalDataServer', 'serviceEndpoint' => 123 },
130 { 'id' => '#lycan', 'type' => 'LycanService', 'serviceEndpoint' => 'https://lycan.feeds.blue' }
131 ]
132 }
133
134 let(:json) { base_json.merge('service' => services) }
135
136 it 'should only keep the valid records' do
137 doc = subject.new(did, json)
138
139 doc.services.length.should == 2
140 doc.services.map(&:key).should == ['atproto_pds', 'lycan']
141 doc.services.map(&:type).should == ['AtprotoPersonalDataServer', 'LycanService']
142 doc.services.map(&:endpoint).should == ['https://pds.dholms.xyz', 'https://lycan.feeds.blue']
143 end
144 end
145 end
146
147 describe 'service helpers' do
148 let(:service_json) {
149 base_json.merge('service' => [
150 { 'id' => '#atproto_pds', 'type' => 'AtprotoPersonalDataServer', 'serviceEndpoint' => 'https://pds.dholms.xyz' },
151 { 'id' => '#atproto_labeler', 'type' => 'AtprotoLabeler', 'serviceEndpoint' => 'https://labels.dholms.xyz' },
152 { 'id' => '#lycan', 'type' => 'LycanService', 'serviceEndpoint' => 'https://lycan.feeds.blue' }
153 ])
154 }
155
156 describe '#pds_endpoint' do
157 it 'should return the endpoint of #atproto_pds' do
158 doc = subject.new(did, service_json)
159 doc.pds_endpoint.should == 'https://pds.dholms.xyz'
160 end
161 end
162
163 describe '#pds_host' do
164 it 'should return the host part of #atproto_pds endpoint' do
165 doc = subject.new(did, service_json)
166 doc.pds_host.should == 'pds.dholms.xyz'
167 end
168 end
169
170 describe '#labeler_endpoint' do
171 it 'should return the endpoint of #atproto_labeler' do
172 doc = subject.new(did, service_json)
173 doc.labeler_endpoint.should == 'https://labels.dholms.xyz'
174 end
175 end
176
177 describe '#labeler_host' do
178 it 'should return the host part of #atproto_labeler endpoint' do
179 doc = subject.new(did, service_json)
180 doc.labeler_host.should == 'labels.dholms.xyz'
181 end
182 end
183
184 describe '#get_service' do
185 it 'should fetch a service by key and type' do
186 doc = subject.new(did, service_json)
187
188 lycan = doc.get_service('lycan', 'LycanService')
189 lycan.should_not be_nil
190 lycan.endpoint.should == 'https://lycan.feeds.blue'
191 end
192
193 it 'should return nil if none of the services match' do
194 doc = subject.new(did, service_json)
195
196 result = doc.get_service('lycan', 'AtprotoLabeler')
197 result.should be_nil
198
199 result = doc.get_service('atproto_pds', 'PDS')
200 result.should be_nil
201
202 result = doc.get_service('unknown', 'Test')
203 result.should be_nil
204 end
205 end
206
207 it 'should expose the "labeller" aliases for endpoint and host' do
208 doc = subject.new(did, service_json)
209
210 doc.labeller_endpoint.should == 'https://labels.dholms.xyz'
211 doc.labeller_host.should == 'labels.dholms.xyz'
212 end
213
214 describe 'if there is no matching service' do
215 let(:service_json) {
216 base_json.merge('service' => [
217 { 'id' => '#lycan', 'type' => 'LycanService', 'serviceEndpoint' => 'https://lycan.feeds.blue' }
218 ])
219 }
220
221 it 'should return nil from the relevant methods' do
222 doc = subject.new(did, service_json)
223
224 doc.pds_endpoint.should be_nil
225 doc.pds_host.should be_nil
226 doc.labeller_endpoint.should be_nil
227 doc.labeller_host.should be_nil
228 doc.labeler_endpoint.should be_nil
229 doc.labeler_host.should be_nil
230 end
231 end
232 end
233end