A library for handling DID identifiers used in Bluesky AT Protocol
at master 441 lines 13 kB view raw
1require 'time' 2 3describe DIDKit::PLCOperation do 4 subject { described_class } 5 6 let(:base_json) { load_did_json('bnewbold_log.json').last } 7 8 describe '#initialize' do 9 context 'with a valid plc operation' do 10 let(:json) { base_json } 11 12 it 'should return a PLCOperation with parsed data' do 13 op = subject.new(json) 14 15 op.json.should == json 16 op.type.should == :plc_operation 17 op.did.should == 'did:plc:44ybard66vv44zksje25o7dz' 18 op.cid.should == 'bafyreiaoaelqu32ngmqd2mt3v3zvek7k34cvo7lvmk3kseuuaag5eptg5m' 19 op.created_at.should be_a(Time) 20 op.created_at.should == Time.parse("2025-06-06T00:34:40.824Z") 21 op.handles.should == ['bnewbold.net'] 22 op.services.map(&:key).should == ['atproto_pds'] 23 end 24 end 25 26 context 'when argument is not a hash' do 27 let(:json) { [base_json] } 28 29 it 'should raise a format error' do 30 expect { subject.new(json) }.to raise_error(DIDKit::FormatError) 31 end 32 end 33 34 context 'when did is missing' do 35 let(:json) { base_json.tap { |h| h.delete('did') }} 36 37 it 'should raise a format error' do 38 expect { subject.new(json) }.to raise_error(DIDKit::FormatError) 39 end 40 end 41 42 context 'when did is not a string' do 43 let(:json) { base_json.merge('did' => 123) } 44 45 it 'should raise a format error' do 46 expect { subject.new(json) }.to raise_error(DIDKit::FormatError) 47 end 48 end 49 50 context "when did doesn't start with did:" do 51 let(:json) { base_json.merge('did' => 'foobar') } 52 53 it 'should raise a format error' do 54 expect { subject.new(json) }.to raise_error(DIDKit::FormatError) 55 end 56 end 57 58 context 'when cid is missing' do 59 let(:json) { base_json.tap { |h| h.delete('cid') }} 60 61 it 'should raise a format error' do 62 expect { subject.new(json) }.to raise_error(DIDKit::FormatError) 63 end 64 end 65 66 context 'when cid is not a string' do 67 let(:json) { base_json.merge('cid' => 700) } 68 69 it 'should raise a format error' do 70 expect { subject.new(json) }.to raise_error(DIDKit::FormatError) 71 end 72 end 73 74 context 'when createdAt is missing' do 75 let(:json) { base_json.tap { |h| h.delete('createdAt') }} 76 77 it 'should raise a format error' do 78 expect { subject.new(json) }.to raise_error(DIDKit::FormatError) 79 end 80 end 81 82 context 'when createdAt is invalid' do 83 let(:json) { base_json.merge('createdAt' => 123) } 84 85 it 'should raise a format error' do 86 expect { subject.new(json) }.to raise_error(DIDKit::FormatError) 87 end 88 end 89 90 context 'when operation block is missing' do 91 let(:json) { base_json.tap { |h| h.delete('operation') }} 92 93 it 'should raise a format error' do 94 expect { subject.new(json) }.to raise_error(DIDKit::FormatError) 95 end 96 end 97 98 context 'when operation block is not a hash' do 99 let(:json) { base_json.merge('operation' => 'invalid') } 100 101 it 'should raise a format error' do 102 expect { subject.new(json) }.to raise_error(DIDKit::FormatError) 103 end 104 end 105 106 context 'when operation type is missing' do 107 let(:json) { base_json.tap { |h| h['operation'].delete('type') }} 108 109 it 'should raise a format error' do 110 expect { subject.new(json) }.to raise_error(DIDKit::FormatError) 111 end 112 end 113 114 context 'when operation type is not a string' do 115 let(:json) { base_json.tap { |h| h['operation']['type'] = 5 }} 116 117 it 'should raise a format error' do 118 expect { subject.new(json) }.to raise_error(DIDKit::FormatError) 119 end 120 end 121 122 context 'when operation type is not plc_operation' do 123 let(:json) { base_json.tap { |h| h['operation']['type'] = 'other' }} 124 125 it 'should not raise an error' do 126 expect { subject.new(json) }.not_to raise_error 127 end 128 129 it 'should return the operation type' do 130 op = subject.new(json) 131 op.type.should == :other 132 end 133 134 it 'should not try to parse services' do 135 json['services'] = nil 136 137 expect { subject.new(json) }.not_to raise_error 138 end 139 140 it 'should return nil from services' do 141 op = subject.new(json) 142 op.services.should be_nil 143 end 144 145 it 'should not try to parse handles' do 146 json['alsoKnownAs'] = nil 147 148 expect { subject.new(json) }.not_to raise_error 149 end 150 151 it 'should return nil from handles' do 152 op = subject.new(json) 153 op.handles.should be_nil 154 end 155 end 156 157 context 'when alsoKnownAs is not an array' do 158 let(:json) { base_json.tap { |h| h['operation']['alsoKnownAs'] = 'at://dholms.xyz' }} 159 160 it 'should raise a format error' do 161 expect { subject.new(json) }.to raise_error(DIDKit::FormatError) 162 end 163 end 164 165 context 'when alsoKnownAs elements are not strings' do 166 let(:json) { base_json.tap { |h| h['operation']['alsoKnownAs'] = [666] }} 167 168 it 'should raise a format error' do 169 expect { subject.new(json) }.to raise_error(DIDKit::FormatError) 170 end 171 end 172 173 context 'when alsoKnownAs contains multiple handles' do 174 let(:json) { 175 base_json.tap { |h| 176 h['operation']['alsoKnownAs'] = [ 177 'at://dholms.xyz', 178 'https://example.com', 179 'at://other.handle' 180 ] 181 } 182 } 183 184 it 'should pick those starting with at:// and remove the prefixes' do 185 op = subject.new(json) 186 op.handles.should == ['dholms.xyz', 'other.handle'] 187 end 188 189 it 'should return all entries in #also_known_as' do 190 op = subject.new(json) 191 op.also_known_as.should == ['at://dholms.xyz', 'https://example.com', 'at://other.handle'] 192 end 193 end 194 195 context 'when services are missing' do 196 let(:json) { base_json.tap { |h| h['operation'].delete('services') }} 197 198 it 'should raise a format error' do 199 expect { subject.new(json) }.to raise_error(DIDKit::FormatError) 200 end 201 end 202 203 context 'when services entry is not a hash' do 204 let(:json) { 205 base_json.tap { |h| 206 h['operation']['services'] = [ 207 { 208 "id": "#atproto_pds", 209 "type": "AtprotoPersonalDataServer", 210 "serviceEndpoint": "https://pds.dholms.xyz" 211 } 212 ] 213 } 214 } 215 216 it 'should raise a format error' do 217 expect { subject.new(json) }.to raise_error(DIDKit::FormatError) 218 end 219 end 220 221 context 'when a service entry type is missing' do 222 let(:json) { 223 base_json.tap { |h| 224 h['operation']['services'] = { 225 "atproto_pds" => { "endpoint" => "https://pds.dholms.xyz" } 226 } 227 } 228 } 229 230 it 'should raise a format error' do 231 expect { subject.new(json) }.to raise_error(DIDKit::FormatError) 232 end 233 end 234 235 context 'when a service entry type is not a string' do 236 let(:json) { 237 base_json.tap { |h| 238 h['operation']['services'] = { 239 "atproto_pds" => { "type" => 77, "endpoint" => "https://pds.dholms.xyz" } 240 } 241 } 242 } 243 244 it 'should raise a format error' do 245 expect { subject.new(json) }.to raise_error(DIDKit::FormatError) 246 end 247 end 248 249 context 'when a service entry endpoint is missing' do 250 let(:json) { 251 base_json.tap { |h| 252 h['operation']['services'] = { 253 "atproto_pds" => { "type" => "AtprotoPersonalDataServer" } 254 } 255 } 256 } 257 258 it 'should raise a format error' do 259 expect { subject.new(json) }.to raise_error(DIDKit::FormatError) 260 end 261 end 262 263 context 'when a service entry endpoint is not a string' do 264 let(:json) { 265 base_json.tap { |h| 266 h['operation']['services'] = { 267 "atproto_pds" => { "type" => "AtprotoPersonalDataServer", "endpoint" => { :host => 'localhost' }} 268 } 269 } 270 } 271 272 it 'should raise a format error' do 273 expect { subject.new(json) }.to raise_error(DIDKit::FormatError) 274 end 275 end 276 277 context 'when a service entry endpoint is not an URI' do 278 let(:json) { 279 base_json.tap { |h| 280 h['operation']['services'] = { 281 "atproto_pds" => { "type" => "AtprotoPersonalDataServer", "endpoint" => "2 + 2" } 282 } 283 } 284 } 285 286 it 'should *not* raise a format error' do 287 expect { subject.new(json) }.to_not raise_error 288 end 289 end 290 291 context 'when services are valid' do 292 let(:json) { 293 base_json.tap { |h| 294 h['operation']['services'] = { 295 "atproto_pds" => { 296 "type" => "AtprotoPersonalDataServer", 297 "endpoint" => "https://pds.dholms.xyz" 298 }, 299 "atproto_labeler" => { 300 "type" => "AtprotoLabeler", 301 "endpoint" => "https://labeler.example.com" 302 }, 303 "custom_service" => { 304 "type" => "OtherService", 305 "endpoint" => "https://custom.example.com" 306 } 307 } 308 } 309 } 310 311 it 'should parse services into ServiceRecords' do 312 op = subject.new(json) 313 314 op.services.length.should == 3 315 op.services.each { |s| s.should be_a(DIDKit::ServiceRecord) } 316 317 pds, labeller, custom = op.services 318 319 pds.type.should == 'AtprotoPersonalDataServer' 320 pds.endpoint.should == 'https://pds.dholms.xyz' 321 322 labeller.type.should == 'AtprotoLabeler' 323 labeller.endpoint.should == 'https://labeler.example.com' 324 325 custom.type.should == 'OtherService' 326 custom.endpoint.should == 'https://custom.example.com' 327 end 328 329 it 'should allow fetching services by key + type' do 330 op = subject.new(json) 331 332 custom = op.get_service('custom_service', 'OtherService') 333 custom.should be_a(DIDKit::ServiceRecord) 334 custom.endpoint.should == 'https://custom.example.com' 335 end 336 337 describe '#pds_endpoint' do 338 it 'should return the endpoint of #atproto_pds' do 339 op = subject.new(json) 340 op.pds_endpoint.should == 'https://pds.dholms.xyz' 341 end 342 end 343 344 describe '#pds_host' do 345 it 'should return the host part of #atproto_pds endpoint' do 346 op = subject.new(json) 347 op.pds_host.should == 'pds.dholms.xyz' 348 end 349 end 350 351 describe '#labeler_endpoint' do 352 it 'should return the endpoint of #atproto_labeler' do 353 op = subject.new(json) 354 op.labeler_endpoint.should == 'https://labeler.example.com' 355 end 356 end 357 358 describe '#labeler_host' do 359 it 'should return the host part of #atproto_labeler endpoint' do 360 op = subject.new(json) 361 op.labeler_host.should == 'labeler.example.com' 362 end 363 end 364 365 it 'should expose the "labeller" aliases for endpoint and host' do 366 op = subject.new(json) 367 368 op.labeller_endpoint.should == 'https://labeler.example.com' 369 op.labeller_host.should == 'labeler.example.com' 370 end 371 end 372 373 context 'when services are valid but the specific ones are missing' do 374 let(:json) { 375 base_json.tap { |h| 376 h['operation']['services'] = { 377 "custom_service" => { 378 "type" => "CustomService", 379 "endpoint" => "https://custom.example.com" 380 } 381 } 382 } 383 } 384 385 it 'should parse service records' do 386 op = subject.new(json) 387 op.services.length.should == 1 388 end 389 390 describe '#get_service' do 391 it 'should return nil' do 392 op = subject.new(json) 393 other = op.get_service('other_service', 'OtherService') 394 other.should be_nil 395 end 396 end 397 398 describe '#pds_endpoint' do 399 it 'should return nil' do 400 op = subject.new(json) 401 op.pds_endpoint.should be_nil 402 op.pds_host.should be_nil 403 end 404 end 405 406 describe '#labeler_endpoint' do 407 it 'should return nil' do 408 op = subject.new(json) 409 op.labeler_endpoint.should be_nil 410 op.labeller_endpoint.should be_nil 411 op.labeler_host.should be_nil 412 op.labeller_host.should be_nil 413 end 414 end 415 end 416 417 context "when some services have endpoints that aren't valid URIs" do 418 let(:json) { 419 base_json.tap { |h| 420 h['operation']['services'] = { 421 "atproto_labeler" => { 422 "type" => "AtprotoLabeler", 423 "endpoint" => "bla bla bla" 424 }, 425 "custom_service" => { 426 "type" => "OtherService", 427 "endpoint" => "https://custom.example.com" 428 } 429 } 430 } 431 } 432 433 it 'should only return valid services' do 434 op = subject.new(json) 435 436 op.services.length.should == 1 437 op.services[0].type == 'OtherService' 438 end 439 end 440 end 441end