Rearrange hypothesis tests in the `vault` module
Marco Ricci

Marco Ricci commited on 2025-01-26 20:10:35
Zeige 1 geänderte Dateien mit 152 Einfügungen und 156 Löschungen.


Just like in 9db6c6591de054433ae98d59f7930364e7d03286 for the
`ssh_agent` tests, arrange the `vault` hypothesis tests so that they lie
next to the respective non-hypothesis test, if any.  Also rename and/or
renumber them as necessary.
... ...
@@ -77,6 +77,29 @@ class TestVault:
77 77
             Vault(phrase=self.phrase, length=4).generate('google') == b'xDFu'
78 78
         )
79 79
 
80
+    @tests.hypothesis_settings_coverage_compatible
81
+    @hypothesis.given(
82
+        phrase=strategies.one_of(
83
+            strategies.binary(min_size=1, max_size=100),
84
+            strategies.text(
85
+                min_size=1,
86
+                max_size=100,
87
+                alphabet=strategies.characters(max_codepoint=255),
88
+            ),
89
+        ),
90
+        length=strategies.integers(min_value=1, max_value=200),
91
+        service=strategies.text(min_size=1, max_size=100),
92
+    )
93
+    def test_210a_password_with_length(
94
+        self,
95
+        phrase: str | bytes,
96
+        length: int,
97
+        service: str,
98
+    ) -> None:
99
+        """Derived passphrases have the requested length."""
100
+        password = Vault(phrase=phrase, length=length).generate(service)
101
+        assert len(password) == length
102
+
80 103
     def test_211_repetition_limit(self) -> None:
81 104
         """Deriving a passphrase adheres to imposed repetition limits."""
82 105
         assert (
... ...
@@ -138,6 +161,105 @@ class TestVault:
138 161
             == b': : fv_wqt>a-4w1S  R'
139 162
         )
140 163
 
164
+    @tests.hypothesis_settings_coverage_compatible
165
+    @hypothesis.given(
166
+        phrase=strategies.one_of(
167
+            strategies.binary(min_size=1), strategies.text(min_size=1)
168
+        ),
169
+        config=tests.vault_full_service_config(),
170
+        service=strategies.text(min_size=1),
171
+    )
172
+    # regression test
173
+    @hypothesis.example(
174
+        phrase=b'\x00',
175
+        config={
176
+            'lower': 0,
177
+            'upper': 0,
178
+            'number': 0,
179
+            'space': 2,
180
+            'dash': 0,
181
+            'symbol': 1,
182
+            'repeat': 2,
183
+            'length': 3,
184
+        },
185
+        service='0',
186
+    )
187
+    # regression test
188
+    @hypothesis.example(
189
+        phrase=b'\x00',
190
+        config={
191
+            'lower': 0,
192
+            'upper': 0,
193
+            'number': 0,
194
+            'space': 1,
195
+            'dash': 0,
196
+            'symbol': 0,
197
+            'repeat': 9,
198
+            'length': 5,
199
+        },
200
+        service='0',
201
+    )
202
+    # branch coverage: case `repeat = 0` in `if config[repeat]` below
203
+    @hypothesis.example(
204
+        phrase=b'\x00',
205
+        config={
206
+            'lower': 0,
207
+            'upper': 0,
208
+            'number': 0,
209
+            'space': 1,
210
+            'dash': 0,
211
+            'symbol': 0,
212
+            'repeat': 0,
213
+            'length': 5,
214
+        },
215
+        service='0',
216
+    )
217
+    def test_217a_all_length_character_and_occurrence_constraints_satisfied(
218
+        self,
219
+        phrase: str | bytes,
220
+        config: dict[str, int],
221
+        service: str,
222
+    ) -> None:
223
+        """Derived passphrases obey character and occurrence restraints."""
224
+        try:
225
+            password = Vault(phrase=phrase, **config).generate(service)
226
+        except ValueError as exc:
227
+            if 'no allowed characters left' in exc.args:
228
+                return
229
+            raise  # pragma: no cover
230
+        n = len(password)
231
+        assert n == config['length'], 'Password has wrong length.'
232
+        for key in ('lower', 'upper', 'number', 'space', 'dash', 'symbol'):
233
+            if config[key] > 0:
234
+                assert (
235
+                    sum(c in Vault._CHARSETS[key] for c in password)
236
+                    >= config[key]
237
+                ), (
238
+                    'Password does not satisfy '
239
+                    'character occurrence constraints.'
240
+                )
241
+            elif key in {'dash', 'symbol'}:
242
+                # Character classes overlap, so "forbidden" characters may
243
+                # appear via the other character class.
244
+                assert True
245
+            else:
246
+                assert sum(c in Vault._CHARSETS[key] for c in password) == 0, (
247
+                    'Password does not satisfy character ban constraints.'
248
+                )
249
+
250
+        T = TypeVar('T', str, bytes)
251
+
252
+        def length_r_substrings(string: T, *, r: int) -> Iterator[T]:
253
+            for i in range(len(string) - (r - 1)):
254
+                yield string[i : i + r]
255
+
256
+        repeat = config['repeat']
257
+        if repeat:
258
+            for snippet in length_r_substrings(password, r=(repeat + 1)):
259
+                assert len(set(snippet)) > 1, (
260
+                    'Password does not satisfy character repeat constraints.'
261
+                )
262
+
141 263
     def test_218_only_numbers_and_very_high_repetition_limit(self) -> None:
