A library for handling DID identifiers used in Bluesky AT Protocol
at master 295 lines 10 kB view raw
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