A library for handling DID identifiers used in Bluesky AT Protocol

added tests for Document

+247 -4
+6 -4
lib/didkit/document.rb
··· 25 25 @handles = parse_also_known_as(json['alsoKnownAs'] || []) 26 26 end 27 27 28 + def get_verified_handle 29 + Resolver.new.get_verified_handle(self) 30 + end 31 + 32 + private 33 + 28 34 def parse_services(service_data) 29 35 raise FormatError, "Invalid service data" unless service_data.is_a?(Array) && service_data.all? { |x| x.is_a?(Hash) } 30 36 ··· 39 45 end 40 46 41 47 services 42 - end 43 - 44 - def get_verified_handle 45 - Resolver.new.get_verified_handle(self) 46 48 end 47 49 end 48 50 end
+236
spec/document_spec.rb
··· 1 + describe DIDKit::Document do 2 + subject { described_class } 3 + 4 + let(:did_string) { 'did:plc:yk4dd2qkboz2yv6tpubpc6co' } 5 + let(:did) { DID.new(did_string) } 6 + let(:json_data) { load_did_json('dholms.json') } 7 + 8 + describe '#initialize' do 9 + let(:base_json) { json_data } 10 + 11 + context 'with valid input' do 12 + let(:json) { base_json } 13 + 14 + it 'should return a Document object' do 15 + doc = subject.new(did, json) 16 + 17 + doc.should be_a(DIDKit::Document) 18 + doc.did.should == did 19 + doc.json.should == json 20 + end 21 + 22 + it 'should parse services from the JSON' do 23 + doc = subject.new(did, json) 24 + 25 + doc.services.should be_an(Array) 26 + doc.services.length.should == 1 27 + 28 + doc.services[0].should be_a(DIDKit::ServiceRecord) 29 + doc.services[0].key.should == 'atproto_pds' 30 + doc.services[0].type.should == 'AtprotoPersonalDataServer' 31 + doc.services[0].endpoint.should == 'https://pds.dholms.xyz' 32 + end 33 + 34 + it 'should parse handles from the JSON' do 35 + doc = subject.new(did, json) 36 + 37 + doc.handles.should == ['dholms.xyz'] 38 + end 39 + end 40 + 41 + context 'when id is missing' do 42 + let(:json) { base_json.dup.tap { |h| h.delete('id') } } 43 + 44 + it 'should raise a format error' do 45 + expect { 46 + subject.new(did, json) 47 + }.to raise_error(DIDKit::Document::FormatError) 48 + end 49 + end 50 + 51 + context 'when id is not a string' do 52 + let(:json) { base_json.merge('id' => 123) } 53 + 54 + it 'should raise a format error' do 55 + expect { 56 + subject.new(did, json) 57 + }.to raise_error(DIDKit::Document::FormatError) 58 + end 59 + end 60 + 61 + context 'when id does not match the DID' do 62 + let(:json) { base_json.merge('id' => 'did:plc:notmatching') } 63 + 64 + it 'should raise a format error' do 65 + expect { 66 + subject.new(did, json) 67 + }.to raise_error(DIDKit::Document::FormatError) 68 + end 69 + end 70 + 71 + context 'when alsoKnownAs is not an array' do 72 + let(:json) { base_json.merge('alsoKnownAs' => 'at://dholms.xyz') } 73 + 74 + it 'should raise an AtHandles format error' do 75 + expect { 76 + subject.new(did, json) 77 + }.to raise_error(DIDKit::AtHandles::FormatError) 78 + end 79 + end 80 + 81 + context 'when alsoKnownAs elements are not strings' do 82 + let(:json) { base_json.merge('alsoKnownAs' => [666]) } 83 + 84 + it 'should raise an AtHandles format error' do 85 + expect { 86 + subject.new(did, json) 87 + }.to raise_error(DIDKit::AtHandles::FormatError) 88 + end 89 + end 90 + 91 + context 'when alsoKnownAs contains multiple handles' do 92 + let(:json) { 93 + base_json.merge('alsoKnownAs' => [ 94 + 'at://dholms.xyz', 95 + 'https://example.com', 96 + 'at://other.handle' 97 + ]) 98 + } 99 + 100 + it 'should pick those starting with at:// and remove the prefixes' do 101 + doc = subject.new(did, json) 102 + doc.handles.should == ['dholms.xyz', 'other.handle'] 103 + end 104 + end 105 + 106 + context 'when service is not an array' do 107 + let(:json) { base_json.merge('service' => 'not-an-array') } 108 + 109 + it 'should raise a format error' do 110 + expect { 111 + subject.new(did, json) 112 + }.to raise_error(DIDKit::Document::FormatError) 113 + end 114 + end 115 + 116 + context 'when service entries are not hashes' do 117 + let(:json) { base_json.merge('service' => ['invalid']) } 118 + 119 + it 'should raise a format error' do 120 + expect { 121 + subject.new(did, json) 122 + }.to raise_error(DIDKit::Document::FormatError) 123 + end 124 + end 125 + 126 + context 'when service entries are partially valid' do 127 + let(:services) { 128 + [ 129 + { 'id' => '#atproto_pds', 'type' => 'AtprotoPersonalDataServer', 'serviceEndpoint' => 'https://pds.dholms.xyz' }, 130 + { 'id' => 'not_a_hash', 'type' => 'AtprotoPersonalDataServer', 'serviceEndpoint' => 'https://pds.dholms.xyz' }, 131 + { 'id' => '#wrong_type', 'type' => 123, 'serviceEndpoint' => 'https://pds.dholms.xyz' }, 132 + { 'id' => '#wrong_endpoint', 'type' => 'AtprotoPersonalDataServer', 'serviceEndpoint' => 123 }, 133 + { 'id' => '#lycan', 'type' => 'LycanService', 'serviceEndpoint' => 'https://lycan.feeds.blue' } 134 + ] 135 + } 136 + 137 + let(:json) { base_json.merge('service' => services) } 138 + 139 + it 'should only keep the valid records' do 140 + doc = subject.new(did, json) 141 + 142 + doc.services.length.should == 2 143 + doc.services.map(&:key).should == ['atproto_pds', 'lycan'] 144 + doc.services.map(&:type).should == ['AtprotoPersonalDataServer', 'LycanService'] 145 + doc.services.map(&:endpoint).should == ['https://pds.dholms.xyz', 'https://lycan.feeds.blue'] 146 + end 147 + end 148 + end 149 + 150 + describe 'service helpers' do 151 + let(:service_json) { 152 + json_data.merge('service' => [ 153 + { 'id' => '#atproto_pds', 'type' => 'AtprotoPersonalDataServer', 'serviceEndpoint' => 'https://pds.dholms.xyz' }, 154 + { 'id' => '#atproto_labeler', 'type' => 'AtprotoLabeler', 'serviceEndpoint' => 'https://labels.dholms.xyz' }, 155 + { 'id' => '#lycan', 'type' => 'LycanService', 'serviceEndpoint' => 'https://lycan.feeds.blue' } 156 + ]) 157 + } 158 + 159 + describe '#pds_endpoint' do 160 + it 'should return the endpoint of #atproto_pds' do 161 + doc = subject.new(did, service_json) 162 + doc.pds_endpoint.should == 'https://pds.dholms.xyz' 163 + end 164 + end 165 + 166 + describe '#pds_host' do 167 + it 'should return the host part of #atproto_pds endpoint' do 168 + doc = subject.new(did, service_json) 169 + doc.pds_host.should == 'pds.dholms.xyz' 170 + end 171 + end 172 + 173 + describe '#labeler_endpoint' do 174 + it 'should return the endpoint of #atproto_labeler' do 175 + doc = subject.new(did, service_json) 176 + doc.labeler_endpoint.should == 'https://labels.dholms.xyz' 177 + end 178 + end 179 + 180 + describe '#pds_host' do 181 + it 'should return the host part of #atproto_labeler endpoint' do 182 + doc = subject.new(did, service_json) 183 + doc.labeler_host.should == 'labels.dholms.xyz' 184 + end 185 + end 186 + 187 + describe '#get_service' do 188 + it 'should fetch a service by key and type' do 189 + doc = subject.new(did, service_json) 190 + 191 + lycan = doc.get_service('lycan', 'LycanService') 192 + lycan.should_not be_nil 193 + lycan.endpoint.should == 'https://lycan.feeds.blue' 194 + end 195 + 196 + it 'should return nil if none of the services match' do 197 + doc = subject.new(did, service_json) 198 + 199 + result = doc.get_service('lycan', 'AtprotoLabeler') 200 + result.should be_nil 201 + 202 + result = doc.get_service('atproto_pds', 'PDS') 203 + result.should be_nil 204 + 205 + result = doc.get_service('unknown', 'Test') 206 + result.should be_nil 207 + end 208 + end 209 + 210 + it 'should expose the "labeller" aliases for endpoint and host' do 211 + doc = subject.new(did, service_json) 212 + 213 + doc.labeller_endpoint.should == 'https://labels.dholms.xyz' 214 + doc.labeller_host.should == 'labels.dholms.xyz' 215 + end 216 + 217 + describe 'if there is no matching service' do 218 + let(:service_json) { 219 + json_data.merge('service' => [ 220 + { 'id' => '#lycan', 'type' => 'LycanService', 'serviceEndpoint' => 'https://lycan.feeds.blue' } 221 + ]) 222 + } 223 + 224 + it 'should return nil from the relevant methods' do 225 + doc = subject.new(did, service_json) 226 + 227 + doc.pds_endpoint.should be_nil 228 + doc.pds_host.should be_nil 229 + doc.labeller_endpoint.should be_nil 230 + doc.labeller_host.should be_nil 231 + doc.labeler_endpoint.should be_nil 232 + doc.labeler_host.should be_nil 233 + end 234 + end 235 + end 236 + end
+5
spec/spec_helper.rb
··· 1 1 # frozen_string_literal: true 2 2 3 3 require 'didkit' 4 + require 'json' 4 5 require 'webmock/rspec' 5 6 6 7 RSpec.configure do |config| ··· 21 22 def load_did_file(name) 22 23 File.read(File.join(__dir__, 'dids', name)) 23 24 end 25 + 26 + def load_did_json(name) 27 + JSON.parse(load_did_file(name)) 28 + end