Marco Ricci commited on 2025-01-26 16:34:47
Zeige 1 geänderte Dateien mit 127 Einfügungen und 102 Löschungen.
Arrange the hypothesis tests so that they lie next to the respective non-hypothesis test, if any, instead of bundling them all in the same testing class. By doing this, related tests are closer to each other, if possible. Use the hypothesis mark instead of the testing class if filtering is required. Additionally, since the helper functions `as_ssh_string`, `canonicalize1` and `canonicalize2` now are visible in and assigned to a broader context, give them proper docstrings.
... | ... |
@@ -31,6 +31,51 @@ if TYPE_CHECKING: |
31 | 31 |
class TestStaticFunctionality: |
32 | 32 |
"""Test the static functionality of the `ssh_agent` module.""" |
33 | 33 |
|
34 |
+ @staticmethod |
|
35 |
+ def as_ssh_string(bytestring: bytes) -> bytes: |
|
36 |
+ """Return an encoded SSH string from a bytestring. |
|
37 |
+ |
|
38 |
+ This is a helper function for hypothesis data generation. |
|
39 |
+ |
|
40 |
+ """ |
|
41 |
+ return int.to_bytes(len(bytestring), 4, 'big') + bytestring |
|
42 |
+ |
|
43 |
+ @staticmethod |
|
44 |
+ def canonicalize1(data: bytes) -> bytes: |
|
45 |
+ """Return an encoded SSH string from a bytestring. |
|
46 |
+ |
|
47 |
+ This is a helper function for hypothesis testing. |
|
48 |
+ |
|
49 |
+ References: |
|
50 |
+ |
|
51 |
+ * [David R. MacIver: Another invariant to test for |
|
52 |
+ encoders][DECODE_ENCODE] |
|
53 |
+ |
|
54 |
+ [DECODE_ENCODE]: https://hypothesis.works/articles/canonical-serialization/ |
|
55 |
+ |
|
56 |
+ """ |
|
57 |
+ return ssh_agent.SSHAgentClient.string( |
|
58 |
+ ssh_agent.SSHAgentClient.unstring(data) |
|
59 |
+ ) |
|
60 |
+ |
|
61 |
+ @staticmethod |
|
62 |
+ def canonicalize2(data: bytes) -> bytes: |
|
63 |
+ """Return an encoded SSH string from a bytestring. |
|
64 |
+ |
|
65 |
+ This is a helper function for hypothesis testing. |
|
66 |
+ |
|
67 |
+ References: |
|
68 |
+ |
|
69 |
+ * [David R. MacIver: Another invariant to test for |
|
70 |
+ encoders][DECODE_ENCODE] |
|
71 |
+ |
|
72 |
+ [DECODE_ENCODE]: https://hypothesis.works/articles/canonical-serialization/ |
|
73 |
+ |
|
74 |
+ """ |
|
75 |
+ unstringed, trailer = ssh_agent.SSHAgentClient.unstring_prefix(data) |
|
76 |
+ assert not trailer |
|
77 |
+ return ssh_agent.SSHAgentClient.string(unstringed) |
|
78 |
+ |
|
34 | 79 |
# TODO(the-13th-letter): Re-evaluate if this check is worth keeping. |
35 | 80 |
# It cannot provide true tamper-resistence, but probably appears to. |
36 | 81 |
@pytest.mark.parametrize( |
... | ... |
@@ -155,6 +200,25 @@ class TestStaticFunctionality: |
155 | 200 |
uint32 = ssh_agent.SSHAgentClient.uint32 |
156 | 201 |
assert uint32(input) == expected |
157 | 202 |
|
203 |
+ @hypothesis.given(strategies.integers(min_value=0, max_value=0xFFFFFFFF)) |
|
204 |
+ @hypothesis.example(0xDEADBEEF).via('manual, pre-hypothesis example') |
|
205 |
+ def test_210a_uint32_from_number(self, num: int) -> None: |
|
206 |
+ """`uint32` encoding works, starting from numbers.""" |
|
207 |
+ uint32 = ssh_agent.SSHAgentClient.uint32 |
|
208 |
+ assert int.from_bytes(uint32(num), 'big', signed=False) == num |
|
209 |
+ |
|
210 |
+ @hypothesis.given(strategies.binary(min_size=4, max_size=4)) |
|
211 |
+ @hypothesis.example(b'\xde\xad\xbe\xef').via( |
|
212 |
+ 'manual, pre-hypothesis example' |
|
213 |
+ ) |
|
214 |
+ def test_210b_uint32_from_bytestring(self, bytestring: bytes) -> None: |
|
215 |
+ """`uint32` encoding works, starting from length four byte strings.""" |
|
216 |
+ uint32 = ssh_agent.SSHAgentClient.uint32 |
|
217 |
+ assert ( |
|
218 |
+ uint32(int.from_bytes(bytestring, 'big', signed=False)) |
|
219 |
+ == bytestring |
|
220 |
+ ) |
|
221 |
+ |
|
158 | 222 |
@pytest.mark.parametrize( |
159 | 223 |
['input', 'expected'], |
160 | 224 |
[ |
... | ... |
@@ -182,6 +246,17 @@ class TestStaticFunctionality: |
182 | 246 |
string = ssh_agent.SSHAgentClient.string |
183 | 247 |
assert bytes(string(input)) == expected |
184 | 248 |
|
249 |
+ @hypothesis.given(strategies.binary(max_size=0x0001FFFF)) |
|
250 |
+ @hypothesis.example(b'DEADBEEF' * 10000).via( |
|
251 |
+ 'manual, pre-hypothesis example with highest order bit set' |
|
252 |
+ ) |
|
253 |
+ def test_211a_string_from_bytestring(self, bytestring: bytes) -> None: |
|
254 |
+ """SSH string encoding works, starting from a byte string.""" |
|
255 |
+ res = ssh_agent.SSHAgentClient.string(bytestring) |
|
256 |
+ assert res.startswith((b'\x00\x00', b'\x00\x01')) |
|
257 |
+ assert int.from_bytes(res[:4], 'big', signed=False) == len(bytestring) |
|
258 |
+ assert res[4:] == bytestring |
|
259 |
+ |
|
185 | 260 |
@pytest.mark.parametrize( |
186 | 261 |
['input', 'expected'], |
187 | 262 |
[ |
... | ... |
@@ -209,6 +284,58 @@ class TestStaticFunctionality: |
209 | 284 |
b'', |
210 | 285 |
) |
211 | 286 |
|
287 |
+ @hypothesis.given(strategies.binary(max_size=0x00FFFFFF)) |
|
288 |
+ @hypothesis.example(b'\x00\x00\x00\x07ssh-rsa').via( |
|
289 |
+ 'manual, pre-hypothesis example to attempt to detect double-decoding' |
|
290 |
+ ) |
|
291 |
+ @hypothesis.example(b'\x00\x00\x00\x01').via( |
|
292 |
+ 'detect no-op encoding via ill-formed SSH string' |
|
293 |
+ ) |
|
294 |
+ def test_212a_unstring_of_string_of_data(self, bytestring: bytes) -> None: |
|
295 |
+ """SSH string decoding of encoded SSH strings works. |
|
296 |
+ |
|
297 |
+ References: |
|
298 |
+ |
|
299 |
+ * [David R. MacIver: The Encode/Decode invariant][ENCODE_DECODE] |
|
300 |
+ |
|
301 |
+ [ENCODE_DECODE]: https://hypothesis.works/articles/encode-decode-invariant/ |
|
302 |
+ |
|
303 |
+ """ |
|
304 |
+ string = ssh_agent.SSHAgentClient.string |
|
305 |
+ unstring = ssh_agent.SSHAgentClient.unstring |
|
306 |
+ unstring_prefix = ssh_agent.SSHAgentClient.unstring_prefix |
|
307 |
+ encoded = string(bytestring) |
|
308 |
+ assert unstring(encoded) == bytestring |
|
309 |
+ assert unstring_prefix(encoded) == (bytestring, b'') |
|
310 |
+ trailing_data = b' trailing data' |
|
311 |
+ encoded2 = string(bytestring) + trailing_data |
|
312 |
+ assert unstring_prefix(encoded2) == (bytestring, trailing_data) |
|
313 |
+ |
|
314 |
+ @hypothesis.given( |
|
315 |
+ strategies.binary(max_size=0x00FFFFFF).map( |
|
316 |
+ # Scoping issues, and the fact that staticmethod objects |
|
317 |
+ # (before class finalization) are not callable, necessitate |
|
318 |
+ # wrapping this staticmethod call in a lambda. |
|
319 |
+ lambda x: TestStaticFunctionality.as_ssh_string(x) # noqa: PLW0108 |
|
320 |
+ ), |
|
321 |
+ ) |
|
322 |
+ def test_212b_string_of_unstring_of_data(self, encoded: bytes) -> None: |
|
323 |
+ """SSH string decoding of encoded SSH strings works. |
|
324 |
+ |
|
325 |
+ References: |
|
326 |
+ |
|
327 |
+ * [David R. MacIver: Another invariant to test for |
|
328 |
+ encoders][DECODE_ENCODE] |
|
329 |
+ |
|
330 |
+ [DECODE_ENCODE]: https://hypothesis.works/articles/canonical-serialization/ |
|
331 |
+ |
|
332 |
+ """ |
|
333 |
+ canonical_functions = [self.canonicalize1, self.canonicalize2] |
|
334 |
+ for canon1 in canonical_functions: |
|
335 |
+ for canon2 in canonical_functions: |
|
336 |
+ assert canon1(encoded) == canon2(encoded) |
|
337 |
+ assert canon1(canon2(encoded)) == canon1(encoded) |
|
338 |
+ |
|
212 | 339 |
@pytest.mark.parametrize( |
213 | 340 |
['value', 'exc_type', 'exc_pattern'], |
214 | 341 |
[ |
... | ... |
@@ -814,105 +941,3 @@ class TestAgentInteraction: |
814 | 941 |
match=r'Malformed response|does not match request', |
815 | 942 |
): |
816 | 943 |
client.query_extensions() |
817 |
- |
|
818 |
- |
|
819 |
-class TestHypotheses: |
|
820 |
- """Test properties via hypothesis.""" |
|
821 |
- |
|
822 |
- @staticmethod |
|
823 |
- def as_ssh_string(bytestring: bytes) -> bytes: |
|
824 |
- return int.to_bytes(len(bytestring), 4, 'big') + bytestring |
|
825 |
- |
|
826 |
- @staticmethod |
|
827 |
- def canonicalize1(data: bytes) -> bytes: |
|
828 |
- return ssh_agent.SSHAgentClient.string( |
|
829 |
- ssh_agent.SSHAgentClient.unstring(data) |
|
830 |
- ) |
|
831 |
- |
|
832 |
- @staticmethod |
|
833 |
- def canonicalize2(data: bytes) -> bytes: |
|
834 |
- unstringed, trailer = ssh_agent.SSHAgentClient.unstring_prefix(data) |
|
835 |
- assert not trailer |
|
836 |
- return ssh_agent.SSHAgentClient.string(unstringed) |
|
837 |
- |
|
838 |
- @hypothesis.given(strategies.integers(min_value=0, max_value=0xFFFFFFFF)) |
|
839 |
- @hypothesis.example(0xDEADBEEF).via('manual, pre-hypothesis example') |
|
840 |
- def test_210a_uint32_from_number(self, num: int) -> None: |
|
841 |
- """`uint32` encoding works, starting from numbers.""" |
|
842 |
- uint32 = ssh_agent.SSHAgentClient.uint32 |
|
843 |
- assert int.from_bytes(uint32(num), 'big', signed=False) == num |
|
844 |
- |
|
845 |
- @hypothesis.given(strategies.binary(min_size=4, max_size=4)) |
|
846 |
- @hypothesis.example(b'\xde\xad\xbe\xef').via( |
|
847 |
- 'manual, pre-hypothesis example' |
|
848 |
- ) |
|
849 |
- def test_210b_uint32_from_bytestring(self, bytestring: bytes) -> None: |
|
850 |
- """`uint32` encoding works, starting from length four byte strings.""" |
|
851 |
- uint32 = ssh_agent.SSHAgentClient.uint32 |
|
852 |
- assert ( |
|
853 |
- uint32(int.from_bytes(bytestring, 'big', signed=False)) |
|
854 |
- == bytestring |
|
855 |
- ) |
|
856 |
- |
|
857 |
- @hypothesis.given(strategies.binary(max_size=0x0001FFFF)) |
|
858 |
- @hypothesis.example(b'DEADBEEF' * 10000).via( |
|
859 |
- 'manual, pre-hypothesis example with highest order bit set' |
|
860 |
- ) |
|
861 |
- def test_211a_string_from_bytestring(self, bytestring: bytes) -> None: |
|
862 |
- """SSH string encoding works, starting from a byte string.""" |
|
863 |
- res = ssh_agent.SSHAgentClient.string(bytestring) |
|
864 |
- assert res.startswith((b'\x00\x00', b'\x00\x01')) |
|
865 |
- assert int.from_bytes(res[:4], 'big', signed=False) == len(bytestring) |
|
866 |
- assert res[4:] == bytestring |
|
867 |
- |
|
868 |
- @hypothesis.given(strategies.binary(max_size=0x00FFFFFF)) |
|
869 |
- @hypothesis.example(b'\x00\x00\x00\x07ssh-rsa').via( |
|
870 |
- 'manual, pre-hypothesis example to attempt to detect double-decoding' |
|
871 |
- ) |
|
872 |
- @hypothesis.example(b'\x00\x00\x00\x01').via( |
|
873 |
- 'detect no-op encoding via ill-formed SSH string' |
|
874 |
- ) |
|
875 |
- def test_212a_unstring_of_string_of_data(self, bytestring: bytes) -> None: |
|
876 |
- """SSH string decoding of encoded SSH strings works. |
|
877 |
- |
|
878 |
- References: |
|
879 |
- |
|
880 |
- * [David R. MacIver: The Encode/Decode invariant][ENCODE_DECODE] |
|
881 |
- |
|
882 |
- [ENCODE_DECODE]: https://hypothesis.works/articles/encode-decode-invariant/ |
|
883 |
- |
|
884 |
- """ |
|
885 |
- string = ssh_agent.SSHAgentClient.string |
|
886 |
- unstring = ssh_agent.SSHAgentClient.unstring |
|
887 |
- unstring_prefix = ssh_agent.SSHAgentClient.unstring_prefix |
|
888 |
- encoded = string(bytestring) |
|
889 |
- assert unstring(encoded) == bytestring |
|
890 |
- assert unstring_prefix(encoded) == (bytestring, b'') |
|
891 |
- trailing_data = b' trailing data' |
|
892 |
- encoded2 = string(bytestring) + trailing_data |
|
893 |
- assert unstring_prefix(encoded2) == (bytestring, trailing_data) |
|
894 |
- |
|
895 |
- @hypothesis.given( |
|
896 |
- strategies.binary(max_size=0x00FFFFFF).map( |
|
897 |
- # Scoping issues, and the fact that staticmethod objects |
|
898 |
- # (before class finalization) are not callable, necessitate |
|
899 |
- # wrapping this staticmethod call in a lambda. |
|
900 |
- lambda x: TestHypotheses.as_ssh_string(x) # noqa: PLW0108 |
|
901 |
- ), |
|
902 |
- ) |
|
903 |
- def test_212b_string_of_unstring_of_data(self, encoded: bytes) -> None: |
|
904 |
- """SSH string decoding of encoded SSH strings works. |
|
905 |
- |
|
906 |
- References: |
|
907 |
- |
|
908 |
- * [David R. MacIver: Another invariant to test for |
|
909 |
- encoders][DECODE_ENCODE] |
|
910 |
- |
|
911 |
- [DECODE_ENCODE]: https://hypothesis.works/articles/canonical-serialization/ |
|
912 |
- |
|
913 |
- """ |
|
914 |
- canonical_functions = [self.canonicalize1, self.canonicalize2] |
|
915 |
- for canon1 in canonical_functions: |
|
916 |
- for canon2 in canonical_functions: |
|
917 |
- assert canon1(encoded) == canon2(encoded) |
|
918 |
- assert canon1(canon2(encoded)) == canon1(encoded) |
|
919 | 944 |