···11+"""Test cases for the DID class in atpasser.uri.identifier module."""
22+33+import pytest
44+from atpasser.uri.identifier import DID
55+from atpasser.uri.exceptions import InvalidDIDError, ResolutionError
66+77+88+class TestDID:
99+ """Test cases for the DID class."""
1010+1111+ def test_valid_did_plc(self):
1212+ """Test creating a DID with a valid did:plc format."""
1313+ did_str = "did:plc:z72i7hdynmk6r22z27h6tvur"
1414+ did = DID(did_str)
1515+1616+ assert str(did) == did_str
1717+ assert did.uri == did_str
1818+1919+ def test_valid_did_web(self):
2020+ """Test creating a DID with a valid did:web format."""
2121+ did_str = "did:web:blueskyweb.xyz"
2222+ did = DID(did_str)
2323+2424+ assert str(did) == did_str
2525+ assert did.uri == did_str
2626+2727+ def test_valid_did_with_various_characters(self):
2828+ """Test creating a DID with various valid characters."""
2929+ did_str = "did:method:val:two-with_underscores.and-dashes"
3030+ did = DID(did_str)
3131+3232+ assert str(did) == did_str
3333+ assert did.uri == did_str
3434+3535+ def test_invalid_did_wrong_format(self):
3636+ """Test that a DID with wrong format raises InvalidDIDError."""
3737+ did_str = "not-a-did"
3838+3939+ with pytest.raises(InvalidDIDError, match="invalid format"):
4040+ DID(did_str)
4141+4242+ def test_invalid_did_uppercase_method(self):
4343+ """Test that a DID with uppercase method raises InvalidDIDError."""
4444+ did_str = "did:METHOD:val"
4545+4646+ with pytest.raises(InvalidDIDError, match="invalid format"):
4747+ DID(did_str)
4848+4949+ def test_invalid_did_method_with_numbers(self):
5050+ """Test that a DID with method containing numbers raises InvalidDIDError."""
5151+ did_str = "did:m123:val"
5252+5353+ with pytest.raises(InvalidDIDError, match="invalid format"):
5454+ DID(did_str)
5555+5656+ def test_invalid_did_empty_identifier(self):
5757+ """Test that a DID with empty identifier raises InvalidDIDError."""
5858+ did_str = "did:method:"
5959+6060+ with pytest.raises(InvalidDIDError, match="invalid format"):
6161+ DID(did_str)
6262+6363+ def test_invalid_did_ends_with_colon(self):
6464+ """Test that a DID ending with colon raises InvalidDIDError."""
6565+ did_str = "did:method:val:"
6666+6767+ with pytest.raises(InvalidDIDError, match="invalid format"):
6868+ DID(did_str)
6969+7070+ def test_invalid_did_too_long(self):
7171+ """Test that a DID that is too long raises InvalidDIDError."""
7272+ # Create a DID that exceeds the 2048 character limit
7373+ long_identifier = "a" * 2040
7474+ did_str = f"did:method:{long_identifier}"
7575+7676+ with pytest.raises(InvalidDIDError, match="exceeds maximum length"):
7777+ DID(did_str)
7878+7979+ def test_did_equality(self):
8080+ """Test DID equality comparison."""
8181+ did_str = "did:plc:z72i7hdynmk6r22z27h6tvur"
8282+ did1 = DID(did_str)
8383+ did2 = DID(did_str)
8484+8585+ assert did1 == did2
8686+ assert did1 != "not a did object"
8787+8888+ def test_did_string_representation(self):
8989+ """Test DID string representation."""
9090+ did_str = "did:plc:z72i7hdynmk6r22z27h6tvur"
9191+ did = DID(did_str)
9292+9393+ assert str(did) == did_str
9494+9595+ def test_did_fetch_plc_method(self):
9696+ """Test fetching a DID document for did:plc method."""
9797+ did_str = "did:plc:z72i7hdynmk6r22z27h6tvur"
9898+ did = DID(did_str)
9999+100100+ # This test may fail if there's no internet connection or if the PLC directory is down
101101+ try:
102102+ document = did.fetch()
103103+ assert isinstance(document, list)
104104+ assert len(document) > 0
105105+ except ResolutionError:
106106+ # If resolution fails, we'll skip this test
107107+ pytest.skip("Failed to resolve DID document")
108108+109109+ def test_did_fetch_web_method(self):
110110+ """Test fetching a DID document for did:web method."""
111111+ did_str = "did:web:blueskyweb.xyz"
112112+ did = DID(did_str)
113113+114114+ # This test may fail if there's no internet connection or if the web server is down
115115+ try:
116116+ document = did.fetch()
117117+ assert isinstance(document, list)
118118+ assert len(document) > 0
119119+ except ResolutionError:
120120+ # If resolution fails, we'll skip this test
121121+ pytest.skip("Failed to resolve DID document")
122122+123123+ def test_did_fetch_unsupported_method(self):
124124+ """Test that fetching a DID document with unsupported method raises InvalidDIDError."""
125125+ did_str = "did:unsupported:method"
126126+ did = DID(did_str)
127127+128128+ with pytest.raises(InvalidDIDError, match="unsupported DID method"):
129129+ did.fetch()
130130+131131+ def test_did_fetch_web_empty_domain(self):
132132+ """Test that fetching a DID document with empty domain raises InvalidDIDError."""
133133+ did_str = "did:web:"
134134+ did = DID(did_str)
135135+136136+ with pytest.raises(InvalidDIDError, match="invalid format"):
137137+ did.fetch()
+184
tests/uri/test_handle.py
···11+"""Test cases for the Handle class in atpasser.uri.identifier module."""
22+33+import pytest
44+from atpasser.uri.identifier import Handle
55+from atpasser.uri.exceptions import InvalidHandleError, ResolutionError
66+77+88+class TestHandle:
99+ """Test cases for the Handle class."""
1010+1111+ def test_valid_handle_simple(self):
1212+ """Test creating a Handle with a valid simple format."""
1313+ handle_str = "example.com"
1414+ handle = Handle(handle_str)
1515+1616+ assert str(handle) == handle_str
1717+ assert handle.handle == handle_str
1818+1919+ def test_valid_handle_subdomain(self):
2020+ """Test creating a Handle with a valid subdomain format."""
2121+ handle_str = "subdomain.example.com"
2222+ handle = Handle(handle_str)
2323+2424+ assert str(handle) == handle_str
2525+ assert handle.handle == handle_str
2626+2727+ def test_valid_handle_with_hyphen(self):
2828+ """Test creating a Handle with a valid format containing hyphens."""
2929+ handle_str = "my-example.com"
3030+ handle = Handle(handle_str)
3131+3232+ assert str(handle) == handle_str
3333+ assert handle.handle == handle_str
3434+3535+ def test_valid_handle_with_numbers(self):
3636+ """Test creating a Handle with a valid format containing numbers."""
3737+ handle_str = "example123.com"
3838+ handle = Handle(handle_str)
3939+4040+ assert str(handle) == handle_str
4141+ assert handle.handle == handle_str
4242+4343+ def test_valid_handle_long_domain(self):
4444+ """Test creating a Handle with a valid long domain name."""
4545+ handle_str = "a" * 63 + "." + "b" * 63 + "." + "c" * 63 + ".com"
4646+ handle = Handle(handle_str)
4747+4848+ assert str(handle) == handle_str
4949+ assert handle.handle == handle_str
5050+5151+ def test_invalid_handle_too_long(self):
5252+ """Test that a Handle that is too long raises InvalidHandleError."""
5353+ # Create a handle that exceeds the 253 character limit
5454+ long_handle = "a" * 254
5555+ handle_str = f"{long_handle}.com"
5656+5757+ with pytest.raises(InvalidHandleError, match="exceeds maximum length"):
5858+ Handle(handle_str)
5959+6060+ def test_invalid_handle_no_dot_separator(self):
6161+ """Test that a Handle without a dot separator raises InvalidHandleError."""
6262+ handle_str = "example"
6363+6464+ with pytest.raises(InvalidHandleError, match="invalid format"):
6565+ Handle(handle_str)
6666+6767+ def test_invalid_handle_starts_with_dot(self):
6868+ """Test that a Handle starting with a dot raises InvalidHandleError."""
6969+ handle_str = ".example.com"
7070+7171+ with pytest.raises(InvalidHandleError, match="invalid format"):
7272+ Handle(handle_str)
7373+7474+ def test_invalid_handle_ends_with_dot(self):
7575+ """Test that a Handle ending with a dot raises InvalidHandleError."""
7676+ handle_str = "example.com."
7777+7878+ with pytest.raises(InvalidHandleError, match="invalid format"):
7979+ Handle(handle_str)
8080+8181+ def test_invalid_handle_segment_too_long(self):
8282+ """Test that a Handle with a segment that is too long raises InvalidHandleError."""
8383+ handle_str = f"{'a' * 64}.com"
8484+8585+ with pytest.raises(InvalidHandleError, match="segment length error"):
8686+ Handle(handle_str)
8787+8888+ def test_invalid_handle_segment_empty(self):
8989+ """Test that a Handle with an empty segment raises InvalidHandleError."""
9090+ handle_str = "example..com"
9191+9292+ with pytest.raises(InvalidHandleError, match="segment length error"):
9393+ Handle(handle_str)
9494+9595+ def test_invalid_handle_invalid_characters(self):
9696+ """Test that a Handle with invalid characters raises InvalidHandleError."""
9797+ handle_str = "ex@mple.com"
9898+9999+ with pytest.raises(InvalidHandleError, match="contains invalid characters"):
100100+ Handle(handle_str)
101101+102102+ def test_invalid_handle_segment_starts_with_hyphen(self):
103103+ """Test that a Handle with a segment starting with a hyphen raises InvalidHandleError."""
104104+ handle_str = "-example.com"
105105+106106+ with pytest.raises(InvalidHandleError, match="invalid format"):
107107+ Handle(handle_str)
108108+109109+ def test_invalid_handle_segment_ends_with_hyphen(self):
110110+ """Test that a Handle with a segment ending with a hyphen raises InvalidHandleError."""
111111+ handle_str = "example-.com"
112112+113113+ with pytest.raises(InvalidHandleError, match="invalid format"):
114114+ Handle(handle_str)
115115+116116+ def test_invalid_handle_tld_starts_with_digit(self):
117117+ """Test that a Handle with a TLD starting with a digit raises InvalidHandleError."""
118118+ handle_str = "example.1com"
119119+120120+ with pytest.raises(InvalidHandleError, match="invalid format"):
121121+ Handle(handle_str)
122122+123123+ def test_handle_equality(self):
124124+ """Test Handle equality comparison."""
125125+ handle_str = "example.com"
126126+ handle1 = Handle(handle_str)
127127+ handle2 = Handle(handle_str)
128128+129129+ assert handle1 == handle2
130130+ assert handle1 != "not a handle object"
131131+132132+ def test_handle_string_representation(self):
133133+ """Test Handle string representation."""
134134+ handle_str = "example.com"
135135+ handle = Handle(handle_str)
136136+137137+ assert str(handle) == handle_str
138138+139139+ def test_handle_case_insensitive_storage(self):
140140+ """Test that Handle stores the handle in lowercase."""
141141+ handle_str = "ExAmPlE.CoM"
142142+ handle = Handle(handle_str)
143143+144144+ # The handle should be stored in lowercase
145145+ assert handle.handle == "example.com"
146146+ # The string representation should also return the lowercase form
147147+ assert str(handle) == "example.com"
148148+149149+ def test_handle_to_tid_dns_resolution(self):
150150+ """Test resolving a handle to DID using DNS method."""
151151+ handle_str = "bsky.app"
152152+ handle = Handle(handle_str)
153153+154154+ # This test may fail if there's no internet connection or if DNS resolution fails
155155+ try:
156156+ did = handle.toTID()
157157+ assert did is not None
158158+ assert str(did).startswith("did:")
159159+ except ResolutionError:
160160+ # If resolution fails, we'll skip this test
161161+ pytest.skip("Failed to resolve handle via DNS")
162162+163163+ def test_handle_to_tid_http_resolution(self):
164164+ """Test resolving a handle to DID using HTTP method."""
165165+ handle_str = "blueskyweb.xyz"
166166+ handle = Handle(handle_str)
167167+168168+ # This test may fail if there's no internet connection or if HTTP resolution fails
169169+ try:
170170+ did = handle.toTID()
171171+ assert did is not None
172172+ assert str(did).startswith("did:")
173173+ except ResolutionError:
174174+ # If resolution fails, we'll skip this test
175175+ pytest.skip("Failed to resolve handle via HTTP")
176176+177177+ def test_handle_to_tid_unresolvable(self):
178178+ """Test resolving an unresolvable handle returns None."""
179179+ handle_str = "nonexistent-domain-12345.com"
180180+ handle = Handle(handle_str)
181181+182182+ # This should return None for a non-existent domain
183183+ did = handle.toTID()
184184+ assert did is None
+248
tests/uri/test_nsid.py
···11+"""Test cases for the NSID class in atpasser.uri.nsid module."""
22+33+import pytest
44+from atpasser.uri.nsid import NSID
55+from atpasser.uri.exceptions import InvalidNSIDError, ValidationError
66+77+88+class TestNSID:
99+ """Test cases for the NSID class."""
1010+1111+ def test_valid_nsid_simple(self):
1212+ """Test creating an NSID with a valid simple format."""
1313+ nsid_str = "com.example.recordName"
1414+ nsid = NSID(nsid_str)
1515+1616+ assert str(nsid) == nsid_str
1717+ assert nsid.nsid == nsid_str
1818+ assert nsid.domainAuthority == ["com", "example"]
1919+ assert nsid.domainAuthorityAsText == "com.example"
2020+ assert nsid.name == "recordName"
2121+ assert nsid.fragment is None
2222+2323+ def test_valid_nsid_with_fragment(self):
2424+ """Test creating an NSID with a valid fragment."""
2525+ nsid_str = "com.example.recordName#fragment"
2626+ nsid = NSID(nsid_str)
2727+2828+ assert str(nsid) == nsid_str
2929+ assert nsid.nsid == nsid_str
3030+ assert nsid.domainAuthority == ["com", "example"]
3131+ assert nsid.domainAuthorityAsText == "com.example"
3232+ assert nsid.name == "recordName"
3333+ assert nsid.fragment == "fragment"
3434+3535+ def test_valid_nsid_multiple_segments(self):
3636+ """Test creating an NSID with multiple domain segments."""
3737+ nsid_str = "net.users.bob.ping"
3838+ nsid = NSID(nsid_str)
3939+4040+ assert str(nsid) == nsid_str
4141+ assert nsid.nsid == nsid_str
4242+ assert nsid.domainAuthority == ["net", "users", "bob"]
4343+ assert nsid.domainAuthorityAsText == "net.users.bob"
4444+ assert nsid.name == "ping"
4545+ assert nsid.fragment is None
4646+4747+ def test_valid_nsid_with_hyphens(self):
4848+ """Test creating an NSID with hyphens in domain segments."""
4949+ nsid_str = "a-0.b-1.c.recordName"
5050+ nsid = NSID(nsid_str)
5151+5252+ assert str(nsid) == nsid_str
5353+ assert nsid.nsid == nsid_str
5454+ assert nsid.domainAuthority == ["a-0", "b-1", "c"]
5555+ assert nsid.domainAuthorityAsText == "a-0.b-1.c"
5656+ assert nsid.name == "recordName"
5757+ assert nsid.fragment is None
5858+5959+ def test_valid_nsid_case_sensitivity(self):
6060+ """Test creating an NSID with case-sensitive name."""
6161+ nsid_str = "com.example.fooBar"
6262+ nsid = NSID(nsid_str)
6363+6464+ assert str(nsid) == nsid_str
6565+ assert nsid.nsid == nsid_str
6666+ assert nsid.domainAuthority == ["com", "example"]
6767+ assert nsid.domainAuthorityAsText == "com.example"
6868+ assert nsid.name == "fooBar"
6969+ assert nsid.fragment is None
7070+7171+ def test_valid_nsid_with_numbers_in_name(self):
7272+ """Test creating an NSID with numbers in the name."""
7373+ nsid_str = "com.example.record123"
7474+ nsid = NSID(nsid_str)
7575+7676+ assert str(nsid) == nsid_str
7777+ assert nsid.nsid == nsid_str
7878+ assert nsid.domainAuthority == ["com", "example"]
7979+ assert nsid.domainAuthorityAsText == "com.example"
8080+ assert nsid.name == "record123"
8181+ assert nsid.fragment is None
8282+8383+ def test_invalid_nsid_non_ascii_characters(self):
8484+ """Test that an NSID with non-ASCII characters raises InvalidNSIDError."""
8585+ nsid_str = "com.exa💩ple.thing"
8686+8787+ with pytest.raises(InvalidNSIDError, match="contains invalid characters"):
8888+ NSID(nsid_str)
8989+9090+ def test_invalid_nsid_too_long(self):
9191+ """Test that an NSID that is too long raises InvalidNSIDError."""
9292+ # Create an NSID that exceeds the 317 character limit
9393+ long_segment = "a" * 100
9494+ nsid_str = f"{long_segment}.{long_segment}.{long_segment}.recordName"
9595+9696+ with pytest.raises(InvalidNSIDError, match="domain authority length exceeds limit"):
9797+ NSID(nsid_str)
9898+9999+ def test_invalid_nsid_starts_with_dot(self):
100100+ """Test that an NSID starting with a dot raises InvalidNSIDError."""
101101+ nsid_str = ".com.example.recordName"
102102+103103+ with pytest.raises(InvalidNSIDError, match="invalid format"):
104104+ NSID(nsid_str)
105105+106106+ def test_invalid_nsid_ends_with_dot(self):
107107+ """Test that an NSID ending with a dot raises InvalidNSIDError."""
108108+ nsid_str = "com.example.recordName."
109109+110110+ with pytest.raises(InvalidNSIDError, match="invalid format"):
111111+ NSID(nsid_str)
112112+113113+ def test_invalid_nsid_too_few_segments(self):
114114+ """Test that an NSID with too few segments raises InvalidNSIDError."""
115115+ nsid_str = "com.example"
116116+117117+ with pytest.raises(InvalidNSIDError, match="invalid format"):
118118+ NSID(nsid_str)
119119+120120+ def test_invalid_nsid_domain_authority_too_long(self):
121121+ """Test that an NSID with domain authority that is too long raises InvalidNSIDError."""
122122+ # Create a domain authority that exceeds the 253 character limit
123123+ long_segment = "a" * 63
124124+ nsid_str = f"{long_segment}.{long_segment}.{long_segment}.{long_segment}.recordName"
125125+126126+ with pytest.raises(InvalidNSIDError, match="domain authority length exceeds limit"):
127127+ NSID(nsid_str)
128128+129129+ def test_invalid_nsid_domain_segment_too_long(self):
130130+ """Test that an NSID with a domain segment that is too long raises InvalidNSIDError."""
131131+ nsid_str = f"{'a' * 64}.example.recordName"
132132+133133+ with pytest.raises(InvalidNSIDError, match="segment length error"):
134134+ NSID(nsid_str)
135135+136136+ def test_invalid_nsid_domain_segment_empty(self):
137137+ """Test that an NSID with an empty domain segment raises InvalidNSIDError."""
138138+ nsid_str = "com..example.recordName"
139139+140140+ with pytest.raises(InvalidNSIDError, match="segment length error"):
141141+ NSID(nsid_str)
142142+143143+ def test_invalid_nsid_domain_invalid_characters(self):
144144+ """Test that an NSID with invalid characters in domain raises InvalidNSIDError."""
145145+ nsid_str = "com.ex@mple.recordName"
146146+147147+ with pytest.raises(InvalidNSIDError, match="contains invalid characters"):
148148+ NSID(nsid_str)
149149+150150+ def test_invalid_nsid_domain_segment_starts_with_hyphen(self):
151151+ """Test that an NSID with a domain segment starting with a hyphen raises InvalidNSIDError."""
152152+ nsid_str = "com.-example.recordName"
153153+154154+ with pytest.raises(InvalidNSIDError, match="invalid format"):
155155+ NSID(nsid_str)
156156+157157+ def test_invalid_nsid_domain_segment_ends_with_hyphen(self):
158158+ """Test that an NSID with a domain segment ending with a hyphen raises InvalidNSIDError."""
159159+ nsid_str = "com.example-.recordName"
160160+161161+ with pytest.raises(InvalidNSIDError, match="invalid format"):
162162+ NSID(nsid_str)
163163+164164+ def test_invalid_nsid_tld_starts_with_digit(self):
165165+ """Test that an NSID with a TLD starting with a digit raises InvalidNSIDError."""
166166+ nsid_str = "1com.example.recordName"
167167+168168+ with pytest.raises(InvalidNSIDError, match="invalid format"):
169169+ NSID(nsid_str)
170170+171171+ def test_invalid_nsid_name_empty(self):
172172+ """Test that an NSID with an empty name raises InvalidNSIDError."""
173173+ nsid_str = "com.example."
174174+175175+ with pytest.raises(InvalidNSIDError, match="invalid format"):
176176+ NSID(nsid_str)
177177+178178+ def test_invalid_nsid_name_too_long(self):
179179+ """Test that an NSID with a name that is too long raises InvalidNSIDError."""
180180+ nsid_str = f"com.example.{'a' * 64}"
181181+182182+ with pytest.raises(InvalidNSIDError, match="name length error"):
183183+ NSID(nsid_str)
184184+185185+ def test_invalid_nsid_name_invalid_characters(self):
186186+ """Test that an NSID with invalid characters in name raises InvalidNSIDError."""
187187+ nsid_str = "com.example.record-name"
188188+189189+ with pytest.raises(InvalidNSIDError, match="contains invalid characters"):
190190+ NSID(nsid_str)
191191+192192+ def test_invalid_nsid_name_starts_with_digit(self):
193193+ """Test that an NSID with a name starting with a digit raises InvalidNSIDError."""
194194+ nsid_str = "com.example.1record"
195195+196196+ with pytest.raises(InvalidNSIDError, match="invalid format"):
197197+ NSID(nsid_str)
198198+199199+ def test_invalid_nsid_fragment_empty(self):
200200+ """Test that an NSID with an empty fragment raises InvalidNSIDError."""
201201+ nsid_str = "com.example.recordName#"
202202+203203+ with pytest.raises(InvalidNSIDError, match="fragment length error"):
204204+ NSID(nsid_str)
205205+206206+ def test_invalid_nsid_fragment_too_long(self):
207207+ """Test that an NSID with a fragment that is too long raises InvalidNSIDError."""
208208+ nsid_str = f"com.example.recordName#{'a' * 64}"
209209+210210+ with pytest.raises(InvalidNSIDError, match="fragment length error"):
211211+ NSID(nsid_str)
212212+213213+ def test_invalid_nsid_fragment_invalid_characters(self):
214214+ """Test that an NSID with invalid characters in fragment raises InvalidNSIDError."""
215215+ nsid_str = "com.example.recordName#fragment-with-hyphen"
216216+217217+ with pytest.raises(InvalidNSIDError, match="contains invalid characters"):
218218+ NSID(nsid_str)
219219+220220+ def test_invalid_nsid_fragment_starts_with_digit(self):
221221+ """Test that an NSID with a fragment starting with a digit raises InvalidNSIDError."""
222222+ nsid_str = "com.example.recordName#1fragment"
223223+224224+ with pytest.raises(InvalidNSIDError, match="invalid format"):
225225+ NSID(nsid_str)
226226+227227+ def test_nsid_equality(self):
228228+ """Test NSID equality comparison."""
229229+ nsid_str = "com.example.recordName"
230230+ nsid1 = NSID(nsid_str)
231231+ nsid2 = NSID(nsid_str)
232232+233233+ assert nsid1 == nsid2
234234+ assert nsid1 != "not an nsid object"
235235+236236+ def test_nsid_string_representation(self):
237237+ """Test NSID string representation."""
238238+ nsid_str = "com.example.recordName"
239239+ nsid = NSID(nsid_str)
240240+241241+ assert str(nsid) == nsid_str
242242+243243+ def test_nsid_string_representation_with_fragment(self):
244244+ """Test NSID string representation with fragment."""
245245+ nsid_str = "com.example.recordName#fragment"
246246+ nsid = NSID(nsid_str)
247247+248248+ assert str(nsid) == nsid_str
+110
tests/uri/test_restricted_uri.py
···11+"""Test cases for the RestrictedURI class in atpasser.uri module."""
22+33+import pytest
44+from atpasser.uri import RestrictedURI
55+from atpasser.uri.exceptions import InvalidRestrictedURIError, InvalidURIError
66+77+88+class TestRestrictedURI:
99+ """Test cases for the RestrictedURI class."""
1010+1111+ def test_valid_restricted_uri_with_did_collection_and_rkey(self):
1212+ """Test creating a RestrictedURI with a valid DID, collection, and rkey."""
1313+ uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3jwdwj2ctlk26"
1414+ uri = RestrictedURI(uri_str)
1515+1616+ assert str(uri) == uri_str
1717+ assert uri.authorityAsText == "did:plc:z72i7hdynmk6r22z27h6tvur"
1818+ assert uri.path == ["app.bsky.feed.post", "3jwdwj2ctlk26"]
1919+ assert uri.pathAsText == "app.bsky.feed.post/3jwdwj2ctlk26"
2020+ assert uri.collection is not None
2121+ assert str(uri.collection) == "app.bsky.feed.post"
2222+ assert uri.rkey is not None
2323+ assert str(uri.rkey) == "3jwdwj2ctlk26"
2424+2525+ def test_valid_restricted_uri_with_handle_collection_and_rkey(self):
2626+ """Test creating a RestrictedURI with a valid handle, collection, and rkey."""
2727+ uri_str = "at://bnewbold.bsky.team/app.bsky.feed.post/3jwdwj2ctlk26"
2828+ uri = RestrictedURI(uri_str)
2929+3030+ assert str(uri) == uri_str
3131+ assert uri.authorityAsText == "bnewbold.bsky.team"
3232+ assert uri.path == ["app.bsky.feed.post", "3jwdwj2ctlk26"]
3333+ assert uri.collection is not None
3434+ assert str(uri.collection) == "app.bsky.feed.post"
3535+ assert uri.rkey is not None
3636+ assert str(uri.rkey) == "3jwdwj2ctlk26"
3737+3838+ def test_valid_restricted_uri_with_collection_only(self):
3939+ """Test creating a RestrictedURI with only a collection."""
4040+ uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post"
4141+ uri = RestrictedURI(uri_str)
4242+4343+ assert str(uri) == uri_str
4444+ assert uri.authorityAsText == "did:plc:z72i7hdynmk6r22z27h6tvur"
4545+ assert uri.path == ["app.bsky.feed.post"]
4646+ assert uri.collection is not None
4747+ assert str(uri.collection) == "app.bsky.feed.post"
4848+ assert uri.rkey is None
4949+5050+ def test_valid_restricted_uri_with_authority_only(self):
5151+ """Test creating a RestrictedURI with only an authority."""
5252+ uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur"
5353+ uri = RestrictedURI(uri_str)
5454+5555+ assert str(uri) == uri_str
5656+ assert uri.authorityAsText == "did:plc:z72i7hdynmk6r22z27h6tvur"
5757+ assert uri.path == []
5858+ assert uri.collection is None
5959+ assert uri.rkey is None
6060+6161+ def test_invalid_restricted_uri_with_query(self):
6262+ """Test that a RestrictedURI with query parameters raises InvalidRestrictedURIError."""
6363+ uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post?param1=value1"
6464+6565+ with pytest.raises(InvalidRestrictedURIError, match="query parameters not supported"):
6666+ RestrictedURI(uri_str)
6767+6868+ def test_invalid_restricted_uri_with_fragment(self):
6969+ """Test that a RestrictedURI with a fragment raises InvalidRestrictedURIError."""
7070+ uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3jwdwj2ctlk26#$.some.json.path"
7171+7272+ with pytest.raises(InvalidRestrictedURIError, match="fragments not supported"):
7373+ RestrictedURI(uri_str)
7474+7575+ def test_invalid_restricted_uri_with_invalid_authority(self):
7676+ """Test that a RestrictedURI with invalid authority raises InvalidRestrictedURIError."""
7777+ uri_str = "at://invalid_authority/app.bsky.feed.post/3jwdwj2ctlk26"
7878+7979+ with pytest.raises(InvalidRestrictedURIError, match="invalid authority"):
8080+ RestrictedURI(uri_str)
8181+8282+ def test_invalid_restricted_uri_too_many_path_segments(self):
8383+ """Test that a RestrictedURI with too many path segments raises InvalidRestrictedURIError."""
8484+ uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3jwdwj2ctlk26/extra"
8585+8686+ with pytest.raises(InvalidRestrictedURIError, match="too many path segments"):
8787+ RestrictedURI(uri_str)
8888+8989+ def test_invalid_restricted_uri_base_uri_validation_failure(self):
9090+ """Test that a RestrictedURI with invalid base URI raises InvalidURIError."""
9191+ uri_str = "https://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post"
9292+9393+ with pytest.raises(InvalidURIError, match="invalid format"):
9494+ RestrictedURI(uri_str)
9595+9696+ def test_restricted_uri_equality(self):
9797+ """Test RestrictedURI equality comparison."""
9898+ uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3jwdwj2ctlk26"
9999+ uri1 = RestrictedURI(uri_str)
100100+ uri2 = RestrictedURI(uri_str)
101101+102102+ assert uri1 == uri2
103103+ assert uri1 != "not a uri object"
104104+105105+ def test_restricted_uri_string_representation(self):
106106+ """Test RestrictedURI string representation."""
107107+ uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3jwdwj2ctlk26"
108108+ uri = RestrictedURI(uri_str)
109109+110110+ assert str(uri) == uri_str
+305
tests/uri/test_rkey.py
···11+"""Test cases for the RKey and TID classes in atpasser.uri.rkey module."""
22+33+import pytest
44+import datetime
55+from atpasser.uri.rkey import RKey, TID, importTIDfromInteger, importTIDfromBase32
66+from atpasser.uri.exceptions import InvalidRKeyError
77+88+99+class TestRKey:
1010+ """Test cases for the RKey class."""
1111+1212+ def test_valid_rkey_simple(self):
1313+ """Test creating an RKey with a valid simple format."""
1414+ rkey_str = "3jui7kd54zh2y"
1515+ rkey = RKey(rkey_str)
1616+1717+ assert str(rkey) == rkey_str
1818+ assert rkey.recordKey == rkey_str
1919+2020+ def test_valid_rkey_with_various_characters(self):
2121+ """Test creating an RKey with various valid characters."""
2222+ rkey_str = "example.com"
2323+ rkey = RKey(rkey_str)
2424+2525+ assert str(rkey) == rkey_str
2626+ assert rkey.recordKey == rkey_str
2727+2828+ def test_valid_rkey_with_special_characters(self):
2929+ """Test creating an RKey with valid special characters."""
3030+ rkey_str = "~1.2-3_"
3131+ rkey = RKey(rkey_str)
3232+3333+ assert str(rkey) == rkey_str
3434+ assert rkey.recordKey == rkey_str
3535+3636+ def test_valid_rkey_with_colon(self):
3737+ """Test creating an RKey with a colon."""
3838+ rkey_str = "pre:fix"
3939+ rkey = RKey(rkey_str)
4040+4141+ assert str(rkey) == rkey_str
4242+ assert rkey.recordKey == rkey_str
4343+4444+ def test_valid_rkey_underscore(self):
4545+ """Test creating an RKey with just an underscore."""
4646+ rkey_str = "_"
4747+ rkey = RKey(rkey_str)
4848+4949+ assert str(rkey) == rkey_str
5050+ assert rkey.recordKey == rkey_str
5151+5252+ def test_invalid_rkey_empty(self):
5353+ """Test that an empty RKey raises InvalidRKeyError."""
5454+ rkey_str = ""
5555+5656+ with pytest.raises(InvalidRKeyError, match="record key is empty"):
5757+ RKey(rkey_str)
5858+5959+ def test_invalid_rkey_too_long(self):
6060+ """Test that an RKey that is too long raises InvalidRKeyError."""
6161+ # Create an RKey that exceeds the 512 character limit
6262+ rkey_str = "a" * 513
6363+6464+ with pytest.raises(InvalidRKeyError, match="exceeds maximum length"):
6565+ RKey(rkey_str)
6666+6767+ def test_invalid_rkey_reserved_double_dot(self):
6868+ """Test that an RKey with '..' raises InvalidRKeyError."""
6969+ rkey_str = ".."
7070+7171+ with pytest.raises(InvalidRKeyError, match="reserved value"):
7272+ RKey(rkey_str)
7373+7474+ def test_invalid_rkey_reserved_single_dot(self):
7575+ """Test that an RKey with '.' raises InvalidRKeyError."""
7676+ rkey_str = "."
7777+7878+ with pytest.raises(InvalidRKeyError, match="reserved value"):
7979+ RKey(rkey_str)
8080+8181+ def test_invalid_rkey_invalid_characters(self):
8282+ """Test that an RKey with invalid characters raises InvalidRKeyError."""
8383+ rkey_str = "alpha/beta"
8484+8585+ with pytest.raises(InvalidRKeyError, match="contains invalid characters"):
8686+ RKey(rkey_str)
8787+8888+ def test_invalid_rkey_hash_character(self):
8989+ """Test that an RKey with a hash character raises InvalidRKeyError."""
9090+ rkey_str = "#extra"
9191+9292+ with pytest.raises(InvalidRKeyError, match="contains invalid characters"):
9393+ RKey(rkey_str)
9494+9595+ def test_invalid_rkey_at_character(self):
9696+ """Test that an RKey with an at character raises InvalidRKeyError."""
9797+ rkey_str = "@handle"
9898+9999+ with pytest.raises(InvalidRKeyError, match="contains invalid characters"):
100100+ RKey(rkey_str)
101101+102102+ def test_invalid_rkey_space(self):
103103+ """Test that an RKey with a space raises InvalidRKeyError."""
104104+ rkey_str = "any space"
105105+106106+ with pytest.raises(InvalidRKeyError, match="contains invalid characters"):
107107+ RKey(rkey_str)
108108+109109+ def test_invalid_rkey_plus_character(self):
110110+ """Test that an RKey with a plus character raises InvalidRKeyError."""
111111+ rkey_str = "any+space"
112112+113113+ with pytest.raises(InvalidRKeyError, match="contains invalid characters"):
114114+ RKey(rkey_str)
115115+116116+ def test_invalid_rkey_brackets(self):
117117+ """Test that an RKey with brackets raises InvalidRKeyError."""
118118+ rkey_str = "number[3]"
119119+120120+ with pytest.raises(InvalidRKeyError, match="contains invalid characters"):
121121+ RKey(rkey_str)
122122+123123+ def test_invalid_rkey_parentheses(self):
124124+ """Test that an RKey with parentheses raises InvalidRKeyError."""
125125+ rkey_str = "number(3)"
126126+127127+ with pytest.raises(InvalidRKeyError, match="contains invalid characters"):
128128+ RKey(rkey_str)
129129+130130+ def test_invalid_rkey_quotes(self):
131131+ """Test that an RKey with quotes raises InvalidRKeyError."""
132132+ rkey_str = '"quote"'
133133+134134+ with pytest.raises(InvalidRKeyError, match="contains invalid characters"):
135135+ RKey(rkey_str)
136136+137137+ def test_invalid_rkey_base64_padding(self):
138138+ """Test that an RKey with base64 padding raises InvalidRKeyError."""
139139+ rkey_str = "dHJ1ZQ=="
140140+141141+ with pytest.raises(InvalidRKeyError, match="contains invalid characters"):
142142+ RKey(rkey_str)
143143+144144+ def test_rkey_equality(self):
145145+ """Test RKey equality comparison."""
146146+ rkey_str = "3jui7kd54zh2y"
147147+ rkey1 = RKey(rkey_str)
148148+ rkey2 = RKey(rkey_str)
149149+150150+ assert rkey1 == rkey2
151151+ assert rkey1 != "not an rkey object"
152152+153153+ def test_rkey_string_representation(self):
154154+ """Test RKey string representation."""
155155+ rkey_str = "3jui7kd54zh2y"
156156+ rkey = RKey(rkey_str)
157157+158158+ assert str(rkey) == rkey_str
159159+160160+161161+class TestTID:
162162+ """Test cases for the TID class."""
163163+164164+ def test_tid_creation_default(self):
165165+ """Test creating a TID with default parameters."""
166166+ tid = TID()
167167+168168+ assert isinstance(tid, TID)
169169+ assert isinstance(tid, RKey)
170170+ assert isinstance(tid.timestamp, datetime.datetime)
171171+ assert isinstance(tid.clockIdentifier, int)
172172+ assert 0 <= tid.clockIdentifier < 1024
173173+ assert len(str(tid)) == 13 # TID string is always 13 characters
174174+175175+ def test_tid_creation_with_timestamp(self):
176176+ """Test creating a TID with a specific timestamp."""
177177+ timestamp = datetime.datetime(2023, 1, 1, 12, 0, 0)
178178+ tid = TID(time=timestamp)
179179+180180+ assert tid.timestamp == timestamp
181181+ assert isinstance(tid.clockIdentifier, int)
182182+ assert 0 <= tid.clockIdentifier < 1024
183183+184184+ def test_tid_creation_with_clock_identifier(self):
185185+ """Test creating a TID with a specific clock identifier."""
186186+ clock_id = 42
187187+ tid = TID(clockIdentifier=clock_id)
188188+189189+ assert tid.clockIdentifier == clock_id
190190+ assert isinstance(tid.timestamp, datetime.datetime)
191191+192192+ def test_tid_creation_with_both_parameters(self):
193193+ """Test creating a TID with both timestamp and clock identifier."""
194194+ timestamp = datetime.datetime(2023, 1, 1, 12, 0, 0)
195195+ clock_id = 42
196196+ tid = TID(time=timestamp, clockIdentifier=clock_id)
197197+198198+ assert tid.timestamp == timestamp
199199+ assert tid.clockIdentifier == clock_id
200200+201201+ def test_tid_integer_representation(self):
202202+ """Test TID integer representation."""
203203+ timestamp = datetime.datetime(2023, 1, 1, 12, 0, 0)
204204+ clock_id = 42
205205+ tid = TID(time=timestamp, clockIdentifier=clock_id)
206206+207207+ int_value = int(tid)
208208+ expected_value = int(timestamp.timestamp() * 1000000) * 1024 + clock_id
209209+210210+ assert int_value == expected_value
211211+212212+ def test_tid_string_representation(self):
213213+ """Test TID string representation."""
214214+ tid = TID()
215215+216216+ str_value = str(tid)
217217+ assert len(str_value) == 13
218218+ assert all(c in "234567abcdefghijklmnopqrstuvwxyz" for c in str_value)
219219+220220+ def test_tid_equality_with_tid(self):
221221+ """Test TID equality comparison with another TID."""
222222+ timestamp = datetime.datetime(2023, 1, 1, 12, 0, 0)
223223+ clock_id = 42
224224+ tid1 = TID(time=timestamp, clockIdentifier=clock_id)
225225+ tid2 = TID(time=timestamp, clockIdentifier=clock_id)
226226+227227+ assert tid1 == tid2
228228+229229+ def test_tid_equality_with_rkey(self):
230230+ """Test TID equality comparison with an RKey."""
231231+ timestamp = datetime.datetime(2023, 1, 1, 12, 0, 0)
232232+ clock_id = 42
233233+ tid = TID(time=timestamp, clockIdentifier=clock_id)
234234+ rkey = RKey(str(tid))
235235+236236+ assert tid == rkey
237237+238238+ def test_tid_inequality_with_different_object(self):
239239+ """Test TID inequality comparison with a different object type."""
240240+ tid = TID()
241241+242242+ assert tid != "not a tid object"
243243+244244+ def test_tid_inequality_with_different_timestamp(self):
245245+ """Test TID inequality comparison with different timestamp."""
246246+ timestamp1 = datetime.datetime(2023, 1, 1, 12, 0, 0)
247247+ timestamp2 = datetime.datetime(2023, 1, 1, 12, 0, 1)
248248+ clock_id = 42
249249+ tid1 = TID(time=timestamp1, clockIdentifier=clock_id)
250250+ tid2 = TID(time=timestamp2, clockIdentifier=clock_id)
251251+252252+ assert tid1 != tid2
253253+254254+ def test_tid_inequality_with_different_clock_id(self):
255255+ """Test TID inequality comparison with different clock identifier."""
256256+ timestamp = datetime.datetime(2023, 1, 1, 12, 0, 0)
257257+ clock_id1 = 42
258258+ clock_id2 = 43
259259+ tid1 = TID(time=timestamp, clockIdentifier=clock_id1)
260260+ tid2 = TID(time=timestamp, clockIdentifier=clock_id2)
261261+262262+ assert tid1 != tid2
263263+264264+265265+class TestTIDImportFunctions:
266266+ """Test cases for TID import functions."""
267267+268268+ def test_import_tid_from_integer_default(self):
269269+ """Test importing a TID from integer with default value."""
270270+ tid = importTIDfromInteger()
271271+272272+ assert isinstance(tid, TID)
273273+ assert isinstance(tid.timestamp, datetime.datetime)
274274+ assert isinstance(tid.clockIdentifier, int)
275275+ assert 0 <= tid.clockIdentifier < 1024
276276+277277+ def test_import_tid_from_integer_with_value(self):
278278+ """Test importing a TID from integer with a specific value."""
279279+ timestamp = datetime.datetime(2023, 1, 1, 12, 0, 0)
280280+ clock_id = 42
281281+ original_tid = TID(time=timestamp, clockIdentifier=clock_id)
282282+ int_value = int(original_tid)
283283+284284+ imported_tid = importTIDfromInteger(int_value)
285285+286286+ assert imported_tid.timestamp == timestamp
287287+ assert imported_tid.clockIdentifier == clock_id
288288+289289+ def test_import_tid_from_base32_default(self):
290290+ """Test importing a TID from base32 with default value."""
291291+ tid = importTIDfromBase32()
292292+293293+ assert isinstance(tid, TID)
294294+ assert isinstance(tid.timestamp, datetime.datetime)
295295+ assert isinstance(tid.clockIdentifier, int)
296296+ assert 0 <= tid.clockIdentifier < 1024
297297+298298+ def test_import_tid_from_base32_with_value(self):
299299+ """Test importing a TID from base32 with a specific value."""
300300+ original_tid = TID()
301301+ str_value = str(original_tid)
302302+303303+ imported_tid = importTIDfromBase32(str_value)
304304+305305+ assert int(imported_tid) == int(original_tid)
+122
tests/uri/test_uri.py
···11+"""Test cases for the URI class in atpasser.uri module."""
22+33+import pytest
44+from atpasser.uri import URI
55+from atpasser.uri.exceptions import InvalidURIError, ValidationError
66+77+88+class TestURI:
99+ """Test cases for the URI class."""
1010+1111+ def test_valid_uri_with_did(self):
1212+ """Test creating a URI with a valid DID."""
1313+ uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3jwdwj2ctlk26"
1414+ uri = URI(uri_str)
1515+1616+ assert str(uri) == uri_str
1717+ assert uri.authorityAsText == "did:plc:z72i7hdynmk6r22z27h6tvur"
1818+ assert uri.path == ["app.bsky.feed.post", "3jwdwj2ctlk26"]
1919+ assert uri.pathAsText == "app.bsky.feed.post/3jwdwj2ctlk26"
2020+ assert uri.query is None
2121+ assert uri.queryAsText is None
2222+ assert uri.fragment is None
2323+ assert uri.fragmentAsText is None
2424+2525+ def test_valid_uri_with_handle(self):
2626+ """Test creating a URI with a valid handle."""
2727+ uri_str = "at://bnewbold.bsky.team/app.bsky.feed.post/3jwdwj2ctlk26"
2828+ uri = URI(uri_str)
2929+3030+ assert str(uri) == uri_str
3131+ assert uri.authorityAsText == "bnewbold.bsky.team"
3232+ assert uri.path == ["app.bsky.feed.post", "3jwdwj2ctlk26"]
3333+ assert uri.pathAsText == "app.bsky.feed.post/3jwdwj2ctlk26"
3434+3535+ def test_valid_uri_with_collection_only(self):
3636+ """Test creating a URI with only a collection."""
3737+ uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post"
3838+ uri = URI(uri_str)
3939+4040+ assert str(uri) == uri_str
4141+ assert uri.authorityAsText == "did:plc:z72i7hdynmk6r22z27h6tvur"
4242+ assert uri.path == ["app.bsky.feed.post"]
4343+ assert uri.pathAsText == "app.bsky.feed.post"
4444+4545+ def test_valid_uri_with_authority_only(self):
4646+ """Test creating a URI with only an authority."""
4747+ uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur"
4848+ uri = URI(uri_str)
4949+5050+ assert str(uri) == uri_str
5151+ assert uri.authorityAsText == "did:plc:z72i7hdynmk6r22z27h6tvur"
5252+ assert uri.path == []
5353+ assert uri.pathAsText == ""
5454+5555+ def test_valid_uri_with_query(self):
5656+ """Test creating a URI with query parameters."""
5757+ uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post?param1=value1¶m2=value2"
5858+ uri = URI(uri_str)
5959+6060+ assert uri.query == {"param1": ["value1"], "param2": ["value2"]}
6161+ assert uri.queryAsText == "param1%3Dvalue1%26param2%3Dvalue2"
6262+6363+ def test_valid_uri_with_fragment(self):
6464+ """Test creating a URI with a fragment."""
6565+ uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3jwdwj2ctlk26#$.some.json.path"
6666+ uri = URI(uri_str)
6767+6868+ assert uri.fragment is not None
6969+ assert uri.fragmentAsText == "%24.some.json.path"
7070+7171+ def test_invalid_uri_non_ascii_characters(self):
7272+ """Test that non-ASCII characters in URI raise InvalidURIError."""
7373+ uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/💩"
7474+7575+ with pytest.raises(InvalidURIError, match="contains invalid characters"):
7676+ URI(uri_str)
7777+7878+ def test_invalid_uri_too_long(self):
7979+ """Test that a URI that is too long raises InvalidURIError."""
8080+ # Create a URI that exceeds the 8000 character limit
8181+ long_path = "a" * 8000
8282+ uri_str = f"at://did:plc:z72i7hdynmk6r22z27h6tvur/{long_path}"
8383+8484+ with pytest.raises(InvalidURIError, match="exceeds maximum length"):
8585+ URI(uri_str)
8686+8787+ def test_invalid_uri_wrong_scheme(self):
8888+ """Test that a URI with wrong scheme raises InvalidURIError."""
8989+ uri_str = "https://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3jwdwj2ctlk26"
9090+9191+ with pytest.raises(InvalidURIError, match="invalid format"):
9292+ URI(uri_str)
9393+9494+ def test_invalid_uri_trailing_slash(self):
9595+ """Test that a URI with trailing slash raises InvalidURIError."""
9696+ uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/"
9797+9898+ with pytest.raises(InvalidURIError, match="cannot end with a slash"):
9999+ URI(uri_str)
100100+101101+ def test_invalid_uri_with_userinfo(self):
102102+ """Test that a URI with userinfo raises InvalidURIError."""
103103+ uri_str = "at://user:pass@did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post"
104104+105105+ with pytest.raises(InvalidURIError, match="does not support user information"):
106106+ URI(uri_str)
107107+108108+ def test_uri_equality(self):
109109+ """Test URI equality comparison."""
110110+ uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3jwdwj2ctlk26"
111111+ uri1 = URI(uri_str)
112112+ uri2 = URI(uri_str)
113113+114114+ assert uri1 == uri2
115115+ assert uri1 != "not a uri object"
116116+117117+ def test_uri_string_representation(self):
118118+ """Test URI string representation."""
119119+ uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3jwdwj2ctlk26"
120120+ uri = URI(uri_str)
121121+122122+ assert str(uri) == uri_str