Also regroup the vault tests into smaller groups
Marco Ricci

Marco Ricci commited on 2025-08-14 19:10:17
Zeige 1 geänderte Dateien mit 107 Einfügungen und 38 Löschungen.


As in a2c0a0b3b6f000d824787dabc011041549bda206, introduce further
classes/groupings for the `vault` derivation scheme tests, trimming test
names appropriately.
... ...
@@ -121,6 +121,10 @@ class TestVault:
121 121
 
122 122
     phrase = PHRASE
123 123
 
124
+
125
+class TestPhraseDependence:
126
+    """Test the dependence of the internal hash on the master passphrase."""
127
+
124 128
     @hypothesis.given(
125 129
         phrases=strategies.lists(
126 130
             strategies.binary(min_size=1, max_size=BLOCK_SIZE // 2),
... ...
@@ -134,7 +138,7 @@ class TestVault:
134 138
             max_size=BLOCK_SIZE // 2,
135 139
         ),
136 140
     )
137
-    def test_100a_create_hash_phrase_dependence_small(
141
+    def test_small(
138 142
         self,
139 143
         phrases: list[bytes],
140 144
         service: str,
... ...
@@ -161,7 +165,7 @@ class TestVault:
161 165
             max_size=BLOCK_SIZE // 2,
162 166
         ),
163 167
     )
164
-    def test_100b_create_hash_phrase_dependence_medium(
168
+    def test_medium(
165 169
         self,
166 170
         phrases: list[bytes],
167 171
         service: str,
... ...
@@ -190,7 +194,7 @@ class TestVault:
190 194
             max_size=BLOCK_SIZE // 2,
191 195
         ),
192 196
     )
193
-    def test_100c_create_hash_phrase_dependence_large(
197
+    def test_large(
194 198
         self,
195 199
         phrases: tuple[bytes, bytes],
196 200
         service: str,
... ...
@@ -223,7 +227,7 @@ class TestVault:
223 227
             max_size=BLOCK_SIZE // 2,
224 228
         ),
225 229
     )
226
-    def test_100d_create_hash_phrase_dependence_mixed(
230
+    def test_mixed(
227 231
         self,
228 232
         phrases: list[bytes],
229 233
         service: str,
... ...
@@ -237,6 +241,10 @@ class TestVault:
237 241
             phrase=phrases[0], service=service
238 242
         ) != vault.Vault.create_hash(phrase=phrases[1], service=service)
239 243
 
244
+
245
+class TestServiceNameDependence:
246
+    """Test the dependence of the internal hash on the service name."""
247
+
240 248
     @hypothesis.given(
241 249
         phrase=strategies.text(
242 250
             strategies.characters(min_codepoint=32, max_codepoint=126),
... ...
@@ -250,7 +258,7 @@ class TestVault:
250 258
             unique=True,
251 259
         ),
252 260
     )
253
-    def test_101_create_hash_service_name_dependence(
261
+    def test_service_name_dependence(
254 262
         self,
255 263
         phrase: str,
256 264
         services: list[bytes],
... ...
@@ -260,6 +268,10 @@ class TestVault:
260 268
             phrase=phrase, service=services[0]
261 269
         ) != vault.Vault.create_hash(phrase=phrase, service=services[1])
262 270
 
271
+
272
+class TestInterchangablePhrases:
273
+    """Test the interchangability of certain master passphrases."""
274
+
263 275
     @hypothesis.given(
264 276
         phrases=strategies.binary(max_size=BLOCK_SIZE // 2).flatmap(
265 277
             lambda bs: strategies.tuples(
... ...
@@ -276,7 +288,7 @@ class TestVault:
276 288
             max_size=32,
277 289
         ),
278 290
     )
279
-    def test_102a_interchangable_phrases_small(
291
+    def test_small(
280 292
         self,
281 293
         phrases: tuple[bytes, bytes],
282 294
         service: str,
... ...
@@ -307,7 +319,7 @@ class TestVault:
307 319
             max_size=32,
308 320
         ),
309 321
     )
310
-    def test_102b_interchangable_phrases_large(
322
+    def test_large(
311 323
         self,
312 324
         phrases: tuple[bytes, bytes],
313 325
         service: str,
... ...
@@ -318,20 +330,27 @@ class TestVault:
318 330
             phrase=phrases[0], service=service
319 331
         ) == vault.Vault.create_hash(phrase=phrases[1], service=service)
320 332
 
333
+
334
+class TestBasicFunctionalityFromUpstream(TestVault):
335
+    """Test passphrase derivation with the "vault" scheme: upstream tests."""
336
+
321 337
     @Parametrize.SAMPLE_SERVICES_AND_PHRASES
322
-    def test_200_basic_configuration(
338
+    def test_basic_configuration(
323 339
         self, service: bytes | str, expected: bytes
324 340
     ) -> None:
325 341
         """Deriving a passphrase principally works."""
326 342
         assert vault.Vault(phrase=self.phrase).generate(service) == expected
327 343
 
328
-    def test_201_phrase_dependence(self) -> None:
344
+    def test_phrase_dependence(self) -> None:
329 345
         """The derived passphrase is dependent on the master passphrase."""
330 346
         assert (
331 347
             vault.Vault(phrase=(self.phrase + b"X")).generate("google")
332 348
             == b"n+oIz6sL>K*lTEWYRO%7"
333 349
         )
334 350
 
351
+    # TODO(the-13th-letter): Retire this test in favor of
352
+    # TestPhraseDependence.  The first example is a "short" example, the
353
+    # second is a "mixed" one.
335 354
     @hypothesis.given(
336 355
         phrases=strategies.lists(
337 356
             strategies.binary(min_size=1, max_size=32),
... ...
@@ -366,7 +385,7 @@ class TestVault:
366 385
         ),
367 386
         raises=AssertionError,
368 387
     )
369
-    def test_201a_phrase_dependence(
388
+    def test_xxx_phrase_dependence(
370 389
         self,
371 390
         phrases: list[bytes],
372 391
         service: str,
... ...
@@ -384,19 +403,28 @@ class TestVault:
384 403
             phrase=phrases[1]
385 404
         ).generate(service)
386 405
 
387
-    def test_202a_reproducibility_and_bytes_service_name(self) -> None:
406
+
407
+class TestStringAndBinaryExchangability(TestVault):
408
+    """Test the exchangability of text and byte strings in the "vault" scheme.
409
+
410
+    This specifically refers to ASCII-cleanliness, and buffer-type
411
+    independence.
412
+
413
+    """
414
+
415
+    def test_bytes_service_name(self) -> None:
388 416
         """Deriving a passphrase works equally for byte strings."""
389 417
         assert vault.Vault(phrase=self.phrase).generate(
390 418
             b"google"
391 419
         ) == vault.Vault(phrase=self.phrase).generate("google")
392 420
 
393
-    def test_202b_reproducibility_and_bytearray_service_name(self) -> None:
421
+    def test_bytearray_service_name(self) -> None:
394 422
         """Deriving a passphrase works equally for byte arrays."""
395 423
         assert vault.Vault(phrase=self.phrase).generate(
396 424
             b"google"
397 425
         ) == vault.Vault(phrase=self.phrase).generate(bytearray(b"google"))
398 426
 
399
-    def test_202c_reproducibility_and_buffer_like_service_name(self) -> None:
427
+    def test_buffer_like_service_name(self) -> None:
400 428
         """Deriving a passphrase works equally for memory views."""
401 429
         assert vault.Vault(phrase=self.phrase).generate(
402 430
             b"google"
... ...
@@ -414,7 +442,7 @@ class TestVault:
414 442
             max_size=32,
415 443
         ),
416 444
     )
417
-    def test_203a_reproducibility_and_binary_phrases(
445
+    def test_binary_phrases(
418 446
         self,
419 447
         phrase: str,
420 448
         service: str,
... ...
@@ -450,7 +478,7 @@ class TestVault:
450 478
             max_size=32,
451 479
         ),
452 480
     )
453
-    def test_203b_reproducibility_and_binary_service_name(
481
+    def test_binary_service_name(
454 482
         self,
455 483
         phrase: str,
456 484
         service: str,
... ...
@@ -474,6 +502,8 @@ class TestVault:
474 502
                 "service name generate different passphrases"
475 503
             )
476 504
 
505
+    # TODO(the-13th-letter): Retire this test in favor of
506
+    # TestServiceNameDependence.
477 507
     @hypothesis.given(
478 508
         phrase=strategies.text(
479 509
             strategies.characters(min_codepoint=32, max_codepoint=126),
... ...
@@ -487,7 +517,7 @@ class TestVault:
487 517
             unique=True,
488 518
         ),
489 519
     )
490
-    def test_204a_service_name_dependence(
520
+    def test_xxx_service_name_dependence(
491 521
         self,
492 522
         phrase: str,
493 523
         services: list[bytes],
... ...
@@ -497,6 +527,9 @@ class TestVault:
497 527
             phrase=phrase
498 528
         ).generate(services[1])
499 529
 
530
+    # TODO(the-13th-letter): Move this test into TestServiceNameDependence
531
+    # and write a counterpart for TestPhraseDependence. Or, generalize
532
+    # this test into a class TestConfigDependence.
500 533
     @hypothesis.given(
501 534
         phrase=strategies.text(
502 535
             strategies.characters(min_codepoint=32, max_codepoint=126),
... ...
@@ -511,7 +544,7 @@ class TestVault:
511 544
             unique=True,
512 545
         ),
513 546
     )
514
-    def test_204b_service_name_dependence_with_config(
547
+    def test_service_name_dependence_with_config(
515 548
         self,
516 549
         phrase: str,
517 550
         config: dict[str, int],
... ...
@@ -533,13 +566,25 @@ class TestVault:
533 566
             # implementation, and should be raised.
534 567
             raise
535 568
 
536
-    def test_210_nonstandard_length(self) -> None:
569
+
570
+# TODO(the-13th-letter): State machine, for master passphrase, service
571
+# name and config dependence?
572
+
573
+
574
+class TestConstraintSatisfactionFromUpstream001(TestVault):
575
+    """Test passphrase derivation with the "vault" scheme: upstream tests."""
576
+
577
+    def test_nonstandard_length(self) -> None:
537 578
         """Deriving a passphrase adheres to imposed length limits."""
538 579
         assert (
539 580
             vault.Vault(phrase=self.phrase, length=4).generate("google")
540 581
             == b"xDFu"
541 582
         )
542 583
 
584
+
585
+class TestConstraintSatisfactionThoroughness001(TestVault):
586
+    """Test passphrase derivation with the "vault" scheme: constraint satisfaction."""
587
+
543 588
     @hypothesis.given(
544 589
         phrase=strategies.one_of(
545 590
             strategies.binary(min_size=1, max_size=100),
... ...
@@ -552,7 +597,7 @@ class TestVault:
552 597
         length=strategies.integers(min_value=1, max_value=200),
553 598
         service=strategies.text(min_size=1, max_size=100),
554 599
     )
555
-    def test_210a_password_with_length(
600
+    def test_password_with_length(
556 601
         self,
557 602
         phrase: str | bytes,
558 603
         length: int,
... ...
@@ -562,7 +607,11 @@ class TestVault:
562 607
         password = vault.Vault(phrase=phrase, length=length).generate(service)
563 608
         assert len(password) == length
564 609
 
565
-    def test_211_repetition_limit(self) -> None:
610
+
611
+class TestConstraintSatisfactionFromUpstream002(TestVault):
612
+    """Test passphrase derivation with the "vault" scheme: upstream tests."""
613
+
614
+    def test_repetition_limit(self) -> None:
566 615
         """Deriving a passphrase adheres to imposed repetition limits."""
567 616
         assert (
568 617
             vault.Vault(
... ...
@@ -571,21 +620,21 @@ class TestVault:
571 620
             == b"IVTDzACftqopUXqDHPkuCIhV"
572 621
         )
573 622
 
574
-    def test_212_without_symbols(self) -> None:
623
+    def test_without_symbols(self) -> None:
575 624
         """Deriving a passphrase adheres to imposed limits on symbols."""
576 625
         assert (
577 626
             vault.Vault(phrase=self.phrase, symbol=0).generate("google")
578 627
             == b"XZ4wRe0bZCazbljCaMqR"
579 628
         )
580 629
 
581
-    def test_213_no_numbers(self) -> None:
630
+    def test_no_numbers(self) -> None:
582 631
         """Deriving a passphrase adheres to imposed limits on numbers."""
583 632
         assert (
584 633
             vault.Vault(phrase=self.phrase, number=0).generate("google")
585 634
             == b"_*$TVH.%^aZl(LUeOT?>"
586 635
         )
587 636
 
588
-    def test_214_no_lowercase_letters(self) -> None:
637
+    def test_no_lowercase_letters(self) -> None:
589 638
         """
590 639
         Deriving a passphrase adheres to imposed limits on lowercase letters.
591 640
         """
... ...
@@ -594,7 +643,7 @@ class TestVault:
594 643
             == b":{?)+7~@OA:L]!0E$)(+"
595 644
         )
596 645
 
597
-    def test_215_at_least_5_digits(self) -> None:
646
+    def test_at_least_5_digits(self) -> None:
598 647
         """Deriving a passphrase adheres to imposed counts of numbers."""
599 648
         assert (
600 649
             vault.Vault(phrase=self.phrase, length=8, number=5).generate(
... ...
@@ -603,14 +652,14 @@ class TestVault:
603 652
             == b"i0908.7["
604 653
         )
605 654
 
606
-    def test_216_lots_of_spaces(self) -> None:
655
+    def test_lots_of_spaces(self) -> None:
607 656
         """Deriving a passphrase adheres to imposed counts of spaces."""
608 657
         assert (
609 658
             vault.Vault(phrase=self.phrase, space=12).generate("songkick")
610 659
             == b" c   6 Bq  % 5fR    "
611 660
         )
612 661
 
613
-    def test_217_all_character_classes(self) -> None:
662
+    def test_all_character_classes(self) -> None:
614 663
         """Deriving a passphrase adheres to imposed counts of all types."""
615 664
         assert (
616 665
             vault.Vault(
... ...
@@ -625,6 +674,10 @@ class TestVault:
625 674
             == b": : fv_wqt>a-4w1S  R"
626 675
         )
627 676
 
677
+
678
+class TestConstraintSatisfactionHeavyDuty001(TestVault):
679
+    """Test passphrase derivation with the "vault" scheme: constraint satisfaction."""
680
+
628 681
     @hypothesis.given(
629 682
         phrase=strategies.one_of(
630 683
             strategies.binary(min_size=1), strategies.text(min_size=1)
... ...
@@ -674,7 +727,7 @@ class TestVault:
674 727
         },
675 728
         service="0",
676 729
     ).via('branch coverage (test function): "no repeats" case')
677
-    def test_217a_all_length_character_and_occurrence_constraints_satisfied(
730
+    def test_all_length_character_and_occurrence_constraints_satisfied(
678 731
         self,
679 732
         phrase: str | bytes,
680 733
         config: dict[str, int],
... ...
@@ -726,7 +779,11 @@ class TestVault:
726 779
                     "Password does not satisfy character repeat constraints."
727 780
                 )
728 781
 
729
-    def test_218_only_numbers_and_very_high_repetition_limit(self) -> None:
782
+
783
+class TestConstraintSatisfactionFromUpstream003(TestVault):
784
+    """Test passphrase derivation with the "vault" scheme: upstream tests."""
785
+
786
+    def test_only_numbers_and_very_high_repetition_limit(self) -> None:
730 787
         """Deriving a passphrase adheres to imposed repetition limits.
731 788
 
732 789
         This example is checked explicitly against forbidden substrings.
... ...
@@ -757,6 +814,10 @@ class TestVault:
757 814
         for substring in forbidden_substrings:
758 815
             assert substring not in generated
759 816
 
817
+
818
+class TestConstraintSatisfactionThoroughness002(TestVault):
819
+    """Test passphrase derivation with the "vault" scheme: constraint satisfaction."""
820
+
760 821
     # This test has time complexity `O(length * repeat)`, both of which
761 822
     # are chosen by hypothesis and thus outside our control.
762 823
     @hypothesis.settings(deadline=None)
... ...
@@ -787,19 +848,27 @@ class TestVault:
787 848
         for i in range((length + 1) - (repeat + 1)):
788 849
             assert len(set(password[i : i + repeat + 1])) > 1
789 850
 
790
-    def test_219_very_limited_character_set(self) -> None:
851
+
852
+class TestConstraintSatisfactionFromUpstream004(TestVault):
853
+    """Test passphrase derivation with the "vault" scheme: upstream tests."""
854
+
855
+    def test_very_limited_character_set(self) -> None:
791 856
         """Deriving a passphrase works even with limited character sets."""
792 857
         generated = vault.Vault(
793 858
             phrase=b"", length=24, lower=0, upper=0, space=0, symbol=0
794 859
         ).generate("testing")
795 860
         assert generated == b"763252593304946694588866"
796 861
 
797
-    def test_220_character_set_subtraction(self) -> None:
862
+
863
+class TestUtilities(TestVault):
864
+    """Test passphrase derivation with the "vault" scheme: utility tests."""
865
+
866
+    def test_character_set_subtraction(self) -> None:
798 867
         """Removing allowed characters internally works."""
799 868
         assert vault.Vault._subtract(b"be", b"abcdef") == bytearray(b"acdf")
800 869
 
801 870
     @Parametrize.ENTROPY_RESULTS
802
-    def test_221_entropy(
871
+    def test_entropy(
803 872
         self, length: int, settings: dict[str, int], entropy: int
804 873
     ) -> None:
805 874
         """Estimating the entropy and sufficient hash length works."""
... ...
@@ -812,7 +881,7 @@ class TestVault:
812 881
             )
813 882
         assert v._estimate_sufficient_hash_length(8.0) >= entropy
814 883
 
815
-    def test_222_hash_length_estimation(self) -> None:
884
+    def test_hash_length_estimation(self) -> None:
816 885
         """
817 886
         Estimating the entropy and hash length for degenerate cases works.
818 887
         """
... ...
@@ -829,7 +898,7 @@ class TestVault:
829 898
         assert v._estimate_sufficient_hash_length() > 0
830 899
 
831 900
     @Parametrize.SAMPLE_SERVICES_AND_PHRASES
832
-    def test_223_hash_length_expansion(
901
+    def test_hash_length_expansion(
833 902
         self,
834 903
         monkeypatch: pytest.MonkeyPatch,
835 904
         service: str | bytes,
... ...
@@ -848,7 +917,7 @@ class TestVault:
848 917
         assert v.generate(service) == expected
849 918
 
850 919
     @Parametrize.BINARY_STRINGS
851
-    def test_224_binary_strings(self, s: str | bytes | bytearray) -> None:
920
+    def test_binary_strings(self, s: str | bytes | bytearray) -> None:
852 921
         """Byte string conversion is idempotent."""
853 922
         binstr = vault.Vault._get_binary_string
854 923
         if isinstance(s, str):
... ...
@@ -858,14 +927,14 @@ class TestVault:
858 927
             assert binstr(s) == bytes(s)
859 928
             assert binstr(binstr(s)) == bytes(s)
860 929
 
861
-    def test_310_too_many_symbols(self) -> None:
930
+    def test_too_many_symbols(self) -> None:
862 931
         """Deriving short passphrases with large length constraints fails."""
863 932
         with pytest.raises(
864 933
             ValueError, match="requested passphrase length too short"
865 934
         ):
866 935
             vault.Vault(phrase=self.phrase, symbol=100)
867 936
 
868
-    def test_311_no_viable_characters(self) -> None:
937
+    def test_no_viable_characters(self) -> None:
869 938
         """Deriving passphrases without allowed characters fails."""
870 939
         with pytest.raises(ValueError, match="no allowed characters left"):
871 940
             vault.Vault(
... ...
@@ -878,14 +947,14 @@ class TestVault:
878 947
                 symbol=0,
879 948
             )
880 949
 
881
-    def test_320_character_set_subtraction_duplicate(self) -> None:
950
+    def test_character_set_subtraction_duplicate(self) -> None:
882 951
         """Character sets do not contain duplicate characters."""
883 952
         with pytest.raises(ValueError, match="duplicate characters"):
884 953
             vault.Vault._subtract(b"abcdef", b"aabbccddeeff")
885 954
         with pytest.raises(ValueError, match="duplicate characters"):
886 955
             vault.Vault._subtract(b"aabbccddeeff", b"abcdef")
887 956
 
888
-    def test_322_hash_length_estimation(self) -> None:
957
+    def test_invalid_hash_length_estimation_safety_factor(self) -> None:
889 958
         """Hash length estimation rejects invalid safety factors."""
890 959
         v = vault.Vault(phrase=self.phrase)
891 960
         with pytest.raises(ValueError, match="invalid safety factor"):
892 961