142 264
         """Deriving a passphrase adheres to imposed repetition limits.
143 265
 
... ...
@@ -169,6 +291,36 @@ class TestVault:
169 291
         for substring in forbidden_substrings:
170 292
             assert substring not in generated
171 293
 
294
+    # This test has time complexity `O(length * repeat)`, both of which
295
+    # are chosen by hypothesis and thus outside our control.
296
+    @hypothesis.settings(deadline=None)
297
+    @hypothesis.given(
298
+        phrase=strategies.one_of(
299
+            strategies.binary(min_size=1, max_size=100),
300
+            strategies.text(
301
+                min_size=1,
302
+                max_size=100,
303
+                alphabet=strategies.characters(max_codepoint=255),
304
+            ),
305
+        ),
306
+        length=strategies.integers(min_value=2, max_value=200),
307
+        repeat=strategies.integers(min_value=1, max_value=200),
308
+        service=strategies.text(min_size=1, max_size=1000),
309
+    )
310
+    def test_218a_arbitrary_repetition_limit(
311
+        self,
312
+        phrase: str | bytes,
313
+        length: int,
314
+        repeat: int,
315
+        service: str,
316
+    ) -> None:
317
+        """Derived passphrases obey the given occurrence constraint."""
318
+        password = Vault(phrase=phrase, length=length, repeat=repeat).generate(
319
+            service
320
+        )
321
+        for i in range((length + 1) - (repeat + 1)):
322
+            assert len(set(password[i : i + repeat + 1])) > 1
323
+
172 324
     def test_219_very_limited_character_set(self) -> None:
173 325
         """Deriving a passphrase works even with limited character sets."""
174 326
         generated = Vault(
... ...
@@ -310,159 +462,3 @@ class TestVault:
310 462
             TypeError, match='invalid safety factor: not a float'
311 463
         ):
312 464
             assert v._estimate_sufficient_hash_length(None)  # type: ignore[arg-type]
313
-
314
-
315
-class TestHypotheses:
316
-    """Test properties via hypothesis."""
317
-
318
-    @tests.hypothesis_settings_coverage_compatible
319
-    @hypothesis.given(
320
-        phrase=strategies.one_of(
321
-            strategies.binary(min_size=1), strategies.text(min_size=1)
322
-        ),
323
-        config=tests.vault_full_service_config(),
324
-        service=strategies.text(min_size=1),
325
-    )
326
-    # regression test
327
-    @hypothesis.example(
328
-        phrase=b'\x00',
329
-        config={
330
-            'lower': 0,
331
-            'upper': 0,
332
-            'number': 0,
333
-            'space': 2,
334
-            'dash': 0,
335
-            'symbol': 1,
336
-            'repeat': 2,
337
-            'length': 3,
338
-        },
339
-        service='0',
340
-    )
341
-    # regression test
342
-    @hypothesis.example(
343
-        phrase=b'\x00',
344
-        config={
345
-            'lower': 0,
346
-            'upper': 0,
347
-            'number': 0,
348
-            'space': 1,
349
-            'dash': 0,
350
-            'symbol': 0,
351
-            'repeat': 9,
352
-            'length': 5,
353
-        },
354
-        service='0',
355
-    )
356
-    # branch coverage: case `repeat = 0` in `if config[repeat]` below
357
-    @hypothesis.example(
358
-        phrase=b'\x00',
359
-        config={
360
-            'lower': 0,
361
-            'upper': 0,
362
-            'number': 0,
363
-            'space': 1,
364
-            'dash': 0,
365
-            'symbol': 0,
366
-            'repeat': 0,
367
-            'length': 5,
368
-        },
369
-        service='0',
370
-    )
371
-    def test_100_all_length_character_and_occurrence_constraints_satisfied(
372
-        self,
373
-        phrase: str | bytes,
374
-        config: dict[str, int],
375
-        service: str,
376
-    ) -> None:
377
-        """Derived passphrases obey character and occurrence restraints."""
378
-        try:
379
-            password = Vault(phrase=phrase, **config).generate(service)
380
-        except ValueError as exc:
381
-            if 'no allowed characters left' in exc.args:
382
-                return
383
-            raise  # pragma: no cover
384
-        n = len(password)
385
-        assert n == config['length'], 'Password has wrong length.'
386
-        for key in ('lower', 'upper', 'number', 'space', 'dash', 'symbol'):
387
-            if config[key] > 0:
388
-                assert (
389
-                    sum(c in Vault._CHARSETS[key] for c in password)
390
-                    >= config[key]
391
-                ), (
392
-                    'Password does not satisfy '
393
-                    'character occurrence constraints.'
394
-                )
395
-            elif key in {'dash', 'symbol'}:
396
-                # Character classes overlap, so "forbidden" characters may
397
-                # appear via the other character class.
398
-                assert True
399
-            else:
400
-                assert sum(c in Vault._CHARSETS[key] for c in password) == 0, (
401
-                    'Password does not satisfy character ban constraints.'
402
-                )
403
-
404
-        T = TypeVar('T', str, bytes)
405
-
406
-        def length_r_substrings(string: T, *, r: int) -> Iterator[T]:
407
-            for i in range(len(string) - (r - 1)):
408
-                yield string[i : i + r]
409
-
410
-        repeat = config['repeat']
411
-        if repeat:
412
-            for snippet in length_r_substrings(password, r=(repeat + 1)):
413
-                assert len(set(snippet)) > 1, (
414
-                    'Password does not satisfy character repeat constraints.'
415
-                )
416
-
417
-    @tests.hypothesis_settings_coverage_compatible
418
-    @hypothesis.given(
419
-        phrase=strategies.one_of(
420
-            strategies.binary(min_size=1, max_size=100),
421
-            strategies.text(
422
-                min_size=1,
423
-                max_size=100,
424
-                alphabet=strategies.characters(max_codepoint=255),
425
-            ),
426
-        ),
427
-        length=strategies.integers(min_value=1, max_value=200),
428
-        service=strategies.text(min_size=1, max_size=100),
429
-    )
430
-    def test_101_password_with_length(
431
-        self,
432
-        phrase: str | bytes,
433
-        length: int,
434
-        service: str,
435
-    ) -> None:
436
-        """Derived passphrases have the requested length."""
437
-        password = Vault(phrase=phrase, length=length).generate(service)
438
-        assert len(password) == length
439
-
440
-    # This test has time complexity `O(length * repeat)`, both of which
441
-    # are chosen by hypothesis and thus outside our control.
442
-    @hypothesis.settings(deadline=None)
443
-    @hypothesis.given(
444
-        phrase=strategies.one_of(
445
-            strategies.binary(min_size=1, max_size=100),
446
-            strategies.text(
447
-                min_size=1,
448
-                max_size=100,
449
-                alphabet=strategies.characters(max_codepoint=255),
450
-            ),
451
-        ),
452
-        length=strategies.integers(min_value=2, max_value=200),
453
-        repeat=strategies.integers(min_value=1, max_value=200),
454
-        service=strategies.text(min_size=1, max_size=1000),
455
-    )
456
-    def test_102_password_with_repeat(
457
-        self,
458
-        phrase: str | bytes,
459
-        length: int,
460
-        repeat: int,
461
-        service: str,
462
-    ) -> None:
463
-        """Derived passphrases obey the given occurrence constraint."""
464
-        password = Vault(phrase=phrase, length=length, repeat=repeat).generate(
465
-            service
466
-        )
467
-        for i in range((length + 1) - (repeat + 1)):
468
-            assert len(set(password[i : i + repeat + 1])) > 1
469 465