5
fork

Configure Feed

Select the types of activity you want to include in your feed.

add tests for uri

+1112 -1
+3
src/atpasser/uri/__init__.py
··· 144 144 "JSONPath parsing failed", 145 145 f"Failed to parse JSONPath fragment '{fragment}': {str(e)}", 146 146 ) 147 + else: 148 + self.fragment = None 149 + self.fragmentAsText = None 147 150 148 151 if query != None: 149 152 try:
+1 -1
src/atpasser/uri/identifier.py
··· 197 197 "Handle's top-level domain cannot start with a digit", 198 198 ) 199 199 200 - self.handle = handle 200 + self.handle = handle.lower() 201 201 202 202 def __str__(self) -> str: 203 203 """Convert the Handle object to its string representation.
+1
tests/__init__.py
··· 1 + """Test package for atpasser."""
+1
tests/uri/__init__.py
··· 1 + """Test package for atpasser.uri module."""
+137
tests/uri/test_did.py
··· 1 + """Test cases for the DID class in atpasser.uri.identifier module.""" 2 + 3 + import pytest 4 + from atpasser.uri.identifier import DID 5 + from atpasser.uri.exceptions import InvalidDIDError, ResolutionError 6 + 7 + 8 + class TestDID: 9 + """Test cases for the DID class.""" 10 + 11 + def test_valid_did_plc(self): 12 + """Test creating a DID with a valid did:plc format.""" 13 + did_str = "did:plc:z72i7hdynmk6r22z27h6tvur" 14 + did = DID(did_str) 15 + 16 + assert str(did) == did_str 17 + assert did.uri == did_str 18 + 19 + def test_valid_did_web(self): 20 + """Test creating a DID with a valid did:web format.""" 21 + did_str = "did:web:blueskyweb.xyz" 22 + did = DID(did_str) 23 + 24 + assert str(did) == did_str 25 + assert did.uri == did_str 26 + 27 + def test_valid_did_with_various_characters(self): 28 + """Test creating a DID with various valid characters.""" 29 + did_str = "did:method:val:two-with_underscores.and-dashes" 30 + did = DID(did_str) 31 + 32 + assert str(did) == did_str 33 + assert did.uri == did_str 34 + 35 + def test_invalid_did_wrong_format(self): 36 + """Test that a DID with wrong format raises InvalidDIDError.""" 37 + did_str = "not-a-did" 38 + 39 + with pytest.raises(InvalidDIDError, match="invalid format"): 40 + DID(did_str) 41 + 42 + def test_invalid_did_uppercase_method(self): 43 + """Test that a DID with uppercase method raises InvalidDIDError.""" 44 + did_str = "did:METHOD:val" 45 + 46 + with pytest.raises(InvalidDIDError, match="invalid format"): 47 + DID(did_str) 48 + 49 + def test_invalid_did_method_with_numbers(self): 50 + """Test that a DID with method containing numbers raises InvalidDIDError.""" 51 + did_str = "did:m123:val" 52 + 53 + with pytest.raises(InvalidDIDError, match="invalid format"): 54 + DID(did_str) 55 + 56 + def test_invalid_did_empty_identifier(self): 57 + """Test that a DID with empty identifier raises InvalidDIDError.""" 58 + did_str = "did:method:" 59 + 60 + with pytest.raises(InvalidDIDError, match="invalid format"): 61 + DID(did_str) 62 + 63 + def test_invalid_did_ends_with_colon(self): 64 + """Test that a DID ending with colon raises InvalidDIDError.""" 65 + did_str = "did:method:val:" 66 + 67 + with pytest.raises(InvalidDIDError, match="invalid format"): 68 + DID(did_str) 69 + 70 + def test_invalid_did_too_long(self): 71 + """Test that a DID that is too long raises InvalidDIDError.""" 72 + # Create a DID that exceeds the 2048 character limit 73 + long_identifier = "a" * 2040 74 + did_str = f"did:method:{long_identifier}" 75 + 76 + with pytest.raises(InvalidDIDError, match="exceeds maximum length"): 77 + DID(did_str) 78 + 79 + def test_did_equality(self): 80 + """Test DID equality comparison.""" 81 + did_str = "did:plc:z72i7hdynmk6r22z27h6tvur" 82 + did1 = DID(did_str) 83 + did2 = DID(did_str) 84 + 85 + assert did1 == did2 86 + assert did1 != "not a did object" 87 + 88 + def test_did_string_representation(self): 89 + """Test DID string representation.""" 90 + did_str = "did:plc:z72i7hdynmk6r22z27h6tvur" 91 + did = DID(did_str) 92 + 93 + assert str(did) == did_str 94 + 95 + def test_did_fetch_plc_method(self): 96 + """Test fetching a DID document for did:plc method.""" 97 + did_str = "did:plc:z72i7hdynmk6r22z27h6tvur" 98 + did = DID(did_str) 99 + 100 + # This test may fail if there's no internet connection or if the PLC directory is down 101 + try: 102 + document = did.fetch() 103 + assert isinstance(document, list) 104 + assert len(document) > 0 105 + except ResolutionError: 106 + # If resolution fails, we'll skip this test 107 + pytest.skip("Failed to resolve DID document") 108 + 109 + def test_did_fetch_web_method(self): 110 + """Test fetching a DID document for did:web method.""" 111 + did_str = "did:web:blueskyweb.xyz" 112 + did = DID(did_str) 113 + 114 + # This test may fail if there's no internet connection or if the web server is down 115 + try: 116 + document = did.fetch() 117 + assert isinstance(document, list) 118 + assert len(document) > 0 119 + except ResolutionError: 120 + # If resolution fails, we'll skip this test 121 + pytest.skip("Failed to resolve DID document") 122 + 123 + def test_did_fetch_unsupported_method(self): 124 + """Test that fetching a DID document with unsupported method raises InvalidDIDError.""" 125 + did_str = "did:unsupported:method" 126 + did = DID(did_str) 127 + 128 + with pytest.raises(InvalidDIDError, match="unsupported DID method"): 129 + did.fetch() 130 + 131 + def test_did_fetch_web_empty_domain(self): 132 + """Test that fetching a DID document with empty domain raises InvalidDIDError.""" 133 + did_str = "did:web:" 134 + did = DID(did_str) 135 + 136 + with pytest.raises(InvalidDIDError, match="invalid format"): 137 + did.fetch()
+184
tests/uri/test_handle.py
··· 1 + """Test cases for the Handle class in atpasser.uri.identifier module.""" 2 + 3 + import pytest 4 + from atpasser.uri.identifier import Handle 5 + from atpasser.uri.exceptions import InvalidHandleError, ResolutionError 6 + 7 + 8 + class TestHandle: 9 + """Test cases for the Handle class.""" 10 + 11 + def test_valid_handle_simple(self): 12 + """Test creating a Handle with a valid simple format.""" 13 + handle_str = "example.com" 14 + handle = Handle(handle_str) 15 + 16 + assert str(handle) == handle_str 17 + assert handle.handle == handle_str 18 + 19 + def test_valid_handle_subdomain(self): 20 + """Test creating a Handle with a valid subdomain format.""" 21 + handle_str = "subdomain.example.com" 22 + handle = Handle(handle_str) 23 + 24 + assert str(handle) == handle_str 25 + assert handle.handle == handle_str 26 + 27 + def test_valid_handle_with_hyphen(self): 28 + """Test creating a Handle with a valid format containing hyphens.""" 29 + handle_str = "my-example.com" 30 + handle = Handle(handle_str) 31 + 32 + assert str(handle) == handle_str 33 + assert handle.handle == handle_str 34 + 35 + def test_valid_handle_with_numbers(self): 36 + """Test creating a Handle with a valid format containing numbers.""" 37 + handle_str = "example123.com" 38 + handle = Handle(handle_str) 39 + 40 + assert str(handle) == handle_str 41 + assert handle.handle == handle_str 42 + 43 + def test_valid_handle_long_domain(self): 44 + """Test creating a Handle with a valid long domain name.""" 45 + handle_str = "a" * 63 + "." + "b" * 63 + "." + "c" * 63 + ".com" 46 + handle = Handle(handle_str) 47 + 48 + assert str(handle) == handle_str 49 + assert handle.handle == handle_str 50 + 51 + def test_invalid_handle_too_long(self): 52 + """Test that a Handle that is too long raises InvalidHandleError.""" 53 + # Create a handle that exceeds the 253 character limit 54 + long_handle = "a" * 254 55 + handle_str = f"{long_handle}.com" 56 + 57 + with pytest.raises(InvalidHandleError, match="exceeds maximum length"): 58 + Handle(handle_str) 59 + 60 + def test_invalid_handle_no_dot_separator(self): 61 + """Test that a Handle without a dot separator raises InvalidHandleError.""" 62 + handle_str = "example" 63 + 64 + with pytest.raises(InvalidHandleError, match="invalid format"): 65 + Handle(handle_str) 66 + 67 + def test_invalid_handle_starts_with_dot(self): 68 + """Test that a Handle starting with a dot raises InvalidHandleError.""" 69 + handle_str = ".example.com" 70 + 71 + with pytest.raises(InvalidHandleError, match="invalid format"): 72 + Handle(handle_str) 73 + 74 + def test_invalid_handle_ends_with_dot(self): 75 + """Test that a Handle ending with a dot raises InvalidHandleError.""" 76 + handle_str = "example.com." 77 + 78 + with pytest.raises(InvalidHandleError, match="invalid format"): 79 + Handle(handle_str) 80 + 81 + def test_invalid_handle_segment_too_long(self): 82 + """Test that a Handle with a segment that is too long raises InvalidHandleError.""" 83 + handle_str = f"{'a' * 64}.com" 84 + 85 + with pytest.raises(InvalidHandleError, match="segment length error"): 86 + Handle(handle_str) 87 + 88 + def test_invalid_handle_segment_empty(self): 89 + """Test that a Handle with an empty segment raises InvalidHandleError.""" 90 + handle_str = "example..com" 91 + 92 + with pytest.raises(InvalidHandleError, match="segment length error"): 93 + Handle(handle_str) 94 + 95 + def test_invalid_handle_invalid_characters(self): 96 + """Test that a Handle with invalid characters raises InvalidHandleError.""" 97 + handle_str = "ex@mple.com" 98 + 99 + with pytest.raises(InvalidHandleError, match="contains invalid characters"): 100 + Handle(handle_str) 101 + 102 + def test_invalid_handle_segment_starts_with_hyphen(self): 103 + """Test that a Handle with a segment starting with a hyphen raises InvalidHandleError.""" 104 + handle_str = "-example.com" 105 + 106 + with pytest.raises(InvalidHandleError, match="invalid format"): 107 + Handle(handle_str) 108 + 109 + def test_invalid_handle_segment_ends_with_hyphen(self): 110 + """Test that a Handle with a segment ending with a hyphen raises InvalidHandleError.""" 111 + handle_str = "example-.com" 112 + 113 + with pytest.raises(InvalidHandleError, match="invalid format"): 114 + Handle(handle_str) 115 + 116 + def test_invalid_handle_tld_starts_with_digit(self): 117 + """Test that a Handle with a TLD starting with a digit raises InvalidHandleError.""" 118 + handle_str = "example.1com" 119 + 120 + with pytest.raises(InvalidHandleError, match="invalid format"): 121 + Handle(handle_str) 122 + 123 + def test_handle_equality(self): 124 + """Test Handle equality comparison.""" 125 + handle_str = "example.com" 126 + handle1 = Handle(handle_str) 127 + handle2 = Handle(handle_str) 128 + 129 + assert handle1 == handle2 130 + assert handle1 != "not a handle object" 131 + 132 + def test_handle_string_representation(self): 133 + """Test Handle string representation.""" 134 + handle_str = "example.com" 135 + handle = Handle(handle_str) 136 + 137 + assert str(handle) == handle_str 138 + 139 + def test_handle_case_insensitive_storage(self): 140 + """Test that Handle stores the handle in lowercase.""" 141 + handle_str = "ExAmPlE.CoM" 142 + handle = Handle(handle_str) 143 + 144 + # The handle should be stored in lowercase 145 + assert handle.handle == "example.com" 146 + # The string representation should also return the lowercase form 147 + assert str(handle) == "example.com" 148 + 149 + def test_handle_to_tid_dns_resolution(self): 150 + """Test resolving a handle to DID using DNS method.""" 151 + handle_str = "bsky.app" 152 + handle = Handle(handle_str) 153 + 154 + # This test may fail if there's no internet connection or if DNS resolution fails 155 + try: 156 + did = handle.toTID() 157 + assert did is not None 158 + assert str(did).startswith("did:") 159 + except ResolutionError: 160 + # If resolution fails, we'll skip this test 161 + pytest.skip("Failed to resolve handle via DNS") 162 + 163 + def test_handle_to_tid_http_resolution(self): 164 + """Test resolving a handle to DID using HTTP method.""" 165 + handle_str = "blueskyweb.xyz" 166 + handle = Handle(handle_str) 167 + 168 + # This test may fail if there's no internet connection or if HTTP resolution fails 169 + try: 170 + did = handle.toTID() 171 + assert did is not None 172 + assert str(did).startswith("did:") 173 + except ResolutionError: 174 + # If resolution fails, we'll skip this test 175 + pytest.skip("Failed to resolve handle via HTTP") 176 + 177 + def test_handle_to_tid_unresolvable(self): 178 + """Test resolving an unresolvable handle returns None.""" 179 + handle_str = "nonexistent-domain-12345.com" 180 + handle = Handle(handle_str) 181 + 182 + # This should return None for a non-existent domain 183 + did = handle.toTID() 184 + assert did is None
+248
tests/uri/test_nsid.py
··· 1 + """Test cases for the NSID class in atpasser.uri.nsid module.""" 2 + 3 + import pytest 4 + from atpasser.uri.nsid import NSID 5 + from atpasser.uri.exceptions import InvalidNSIDError, ValidationError 6 + 7 + 8 + class TestNSID: 9 + """Test cases for the NSID class.""" 10 + 11 + def test_valid_nsid_simple(self): 12 + """Test creating an NSID with a valid simple format.""" 13 + nsid_str = "com.example.recordName" 14 + nsid = NSID(nsid_str) 15 + 16 + assert str(nsid) == nsid_str 17 + assert nsid.nsid == nsid_str 18 + assert nsid.domainAuthority == ["com", "example"] 19 + assert nsid.domainAuthorityAsText == "com.example" 20 + assert nsid.name == "recordName" 21 + assert nsid.fragment is None 22 + 23 + def test_valid_nsid_with_fragment(self): 24 + """Test creating an NSID with a valid fragment.""" 25 + nsid_str = "com.example.recordName#fragment" 26 + nsid = NSID(nsid_str) 27 + 28 + assert str(nsid) == nsid_str 29 + assert nsid.nsid == nsid_str 30 + assert nsid.domainAuthority == ["com", "example"] 31 + assert nsid.domainAuthorityAsText == "com.example" 32 + assert nsid.name == "recordName" 33 + assert nsid.fragment == "fragment" 34 + 35 + def test_valid_nsid_multiple_segments(self): 36 + """Test creating an NSID with multiple domain segments.""" 37 + nsid_str = "net.users.bob.ping" 38 + nsid = NSID(nsid_str) 39 + 40 + assert str(nsid) == nsid_str 41 + assert nsid.nsid == nsid_str 42 + assert nsid.domainAuthority == ["net", "users", "bob"] 43 + assert nsid.domainAuthorityAsText == "net.users.bob" 44 + assert nsid.name == "ping" 45 + assert nsid.fragment is None 46 + 47 + def test_valid_nsid_with_hyphens(self): 48 + """Test creating an NSID with hyphens in domain segments.""" 49 + nsid_str = "a-0.b-1.c.recordName" 50 + nsid = NSID(nsid_str) 51 + 52 + assert str(nsid) == nsid_str 53 + assert nsid.nsid == nsid_str 54 + assert nsid.domainAuthority == ["a-0", "b-1", "c"] 55 + assert nsid.domainAuthorityAsText == "a-0.b-1.c" 56 + assert nsid.name == "recordName" 57 + assert nsid.fragment is None 58 + 59 + def test_valid_nsid_case_sensitivity(self): 60 + """Test creating an NSID with case-sensitive name.""" 61 + nsid_str = "com.example.fooBar" 62 + nsid = NSID(nsid_str) 63 + 64 + assert str(nsid) == nsid_str 65 + assert nsid.nsid == nsid_str 66 + assert nsid.domainAuthority == ["com", "example"] 67 + assert nsid.domainAuthorityAsText == "com.example" 68 + assert nsid.name == "fooBar" 69 + assert nsid.fragment is None 70 + 71 + def test_valid_nsid_with_numbers_in_name(self): 72 + """Test creating an NSID with numbers in the name.""" 73 + nsid_str = "com.example.record123" 74 + nsid = NSID(nsid_str) 75 + 76 + assert str(nsid) == nsid_str 77 + assert nsid.nsid == nsid_str 78 + assert nsid.domainAuthority == ["com", "example"] 79 + assert nsid.domainAuthorityAsText == "com.example" 80 + assert nsid.name == "record123" 81 + assert nsid.fragment is None 82 + 83 + def test_invalid_nsid_non_ascii_characters(self): 84 + """Test that an NSID with non-ASCII characters raises InvalidNSIDError.""" 85 + nsid_str = "com.exa💩ple.thing" 86 + 87 + with pytest.raises(InvalidNSIDError, match="contains invalid characters"): 88 + NSID(nsid_str) 89 + 90 + def test_invalid_nsid_too_long(self): 91 + """Test that an NSID that is too long raises InvalidNSIDError.""" 92 + # Create an NSID that exceeds the 317 character limit 93 + long_segment = "a" * 100 94 + nsid_str = f"{long_segment}.{long_segment}.{long_segment}.recordName" 95 + 96 + with pytest.raises(InvalidNSIDError, match="domain authority length exceeds limit"): 97 + NSID(nsid_str) 98 + 99 + def test_invalid_nsid_starts_with_dot(self): 100 + """Test that an NSID starting with a dot raises InvalidNSIDError.""" 101 + nsid_str = ".com.example.recordName" 102 + 103 + with pytest.raises(InvalidNSIDError, match="invalid format"): 104 + NSID(nsid_str) 105 + 106 + def test_invalid_nsid_ends_with_dot(self): 107 + """Test that an NSID ending with a dot raises InvalidNSIDError.""" 108 + nsid_str = "com.example.recordName." 109 + 110 + with pytest.raises(InvalidNSIDError, match="invalid format"): 111 + NSID(nsid_str) 112 + 113 + def test_invalid_nsid_too_few_segments(self): 114 + """Test that an NSID with too few segments raises InvalidNSIDError.""" 115 + nsid_str = "com.example" 116 + 117 + with pytest.raises(InvalidNSIDError, match="invalid format"): 118 + NSID(nsid_str) 119 + 120 + def test_invalid_nsid_domain_authority_too_long(self): 121 + """Test that an NSID with domain authority that is too long raises InvalidNSIDError.""" 122 + # Create a domain authority that exceeds the 253 character limit 123 + long_segment = "a" * 63 124 + nsid_str = f"{long_segment}.{long_segment}.{long_segment}.{long_segment}.recordName" 125 + 126 + with pytest.raises(InvalidNSIDError, match="domain authority length exceeds limit"): 127 + NSID(nsid_str) 128 + 129 + def test_invalid_nsid_domain_segment_too_long(self): 130 + """Test that an NSID with a domain segment that is too long raises InvalidNSIDError.""" 131 + nsid_str = f"{'a' * 64}.example.recordName" 132 + 133 + with pytest.raises(InvalidNSIDError, match="segment length error"): 134 + NSID(nsid_str) 135 + 136 + def test_invalid_nsid_domain_segment_empty(self): 137 + """Test that an NSID with an empty domain segment raises InvalidNSIDError.""" 138 + nsid_str = "com..example.recordName" 139 + 140 + with pytest.raises(InvalidNSIDError, match="segment length error"): 141 + NSID(nsid_str) 142 + 143 + def test_invalid_nsid_domain_invalid_characters(self): 144 + """Test that an NSID with invalid characters in domain raises InvalidNSIDError.""" 145 + nsid_str = "com.ex@mple.recordName" 146 + 147 + with pytest.raises(InvalidNSIDError, match="contains invalid characters"): 148 + NSID(nsid_str) 149 + 150 + def test_invalid_nsid_domain_segment_starts_with_hyphen(self): 151 + """Test that an NSID with a domain segment starting with a hyphen raises InvalidNSIDError.""" 152 + nsid_str = "com.-example.recordName" 153 + 154 + with pytest.raises(InvalidNSIDError, match="invalid format"): 155 + NSID(nsid_str) 156 + 157 + def test_invalid_nsid_domain_segment_ends_with_hyphen(self): 158 + """Test that an NSID with a domain segment ending with a hyphen raises InvalidNSIDError.""" 159 + nsid_str = "com.example-.recordName" 160 + 161 + with pytest.raises(InvalidNSIDError, match="invalid format"): 162 + NSID(nsid_str) 163 + 164 + def test_invalid_nsid_tld_starts_with_digit(self): 165 + """Test that an NSID with a TLD starting with a digit raises InvalidNSIDError.""" 166 + nsid_str = "1com.example.recordName" 167 + 168 + with pytest.raises(InvalidNSIDError, match="invalid format"): 169 + NSID(nsid_str) 170 + 171 + def test_invalid_nsid_name_empty(self): 172 + """Test that an NSID with an empty name raises InvalidNSIDError.""" 173 + nsid_str = "com.example." 174 + 175 + with pytest.raises(InvalidNSIDError, match="invalid format"): 176 + NSID(nsid_str) 177 + 178 + def test_invalid_nsid_name_too_long(self): 179 + """Test that an NSID with a name that is too long raises InvalidNSIDError.""" 180 + nsid_str = f"com.example.{'a' * 64}" 181 + 182 + with pytest.raises(InvalidNSIDError, match="name length error"): 183 + NSID(nsid_str) 184 + 185 + def test_invalid_nsid_name_invalid_characters(self): 186 + """Test that an NSID with invalid characters in name raises InvalidNSIDError.""" 187 + nsid_str = "com.example.record-name" 188 + 189 + with pytest.raises(InvalidNSIDError, match="contains invalid characters"): 190 + NSID(nsid_str) 191 + 192 + def test_invalid_nsid_name_starts_with_digit(self): 193 + """Test that an NSID with a name starting with a digit raises InvalidNSIDError.""" 194 + nsid_str = "com.example.1record" 195 + 196 + with pytest.raises(InvalidNSIDError, match="invalid format"): 197 + NSID(nsid_str) 198 + 199 + def test_invalid_nsid_fragment_empty(self): 200 + """Test that an NSID with an empty fragment raises InvalidNSIDError.""" 201 + nsid_str = "com.example.recordName#" 202 + 203 + with pytest.raises(InvalidNSIDError, match="fragment length error"): 204 + NSID(nsid_str) 205 + 206 + def test_invalid_nsid_fragment_too_long(self): 207 + """Test that an NSID with a fragment that is too long raises InvalidNSIDError.""" 208 + nsid_str = f"com.example.recordName#{'a' * 64}" 209 + 210 + with pytest.raises(InvalidNSIDError, match="fragment length error"): 211 + NSID(nsid_str) 212 + 213 + def test_invalid_nsid_fragment_invalid_characters(self): 214 + """Test that an NSID with invalid characters in fragment raises InvalidNSIDError.""" 215 + nsid_str = "com.example.recordName#fragment-with-hyphen" 216 + 217 + with pytest.raises(InvalidNSIDError, match="contains invalid characters"): 218 + NSID(nsid_str) 219 + 220 + def test_invalid_nsid_fragment_starts_with_digit(self): 221 + """Test that an NSID with a fragment starting with a digit raises InvalidNSIDError.""" 222 + nsid_str = "com.example.recordName#1fragment" 223 + 224 + with pytest.raises(InvalidNSIDError, match="invalid format"): 225 + NSID(nsid_str) 226 + 227 + def test_nsid_equality(self): 228 + """Test NSID equality comparison.""" 229 + nsid_str = "com.example.recordName" 230 + nsid1 = NSID(nsid_str) 231 + nsid2 = NSID(nsid_str) 232 + 233 + assert nsid1 == nsid2 234 + assert nsid1 != "not an nsid object" 235 + 236 + def test_nsid_string_representation(self): 237 + """Test NSID string representation.""" 238 + nsid_str = "com.example.recordName" 239 + nsid = NSID(nsid_str) 240 + 241 + assert str(nsid) == nsid_str 242 + 243 + def test_nsid_string_representation_with_fragment(self): 244 + """Test NSID string representation with fragment.""" 245 + nsid_str = "com.example.recordName#fragment" 246 + nsid = NSID(nsid_str) 247 + 248 + assert str(nsid) == nsid_str
+110
tests/uri/test_restricted_uri.py
··· 1 + """Test cases for the RestrictedURI class in atpasser.uri module.""" 2 + 3 + import pytest 4 + from atpasser.uri import RestrictedURI 5 + from atpasser.uri.exceptions import InvalidRestrictedURIError, InvalidURIError 6 + 7 + 8 + class TestRestrictedURI: 9 + """Test cases for the RestrictedURI class.""" 10 + 11 + def test_valid_restricted_uri_with_did_collection_and_rkey(self): 12 + """Test creating a RestrictedURI with a valid DID, collection, and rkey.""" 13 + uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3jwdwj2ctlk26" 14 + uri = RestrictedURI(uri_str) 15 + 16 + assert str(uri) == uri_str 17 + assert uri.authorityAsText == "did:plc:z72i7hdynmk6r22z27h6tvur" 18 + assert uri.path == ["app.bsky.feed.post", "3jwdwj2ctlk26"] 19 + assert uri.pathAsText == "app.bsky.feed.post/3jwdwj2ctlk26" 20 + assert uri.collection is not None 21 + assert str(uri.collection) == "app.bsky.feed.post" 22 + assert uri.rkey is not None 23 + assert str(uri.rkey) == "3jwdwj2ctlk26" 24 + 25 + def test_valid_restricted_uri_with_handle_collection_and_rkey(self): 26 + """Test creating a RestrictedURI with a valid handle, collection, and rkey.""" 27 + uri_str = "at://bnewbold.bsky.team/app.bsky.feed.post/3jwdwj2ctlk26" 28 + uri = RestrictedURI(uri_str) 29 + 30 + assert str(uri) == uri_str 31 + assert uri.authorityAsText == "bnewbold.bsky.team" 32 + assert uri.path == ["app.bsky.feed.post", "3jwdwj2ctlk26"] 33 + assert uri.collection is not None 34 + assert str(uri.collection) == "app.bsky.feed.post" 35 + assert uri.rkey is not None 36 + assert str(uri.rkey) == "3jwdwj2ctlk26" 37 + 38 + def test_valid_restricted_uri_with_collection_only(self): 39 + """Test creating a RestrictedURI with only a collection.""" 40 + uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post" 41 + uri = RestrictedURI(uri_str) 42 + 43 + assert str(uri) == uri_str 44 + assert uri.authorityAsText == "did:plc:z72i7hdynmk6r22z27h6tvur" 45 + assert uri.path == ["app.bsky.feed.post"] 46 + assert uri.collection is not None 47 + assert str(uri.collection) == "app.bsky.feed.post" 48 + assert uri.rkey is None 49 + 50 + def test_valid_restricted_uri_with_authority_only(self): 51 + """Test creating a RestrictedURI with only an authority.""" 52 + uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur" 53 + uri = RestrictedURI(uri_str) 54 + 55 + assert str(uri) == uri_str 56 + assert uri.authorityAsText == "did:plc:z72i7hdynmk6r22z27h6tvur" 57 + assert uri.path == [] 58 + assert uri.collection is None 59 + assert uri.rkey is None 60 + 61 + def test_invalid_restricted_uri_with_query(self): 62 + """Test that a RestrictedURI with query parameters raises InvalidRestrictedURIError.""" 63 + uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post?param1=value1" 64 + 65 + with pytest.raises(InvalidRestrictedURIError, match="query parameters not supported"): 66 + RestrictedURI(uri_str) 67 + 68 + def test_invalid_restricted_uri_with_fragment(self): 69 + """Test that a RestrictedURI with a fragment raises InvalidRestrictedURIError.""" 70 + uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3jwdwj2ctlk26#$.some.json.path" 71 + 72 + with pytest.raises(InvalidRestrictedURIError, match="fragments not supported"): 73 + RestrictedURI(uri_str) 74 + 75 + def test_invalid_restricted_uri_with_invalid_authority(self): 76 + """Test that a RestrictedURI with invalid authority raises InvalidRestrictedURIError.""" 77 + uri_str = "at://invalid_authority/app.bsky.feed.post/3jwdwj2ctlk26" 78 + 79 + with pytest.raises(InvalidRestrictedURIError, match="invalid authority"): 80 + RestrictedURI(uri_str) 81 + 82 + def test_invalid_restricted_uri_too_many_path_segments(self): 83 + """Test that a RestrictedURI with too many path segments raises InvalidRestrictedURIError.""" 84 + uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3jwdwj2ctlk26/extra" 85 + 86 + with pytest.raises(InvalidRestrictedURIError, match="too many path segments"): 87 + RestrictedURI(uri_str) 88 + 89 + def test_invalid_restricted_uri_base_uri_validation_failure(self): 90 + """Test that a RestrictedURI with invalid base URI raises InvalidURIError.""" 91 + uri_str = "https://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post" 92 + 93 + with pytest.raises(InvalidURIError, match="invalid format"): 94 + RestrictedURI(uri_str) 95 + 96 + def test_restricted_uri_equality(self): 97 + """Test RestrictedURI equality comparison.""" 98 + uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3jwdwj2ctlk26" 99 + uri1 = RestrictedURI(uri_str) 100 + uri2 = RestrictedURI(uri_str) 101 + 102 + assert uri1 == uri2 103 + assert uri1 != "not a uri object" 104 + 105 + def test_restricted_uri_string_representation(self): 106 + """Test RestrictedURI string representation.""" 107 + uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3jwdwj2ctlk26" 108 + uri = RestrictedURI(uri_str) 109 + 110 + assert str(uri) == uri_str
+305
tests/uri/test_rkey.py
··· 1 + """Test cases for the RKey and TID classes in atpasser.uri.rkey module.""" 2 + 3 + import pytest 4 + import datetime 5 + from atpasser.uri.rkey import RKey, TID, importTIDfromInteger, importTIDfromBase32 6 + from atpasser.uri.exceptions import InvalidRKeyError 7 + 8 + 9 + class TestRKey: 10 + """Test cases for the RKey class.""" 11 + 12 + def test_valid_rkey_simple(self): 13 + """Test creating an RKey with a valid simple format.""" 14 + rkey_str = "3jui7kd54zh2y" 15 + rkey = RKey(rkey_str) 16 + 17 + assert str(rkey) == rkey_str 18 + assert rkey.recordKey == rkey_str 19 + 20 + def test_valid_rkey_with_various_characters(self): 21 + """Test creating an RKey with various valid characters.""" 22 + rkey_str = "example.com" 23 + rkey = RKey(rkey_str) 24 + 25 + assert str(rkey) == rkey_str 26 + assert rkey.recordKey == rkey_str 27 + 28 + def test_valid_rkey_with_special_characters(self): 29 + """Test creating an RKey with valid special characters.""" 30 + rkey_str = "~1.2-3_" 31 + rkey = RKey(rkey_str) 32 + 33 + assert str(rkey) == rkey_str 34 + assert rkey.recordKey == rkey_str 35 + 36 + def test_valid_rkey_with_colon(self): 37 + """Test creating an RKey with a colon.""" 38 + rkey_str = "pre:fix" 39 + rkey = RKey(rkey_str) 40 + 41 + assert str(rkey) == rkey_str 42 + assert rkey.recordKey == rkey_str 43 + 44 + def test_valid_rkey_underscore(self): 45 + """Test creating an RKey with just an underscore.""" 46 + rkey_str = "_" 47 + rkey = RKey(rkey_str) 48 + 49 + assert str(rkey) == rkey_str 50 + assert rkey.recordKey == rkey_str 51 + 52 + def test_invalid_rkey_empty(self): 53 + """Test that an empty RKey raises InvalidRKeyError.""" 54 + rkey_str = "" 55 + 56 + with pytest.raises(InvalidRKeyError, match="record key is empty"): 57 + RKey(rkey_str) 58 + 59 + def test_invalid_rkey_too_long(self): 60 + """Test that an RKey that is too long raises InvalidRKeyError.""" 61 + # Create an RKey that exceeds the 512 character limit 62 + rkey_str = "a" * 513 63 + 64 + with pytest.raises(InvalidRKeyError, match="exceeds maximum length"): 65 + RKey(rkey_str) 66 + 67 + def test_invalid_rkey_reserved_double_dot(self): 68 + """Test that an RKey with '..' raises InvalidRKeyError.""" 69 + rkey_str = ".." 70 + 71 + with pytest.raises(InvalidRKeyError, match="reserved value"): 72 + RKey(rkey_str) 73 + 74 + def test_invalid_rkey_reserved_single_dot(self): 75 + """Test that an RKey with '.' raises InvalidRKeyError.""" 76 + rkey_str = "." 77 + 78 + with pytest.raises(InvalidRKeyError, match="reserved value"): 79 + RKey(rkey_str) 80 + 81 + def test_invalid_rkey_invalid_characters(self): 82 + """Test that an RKey with invalid characters raises InvalidRKeyError.""" 83 + rkey_str = "alpha/beta" 84 + 85 + with pytest.raises(InvalidRKeyError, match="contains invalid characters"): 86 + RKey(rkey_str) 87 + 88 + def test_invalid_rkey_hash_character(self): 89 + """Test that an RKey with a hash character raises InvalidRKeyError.""" 90 + rkey_str = "#extra" 91 + 92 + with pytest.raises(InvalidRKeyError, match="contains invalid characters"): 93 + RKey(rkey_str) 94 + 95 + def test_invalid_rkey_at_character(self): 96 + """Test that an RKey with an at character raises InvalidRKeyError.""" 97 + rkey_str = "@handle" 98 + 99 + with pytest.raises(InvalidRKeyError, match="contains invalid characters"): 100 + RKey(rkey_str) 101 + 102 + def test_invalid_rkey_space(self): 103 + """Test that an RKey with a space raises InvalidRKeyError.""" 104 + rkey_str = "any space" 105 + 106 + with pytest.raises(InvalidRKeyError, match="contains invalid characters"): 107 + RKey(rkey_str) 108 + 109 + def test_invalid_rkey_plus_character(self): 110 + """Test that an RKey with a plus character raises InvalidRKeyError.""" 111 + rkey_str = "any+space" 112 + 113 + with pytest.raises(InvalidRKeyError, match="contains invalid characters"): 114 + RKey(rkey_str) 115 + 116 + def test_invalid_rkey_brackets(self): 117 + """Test that an RKey with brackets raises InvalidRKeyError.""" 118 + rkey_str = "number[3]" 119 + 120 + with pytest.raises(InvalidRKeyError, match="contains invalid characters"): 121 + RKey(rkey_str) 122 + 123 + def test_invalid_rkey_parentheses(self): 124 + """Test that an RKey with parentheses raises InvalidRKeyError.""" 125 + rkey_str = "number(3)" 126 + 127 + with pytest.raises(InvalidRKeyError, match="contains invalid characters"): 128 + RKey(rkey_str) 129 + 130 + def test_invalid_rkey_quotes(self): 131 + """Test that an RKey with quotes raises InvalidRKeyError.""" 132 + rkey_str = '"quote"' 133 + 134 + with pytest.raises(InvalidRKeyError, match="contains invalid characters"): 135 + RKey(rkey_str) 136 + 137 + def test_invalid_rkey_base64_padding(self): 138 + """Test that an RKey with base64 padding raises InvalidRKeyError.""" 139 + rkey_str = "dHJ1ZQ==" 140 + 141 + with pytest.raises(InvalidRKeyError, match="contains invalid characters"): 142 + RKey(rkey_str) 143 + 144 + def test_rkey_equality(self): 145 + """Test RKey equality comparison.""" 146 + rkey_str = "3jui7kd54zh2y" 147 + rkey1 = RKey(rkey_str) 148 + rkey2 = RKey(rkey_str) 149 + 150 + assert rkey1 == rkey2 151 + assert rkey1 != "not an rkey object" 152 + 153 + def test_rkey_string_representation(self): 154 + """Test RKey string representation.""" 155 + rkey_str = "3jui7kd54zh2y" 156 + rkey = RKey(rkey_str) 157 + 158 + assert str(rkey) == rkey_str 159 + 160 + 161 + class TestTID: 162 + """Test cases for the TID class.""" 163 + 164 + def test_tid_creation_default(self): 165 + """Test creating a TID with default parameters.""" 166 + tid = TID() 167 + 168 + assert isinstance(tid, TID) 169 + assert isinstance(tid, RKey) 170 + assert isinstance(tid.timestamp, datetime.datetime) 171 + assert isinstance(tid.clockIdentifier, int) 172 + assert 0 <= tid.clockIdentifier < 1024 173 + assert len(str(tid)) == 13 # TID string is always 13 characters 174 + 175 + def test_tid_creation_with_timestamp(self): 176 + """Test creating a TID with a specific timestamp.""" 177 + timestamp = datetime.datetime(2023, 1, 1, 12, 0, 0) 178 + tid = TID(time=timestamp) 179 + 180 + assert tid.timestamp == timestamp 181 + assert isinstance(tid.clockIdentifier, int) 182 + assert 0 <= tid.clockIdentifier < 1024 183 + 184 + def test_tid_creation_with_clock_identifier(self): 185 + """Test creating a TID with a specific clock identifier.""" 186 + clock_id = 42 187 + tid = TID(clockIdentifier=clock_id) 188 + 189 + assert tid.clockIdentifier == clock_id 190 + assert isinstance(tid.timestamp, datetime.datetime) 191 + 192 + def test_tid_creation_with_both_parameters(self): 193 + """Test creating a TID with both timestamp and clock identifier.""" 194 + timestamp = datetime.datetime(2023, 1, 1, 12, 0, 0) 195 + clock_id = 42 196 + tid = TID(time=timestamp, clockIdentifier=clock_id) 197 + 198 + assert tid.timestamp == timestamp 199 + assert tid.clockIdentifier == clock_id 200 + 201 + def test_tid_integer_representation(self): 202 + """Test TID integer representation.""" 203 + timestamp = datetime.datetime(2023, 1, 1, 12, 0, 0) 204 + clock_id = 42 205 + tid = TID(time=timestamp, clockIdentifier=clock_id) 206 + 207 + int_value = int(tid) 208 + expected_value = int(timestamp.timestamp() * 1000000) * 1024 + clock_id 209 + 210 + assert int_value == expected_value 211 + 212 + def test_tid_string_representation(self): 213 + """Test TID string representation.""" 214 + tid = TID() 215 + 216 + str_value = str(tid) 217 + assert len(str_value) == 13 218 + assert all(c in "234567abcdefghijklmnopqrstuvwxyz" for c in str_value) 219 + 220 + def test_tid_equality_with_tid(self): 221 + """Test TID equality comparison with another TID.""" 222 + timestamp = datetime.datetime(2023, 1, 1, 12, 0, 0) 223 + clock_id = 42 224 + tid1 = TID(time=timestamp, clockIdentifier=clock_id) 225 + tid2 = TID(time=timestamp, clockIdentifier=clock_id) 226 + 227 + assert tid1 == tid2 228 + 229 + def test_tid_equality_with_rkey(self): 230 + """Test TID equality comparison with an RKey.""" 231 + timestamp = datetime.datetime(2023, 1, 1, 12, 0, 0) 232 + clock_id = 42 233 + tid = TID(time=timestamp, clockIdentifier=clock_id) 234 + rkey = RKey(str(tid)) 235 + 236 + assert tid == rkey 237 + 238 + def test_tid_inequality_with_different_object(self): 239 + """Test TID inequality comparison with a different object type.""" 240 + tid = TID() 241 + 242 + assert tid != "not a tid object" 243 + 244 + def test_tid_inequality_with_different_timestamp(self): 245 + """Test TID inequality comparison with different timestamp.""" 246 + timestamp1 = datetime.datetime(2023, 1, 1, 12, 0, 0) 247 + timestamp2 = datetime.datetime(2023, 1, 1, 12, 0, 1) 248 + clock_id = 42 249 + tid1 = TID(time=timestamp1, clockIdentifier=clock_id) 250 + tid2 = TID(time=timestamp2, clockIdentifier=clock_id) 251 + 252 + assert tid1 != tid2 253 + 254 + def test_tid_inequality_with_different_clock_id(self): 255 + """Test TID inequality comparison with different clock identifier.""" 256 + timestamp = datetime.datetime(2023, 1, 1, 12, 0, 0) 257 + clock_id1 = 42 258 + clock_id2 = 43 259 + tid1 = TID(time=timestamp, clockIdentifier=clock_id1) 260 + tid2 = TID(time=timestamp, clockIdentifier=clock_id2) 261 + 262 + assert tid1 != tid2 263 + 264 + 265 + class TestTIDImportFunctions: 266 + """Test cases for TID import functions.""" 267 + 268 + def test_import_tid_from_integer_default(self): 269 + """Test importing a TID from integer with default value.""" 270 + tid = importTIDfromInteger() 271 + 272 + assert isinstance(tid, TID) 273 + assert isinstance(tid.timestamp, datetime.datetime) 274 + assert isinstance(tid.clockIdentifier, int) 275 + assert 0 <= tid.clockIdentifier < 1024 276 + 277 + def test_import_tid_from_integer_with_value(self): 278 + """Test importing a TID from integer with a specific value.""" 279 + timestamp = datetime.datetime(2023, 1, 1, 12, 0, 0) 280 + clock_id = 42 281 + original_tid = TID(time=timestamp, clockIdentifier=clock_id) 282 + int_value = int(original_tid) 283 + 284 + imported_tid = importTIDfromInteger(int_value) 285 + 286 + assert imported_tid.timestamp == timestamp 287 + assert imported_tid.clockIdentifier == clock_id 288 + 289 + def test_import_tid_from_base32_default(self): 290 + """Test importing a TID from base32 with default value.""" 291 + tid = importTIDfromBase32() 292 + 293 + assert isinstance(tid, TID) 294 + assert isinstance(tid.timestamp, datetime.datetime) 295 + assert isinstance(tid.clockIdentifier, int) 296 + assert 0 <= tid.clockIdentifier < 1024 297 + 298 + def test_import_tid_from_base32_with_value(self): 299 + """Test importing a TID from base32 with a specific value.""" 300 + original_tid = TID() 301 + str_value = str(original_tid) 302 + 303 + imported_tid = importTIDfromBase32(str_value) 304 + 305 + assert int(imported_tid) == int(original_tid)
+122
tests/uri/test_uri.py
··· 1 + """Test cases for the URI class in atpasser.uri module.""" 2 + 3 + import pytest 4 + from atpasser.uri import URI 5 + from atpasser.uri.exceptions import InvalidURIError, ValidationError 6 + 7 + 8 + class TestURI: 9 + """Test cases for the URI class.""" 10 + 11 + def test_valid_uri_with_did(self): 12 + """Test creating a URI with a valid DID.""" 13 + uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3jwdwj2ctlk26" 14 + uri = URI(uri_str) 15 + 16 + assert str(uri) == uri_str 17 + assert uri.authorityAsText == "did:plc:z72i7hdynmk6r22z27h6tvur" 18 + assert uri.path == ["app.bsky.feed.post", "3jwdwj2ctlk26"] 19 + assert uri.pathAsText == "app.bsky.feed.post/3jwdwj2ctlk26" 20 + assert uri.query is None 21 + assert uri.queryAsText is None 22 + assert uri.fragment is None 23 + assert uri.fragmentAsText is None 24 + 25 + def test_valid_uri_with_handle(self): 26 + """Test creating a URI with a valid handle.""" 27 + uri_str = "at://bnewbold.bsky.team/app.bsky.feed.post/3jwdwj2ctlk26" 28 + uri = URI(uri_str) 29 + 30 + assert str(uri) == uri_str 31 + assert uri.authorityAsText == "bnewbold.bsky.team" 32 + assert uri.path == ["app.bsky.feed.post", "3jwdwj2ctlk26"] 33 + assert uri.pathAsText == "app.bsky.feed.post/3jwdwj2ctlk26" 34 + 35 + def test_valid_uri_with_collection_only(self): 36 + """Test creating a URI with only a collection.""" 37 + uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post" 38 + uri = URI(uri_str) 39 + 40 + assert str(uri) == uri_str 41 + assert uri.authorityAsText == "did:plc:z72i7hdynmk6r22z27h6tvur" 42 + assert uri.path == ["app.bsky.feed.post"] 43 + assert uri.pathAsText == "app.bsky.feed.post" 44 + 45 + def test_valid_uri_with_authority_only(self): 46 + """Test creating a URI with only an authority.""" 47 + uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur" 48 + uri = URI(uri_str) 49 + 50 + assert str(uri) == uri_str 51 + assert uri.authorityAsText == "did:plc:z72i7hdynmk6r22z27h6tvur" 52 + assert uri.path == [] 53 + assert uri.pathAsText == "" 54 + 55 + def test_valid_uri_with_query(self): 56 + """Test creating a URI with query parameters.""" 57 + uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post?param1=value1&param2=value2" 58 + uri = URI(uri_str) 59 + 60 + assert uri.query == {"param1": ["value1"], "param2": ["value2"]} 61 + assert uri.queryAsText == "param1%3Dvalue1%26param2%3Dvalue2" 62 + 63 + def test_valid_uri_with_fragment(self): 64 + """Test creating a URI with a fragment.""" 65 + uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3jwdwj2ctlk26#$.some.json.path" 66 + uri = URI(uri_str) 67 + 68 + assert uri.fragment is not None 69 + assert uri.fragmentAsText == "%24.some.json.path" 70 + 71 + def test_invalid_uri_non_ascii_characters(self): 72 + """Test that non-ASCII characters in URI raise InvalidURIError.""" 73 + uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/💩" 74 + 75 + with pytest.raises(InvalidURIError, match="contains invalid characters"): 76 + URI(uri_str) 77 + 78 + def test_invalid_uri_too_long(self): 79 + """Test that a URI that is too long raises InvalidURIError.""" 80 + # Create a URI that exceeds the 8000 character limit 81 + long_path = "a" * 8000 82 + uri_str = f"at://did:plc:z72i7hdynmk6r22z27h6tvur/{long_path}" 83 + 84 + with pytest.raises(InvalidURIError, match="exceeds maximum length"): 85 + URI(uri_str) 86 + 87 + def test_invalid_uri_wrong_scheme(self): 88 + """Test that a URI with wrong scheme raises InvalidURIError.""" 89 + uri_str = "https://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3jwdwj2ctlk26" 90 + 91 + with pytest.raises(InvalidURIError, match="invalid format"): 92 + URI(uri_str) 93 + 94 + def test_invalid_uri_trailing_slash(self): 95 + """Test that a URI with trailing slash raises InvalidURIError.""" 96 + uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/" 97 + 98 + with pytest.raises(InvalidURIError, match="cannot end with a slash"): 99 + URI(uri_str) 100 + 101 + def test_invalid_uri_with_userinfo(self): 102 + """Test that a URI with userinfo raises InvalidURIError.""" 103 + uri_str = "at://user:pass@did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post" 104 + 105 + with pytest.raises(InvalidURIError, match="does not support user information"): 106 + URI(uri_str) 107 + 108 + def test_uri_equality(self): 109 + """Test URI equality comparison.""" 110 + uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3jwdwj2ctlk26" 111 + uri1 = URI(uri_str) 112 + uri2 = URI(uri_str) 113 + 114 + assert uri1 == uri2 115 + assert uri1 != "not a uri object" 116 + 117 + def test_uri_string_representation(self): 118 + """Test URI string representation.""" 119 + uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3jwdwj2ctlk26" 120 + uri = URI(uri_str) 121 + 122 + assert str(uri) == uri_str