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 |