Marco Ricci commited on 2024-07-21 10:09:10
Zeige 13 geänderte Dateien mit 1583 Einfügungen und 913 Löschungen.
| ... | ... |
@@ -2,9 +2,7 @@ |
| 2 | 2 |
# |
| 3 | 3 |
# SPDX-License-Identifier: MIT |
| 4 | 4 |
|
| 5 |
-"""Work-alike of vault(1) – a deterministic, stateless password manager |
|
| 6 |
- |
|
| 7 |
-""" # noqa: RUF002 |
|
| 5 |
+"""Work-alike of vault(1) – a deterministic, stateless password manager""" # noqa: RUF002 |
|
| 8 | 6 |
|
| 9 | 7 |
from __future__ import annotations |
| 10 | 8 |
|
| ... | ... |
@@ -19,8 +17,9 @@ from typing_extensions import assert_type |
| 19 | 17 |
import sequin |
| 20 | 18 |
import ssh_agent_client |
| 21 | 19 |
|
| 22 |
-__author__ = "Marco Ricci <m@the13thletter.info>" |
|
| 23 |
-__version__ = "0.1.1" |
|
| 20 |
+__author__ = 'Marco Ricci <m@the13thletter.info>' |
|
| 21 |
+__version__ = '0.1.1' |
|
| 22 |
+ |
|
| 24 | 23 |
|
| 25 | 24 |
class AmbiguousByteRepresentationError(ValueError): |
| 26 | 25 |
"""The object has an ambiguous byte representation.""" |
| ... | ... |
@@ -40,8 +40,10 @@ _CHARSETS = collections.OrderedDict([ |
| 40 | 40 |
]) |
| 41 | 41 |
_CHARSETS['alpha'] = _CHARSETS['lower'] + _CHARSETS['upper'] |
| 42 | 42 |
_CHARSETS['alphanum'] = _CHARSETS['alpha'] + _CHARSETS['number'] |
| 43 |
-_CHARSETS['all'] = (_CHARSETS['alphanum'] + _CHARSETS['space'] |
|
| 44 |
- + _CHARSETS['symbol']) |
|
| 43 |
+_CHARSETS['all'] = ( |
|
| 44 |
+ _CHARSETS['alphanum'] + _CHARSETS['space'] + _CHARSETS['symbol'] |
|
| 45 |
+) |
|
| 46 |
+ |
|
| 45 | 47 |
|
| 46 | 48 |
class Vault: |
| 47 | 49 |
"""A work-alike of James Coglan's vault. |
| ... | ... |
@@ -75,10 +78,16 @@ class Vault: |
| 75 | 78 |
""" |
| 76 | 79 |
|
| 77 | 80 |
def __init__( |
| 78 |
- self, *, phrase: bytes | bytearray | str = b'', |
|
| 79 |
- length: int = 20, repeat: int = 0, lower: int | None = None, |
|
| 80 |
- upper: int | None = None, number: int | None = None, |
|
| 81 |
- space: int | None = None, dash: int | None = None, |
|
| 81 |
+ self, |
|
| 82 |
+ *, |
|
| 83 |
+ phrase: bytes | bytearray | str = b'', |
|
| 84 |
+ length: int = 20, |
|
| 85 |
+ repeat: int = 0, |
|
| 86 |
+ lower: int | None = None, |
|
| 87 |
+ upper: int | None = None, |
|
| 88 |
+ number: int | None = None, |
|
| 89 |
+ space: int | None = None, |
|
| 90 |
+ dash: int | None = None, |
|
| 82 | 91 |
symbol: int | None = None, |
| 83 | 92 |
) -> None: |
| 84 | 93 |
"""Initialize the Vault object. |
| ... | ... |
@@ -177,7 +188,8 @@ class Vault: |
| 177 | 188 |
return math.fsum(math.log2(f) for f in factors) |
| 178 | 189 |
|
| 179 | 190 |
def _estimate_sufficient_hash_length( |
| 180 |
- self, safety_factor: float = 2.0, |
|
| 191 |
+ self, |
|
| 192 |
+ safety_factor: float = 2.0, |
|
| 181 | 193 |
) -> int: |
| 182 | 194 |
"""Estimate the sufficient hash length, given the current settings. |
| 183 | 195 |
|
| ... | ... |
@@ -241,8 +253,11 @@ class Vault: |
| 241 | 253 |
|
| 242 | 254 |
@classmethod |
| 243 | 255 |
def create_hash( |
| 244 |
- cls, phrase: bytes | bytearray | str, |
|
| 245 |
- service: bytes | bytearray, *, length: int = 32, |
|
| 256 |
+ cls, |
|
| 257 |
+ phrase: bytes | bytearray | str, |
|
| 258 |
+ service: bytes | bytearray, |
|
| 259 |
+ *, |
|
| 260 |
+ length: int = 32, |
|
| 246 | 261 |
) -> bytes: |
| 247 | 262 |
r"""Create a pseudorandom byte stream from phrase and service. |
| 248 | 263 |
|
| ... | ... |
@@ -302,11 +317,19 @@ class Vault: |
| 302 | 317 |
phrase = cls._get_binary_string(phrase) |
| 303 | 318 |
assert not isinstance(phrase, str) |
| 304 | 319 |
salt = bytes(service) + cls._UUID |
| 305 |
- return hashlib.pbkdf2_hmac(hash_name='sha1', password=phrase, |
|
| 306 |
- salt=salt, iterations=8, dklen=length) |
|
| 320 |
+ return hashlib.pbkdf2_hmac( |
|
| 321 |
+ hash_name='sha1', |
|
| 322 |
+ password=phrase, |
|
| 323 |
+ salt=salt, |
|
| 324 |
+ iterations=8, |
|
| 325 |
+ dklen=length, |
|
| 326 |
+ ) |
|
| 307 | 327 |
|
| 308 | 328 |
def generate( |
| 309 |
- self, service_name: str | bytes | bytearray, /, *, |
|
| 329 |
+ self, |
|
| 330 |
+ service_name: str | bytes | bytearray, |
|
| 331 |
+ /, |
|
| 332 |
+ *, |
|
| 310 | 333 |
phrase: bytes | bytearray | str = b'', |
| 311 | 334 |
) -> bytes: |
| 312 | 335 |
r"""Generate a service passphrase. |
| ... | ... |
@@ -358,8 +381,11 @@ class Vault: |
| 358 | 381 |
while True: |
| 359 | 382 |
try: |
| 360 | 383 |
required = self._required[:] |
| 361 |
- seq = sequin.Sequin(self.create_hash( |
|
| 362 |
- phrase=phrase, service=service_name, length=hash_length)) |
|
| 384 |
+ seq = sequin.Sequin( |
|
| 385 |
+ self.create_hash( |
|
| 386 |
+ phrase=phrase, service=service_name, length=hash_length |
|
| 387 |
+ ) |
|
| 388 |
+ ) |
|
| 363 | 389 |
result = bytearray() |
| 364 | 390 |
while len(result) < self._length: |
| 365 | 391 |
pos = seq.generate(len(required)) |
| ... | ... |
@@ -374,8 +400,9 @@ class Vault: |
| 374 | 400 |
if self._repeat and result: |
| 375 | 401 |
bad_suffix = bytes(result[-1:]) * (self._repeat - 1) |
| 376 | 402 |
if result.endswith(bad_suffix): |
| 377 |
- charset = self._subtract(bytes(result[-1:]), |
|
| 378 |
- charset) |
|
| 403 |
+ charset = self._subtract( |
|
| 404 |
+ bytes(result[-1:]), charset |
|
| 405 |
+ ) |
|
| 379 | 406 |
pos = seq.generate(len(charset)) |
| 380 | 407 |
result.extend(charset[pos : pos + 1]) |
| 381 | 408 |
except sequin.SequinExhaustedError: |
| ... | ... |
@@ -399,19 +426,16 @@ class Vault: |
| 399 | 426 |
|
| 400 | 427 |
""" |
| 401 | 428 |
deterministic_signature_types = {
|
| 402 |
- 'ssh-ed25519': |
|
| 403 |
- lambda k: k.startswith(b'\x00\x00\x00\x0bssh-ed25519'), |
|
| 404 |
- 'ssh-ed448': |
|
| 405 |
- lambda k: k.startswith(b'\x00\x00\x00\x09ssh-ed448'), |
|
| 406 |
- 'ssh-rsa': |
|
| 407 |
- lambda k: k.startswith(b'\x00\x00\x00\x07ssh-rsa'), |
|
| 429 |
+ 'ssh-ed25519': lambda k: k.startswith( |
|
| 430 |
+ b'\x00\x00\x00\x0bssh-ed25519' |
|
| 431 |
+ ), |
|
| 432 |
+ 'ssh-ed448': lambda k: k.startswith(b'\x00\x00\x00\x09ssh-ed448'), |
|
| 433 |
+ 'ssh-rsa': lambda k: k.startswith(b'\x00\x00\x00\x07ssh-rsa'), |
|
| 408 | 434 |
} |
| 409 | 435 |
return any(v(key) for v in deterministic_signature_types.values()) |
| 410 | 436 |
|
| 411 | 437 |
@classmethod |
| 412 |
- def phrase_from_key( |
|
| 413 |
- cls, key: bytes | bytearray, / |
|
| 414 |
- ) -> bytes: |
|
| 438 |
+ def phrase_from_key(cls, key: bytes | bytearray, /) -> bytes: |
|
| 415 | 439 |
"""Obtain the master passphrase from a configured SSH key. |
| 416 | 440 |
|
| 417 | 441 |
vault allows the usage of certain SSH keys to derive a master |
| ... | ... |
@@ -456,8 +480,10 @@ class Vault: |
| 456 | 480 |
|
| 457 | 481 |
""" |
| 458 | 482 |
if not cls._is_suitable_ssh_key(key): |
| 459 |
- msg = ('unsuitable SSH key: bad key, or '
|
|
| 460 |
- 'signature not deterministic') |
|
| 483 |
+ msg = ( |
|
| 484 |
+ 'unsuitable SSH key: bad key, or ' |
|
| 485 |
+ 'signature not deterministic' |
|
| 486 |
+ ) |
|
| 461 | 487 |
raise ValueError(msg) |
| 462 | 488 |
with ssh_agent_client.SSHAgentClient() as client: |
| 463 | 489 |
raw_sig = client.sign(key, cls._UUID) |
| ... | ... |
@@ -467,7 +493,8 @@ class Vault: |
| 467 | 493 |
|
| 468 | 494 |
@staticmethod |
| 469 | 495 |
def _subtract( |
| 470 |
- charset: bytes | bytearray, allowed: bytes | bytearray, |
|
| 496 |
+ charset: bytes | bytearray, |
|
| 497 |
+ allowed: bytes | bytearray, |
|
| 471 | 498 |
) -> bytearray: |
| 472 | 499 |
"""Remove the characters in charset from allowed. |
| 473 | 500 |
|
| ... | ... |
@@ -489,8 +516,9 @@ class Vault: |
| 489 | 516 |
`allowed` or `charset` contained duplicate characters. |
| 490 | 517 |
|
| 491 | 518 |
""" |
| 492 |
- allowed = (allowed if isinstance(allowed, bytearray) |
|
| 493 |
- else bytearray(allowed)) |
|
| 519 |
+ allowed = ( |
|
| 520 |
+ allowed if isinstance(allowed, bytearray) else bytearray(allowed) |
|
| 521 |
+ ) |
|
| 494 | 522 |
assert_type(allowed, bytearray) |
| 495 | 523 |
msg_dup_characters = 'duplicate characters in set' |
| 496 | 524 |
if len(frozenset(allowed)) != len(allowed): |
| ... | ... |
@@ -2,9 +2,7 @@ |
| 2 | 2 |
# |
| 3 | 3 |
# SPDX-License-Identifier: MIT |
| 4 | 4 |
|
| 5 |
-"""Command-line interface for derivepassphrase. |
|
| 6 |
- |
|
| 7 |
-""" |
|
| 5 |
+"""Command-line interface for derivepassphrase.""" |
|
| 8 | 6 |
|
| 9 | 7 |
from __future__ import annotations |
| 10 | 8 |
|
| ... | ... |
@@ -64,8 +62,9 @@ def _config_filename() -> str | bytes | pathlib.Path: |
| 64 | 62 |
|
| 65 | 63 |
""" |
| 66 | 64 |
path: str | bytes | pathlib.Path |
| 67 |
- path = (os.getenv(PROG_NAME.upper() + '_PATH') |
|
| 68 |
- or click.get_app_dir(PROG_NAME, force_posix=True)) |
|
| 65 |
+ path = os.getenv(PROG_NAME.upper() + '_PATH') or click.get_app_dir( |
|
| 66 |
+ PROG_NAME, force_posix=True |
|
| 67 |
+ ) |
|
| 69 | 68 |
return os.path.join(path, 'settings.json') |
| 70 | 69 |
|
| 71 | 70 |
|
| ... | ... |
@@ -122,8 +121,7 @@ def _save_config(config: dpp_types.VaultConfig, /) -> None: |
| 122 | 121 |
|
| 123 | 122 |
|
| 124 | 123 |
def _get_suitable_ssh_keys( |
| 125 |
- conn: ssh_agent_client.SSHAgentClient | socket.socket | None = None, |
|
| 126 |
- / |
|
| 124 |
+ conn: ssh_agent_client.SSHAgentClient | socket.socket | None = None, / |
|
| 127 | 125 |
) -> Iterator[ssh_agent_client.types.KeyCommentPair]: |
| 128 | 126 |
"""Yield all SSH keys suitable for passphrase derivation. |
| 129 | 127 |
|
| ... | ... |
@@ -188,7 +186,8 @@ def _get_suitable_ssh_keys( |
| 188 | 186 |
|
| 189 | 187 |
|
| 190 | 188 |
def _prompt_for_selection( |
| 191 |
- items: Sequence[str | bytes], heading: str = 'Possible choices:', |
|
| 189 |
+ items: Sequence[str | bytes], |
|
| 190 |
+ heading: str = 'Possible choices:', |
|
| 192 | 191 |
single_choice_prompt: str = 'Confirm this choice?', |
| 193 | 192 |
) -> int: |
| 194 | 193 |
"""Prompt user for a choice among the given items. |
| ... | ... |
@@ -229,26 +228,34 @@ def _prompt_for_selection( |
| 229 | 228 |
choices = click.Choice([''] + [str(i) for i in range(1, n + 1)]) |
| 230 | 229 |
choice = click.prompt( |
| 231 | 230 |
f'Your selection? (1-{n}, leave empty to abort)',
|
| 232 |
- err=True, type=choices, show_choices=False, |
|
| 233 |
- show_default=False, default='') |
|
| 231 |
+ err=True, |
|
| 232 |
+ type=choices, |
|
| 233 |
+ show_choices=False, |
|
| 234 |
+ show_default=False, |
|
| 235 |
+ default='', |
|
| 236 |
+ ) |
|
| 234 | 237 |
if not choice: |
| 235 | 238 |
raise IndexError(_EMPTY_SELECTION) |
| 236 | 239 |
return int(choice) - 1 |
| 237 |
- prompt_suffix = (' '
|
|
| 238 |
- if single_choice_prompt.endswith(tuple('?.!'))
|
|
| 239 |
- else ': ') |
|
| 240 |
+ prompt_suffix = ( |
|
| 241 |
+ ' ' if single_choice_prompt.endswith(tuple('?.!')) else ': '
|
|
| 242 |
+ ) |
|
| 240 | 243 |
try: |
| 241 |
- click.confirm(single_choice_prompt, |
|
| 242 |
- prompt_suffix=prompt_suffix, err=True, |
|
| 243 |
- abort=True, default=False, show_default=False) |
|
| 244 |
+ click.confirm( |
|
| 245 |
+ single_choice_prompt, |
|
| 246 |
+ prompt_suffix=prompt_suffix, |
|
| 247 |
+ err=True, |
|
| 248 |
+ abort=True, |
|
| 249 |
+ default=False, |
|
| 250 |
+ show_default=False, |
|
| 251 |
+ ) |
|
| 244 | 252 |
except click.Abort: |
| 245 | 253 |
raise IndexError(_EMPTY_SELECTION) from None |
| 246 | 254 |
return 0 |
| 247 | 255 |
|
| 248 | 256 |
|
| 249 | 257 |
def _select_ssh_key( |
| 250 |
- conn: ssh_agent_client.SSHAgentClient | socket.socket | None = None, |
|
| 251 |
- / |
|
| 258 |
+ conn: ssh_agent_client.SSHAgentClient | socket.socket | None = None, / |
|
| 252 | 259 |
) -> bytes | bytearray: |
| 253 | 260 |
"""Interactively select an SSH key for passphrase derivation. |
| 254 | 261 |
|
| ... | ... |
@@ -292,14 +299,18 @@ def _select_ssh_key( |
| 292 | 299 |
for key, comment in suitable_keys: |
| 293 | 300 |
keytype = unstring_prefix(key)[0].decode('ASCII')
|
| 294 | 301 |
key_str = base64.standard_b64encode(key).decode('ASCII')
|
| 295 |
- key_prefix = (key_str |
|
| 302 |
+ key_prefix = ( |
|
| 303 |
+ key_str |
|
| 296 | 304 |
if len(key_str) < KEY_DISPLAY_LENGTH + len('...')
|
| 297 |
- else key_str[:KEY_DISPLAY_LENGTH] + '...') |
|
| 305 |
+ else key_str[:KEY_DISPLAY_LENGTH] + '...' |
|
| 306 |
+ ) |
|
| 298 | 307 |
comment_str = comment.decode('UTF-8', errors='replace')
|
| 299 | 308 |
key_listing.append(f'{keytype} {key_prefix} {comment_str}')
|
| 300 | 309 |
choice = _prompt_for_selection( |
| 301 |
- key_listing, heading='Suitable SSH keys:', |
|
| 302 |
- single_choice_prompt='Use this key?') |
|
| 310 |
+ key_listing, |
|
| 311 |
+ heading='Suitable SSH keys:', |
|
| 312 |
+ single_choice_prompt='Use this key?', |
|
| 313 |
+ ) |
|
| 303 | 314 |
return suitable_keys[choice].key |
| 304 | 315 |
|
| 305 | 316 |
|
| ... | ... |
@@ -313,8 +324,9 @@ def _prompt_for_passphrase() -> str: |
| 313 | 324 |
The user input. |
| 314 | 325 |
|
| 315 | 326 |
""" |
| 316 |
- return click.prompt('Passphrase', default='', hide_input=True,
|
|
| 317 |
- show_default=False, err=True) |
|
| 327 |
+ return click.prompt( |
|
| 328 |
+ 'Passphrase', default='', hide_input=True, show_default=False, err=True |
|
| 329 |
+ ) |
|
| 318 | 330 |
|
| 319 | 331 |
|
| 320 | 332 |
class OptionGroupOption(click.Option): |
| ... | ... |
@@ -353,7 +366,9 @@ class CommandWithHelpGroups(click.Command): |
| 353 | 366 |
""" |
| 354 | 367 |
|
| 355 | 368 |
def format_options( |
| 356 |
- self, ctx: click.Context, formatter: click.HelpFormatter, |
|
| 369 |
+ self, |
|
| 370 |
+ ctx: click.Context, |
|
| 371 |
+ formatter: click.HelpFormatter, |
|
| 357 | 372 |
) -> None: |
| 358 | 373 |
r"""Format options on the help listing, grouped into sections. |
| 359 | 374 |
|
| ... | ... |
@@ -398,8 +413,9 @@ class CommandWithHelpGroups(click.Command): |
| 398 | 413 |
group_name = '' |
| 399 | 414 |
help_records.setdefault(group_name, []).append(rec) |
| 400 | 415 |
default_group = help_records.pop('')
|
| 401 |
- default_group_name = ('Other Options' if len(default_group) > 1
|
|
| 402 |
- else 'Options') |
|
| 416 |
+ default_group_name = ( |
|
| 417 |
+ 'Other Options' if len(default_group) > 1 else 'Options' |
|
| 418 |
+ ) |
|
| 403 | 419 |
help_records[default_group_name] = default_group |
| 404 | 420 |
for group_name, records in help_records.items(): |
| 405 | 421 |
with formatter.section(group_name): |
| ... | ... |
@@ -414,31 +430,37 @@ class CommandWithHelpGroups(click.Command): |
| 414 | 430 |
# Concrete option groups used by this command-line interface. |
| 415 | 431 |
class PasswordGenerationOption(OptionGroupOption): |
| 416 | 432 |
"""Password generation options for the CLI.""" |
| 433 |
+ |
|
| 417 | 434 |
option_group_name = 'Password generation' |
| 418 |
- epilog = ''' |
|
| 435 |
+ epilog = """ |
|
| 419 | 436 |
Use NUMBER=0, e.g. "--symbol 0", to exclude a character type |
| 420 | 437 |
from the output. |
| 421 |
- ''' |
|
| 438 |
+ """ |
|
| 422 | 439 |
|
| 423 | 440 |
|
| 424 | 441 |
class ConfigurationOption(OptionGroupOption): |
| 425 | 442 |
"""Configuration options for the CLI.""" |
| 443 |
+ |
|
| 426 | 444 |
option_group_name = 'Configuration' |
| 427 |
- epilog = ''' |
|
| 445 |
+ epilog = """ |
|
| 428 | 446 |
Use $VISUAL or $EDITOR to configure the spawned editor. |
| 429 |
- ''' |
|
| 447 |
+ """ |
|
| 430 | 448 |
|
| 431 | 449 |
|
| 432 | 450 |
class StorageManagementOption(OptionGroupOption): |
| 433 | 451 |
"""Storage management options for the CLI.""" |
| 452 |
+ |
|
| 434 | 453 |
option_group_name = 'Storage management' |
| 435 |
- epilog = ''' |
|
| 454 |
+ epilog = """ |
|
| 436 | 455 |
Using "-" as PATH for standard input/standard output is |
| 437 | 456 |
supported. |
| 438 |
- ''' |
|
| 457 |
+ """ |
|
| 458 |
+ |
|
| 439 | 459 |
|
| 440 | 460 |
def _validate_occurrence_constraint( |
| 441 |
- ctx: click.Context, param: click.Parameter, value: Any, |
|
| 461 |
+ ctx: click.Context, |
|
| 462 |
+ param: click.Parameter, |
|
| 463 |
+ value: Any, |
|
| 442 | 464 |
) -> int | None: |
| 443 | 465 |
"""Check that the occurrence constraint is valid (int, 0 or larger).""" |
| 444 | 466 |
del ctx # Unused. |
| ... | ... |
@@ -460,7 +482,9 @@ def _validate_occurrence_constraint( |
| 460 | 482 |
|
| 461 | 483 |
|
| 462 | 484 |
def _validate_length( |
| 463 |
- ctx: click.Context, param: click.Parameter, value: Any, |
|
| 485 |
+ ctx: click.Context, |
|
| 486 |
+ param: click.Parameter, |
|
| 487 |
+ value: Any, |
|
| 464 | 488 |
) -> int | None: |
| 465 | 489 |
"""Check that the length is valid (int, 1 or larger).""" |
| 466 | 490 |
del ctx # Unused. |
| ... | ... |
@@ -480,7 +504,8 @@ def _validate_length( |
| 480 | 504 |
raise click.BadParameter(msg) |
| 481 | 505 |
return int_value |
| 482 | 506 |
|
| 483 |
-DEFAULT_NOTES_TEMPLATE = '''\ |
|
| 507 |
+ |
|
| 508 |
+DEFAULT_NOTES_TEMPLATE = """\ |
|
| 484 | 509 |
# Enter notes below the line with the cut mark (ASCII scissors and |
| 485 | 510 |
# dashes). Lines above the cut mark (such as this one) will be ignored. |
| 486 | 511 |
# |
| ... | ... |
@@ -490,14 +515,14 @@ DEFAULT_NOTES_TEMPLATE = '''\ |
| 490 | 515 |
# retained. |
| 491 | 516 |
# |
| 492 | 517 |
# - - - - - >8 - - - - - >8 - - - - - >8 - - - - - >8 - - - - - |
| 493 |
-''' |
|
| 518 |
+""" |
|
| 494 | 519 |
DEFAULT_NOTES_MARKER = '# - - - - - >8 - - - - -' |
| 495 | 520 |
|
| 496 | 521 |
|
| 497 | 522 |
@click.command( |
| 498 |
- context_settings={"help_option_names": ["-h", "--help"]},
|
|
| 523 |
+ context_settings={'help_option_names': ['-h', '--help']},
|
|
| 499 | 524 |
cls=CommandWithHelpGroups, |
| 500 |
- epilog=r''' |
|
| 525 |
+ epilog=r""" |
|
| 501 | 526 |
WARNING: There is NO WAY to retrieve the generated passphrases |
| 502 | 527 |
if the master passphrase, the SSH key, or the exact passphrase |
| 503 | 528 |
settings are lost, short of trying out all possible |
| ... | ... |
@@ -510,74 +535,145 @@ DEFAULT_NOTES_MARKER = '# - - - - - >8 - - - - -' |
| 510 | 535 |
`C:\Users\<user>\AppData\Roaming\Derivepassphrase` on Windows. |
| 511 | 536 |
The configuration is NOT encrypted, and you are STRONGLY |
| 512 | 537 |
discouraged from using a stored passphrase. |
| 513 |
- ''', |
|
| 538 |
+ """, |
|
| 514 | 539 |
) |
| 515 |
-@click.option('-p', '--phrase', 'use_phrase', is_flag=True,
|
|
| 540 |
+@click.option( |
|
| 541 |
+ '-p', |
|
| 542 |
+ '--phrase', |
|
| 543 |
+ 'use_phrase', |
|
| 544 |
+ is_flag=True, |
|
| 516 | 545 |
help='prompts you for your passphrase', |
| 517 |
- cls=PasswordGenerationOption) |
|
| 518 |
-@click.option('-k', '--key', 'use_key', is_flag=True,
|
|
| 546 |
+ cls=PasswordGenerationOption, |
|
| 547 |
+) |
|
| 548 |
+@click.option( |
|
| 549 |
+ '-k', |
|
| 550 |
+ '--key', |
|
| 551 |
+ 'use_key', |
|
| 552 |
+ is_flag=True, |
|
| 519 | 553 |
help='uses your SSH private key to generate passwords', |
| 520 |
- cls=PasswordGenerationOption) |
|
| 521 |
-@click.option('-l', '--length', metavar='NUMBER',
|
|
| 554 |
+ cls=PasswordGenerationOption, |
|
| 555 |
+) |
|
| 556 |
+@click.option( |
|
| 557 |
+ '-l', |
|
| 558 |
+ '--length', |
|
| 559 |
+ metavar='NUMBER', |
|
| 522 | 560 |
callback=_validate_length, |
| 523 | 561 |
help='emits password of length NUMBER', |
| 524 |
- cls=PasswordGenerationOption) |
|
| 525 |
-@click.option('-r', '--repeat', metavar='NUMBER',
|
|
| 562 |
+ cls=PasswordGenerationOption, |
|
| 563 |
+) |
|
| 564 |
+@click.option( |
|
| 565 |
+ '-r', |
|
| 566 |
+ '--repeat', |
|
| 567 |
+ metavar='NUMBER', |
|
| 526 | 568 |
callback=_validate_occurrence_constraint, |
| 527 | 569 |
help='allows maximum of NUMBER repeated adjacent chars', |
| 528 |
- cls=PasswordGenerationOption) |
|
| 529 |
-@click.option('--lower', metavar='NUMBER',
|
|
| 570 |
+ cls=PasswordGenerationOption, |
|
| 571 |
+) |
|
| 572 |
+@click.option( |
|
| 573 |
+ '--lower', |
|
| 574 |
+ metavar='NUMBER', |
|
| 530 | 575 |
callback=_validate_occurrence_constraint, |
| 531 | 576 |
help='includes at least NUMBER lowercase letters', |
| 532 |
- cls=PasswordGenerationOption) |
|
| 533 |
-@click.option('--upper', metavar='NUMBER',
|
|
| 577 |
+ cls=PasswordGenerationOption, |
|
| 578 |
+) |
|
| 579 |
+@click.option( |
|
| 580 |
+ '--upper', |
|
| 581 |
+ metavar='NUMBER', |
|
| 534 | 582 |
callback=_validate_occurrence_constraint, |
| 535 | 583 |
help='includes at least NUMBER uppercase letters', |
| 536 |
- cls=PasswordGenerationOption) |
|
| 537 |
-@click.option('--number', metavar='NUMBER',
|
|
| 584 |
+ cls=PasswordGenerationOption, |
|
| 585 |
+) |
|
| 586 |
+@click.option( |
|
| 587 |
+ '--number', |
|
| 588 |
+ metavar='NUMBER', |
|
| 538 | 589 |
callback=_validate_occurrence_constraint, |
| 539 | 590 |
help='includes at least NUMBER digits', |
| 540 |
- cls=PasswordGenerationOption) |
|
| 541 |
-@click.option('--space', metavar='NUMBER',
|
|
| 591 |
+ cls=PasswordGenerationOption, |
|
| 592 |
+) |
|
| 593 |
+@click.option( |
|
| 594 |
+ '--space', |
|
| 595 |
+ metavar='NUMBER', |
|
| 542 | 596 |
callback=_validate_occurrence_constraint, |
| 543 | 597 |
help='includes at least NUMBER spaces', |
| 544 |
- cls=PasswordGenerationOption) |
|
| 545 |
-@click.option('--dash', metavar='NUMBER',
|
|
| 598 |
+ cls=PasswordGenerationOption, |
|
| 599 |
+) |
|
| 600 |
+@click.option( |
|
| 601 |
+ '--dash', |
|
| 602 |
+ metavar='NUMBER', |
|
| 546 | 603 |
callback=_validate_occurrence_constraint, |
| 547 | 604 |
help='includes at least NUMBER "-" or "_"', |
| 548 |
- cls=PasswordGenerationOption) |
|
| 549 |
-@click.option('--symbol', metavar='NUMBER',
|
|
| 605 |
+ cls=PasswordGenerationOption, |
|
| 606 |
+) |
|
| 607 |
+@click.option( |
|
| 608 |
+ '--symbol', |
|
| 609 |
+ metavar='NUMBER', |
|
| 550 | 610 |
callback=_validate_occurrence_constraint, |
| 551 | 611 |
help='includes at least NUMBER symbol chars', |
| 552 |
- cls=PasswordGenerationOption) |
|
| 553 |
-@click.option('-n', '--notes', 'edit_notes', is_flag=True,
|
|
| 612 |
+ cls=PasswordGenerationOption, |
|
| 613 |
+) |
|
| 614 |
+@click.option( |
|
| 615 |
+ '-n', |
|
| 616 |
+ '--notes', |
|
| 617 |
+ 'edit_notes', |
|
| 618 |
+ is_flag=True, |
|
| 554 | 619 |
help='spawn an editor to edit notes for SERVICE', |
| 555 |
- cls=ConfigurationOption) |
|
| 556 |
-@click.option('-c', '--config', 'store_config_only', is_flag=True,
|
|
| 620 |
+ cls=ConfigurationOption, |
|
| 621 |
+) |
|
| 622 |
+@click.option( |
|
| 623 |
+ '-c', |
|
| 624 |
+ '--config', |
|
| 625 |
+ 'store_config_only', |
|
| 626 |
+ is_flag=True, |
|
| 557 | 627 |
help='saves the given settings for SERVICE or global', |
| 558 |
- cls=ConfigurationOption) |
|
| 559 |
-@click.option('-x', '--delete', 'delete_service_settings', is_flag=True,
|
|
| 628 |
+ cls=ConfigurationOption, |
|
| 629 |
+) |
|
| 630 |
+@click.option( |
|
| 631 |
+ '-x', |
|
| 632 |
+ '--delete', |
|
| 633 |
+ 'delete_service_settings', |
|
| 634 |
+ is_flag=True, |
|
| 560 | 635 |
help='deletes settings for SERVICE', |
| 561 |
- cls=ConfigurationOption) |
|
| 562 |
-@click.option('--delete-globals', is_flag=True,
|
|
| 636 |
+ cls=ConfigurationOption, |
|
| 637 |
+) |
|
| 638 |
+@click.option( |
|
| 639 |
+ '--delete-globals', |
|
| 640 |
+ is_flag=True, |
|
| 563 | 641 |
help='deletes the global shared settings', |
| 564 |
- cls=ConfigurationOption) |
|
| 565 |
-@click.option('-X', '--clear', 'clear_all_settings', is_flag=True,
|
|
| 642 |
+ cls=ConfigurationOption, |
|
| 643 |
+) |
|
| 644 |
+@click.option( |
|
| 645 |
+ '-X', |
|
| 646 |
+ '--clear', |
|
| 647 |
+ 'clear_all_settings', |
|
| 648 |
+ is_flag=True, |
|
| 566 | 649 |
help='deletes all settings', |
| 567 |
- cls=ConfigurationOption) |
|
| 568 |
-@click.option('-e', '--export', 'export_settings', metavar='PATH',
|
|
| 650 |
+ cls=ConfigurationOption, |
|
| 651 |
+) |
|
| 652 |
+@click.option( |
|
| 653 |
+ '-e', |
|
| 654 |
+ '--export', |
|
| 655 |
+ 'export_settings', |
|
| 656 |
+ metavar='PATH', |
|
| 569 | 657 |
type=click.Path(file_okay=True, allow_dash=True, exists=False), |
| 570 | 658 |
help='export all saved settings into file PATH', |
| 571 |
- cls=StorageManagementOption) |
|
| 572 |
-@click.option('-i', '--import', 'import_settings', metavar='PATH',
|
|
| 659 |
+ cls=StorageManagementOption, |
|
| 660 |
+) |
|
| 661 |
+@click.option( |
|
| 662 |
+ '-i', |
|
| 663 |
+ '--import', |
|
| 664 |
+ 'import_settings', |
|
| 665 |
+ metavar='PATH', |
|
| 573 | 666 |
type=click.Path(file_okay=True, allow_dash=True, exists=False), |
| 574 | 667 |
help='import saved settings from file PATH', |
| 575 |
- cls=StorageManagementOption) |
|
| 668 |
+ cls=StorageManagementOption, |
|
| 669 |
+) |
|
| 576 | 670 |
@click.version_option(version=dpp.__version__, prog_name=PROG_NAME) |
| 577 | 671 |
@click.argument('service', required=False)
|
| 578 | 672 |
@click.pass_context |
| 579 | 673 |
def derivepassphrase( |
| 580 |
- ctx: click.Context, /, *, |
|
| 674 |
+ ctx: click.Context, |
|
| 675 |
+ /, |
|
| 676 |
+ *, |
|
| 581 | 677 |
service: str | None = None, |
| 582 | 678 |
use_phrase: bool = False, |
| 583 | 679 |
use_key: bool = False, |
| ... | ... |
@@ -704,7 +800,8 @@ def derivepassphrase( |
| 704 | 800 |
group = StorageManagementOption |
| 705 | 801 |
case OptionGroupOption(): |
| 706 | 802 |
raise AssertionError( # noqa: TRY003 |
| 707 |
- f'Unknown option group for {param!r}') # noqa: EM102
|
|
| 803 |
+ f'Unknown option group for {param!r}' # noqa: EM102
|
|
| 804 |
+ ) |
|
| 708 | 805 |
case _: |
| 709 | 806 |
group = click.Option |
| 710 | 807 |
options_in_group.setdefault(group, []).append(param) |
| ... | ... |
@@ -716,7 +813,8 @@ def derivepassphrase( |
| 716 | 813 |
return bool(ctx.params.get(param.human_readable_name)) |
| 717 | 814 |
|
| 718 | 815 |
def check_incompatible_options( |
| 719 |
- param: click.Parameter | str, *incompatible: click.Parameter | str, |
|
| 816 |
+ param: click.Parameter | str, |
|
| 817 |
+ *incompatible: click.Parameter | str, |
|
| 720 | 818 |
) -> None: |
| 721 | 819 |
if isinstance(param, str): |
| 722 | 820 |
param = params_by_str[param] |
| ... | ... |
@@ -731,7 +829,8 @@ def derivepassphrase( |
| 731 | 829 |
opt_str = param.opts[0] |
| 732 | 830 |
other_str = other.opts[0] |
| 733 | 831 |
raise click.BadOptionUsage( |
| 734 |
- opt_str, f'mutually exclusive with {other_str}', ctx=ctx)
|
|
| 832 |
+ opt_str, f'mutually exclusive with {other_str}', ctx=ctx
|
|
| 833 |
+ ) |
|
| 735 | 834 |
|
| 736 | 835 |
def get_config() -> dpp_types.VaultConfig: |
| 737 | 836 |
try: |
| ... | ... |
@@ -748,15 +847,20 @@ def derivepassphrase( |
| 748 | 847 |
for opt in options_in_group[group]: |
| 749 | 848 |
if opt != params_by_str['--config']: |
| 750 | 849 |
check_incompatible_options( |
| 751 |
- opt, *options_in_group[PasswordGenerationOption]) |
|
| 850 |
+ opt, *options_in_group[PasswordGenerationOption] |
|
| 851 |
+ ) |
|
| 752 | 852 |
|
| 753 | 853 |
for group in (ConfigurationOption, StorageManagementOption): |
| 754 | 854 |
for opt in options_in_group[group]: |
| 755 | 855 |
check_incompatible_options( |
| 756 |
- opt, *options_in_group[ConfigurationOption], |
|
| 757 |
- *options_in_group[StorageManagementOption]) |
|
| 758 |
- sv_options = (options_in_group[PasswordGenerationOption] + |
|
| 759 |
- [params_by_str['--notes'], params_by_str['--delete']]) |
|
| 856 |
+ opt, |
|
| 857 |
+ *options_in_group[ConfigurationOption], |
|
| 858 |
+ *options_in_group[StorageManagementOption], |
|
| 859 |
+ ) |
|
| 860 |
+ sv_options = options_in_group[PasswordGenerationOption] + [ |
|
| 861 |
+ params_by_str['--notes'], |
|
| 862 |
+ params_by_str['--delete'], |
|
| 863 |
+ ] |
|
| 760 | 864 |
sv_options.remove(params_by_str['--key']) |
| 761 | 865 |
sv_options.remove(params_by_str['--phrase']) |
| 762 | 866 |
for param in sv_options: |
| ... | ... |
@@ -765,16 +869,17 @@ def derivepassphrase( |
| 765 | 869 |
msg = f'{opt_str} requires a SERVICE'
|
| 766 | 870 |
raise click.UsageError(msg) |
| 767 | 871 |
for param in [params_by_str['--key'], params_by_str['--phrase']]: |
| 768 |
- if ( |
|
| 769 |
- is_param_set(param) |
|
| 770 |
- and not (service or is_param_set(params_by_str['--config'])) |
|
| 872 |
+ if is_param_set(param) and not ( |
|
| 873 |
+ service or is_param_set(params_by_str['--config']) |
|
| 771 | 874 |
): |
| 772 | 875 |
opt_str = param.opts[0] |
| 773 | 876 |
msg = f'{opt_str} requires a SERVICE or --config'
|
| 774 | 877 |
raise click.UsageError(msg) |
| 775 |
- no_sv_options = [params_by_str['--delete-globals'], |
|
| 878 |
+ no_sv_options = [ |
|
| 879 |
+ params_by_str['--delete-globals'], |
|
| 776 | 880 |
params_by_str['--clear'], |
| 777 |
- *options_in_group[StorageManagementOption]] |
|
| 881 |
+ *options_in_group[StorageManagementOption], |
|
| 882 |
+ ] |
|
| 778 | 883 |
for param in no_sv_options: |
| 779 | 884 |
if is_param_set(param) and service: |
| 780 | 885 |
opt_str = param.opts[0] |
| ... | ... |
@@ -784,10 +889,9 @@ def derivepassphrase( |
| 784 | 889 |
if edit_notes: |
| 785 | 890 |
assert service is not None |
| 786 | 891 |
configuration = get_config() |
| 787 |
- text = (DEFAULT_NOTES_TEMPLATE + |
|
| 788 |
- configuration['services'] |
|
| 789 |
- .get(service, cast(dpp_types.VaultConfigServicesSettings, {}))
|
|
| 790 |
- .get('notes', ''))
|
|
| 892 |
+ text = DEFAULT_NOTES_TEMPLATE + configuration['services'].get( |
|
| 893 |
+ service, cast(dpp_types.VaultConfigServicesSettings, {})
|
|
| 894 |
+ ).get('notes', '')
|
|
| 791 | 895 |
notes_value = click.edit(text=text) |
| 792 | 896 |
if notes_value is not None: |
| 793 | 897 |
notes_lines = collections.deque(notes_value.splitlines(True)) |
| ... | ... |
@@ -800,7 +904,8 @@ def derivepassphrase( |
| 800 | 904 |
if not notes_value.strip(): |
| 801 | 905 |
ctx.fail('not saving new notes: user aborted request')
|
| 802 | 906 |
configuration['services'].setdefault(service, {})['notes'] = (
|
| 803 |
- notes_value.strip('\n'))
|
|
| 907 |
+ notes_value.strip('\n')
|
|
| 908 |
+ ) |
|
| 804 | 909 |
_save_config(configuration) |
| 805 | 910 |
elif delete_service_settings: |
| 806 | 911 |
assert service is not None |
| ... | ... |
@@ -818,9 +923,11 @@ def derivepassphrase( |
| 818 | 923 |
elif import_settings: |
| 819 | 924 |
try: |
| 820 | 925 |
# TODO: keep track of auto-close; try os.dup if feasible |
| 821 |
- infile = (cast(TextIO, import_settings) |
|
| 926 |
+ infile = ( |
|
| 927 |
+ cast(TextIO, import_settings) |
|
| 822 | 928 |
if hasattr(import_settings, 'close') |
| 823 |
- else click.open_file(os.fspath(import_settings), 'rt')) |
|
| 929 |
+ else click.open_file(os.fspath(import_settings), 'rt') |
|
| 930 |
+ ) |
|
| 824 | 931 |
with infile: |
| 825 | 932 |
maybe_config = json.load(infile) |
| 826 | 933 |
except json.JSONDecodeError as e: |
| ... | ... |
@@ -835,9 +942,11 @@ def derivepassphrase( |
| 835 | 942 |
configuration = get_config() |
| 836 | 943 |
try: |
| 837 | 944 |
# TODO: keep track of auto-close; try os.dup if feasible |
| 838 |
- outfile = (cast(TextIO, export_settings) |
|
| 945 |
+ outfile = ( |
|
| 946 |
+ cast(TextIO, export_settings) |
|
| 839 | 947 |
if hasattr(export_settings, 'close') |
| 840 |
- else click.open_file(os.fspath(export_settings), 'wt')) |
|
| 948 |
+ else click.open_file(os.fspath(export_settings), 'wt') |
|
| 949 |
+ ) |
|
| 841 | 950 |
with outfile: |
| 842 | 951 |
json.dump(configuration, outfile) |
| 843 | 952 |
except OSError as e: |
| ... | ... |
@@ -849,20 +958,36 @@ def derivepassphrase( |
| 849 | 958 |
# have a type guarding function anyway, assert that we didn't |
| 850 | 959 |
# make any mistakes at the end instead. |
| 851 | 960 |
global_keys = {'key', 'phrase'}
|
| 852 |
- service_keys = {'key', 'phrase', 'length', 'repeat', 'lower',
|
|
| 853 |
- 'upper', 'number', 'space', 'dash', 'symbol'} |
|
| 961 |
+ service_keys = {
|
|
| 962 |
+ 'key', |
|
| 963 |
+ 'phrase', |
|
| 964 |
+ 'length', |
|
| 965 |
+ 'repeat', |
|
| 966 |
+ 'lower', |
|
| 967 |
+ 'upper', |
|
| 968 |
+ 'number', |
|
| 969 |
+ 'space', |
|
| 970 |
+ 'dash', |
|
| 971 |
+ 'symbol', |
|
| 972 |
+ } |
|
| 854 | 973 |
settings: collections.ChainMap[str, Any] = collections.ChainMap( |
| 855 |
- {k: v for k, v in locals().items()
|
|
| 856 |
- if k in service_keys and v is not None}, |
|
| 857 |
- cast(dict[str, Any], |
|
| 858 |
- configuration['services'].get(service or '', {})),
|
|
| 974 |
+ {
|
|
| 975 |
+ k: v |
|
| 976 |
+ for k, v in locals().items() |
|
| 977 |
+ if k in service_keys and v is not None |
|
| 978 |
+ }, |
|
| 979 |
+ cast( |
|
| 980 |
+ dict[str, Any], |
|
| 981 |
+ configuration['services'].get(service or '', {}),
|
|
| 982 |
+ ), |
|
| 859 | 983 |
{},
|
| 860 |
- cast(dict[str, Any], configuration.get('global', {}))
|
|
| 984 |
+ cast(dict[str, Any], configuration.get('global', {})),
|
|
| 861 | 985 |
) |
| 862 | 986 |
if use_key: |
| 863 | 987 |
try: |
| 864 |
- key = base64.standard_b64encode( |
|
| 865 |
- _select_ssh_key()).decode('ASCII')
|
|
| 988 |
+ key = base64.standard_b64encode(_select_ssh_key()).decode( |
|
| 989 |
+ 'ASCII' |
|
| 990 |
+ ) |
|
| 866 | 991 |
except IndexError: |
| 867 | 992 |
ctx.fail('no valid SSH key selected')
|
| 868 | 993 |
except (LookupError, RuntimeError) as e: |
| ... | ... |
@@ -875,8 +1000,11 @@ def derivepassphrase( |
| 875 | 1000 |
phrase = maybe_phrase |
| 876 | 1001 |
if store_config_only: |
| 877 | 1002 |
view: collections.ChainMap[str, Any] |
| 878 |
- view = (collections.ChainMap(*settings.maps[:2]) if service |
|
| 879 |
- else settings.parents.parents) |
|
| 1003 |
+ view = ( |
|
| 1004 |
+ collections.ChainMap(*settings.maps[:2]) |
|
| 1005 |
+ if service |
|
| 1006 |
+ else settings.parents.parents |
|
| 1007 |
+ ) |
|
| 880 | 1008 |
if use_key: |
| 881 | 1009 |
view['key'] = key |
| 882 | 1010 |
for m in view.maps: |
| ... | ... |
@@ -887,25 +1015,29 @@ def derivepassphrase( |
| 887 | 1015 |
m.pop('key', '')
|
| 888 | 1016 |
if not view.maps[0]: |
| 889 | 1017 |
settings_type = 'service' if service else 'global' |
| 890 |
- msg = (f'cannot update {settings_type} settings without '
|
|
| 891 |
- f'actual settings') |
|
| 1018 |
+ msg = ( |
|
| 1019 |
+ f'cannot update {settings_type} settings without '
|
|
| 1020 |
+ f'actual settings' |
|
| 1021 |
+ ) |
|
| 892 | 1022 |
raise click.UsageError(msg) |
| 893 | 1023 |
if service: |
| 894 |
- configuration['services'].setdefault( |
|
| 895 |
- service, {}).update(view) # type: ignore[typeddict-item]
|
|
| 1024 |
+ configuration['services'].setdefault(service, {}).update(view) # type: ignore[typeddict-item]
|
|
| 896 | 1025 |
else: |
| 897 |
- configuration.setdefault( |
|
| 898 |
- 'global', {}).update(view) # type: ignore[typeddict-item]
|
|
| 899 |
- assert dpp_types.is_vault_config(configuration), ( |
|
| 900 |
- f'invalid vault configuration: {configuration!r}'
|
|
| 901 |
- ) |
|
| 1026 |
+ configuration.setdefault('global', {}).update(view) # type: ignore[typeddict-item]
|
|
| 1027 |
+ assert dpp_types.is_vault_config( |
|
| 1028 |
+ configuration |
|
| 1029 |
+ ), f'invalid vault configuration: {configuration!r}'
|
|
| 902 | 1030 |
_save_config(configuration) |
| 903 | 1031 |
else: |
| 904 | 1032 |
if not service: |
| 905 | 1033 |
msg = 'SERVICE is required' |
| 906 | 1034 |
raise click.UsageError(msg) |
| 907 |
- kwargs: dict[str, Any] = {k: v for k, v in settings.items()
|
|
| 908 |
- if k in service_keys and v is not None} |
|
| 1035 |
+ kwargs: dict[str, Any] = {
|
|
| 1036 |
+ k: v |
|
| 1037 |
+ for k, v in settings.items() |
|
| 1038 |
+ if k in service_keys and v is not None |
|
| 1039 |
+ } |
|
| 1040 |
+ |
|
| 909 | 1041 |
# If either --key or --phrase are given, use that setting. |
| 910 | 1042 |
# Otherwise, if both key and phrase are set in the config, |
| 911 | 1043 |
# one must be global (ignore it) and one must be |
| ... | ... |
@@ -915,10 +1047,12 @@ def derivepassphrase( |
| 915 | 1047 |
# derivepassphrase.Vault.phrase_from_key if a key is |
| 916 | 1048 |
# given. Finally, if nothing is set, error out. |
| 917 | 1049 |
def key_to_phrase( |
| 918 |
- key: str | bytes | bytearray |
|
| 1050 |
+ key: str | bytes | bytearray, |
|
| 919 | 1051 |
) -> bytes | bytearray: |
| 920 | 1052 |
return dpp.Vault.phrase_from_key( |
| 921 |
- base64.standard_b64decode(key)) |
|
| 1053 |
+ base64.standard_b64decode(key) |
|
| 1054 |
+ ) |
|
| 1055 |
+ |
|
| 922 | 1056 |
if use_key or use_phrase: |
| 923 | 1057 |
if use_key: |
| 924 | 1058 |
kwargs['phrase'] = key_to_phrase(key) |
| ... | ... |
@@ -935,8 +1069,10 @@ def derivepassphrase( |
| 935 | 1069 |
elif kwargs.get('phrase'):
|
| 936 | 1070 |
pass |
| 937 | 1071 |
else: |
| 938 |
- msg = ('no passphrase or key given on command-line '
|
|
| 939 |
- 'or in configuration') |
|
| 1072 |
+ msg = ( |
|
| 1073 |
+ 'no passphrase or key given on command-line ' |
|
| 1074 |
+ 'or in configuration' |
|
| 1075 |
+ ) |
|
| 940 | 1076 |
raise click.UsageError(msg) |
| 941 | 1077 |
vault = dpp.Vault(**kwargs) |
| 942 | 1078 |
result = vault.generate(service) |
| ... | ... |
@@ -2,9 +2,7 @@ |
| 2 | 2 |
# |
| 3 | 3 |
# SPDX-License-Identifier: MIT |
| 4 | 4 |
|
| 5 |
-"""Common typing declarations for the parent module. |
|
| 6 |
- |
|
| 7 |
-""" |
|
| 5 |
+"""Common typing declarations for the parent module.""" |
|
| 8 | 6 |
|
| 9 | 7 |
from __future__ import annotations |
| 10 | 8 |
|
| ... | ... |
@@ -79,9 +80,13 @@ class VaultConfigServicesSettings(VaultConfigGlobalSettings, total=False): |
| 79 | 80 |
symbol: NotRequired[int] |
| 80 | 81 |
|
| 81 | 82 |
|
| 82 |
-_VaultConfig = TypedDict('_VaultConfig',
|
|
| 83 |
+_VaultConfig = TypedDict( |
|
| 84 |
+ '_VaultConfig', |
|
| 83 | 85 |
{'global': NotRequired[VaultConfigGlobalSettings]},
|
| 84 |
- total=False) |
|
| 86 |
+ total=False, |
|
| 87 |
+) |
|
| 88 |
+ |
|
| 89 |
+ |
|
| 85 | 90 |
class VaultConfig(TypedDict, _VaultConfig, total=False): |
| 86 | 91 |
r"""Configuration for vault. |
| 87 | 92 |
|
| ... | ... |
@@ -32,8 +32,9 @@ if TYPE_CHECKING: |
| 32 | 32 |
from collections.abc import Iterator, Sequence |
| 33 | 33 |
|
| 34 | 34 |
__all__ = ('Sequin', 'SequinExhaustedError')
|
| 35 |
-__author__ = "Marco Ricci <m@the13thletter.info>" |
|
| 36 |
-__version__ = "0.1.1" |
|
| 35 |
+__author__ = 'Marco Ricci <m@the13thletter.info>' |
|
| 36 |
+__version__ = '0.1.1' |
|
| 37 |
+ |
|
| 37 | 38 |
|
| 38 | 39 |
class Sequin: |
| 39 | 40 |
"""Generate pseudorandom non-negative numbers in different ranges. |
| ... | ... |
@@ -60,7 +62,9 @@ class Sequin: |
| 60 | 62 |
def __init__( |
| 61 | 63 |
self, |
| 62 | 64 |
sequence: str | bytes | bytearray | Sequence[int], |
| 63 |
- /, *, is_bitstring: bool = False |
|
| 65 |
+ /, |
|
| 66 |
+ *, |
|
| 67 |
+ is_bitstring: bool = False, |
|
| 64 | 68 |
): |
| 65 | 69 |
"""Initialize the Sequin. |
| 66 | 70 |
|
| ... | ... |
@@ -126,8 +134,7 @@ class Sequin: |
| 126 | 134 |
sequences. |
| 127 | 135 |
|
| 128 | 136 |
Examples: |
| 129 |
- >>> seq = Sequin([1, 0, 1, 0, 0, 1, 0, 0, 0, 1], |
|
| 130 |
- ... is_bitstring=True) |
|
| 137 |
+ >>> seq = Sequin([1, 0, 1, 0, 0, 1, 0, 0, 0, 1], is_bitstring=True) |
|
| 131 | 138 |
>>> seq.bases |
| 132 | 139 |
{2: deque([1, 0, 1, 0, 0, 1, 0, 0, 0, 1])}
|
| 133 | 140 |
>>> seq._all_or_nothing_shift(3) |
| ... | ... |
@@ -163,9 +170,7 @@ class Sequin: |
| 163 | 170 |
return tuple(stash) |
| 164 | 171 |
|
| 165 | 172 |
@staticmethod |
| 166 |
- def _big_endian_number( |
|
| 167 |
- digits: Sequence[int], /, *, base: int = 2 |
|
| 168 |
- ) -> int: |
|
| 173 |
+ def _big_endian_number(digits: Sequence[int], /, *, base: int = 2) -> int: |
|
| 169 | 174 |
"""Evaluate the given integer sequence as a big endian number. |
| 170 | 175 |
|
| 171 | 176 |
Args: |
| ... | ... |
@@ -231,10 +236,14 @@ class Sequin: |
| 231 | 236 |
The sequin is exhausted. |
| 232 | 237 |
|
| 233 | 238 |
Examples: |
| 234 |
- >>> seq = Sequin([1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1], |
|
| 235 |
- ... is_bitstring=True) |
|
| 236 |
- >>> seq2 = Sequin([1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1], |
|
| 237 |
- ... is_bitstring=True) |
|
| 239 |
+ >>> seq = Sequin( |
|
| 240 |
+ ... [1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1], |
|
| 241 |
+ ... is_bitstring=True, |
|
| 242 |
+ ... ) |
|
| 243 |
+ >>> seq2 = Sequin( |
|
| 244 |
+ ... [1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1], |
|
| 245 |
+ ... is_bitstring=True, |
|
| 246 |
+ ... ) |
|
| 238 | 247 |
>>> seq.generate(5) |
| 239 | 248 |
3 |
| 240 | 249 |
>>> seq.generate(5) |
| ... | ... |
@@ -266,9 +275,7 @@ class Sequin: |
| 266 | 275 |
raise SequinExhaustedError |
| 267 | 276 |
return value |
| 268 | 277 |
|
| 269 |
- def _generate_inner( |
|
| 270 |
- self, n: int, /, *, base: int = 2 |
|
| 271 |
- ) -> int: |
|
| 278 |
+ def _generate_inner(self, n: int, /, *, base: int = 2) -> int: |
|
| 272 | 279 |
"""Recursive call to generate a base `n` non-negative integer. |
| 273 | 280 |
|
| 274 | 281 |
We first determine the correct exponent `k` to generate base `n` |
| ... | ... |
@@ -298,10 +305,14 @@ class Sequin: |
| 298 | 305 |
The range is empty. |
| 299 | 306 |
|
| 300 | 307 |
Examples: |
| 301 |
- >>> seq = Sequin([1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1], |
|
| 302 |
- ... is_bitstring=True) |
|
| 303 |
- >>> seq2 = Sequin([1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1], |
|
| 304 |
- ... is_bitstring=True) |
|
| 308 |
+ >>> seq = Sequin( |
|
| 309 |
+ ... [1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1], |
|
| 310 |
+ ... is_bitstring=True, |
|
| 311 |
+ ... ) |
|
| 312 |
+ >>> seq2 = Sequin( |
|
| 313 |
+ ... [1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1], |
|
| 314 |
+ ... is_bitstring=True, |
|
| 315 |
+ ... ) |
|
| 305 | 316 |
>>> seq._generate_inner(5) |
| 306 | 317 |
3 |
| 307 | 318 |
>>> seq._generate_inner(5) |
| ... | ... |
@@ -21,8 +21,8 @@ if TYPE_CHECKING: |
| 21 | 21 |
from collections.abc import Sequence |
| 22 | 22 |
|
| 23 | 23 |
__all__ = ('SSHAgentClient',)
|
| 24 |
-__author__ = "Marco Ricci <m@the13thletter.info>" |
|
| 25 |
-__version__ = "0.1.1" |
|
| 24 |
+__author__ = 'Marco Ricci <m@the13thletter.info>' |
|
| 25 |
+__version__ = '0.1.1' |
|
| 26 | 26 |
|
| 27 | 27 |
# In SSH bytestrings, the "length" of the byte string is stored as |
| 28 | 28 |
# a 4-byte/32-bit unsigned integer at the beginning. |
| ... | ... |
@@ -105,8 +110,7 @@ class SSHAgentClient: |
| 105 | 110 |
) -> bool: |
| 106 | 111 |
"""Close socket connection upon context manager completion.""" |
| 107 | 112 |
return bool( |
| 108 |
- self._connection.__exit__( |
|
| 109 |
- exc_type, exc_val, exc_tb) # type: ignore[func-returns-value] |
|
| 113 |
+ self._connection.__exit__(exc_type, exc_val, exc_tb) # type: ignore[func-returns-value] |
|
| 110 | 114 |
) |
| 111 | 115 |
|
| 112 | 116 |
@staticmethod |
| ... | ... |
@@ -172,17 +176,16 @@ class SSHAgentClient: |
| 172 | 176 |
Examples: |
| 173 | 177 |
>>> bytes(SSHAgentClient.unstring(b'\x00\x00\x00\x07ssh-rsa')) |
| 174 | 178 |
b'ssh-rsa' |
| 175 |
- >>> bytes(SSHAgentClient.unstring( |
|
| 176 |
- ... SSHAgentClient.string(b'ssh-ed25519'))) |
|
| 179 |
+ >>> bytes( |
|
| 180 |
+ ... SSHAgentClient.unstring(SSHAgentClient.string(b'ssh-ed25519')) |
|
| 181 |
+ ... ) |
|
| 177 | 182 |
b'ssh-ed25519' |
| 178 | 183 |
|
| 179 |
- """ |
|
| 184 |
+ """ # noqa: E501 |
|
| 180 | 185 |
n = len(bytestring) |
| 181 | 186 |
msg = 'malformed SSH byte string' |
| 182 |
- if ( |
|
| 183 |
- n < HEAD_LEN |
|
| 184 |
- or n != HEAD_LEN + int.from_bytes(bytestring[:HEAD_LEN], 'big', |
|
| 185 |
- signed=False) |
|
| 187 |
+ if n < HEAD_LEN or n != HEAD_LEN + int.from_bytes( |
|
| 188 |
+ bytestring[:HEAD_LEN], 'big', signed=False |
|
| 186 | 189 |
): |
| 187 | 190 |
raise ValueError(msg) |
| 188 | 191 |
return bytestring[HEAD_LEN:] |
| ... | ... |
@@ -209,11 +212,13 @@ class SSHAgentClient: |
| 209 | 212 |
|
| 210 | 213 |
Examples: |
| 211 | 214 |
>>> a, b = SSHAgentClient.unstring_prefix( |
| 212 |
- ... b'\x00\x00\x00\x07ssh-rsa____trailing data') |
|
| 215 |
+ ... b'\x00\x00\x00\x07ssh-rsa____trailing data' |
|
| 216 |
+ ... ) |
|
| 213 | 217 |
>>> (bytes(a), bytes(b)) |
| 214 | 218 |
(b'ssh-rsa', b'____trailing data') |
| 215 | 219 |
>>> a, b = SSHAgentClient.unstring_prefix( |
| 216 |
- ... SSHAgentClient.string(b'ssh-ed25519')) |
|
| 220 |
+ ... SSHAgentClient.string(b'ssh-ed25519') |
|
| 221 |
+ ... ) |
|
| 217 | 222 |
>>> (bytes(a), bytes(b)) |
| 218 | 223 |
(b'ssh-ed25519', b'') |
| 219 | 224 |
|
| ... | ... |
@@ -225,7 +230,10 @@ class SSHAgentClient: |
| 225 | 230 |
m = int.from_bytes(bytestring[:HEAD_LEN], 'big', signed=False) |
| 226 | 231 |
if m + HEAD_LEN > n: |
| 227 | 232 |
raise ValueError(msg) |
| 228 |
- return (bytestring[HEAD_LEN:m + HEAD_LEN], bytestring[m + HEAD_LEN:]) |
|
| 233 |
+ return ( |
|
| 234 |
+ bytestring[HEAD_LEN : m + HEAD_LEN], |
|
| 235 |
+ bytestring[m + HEAD_LEN :], |
|
| 236 |
+ ) |
|
| 229 | 237 |
|
| 230 | 238 |
def request( |
| 231 | 239 |
self, code: int, payload: bytes | bytearray, / |
| ... | ... |
@@ -282,12 +290,16 @@ class SSHAgentClient: |
| 282 | 290 |
|
| 283 | 291 |
""" |
| 284 | 292 |
response_code, response = self.request( |
| 285 |
- ssh_types.SSH_AGENTC.REQUEST_IDENTITIES.value, b'') |
|
| 293 |
+ ssh_types.SSH_AGENTC.REQUEST_IDENTITIES.value, b'' |
|
| 294 |
+ ) |
|
| 286 | 295 |
if response_code != ssh_types.SSH_AGENT.IDENTITIES_ANSWER.value: |
| 287 |
- msg = (f'error return from SSH agent: ' |
|
| 288 |
- f'{response_code = }, {response = }')
|
|
| 296 |
+ msg = ( |
|
| 297 |
+ f'error return from SSH agent: ' |
|
| 298 |
+ f'{response_code = }, {response = }'
|
|
| 299 |
+ ) |
|
| 289 | 300 |
raise RuntimeError(msg) |
| 290 | 301 |
response_stream = collections.deque(response) |
| 302 |
+ |
|
| 291 | 303 |
def shift(num: int) -> bytes: |
| 292 | 304 |
buf = collections.deque(b'') |
| 293 | 305 |
for _ in range(num): |
| ... | ... |
@@ -314,8 +327,13 @@ class SSHAgentClient: |
| 314 | 327 |
return keys |
| 315 | 328 |
|
| 316 | 329 |
def sign( |
| 317 |
- self, /, key: bytes | bytearray, payload: bytes | bytearray, |
|
| 318 |
- *, flags: int = 0, check_if_key_loaded: bool = False, |
|
| 330 |
+ self, |
|
| 331 |
+ /, |
|
| 332 |
+ key: bytes | bytearray, |
|
| 333 |
+ payload: bytes | bytearray, |
|
| 334 |
+ *, |
|
| 335 |
+ flags: int = 0, |
|
| 336 |
+ check_if_key_loaded: bool = False, |
|
| 319 | 337 |
) -> bytes | bytearray: |
| 320 | 338 |
"""Request the SSH agent sign the payload with the key. |
| 321 | 339 |
|
| ... | ... |
@@ -361,7 +379,8 @@ class SSHAgentClient: |
| 361 | 379 |
request_data.extend(self.string(payload)) |
| 362 | 380 |
request_data.extend(self.uint32(flags)) |
| 363 | 381 |
response_code, response = self.request( |
| 364 |
- ssh_types.SSH_AGENTC.SIGN_REQUEST.value, request_data) |
|
| 382 |
+ ssh_types.SSH_AGENTC.SIGN_REQUEST.value, request_data |
|
| 383 |
+ ) |
|
| 365 | 384 |
if response_code != ssh_types.SSH_AGENT.SIGN_RESPONSE.value: |
| 366 | 385 |
msg = f'signing data failed: {response_code = }, {response = }'
|
| 367 | 386 |
raise RuntimeError(msg) |
| ... | ... |
@@ -33,38 +33,39 @@ if TYPE_CHECKING: |
| 33 | 33 |
expected_signature: bytes | None |
| 34 | 34 |
derived_passphrase: bytes | str | None |
| 35 | 35 |
|
| 36 |
+ |
|
| 36 | 37 |
SUPPORTED_KEYS: Mapping[str, SSHTestKey] = {
|
| 37 | 38 |
'ed25519': {
|
| 38 |
- 'private_key': rb'''-----BEGIN OPENSSH PRIVATE KEY----- |
|
| 39 |
+ 'private_key': rb"""-----BEGIN OPENSSH PRIVATE KEY----- |
|
| 39 | 40 |
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW |
| 40 | 41 |
QyNTUxOQAAACCBeIFoJtYCSF8P/zJIb+TBMIncHGpFBgnpCQ/7whJpdgAAAKDweO7H8Hju |
| 41 | 42 |
xwAAAAtzc2gtZWQyNTUxOQAAACCBeIFoJtYCSF8P/zJIb+TBMIncHGpFBgnpCQ/7whJpdg |
| 42 | 43 |
AAAEAbM/A869nkWZbe2tp3Dm/L6gitvmpH/aRZt8sBII3ExYF4gWgm1gJIXw//Mkhv5MEw |
| 43 | 44 |
idwcakUGCekJD/vCEml2AAAAG3Rlc3Qga2V5IHdpdGhvdXQgcGFzc3BocmFzZQEC |
| 44 | 45 |
-----END OPENSSH PRIVATE KEY----- |
| 45 |
-''', |
|
| 46 |
- 'public_key': rb'''ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIF4gWgm1gJIXw//Mkhv5MEwidwcakUGCekJD/vCEml2 test key without passphrase |
|
| 47 |
-''', # noqa: E501 |
|
| 48 |
- 'public_key_data': bytes.fromhex('''
|
|
| 46 |
+""", |
|
| 47 |
+ 'public_key': rb"""ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIF4gWgm1gJIXw//Mkhv5MEwidwcakUGCekJD/vCEml2 test key without passphrase |
|
| 48 |
+""", # noqa: E501 |
|
| 49 |
+ 'public_key_data': bytes.fromhex("""
|
|
| 49 | 50 |
00 00 00 0b 73 73 68 2d 65 64 32 35 35 31 39 |
| 50 | 51 |
00 00 00 20 |
| 51 | 52 |
81 78 81 68 26 d6 02 48 5f 0f ff 32 48 6f e4 c1 |
| 52 | 53 |
30 89 dc 1c 6a 45 06 09 e9 09 0f fb c2 12 69 76 |
| 53 |
-'''), |
|
| 54 |
- 'expected_signature': bytes.fromhex('''
|
|
| 54 |
+"""), |
|
| 55 |
+ 'expected_signature': bytes.fromhex("""
|
|
| 55 | 56 |
00 00 00 0b 73 73 68 2d 65 64 32 35 35 31 39 |
| 56 | 57 |
00 00 00 40 |
| 57 | 58 |
f0 98 19 80 6c 1a 97 d5 26 03 6e cc e3 65 8f 86 |
| 58 | 59 |
66 07 13 19 13 09 21 33 33 f9 e4 36 53 1d af fd |
| 59 | 60 |
0d 08 1f ec f8 73 9b 8c 5f 55 39 16 7c 53 54 2c |
| 60 | 61 |
1e 52 bb 30 ed 7f 89 e2 2f 69 51 55 d8 9e a6 02 |
| 61 |
- '''), |
|
| 62 |
+ """), |
|
| 62 | 63 |
'derived_passphrase': rb'8JgZgGwal9UmA27M42WPhmYHExkTCSEzM/nkNlMdr/0NCB/s+HObjF9VORZ8U1QsHlK7MO1/ieIvaVFV2J6mAg==', # noqa: E501 |
| 63 | 64 |
}, |
| 64 | 65 |
# Currently only supported by PuTTY (which is deficient in other |
| 65 | 66 |
# niceties of the SSH agent and the agent's client). |
| 66 | 67 |
'ed448': {
|
| 67 |
- 'private_key': rb'''-----BEGIN OPENSSH PRIVATE KEY----- |
|
| 68 |
+ 'private_key': rb"""-----BEGIN OPENSSH PRIVATE KEY----- |
|
| 68 | 69 |
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAASgAAAAlz |
| 69 | 70 |
c2gtZWQ0NDgAAAA54vZy009Wu8wExjvEb3hqtLz1GO/+d5vmGUbErWQ4AUO9mYLT |
| 70 | 71 |
zHJHc2m4s+yWzP29Cc3EcxizLG8AAAAA8BdhfCcXYXwnAAAACXNzaC1lZDQ0OAAA |
| ... | ... |
@@ -74,19 +75,18 @@ ADni9nLTT1a7zATGO8RveGq0vPUY7/53m+YZRsStZDgBQ72ZgtPMckdzabiz7JbM |
| 74 | 75 |
GUbErWQ4AUO9mYLTzHJHc2m4s+yWzP29Cc3EcxizLG8AAAAAG3Rlc3Qga2V5IHdp |
| 75 | 76 |
dGhvdXQgcGFzc3BocmFzZQECAwQFBgcICQ== |
| 76 | 77 |
-----END OPENSSH PRIVATE KEY----- |
| 77 |
-''', |
|
| 78 |
- 'public_key': rb'''ssh-ed448 AAAACXNzaC1lZDQ0OAAAADni9nLTT1a7zATGO8RveGq0vPUY7/53m+YZRsStZDgBQ72ZgtPMckdzabiz7JbM/b0JzcRzGLMsbwA= test key without passphrase |
|
| 79 |
-''', # noqa: E501 |
|
| 80 |
- 'public_key_data': bytes.fromhex('''
|
|
| 78 |
+""", |
|
| 79 |
+ 'public_key': rb"""ssh-ed448 AAAACXNzaC1lZDQ0OAAAADni9nLTT1a7zATGO8RveGq0vPUY7/53m+YZRsStZDgBQ72ZgtPMckdzabiz7JbM/b0JzcRzGLMsbwA= test key without passphrase |
|
| 80 |
+""", # noqa: E501 |
|
| 81 |
+ 'public_key_data': bytes.fromhex("""
|
|
| 81 | 82 |
00 00 00 09 73 73 68 2d 65 64 34 34 38 |
| 82 | 83 |
00 00 00 39 |
| 83 | 84 |
e2 f6 72 d3 4f 56 bb cc 04 c6 3b c4 6f 78 6a b4 |
| 84 | 85 |
bc f5 18 ef fe 77 9b e6 19 46 c4 ad 64 38 01 43 |
| 85 | 86 |
bd 99 82 d3 cc 72 47 73 69 b8 b3 ec 96 cc fd bd |
| 86 | 87 |
09 cd c4 73 18 b3 2c 6f 00 |
| 87 |
- '''), |
|
| 88 |
- |
|
| 89 |
- 'expected_signature': bytes.fromhex('''
|
|
| 88 |
+ """), |
|
| 89 |
+ 'expected_signature': bytes.fromhex("""
|
|
| 90 | 90 |
00 00 00 09 73 73 68 2d 65 64 34 34 38 |
| 91 | 91 |
00 00 00 72 06 86 |
| 92 | 92 |
f4 64 a4 a6 ba d9 c3 22 c4 93 49 99 fc 11 de 67 |
| ... | ... |
@@ -96,11 +96,11 @@ dGhvdXQgcGFzc3BocmFzZQECAwQFBgcICQ== |
| 96 | 96 |
db e5 89 f3 35 be 24 58 90 c6 ca 04 f0 db 88 80 |
| 97 | 97 |
db bd 77 7c 80 20 7f 3a 48 61 f6 1f ae a9 5e 53 |
| 98 | 98 |
7b e0 9d 93 1e ea dc eb b5 cd 56 4c ea 8f 08 00 |
| 99 |
- '''), |
|
| 99 |
+ """), |
|
| 100 | 100 |
'derived_passphrase': rb'Bob0ZKSmutnDIsSTSZn8Ed5nlwjy2Lc8LBPnxRwekqYO2C9tgQOCAONy5DJtctJtMoQ/zKkeVywAmrOZ3kXazi7R2+WJ8zW+JFiQxsoE8NuIgNu9d3yAIH86SGH2H66pXlN74J2THurc67XNVkzqjwgA', # noqa: E501 |
| 101 | 101 |
}, |
| 102 | 102 |
'rsa': {
|
| 103 |
- 'private_key': rb'''-----BEGIN OPENSSH PRIVATE KEY----- |
|
| 103 |
+ 'private_key': rb"""-----BEGIN OPENSSH PRIVATE KEY----- |
|
| 104 | 104 |
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn |
| 105 | 105 |
NhAAAAAwEAAQAAAYEAsaHu6Xs4cVsuDSNJlMCqoPVgmDgEviI8TfXmHKqX3JkIqI3LsvV7 |
| 106 | 106 |
Ijf8WCdTveEq7CkuZhImtsR52AOEVAoU8mDXDNr+nJ5wUPzf1UIaRjDe0lcXW4SlF01hQs |
| ... | ... |
@@ -138,10 +138,10 @@ vC2EAD91xexdorIA5BgXU+qltBqzzBVzVtF7+jOZOjfzOlaTX9I5I5veyeTaTxZj1XXUzi |
| 138 | 138 |
btBNdMEJJp7ifucYmoYAAwE7K+VlWagDEK2y8Mte9y9E+N0uO2j+h85sQt/UIb2iE/vhcg |
| 139 | 139 |
Bgp6142WnSCQAAABt0ZXN0IGtleSB3aXRob3V0IHBhc3NwaHJhc2UB |
| 140 | 140 |
-----END OPENSSH PRIVATE KEY----- |
| 141 |
-''', |
|
| 142 |
- 'public_key': rb'''ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCxoe7pezhxWy4NI0mUwKqg9WCYOAS+IjxN9eYcqpfcmQiojcuy9XsiN/xYJ1O94SrsKS5mEia2xHnYA4RUChTyYNcM2v6cnnBQ/N/VQhpGMN7SVxdbhKUXTWFCwbjBgO6rGyHB6WtoH8vd7TOEPt+NgcXwhsWyoaUUdYTA62V+GF9vEmxMaC4ubgDz+B0QkPnauSoNxmkhcIe0lsLNb1pClZyz88PDnKXCX/d0HuN/HJ+sbPg7dCvOyqFYSyKn3uY6bCXqoIdurxXzH3O7z0P8f5sbmKOrGGKNuNxVRbeVl/D/3uDL0nqsbfUc1qvkfwbJwtMXC4IV6kOZMSk2BAsqh7x48gQ+rhYeEVSi8F3CWs4HJQoqrGt7K9a3mCSlMBHP70u3w6ME7eumoryxlUofewTd17ZEkzdX08l2ZlKzZvwQUrc+xQZ2Uw8z2mfW6Ti4gi0pYGaig7Ke4PwuXpo/C5YAWfeXycsvJZ2uaYRjMdZeJGNAnHLUGLkBscw5aI8= test key without passphrase |
|
| 143 |
-''', # noqa: E501 |
|
| 144 |
- 'public_key_data': bytes.fromhex('''
|
|
| 141 |
+""", |
|
| 142 |
+ 'public_key': rb"""ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCxoe7pezhxWy4NI0mUwKqg9WCYOAS+IjxN9eYcqpfcmQiojcuy9XsiN/xYJ1O94SrsKS5mEia2xHnYA4RUChTyYNcM2v6cnnBQ/N/VQhpGMN7SVxdbhKUXTWFCwbjBgO6rGyHB6WtoH8vd7TOEPt+NgcXwhsWyoaUUdYTA62V+GF9vEmxMaC4ubgDz+B0QkPnauSoNxmkhcIe0lsLNb1pClZyz88PDnKXCX/d0HuN/HJ+sbPg7dCvOyqFYSyKn3uY6bCXqoIdurxXzH3O7z0P8f5sbmKOrGGKNuNxVRbeVl/D/3uDL0nqsbfUc1qvkfwbJwtMXC4IV6kOZMSk2BAsqh7x48gQ+rhYeEVSi8F3CWs4HJQoqrGt7K9a3mCSlMBHP70u3w6ME7eumoryxlUofewTd17ZEkzdX08l2ZlKzZvwQUrc+xQZ2Uw8z2mfW6Ti4gi0pYGaig7Ke4PwuXpo/C5YAWfeXycsvJZ2uaYRjMdZeJGNAnHLUGLkBscw5aI8= test key without passphrase |
|
| 143 |
+""", # noqa: E501 |
|
| 144 |
+ 'public_key_data': bytes.fromhex("""
|
|
| 145 | 145 |
00 00 00 07 73 73 68 2d 72 73 61 |
| 146 | 146 |
00 00 00 03 01 00 01 |
| 147 | 147 |
00 00 01 81 00 |
| ... | ... |
@@ -169,8 +169,8 @@ Bgp6142WnSCQAAABt0ZXN0IGtleSB3aXRob3V0IHBhc3NwaHJhc2UB |
| 169 | 169 |
b8 82 2d 29 60 66 a2 83 b2 9e e0 fc 2e 5e 9a 3f |
| 170 | 170 |
0b 96 00 59 f7 97 c9 cb 2f 25 9d ae 69 84 63 31 |
| 171 | 171 |
d6 5e 24 63 40 9c 72 d4 18 b9 01 b1 cc 39 68 8f |
| 172 |
-'''), |
|
| 173 |
- 'expected_signature': bytes.fromhex('''
|
|
| 172 |
+"""), |
|
| 173 |
+ 'expected_signature': bytes.fromhex("""
|
|
| 174 | 174 |
00 00 00 07 73 73 68 2d 72 73 61 |
| 175 | 175 |
00 00 01 80 |
| 176 | 176 |
a2 10 7c 2e f6 bb 53 a8 74 2a a1 19 99 ad 81 be |
| ... | ... |
@@ -197,14 +197,14 @@ Bgp6142WnSCQAAABt0ZXN0IGtleSB3aXRob3V0IHBhc3NwaHJhc2UB |
| 197 | 197 |
a4 1b bc d2 29 b8 78 61 2b 78 e6 b1 52 b0 d5 ec |
| 198 | 198 |
de 69 2c 48 62 d9 fd d1 9b 6b b0 49 db d3 ff 38 |
| 199 | 199 |
e7 10 d9 2d ce 9f 0d 5e 09 7b 37 d2 7b c3 bf ce |
| 200 |
-'''), |
|
| 200 |
+"""), |
|
| 201 | 201 |
'derived_passphrase': rb'ohB8Lva7U6h0KqEZma2Bvnmc7dadCU5uxRhIM5B3mWj3ngNazU4Y64l9haLurkqS9m/Ouf6GfyprMdpuGv6ipYi4RH+hdnOz7HW10Ka5FZdlCRN9lCHR+10PiyMEd8LDVSKxoAmK9Tgq1n8bhymgJdMlb8tkYQeY3BTFhPiSJF5QEWtJ5fDMKcspqRnYp3EfkQsFsQFLwl8ApbYhv/gsnWebRzsKSWt5Lfwd7Ayw5Sci1an408P530ho6fvvPNwmv8/qKUMBpuPFUZX0Zm2KVeJH7OgwRUyuR+fJpCGLZLq2iPYh+HO5yxGheHWSxlrlZP7tQtmVmeYrbzwWPCh0pHIvDT8sM2eqNRmO57URL7P3asUC4m+jQuNiGZkD6qUg56HjvMgGo7V81nZd329gRoMqCADW09mkwUGM+GBWRYHaO6IWH55OdYMX2sNTwz4ZpBu80im4eGEreOaxUrDV7N5pLEhi2f3Rm2uwSdvT/zjnENktzp8NXgl7N9J7w7/O', # noqa: E501 |
| 202 | 202 |
}, |
| 203 | 203 |
} |
| 204 | 204 |
|
| 205 | 205 |
UNSUITABLE_KEYS: Mapping[str, SSHTestKey] = {
|
| 206 | 206 |
'dsa1024': {
|
| 207 |
- 'private_key': rb'''-----BEGIN OPENSSH PRIVATE KEY----- |
|
| 207 |
+ 'private_key': rb"""-----BEGIN OPENSSH PRIVATE KEY----- |
|
| 208 | 208 |
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABsQAAAAdzc2gtZH |
| 209 | 209 |
NzAAAAgQC7KAZXqBGNVLBQPrcMYAoNW54BhD8aIhe7BDWYzJcsaMt72VKSkguZ8+XR7nRa |
| 210 | 210 |
0C/ZsBi+uJp0dpxy9ZMTOWX4u5YPMeQcXEdGExZIfimGqSOAsy6fCld2IfJZJZExcCmhe9 |
| ... | ... |
@@ -225,10 +225,10 @@ mMbWEYp/C8jC6ikZqmtEOVK7w3woYC4b7BvWEm/zKcOapvD4h0mn8IZGs/7Xt/vISqIKqH |
| 225 | 225 |
u7HfrQhdOiKSa+ZO9AAojbURqrLDRfBJa5dXn2AAAAFQDJHfenj4EJ9WkehpdJatPBlqCW |
| 226 | 226 |
0gAAABt0ZXN0IGtleSB3aXRob3V0IHBhc3NwaHJhc2UBAgMEBQYH |
| 227 | 227 |
-----END OPENSSH PRIVATE KEY----- |
| 228 |
-''', |
|
| 229 |
- 'public_key': rb'''ssh-dss AAAAB3NzaC1kc3MAAACBALsoBleoEY1UsFA+twxgCg1bngGEPxoiF7sENZjMlyxoy3vZUpKSC5nz5dHudFrQL9mwGL64mnR2nHL1kxM5Zfi7lg8x5BxcR0YTFkh+KYapI4CzLp8KV3Yh8lklkTFwKaF71KyOx3dhIA8lGW45cVBz3kxmhHmEzCUgMPxDOsTtAAAAFQD32c5k6B3tocxUahelQQFyfseiywAAAIAuvYCDeHEzesp3HNVTDx9fRVU9c77f4qvyEZ7Qpz/s3BVoFUvUZDx96cG5bKekBRsfTCjeHXCQH/yFfqn5Lxye7msgGVS5U3AvD9shiiEr3wt+pNgr9X6DooP7ybfjC8SJdmarLBjnifZuSxyHU2q+P+02kvMTFLH9dLSRIzVqKAAAAIBtA1E9xUS4YOsRx/7GDm2AB6M9cE9ev8myz4KGTriSbeaKsxiMBbJZi1VyBP7uE5jG1hGKfwvIwuopGaprRDlSu8N8KGAuG+wb1hJv8ynDmqbw+IdJp/CGRrP+17f7yEqiCqh7ux360IXToikmvmTvQAKI21Eaqyw0XwSWuXV59g== test key without passphrase |
|
| 230 |
-''', # noqa: E501 |
|
| 231 |
- 'public_key_data': bytes.fromhex('''
|
|
| 228 |
+""", |
|
| 229 |
+ 'public_key': rb"""ssh-dss AAAAB3NzaC1kc3MAAACBALsoBleoEY1UsFA+twxgCg1bngGEPxoiF7sENZjMlyxoy3vZUpKSC5nz5dHudFrQL9mwGL64mnR2nHL1kxM5Zfi7lg8x5BxcR0YTFkh+KYapI4CzLp8KV3Yh8lklkTFwKaF71KyOx3dhIA8lGW45cVBz3kxmhHmEzCUgMPxDOsTtAAAAFQD32c5k6B3tocxUahelQQFyfseiywAAAIAuvYCDeHEzesp3HNVTDx9fRVU9c77f4qvyEZ7Qpz/s3BVoFUvUZDx96cG5bKekBRsfTCjeHXCQH/yFfqn5Lxye7msgGVS5U3AvD9shiiEr3wt+pNgr9X6DooP7ybfjC8SJdmarLBjnifZuSxyHU2q+P+02kvMTFLH9dLSRIzVqKAAAAIBtA1E9xUS4YOsRx/7GDm2AB6M9cE9ev8myz4KGTriSbeaKsxiMBbJZi1VyBP7uE5jG1hGKfwvIwuopGaprRDlSu8N8KGAuG+wb1hJv8ynDmqbw+IdJp/CGRrP+17f7yEqiCqh7ux360IXToikmvmTvQAKI21Eaqyw0XwSWuXV59g== test key without passphrase |
|
| 230 |
+""", # noqa: E501 |
|
| 231 |
+ 'public_key_data': bytes.fromhex("""
|
|
| 232 | 232 |
00 00 00 07 73 73 68 2d 64 73 73 |
| 233 | 233 |
00 00 00 81 00 |
| 234 | 234 |
bb 28 06 57 a8 11 8d 54 b0 50 3e b7 0c 60 0a 0d |
| ... | ... |
@@ -259,12 +259,12 @@ u7HfrQhdOiKSa+ZO9AAojbURqrLDRfBJa5dXn2AAAAFQDJHfenj4EJ9WkehpdJatPBlqCW |
| 259 | 259 |
a6 f0 f8 87 49 a7 f0 86 46 b3 fe d7 b7 fb c8 4a |
| 260 | 260 |
a2 0a a8 7b bb 1d fa d0 85 d3 a2 29 26 be 64 ef |
| 261 | 261 |
40 02 88 db 51 1a ab 2c 34 5f 04 96 b9 75 79 f6 |
| 262 |
-'''), |
|
| 262 |
+"""), |
|
| 263 | 263 |
'expected_signature': None, |
| 264 | 264 |
'derived_passphrase': None, |
| 265 | 265 |
}, |
| 266 | 266 |
'ecdsa256': {
|
| 267 |
- 'private_key': rb'''-----BEGIN OPENSSH PRIVATE KEY----- |
|
| 267 |
+ 'private_key': rb"""-----BEGIN OPENSSH PRIVATE KEY----- |
|
| 268 | 268 |
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS |
| 269 | 269 |
1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQTLbU0zDwsk2Dvp+VYIrsNVf5gWwz2S |
| 270 | 270 |
3SZ8TbxiQRkpnGSVqyIoHJOJc+NQItAa7xlJ/8Z6gfz57Z3apUkaMJm6AAAAuKeY+YinmP |
| ... | ... |
@@ -273,10 +273,10 @@ Vgiuw1V/mBbDPZLdJnxNvGJBGSmcZJWrIigck4lz41Ai0BrvGUn/xnqB/PntndqlSRowmb |
| 273 | 273 |
oAAAAhAKIl/3n0pKVIxpZkXTGtii782Qr4yIcvHdpxjO/QsIqKAAAAG3Rlc3Qga2V5IHdp |
| 274 | 274 |
dGhvdXQgcGFzc3BocmFzZQECAwQ= |
| 275 | 275 |
-----END OPENSSH PRIVATE KEY----- |
| 276 |
-''', |
|
| 277 |
- 'public_key': rb'''ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBMttTTMPCyTYO+n5Vgiuw1V/mBbDPZLdJnxNvGJBGSmcZJWrIigck4lz41Ai0BrvGUn/xnqB/PntndqlSRowmbo= test key without passphrase |
|
| 278 |
-''', # noqa: E501 |
|
| 279 |
- 'public_key_data': bytes.fromhex('''
|
|
| 276 |
+""", |
|
| 277 |
+ 'public_key': rb"""ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBMttTTMPCyTYO+n5Vgiuw1V/mBbDPZLdJnxNvGJBGSmcZJWrIigck4lz41Ai0BrvGUn/xnqB/PntndqlSRowmbo= test key without passphrase |
|
| 278 |
+""", # noqa: E501 |
|
| 279 |
+ 'public_key_data': bytes.fromhex("""
|
|
| 280 | 280 |
00 00 00 13 65 63 64 73 61 2d 73 68 61 32 2d 6e |
| 281 | 281 |
69 73 74 70 32 35 36 |
| 282 | 282 |
00 00 00 08 6e 69 73 74 70 32 35 36 |
| ... | ... |
@@ -285,12 +285,12 @@ dGhvdXQgcGFzc3BocmFzZQECAwQ= |
| 285 | 285 |
7f 98 16 c3 3d 92 dd 26 7c 4d bc 62 41 19 29 9c |
| 286 | 286 |
64 95 ab 22 28 1c 93 89 73 e3 50 22 d0 1a ef 19 |
| 287 | 287 |
49 ff c6 7a 81 fc f9 ed 9d da a5 49 1a 30 99 ba |
| 288 |
-'''), |
|
| 288 |
+"""), |
|
| 289 | 289 |
'expected_signature': None, |
| 290 | 290 |
'derived_passphrase': None, |
| 291 | 291 |
}, |
| 292 | 292 |
'ecdsa384': {
|
| 293 |
- 'private_key': rb'''-----BEGIN OPENSSH PRIVATE KEY----- |
|
| 293 |
+ 'private_key': rb"""-----BEGIN OPENSSH PRIVATE KEY----- |
|
| 294 | 294 |
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAiAAAABNlY2RzYS |
| 295 | 295 |
1zaGEyLW5pc3RwMzg0AAAACG5pc3RwMzg0AAAAYQSgkOjkAvq7v5vHuj3KBL4/EAWcn5hZ |
| 296 | 296 |
DyKcbyV0eBMGFq7hKXQlZqIahLVqeMR0QqmkxNJ2rly2VHcXneq3vZ+9fIsWCOdYk5WP3N |
| ... | ... |
@@ -300,10 +300,10 @@ au4Sl0JWaiGoS1anjEdEKppMTSdq5ctlR3F53qt72fvXyLFgjnWJOVj9zWT87/ddV5+8Gx |
| 300 | 300 |
JAu0J3Q+cypZuKQVAAAAMQD5sTy8p+B1cn/DhOmXquui1BcxvASqzzevkBlbQoBa73y04B |
| 301 | 301 |
2OdqVOVRkwZWRROz0AAAAbdGVzdCBrZXkgd2l0aG91dCBwYXNzcGhyYXNlAQIDBA== |
| 302 | 302 |
-----END OPENSSH PRIVATE KEY----- |
| 303 |
-''', |
|
| 304 |
- 'public_key': rb'''ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBKCQ6OQC+ru/m8e6PcoEvj8QBZyfmFkPIpxvJXR4EwYWruEpdCVmohqEtWp4xHRCqaTE0nauXLZUdxed6re9n718ixYI51iTlY/c1k/O/3XVefvBsSQLtCd0PnMqWbikFQ== test key without passphrase |
|
| 305 |
-''', # noqa: E501 |
|
| 306 |
- 'public_key_data': bytes.fromhex('''
|
|
| 303 |
+""", |
|
| 304 |
+ 'public_key': rb"""ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBKCQ6OQC+ru/m8e6PcoEvj8QBZyfmFkPIpxvJXR4EwYWruEpdCVmohqEtWp4xHRCqaTE0nauXLZUdxed6re9n718ixYI51iTlY/c1k/O/3XVefvBsSQLtCd0PnMqWbikFQ== test key without passphrase |
|
| 305 |
+""", # noqa: E501 |
|
| 306 |
+ 'public_key_data': bytes.fromhex("""
|
|
| 307 | 307 |
00 00 00 13 |
| 308 | 308 |
65 63 64 73 61 2d 73 68 61 32 2d 6e 69 73 74 70 |
| 309 | 309 |
33 38 34 |
| ... | ... |
@@ -315,7 +315,7 @@ JAu0J3Q+cypZuKQVAAAAMQD5sTy8p+B1cn/DhOmXquui1BcxvASqzzevkBlbQoBa73y04B |
| 315 | 315 |
a4 c4 d2 76 ae 5c b6 54 77 17 9d ea b7 bd 9f bd |
| 316 | 316 |
7c 8b 16 08 e7 58 93 95 8f dc d6 4f ce ff 75 d5 |
| 317 | 317 |
79 fb c1 b1 24 0b b4 27 74 3e 73 2a 59 b8 a4 15 |
| 318 |
-'''), |
|
| 318 |
+"""), |
|
| 319 | 319 |
'expected_signature': None, |
| 320 | 320 |
'derived_passphrase': None, |
| 321 | 321 |
}, |
| ... | ... |
@@ -327,8 +327,16 @@ DUMMY_KEY1 = SUPPORTED_KEYS['ed25519']['public_key_data'] |
| 327 | 327 |
DUMMY_KEY1_B64 = base64.standard_b64encode(DUMMY_KEY1).decode('ASCII')
|
| 328 | 328 |
DUMMY_KEY2 = SUPPORTED_KEYS['rsa']['public_key_data'] |
| 329 | 329 |
DUMMY_KEY2_B64 = base64.standard_b64encode(DUMMY_KEY2).decode('ASCII')
|
| 330 |
-DUMMY_CONFIG_SETTINGS = {"length": 10, "upper": 1, "lower": 1, "repeat": 5,
|
|
| 331 |
- "number": 1, "space": 1, "dash": 1, "symbol": 1} |
|
| 330 |
+DUMMY_CONFIG_SETTINGS = {
|
|
| 331 |
+ 'length': 10, |
|
| 332 |
+ 'upper': 1, |
|
| 333 |
+ 'lower': 1, |
|
| 334 |
+ 'repeat': 5, |
|
| 335 |
+ 'number': 1, |
|
| 336 |
+ 'space': 1, |
|
| 337 |
+ 'dash': 1, |
|
| 338 |
+ 'symbol': 1, |
|
| 339 |
+} |
|
| 332 | 340 |
DUMMY_RESULT_PASSPHRASE = b'.2V_QJkd o' |
| 333 | 341 |
DUMMY_RESULT_KEY1 = b'E<b<{ -7iG'
|
| 334 | 342 |
DUMMY_PHRASE_FROM_KEY1_RAW = ( |
| ... | ... |
@@ -341,30 +349,40 @@ DUMMY_PHRASE_FROM_KEY1_RAW = ( |
| 341 | 349 |
DUMMY_PHRASE_FROM_KEY1 = b'8JgZgGwal9UmA27M42WPhmYHExkTCSEzM/nkNlMdr/0NCB/s+HObjF9VORZ8U1QsHlK7MO1/ieIvaVFV2J6mAg==' # noqa: E501 |
| 342 | 350 |
|
| 343 | 351 |
skip_if_no_agent = pytest.mark.skipif( |
| 344 |
- not os.environ.get('SSH_AUTH_SOCK'), reason='running SSH agent required')
|
|
| 352 |
+ not os.environ.get('SSH_AUTH_SOCK'), reason='running SSH agent required'
|
|
| 353 |
+) |
|
| 354 |
+ |
|
| 345 | 355 |
|
| 346 | 356 |
def list_keys( |
| 347 | 357 |
self: Any = None, |
| 348 | 358 |
) -> list[ssh_agent_client.types.KeyCommentPair]: |
| 349 | 359 |
del self # Unused. |
| 350 | 360 |
Pair = ssh_agent_client.types.KeyCommentPair # noqa: N806 |
| 351 |
- list1 = [Pair(value['public_key_data'], f'{key} test key'.encode('ASCII'))
|
|
| 352 |
- for key, value in SUPPORTED_KEYS.items()] |
|
| 353 |
- list2 = [Pair(value['public_key_data'], f'{key} test key'.encode('ASCII'))
|
|
| 354 |
- for key, value in UNSUITABLE_KEYS.items()] |
|
| 361 |
+ list1 = [ |
|
| 362 |
+ Pair(value['public_key_data'], f'{key} test key'.encode('ASCII'))
|
|
| 363 |
+ for key, value in SUPPORTED_KEYS.items() |
|
| 364 |
+ ] |
|
| 365 |
+ list2 = [ |
|
| 366 |
+ Pair(value['public_key_data'], f'{key} test key'.encode('ASCII'))
|
|
| 367 |
+ for key, value in UNSUITABLE_KEYS.items() |
|
| 368 |
+ ] |
|
| 355 | 369 |
return list1 + list2 |
| 356 | 370 |
|
| 371 |
+ |
|
| 357 | 372 |
def list_keys_singleton( |
| 358 | 373 |
self: Any = None, |
| 359 | 374 |
) -> list[ssh_agent_client.types.KeyCommentPair]: |
| 360 | 375 |
del self # Unused. |
| 361 | 376 |
Pair = ssh_agent_client.types.KeyCommentPair # noqa: N806 |
| 362 |
- list1 = [Pair(value['public_key_data'], f'{key} test key'.encode('ASCII'))
|
|
| 363 |
- for key, value in SUPPORTED_KEYS.items()] |
|
| 377 |
+ list1 = [ |
|
| 378 |
+ Pair(value['public_key_data'], f'{key} test key'.encode('ASCII'))
|
|
| 379 |
+ for key, value in SUPPORTED_KEYS.items() |
|
| 380 |
+ ] |
|
| 364 | 381 |
return list1[:1] |
| 365 | 382 |
|
| 383 |
+ |
|
| 366 | 384 |
def suitable_ssh_keys( |
| 367 |
- conn: Any |
|
| 385 |
+ conn: Any, |
|
| 368 | 386 |
) -> Iterator[ssh_agent_client.types.KeyCommentPair]: |
| 369 | 387 |
del conn # Unused. |
| 370 | 388 |
yield from [ |
| ... | ... |
@@ -377,9 +396,12 @@ def phrase_from_key(key: bytes) -> bytes: |
| 377 | 396 |
return DUMMY_PHRASE_FROM_KEY1 |
| 378 | 397 |
raise KeyError(key) # pragma: no cover |
| 379 | 398 |
|
| 399 |
+ |
|
| 380 | 400 |
@contextlib.contextmanager |
| 381 | 401 |
def isolated_config( |
| 382 |
- monkeypatch: Any, runner: click.testing.CliRunner, config: Any, |
|
| 402 |
+ monkeypatch: Any, |
|
| 403 |
+ runner: click.testing.CliRunner, |
|
| 404 |
+ config: Any, |
|
| 383 | 405 |
): |
| 384 | 406 |
prog_name = derivepassphrase.cli.PROG_NAME |
| 385 | 407 |
env_name = prog_name.replace(' ', '_').upper() + '_PATH'
|
| ... | ... |
@@ -393,7 +415,8 @@ def isolated_config( |
| 393 | 415 |
) |
| 394 | 416 |
with open( |
| 395 | 417 |
derivepassphrase.cli._config_filename(), |
| 396 |
- 'w', encoding='UTF-8', |
|
| 418 |
+ 'w', |
|
| 419 |
+ encoding='UTF-8', |
|
| 397 | 420 |
) as outfile: |
| 398 | 421 |
json.dump(config, outfile) |
| 399 | 422 |
yield |
| ... | ... |
@@ -15,115 +15,149 @@ import derivepassphrase |
| 15 | 15 |
|
| 16 | 16 |
Vault = derivepassphrase.Vault |
| 17 | 17 |
|
| 18 |
-class TestVault: |
|
| 19 | 18 |
|
| 19 |
+class TestVault: |
|
| 20 | 20 |
phrase = b'She cells C shells bye the sea shoars' |
| 21 | 21 |
google_phrase = rb': 4TVH#5:aZl8LueOT\{'
|
| 22 | 22 |
twitter_phrase = rb"[ (HN_N:lI&<ro=)3'g9" |
| 23 | 23 |
|
| 24 |
- @pytest.mark.parametrize(['service', 'expected'], [ |
|
| 24 |
+ @pytest.mark.parametrize( |
|
| 25 |
+ ['service', 'expected'], |
|
| 26 |
+ [ |
|
| 25 | 27 |
(b'google', google_phrase), |
| 26 | 28 |
('twitter', twitter_phrase),
|
| 27 |
- ]) |
|
| 29 |
+ ], |
|
| 30 |
+ ) |
|
| 28 | 31 |
def test_200_basic_configuration(self, service, expected): |
| 29 | 32 |
assert Vault(phrase=self.phrase).generate(service) == expected |
| 30 | 33 |
|
| 31 | 34 |
def test_201_phrase_dependence(self): |
| 32 | 35 |
assert ( |
| 33 |
- Vault(phrase=(self.phrase + b'X')).generate('google') ==
|
|
| 34 |
- b'n+oIz6sL>K*lTEWYRO%7' |
|
| 36 |
+ Vault(phrase=(self.phrase + b'X')).generate('google')
|
|
| 37 |
+ == b'n+oIz6sL>K*lTEWYRO%7' |
|
| 35 | 38 |
) |
| 36 | 39 |
|
| 37 | 40 |
def test_202_reproducibility_and_bytes_service_name(self): |
| 38 |
- assert ( |
|
| 39 |
- Vault(phrase=self.phrase).generate(b'google') == |
|
| 40 |
- Vault(phrase=self.phrase).generate('google')
|
|
| 41 |
- ) |
|
| 41 |
+ assert Vault(phrase=self.phrase).generate(b'google') == Vault( |
|
| 42 |
+ phrase=self.phrase |
|
| 43 |
+ ).generate('google')
|
|
| 42 | 44 |
|
| 43 | 45 |
def test_203_reproducibility_and_bytearray_service_name(self): |
| 44 |
- assert ( |
|
| 45 |
- Vault(phrase=self.phrase).generate(b'google') == |
|
| 46 |
- Vault(phrase=self.phrase).generate(bytearray(b'google')) |
|
| 47 |
- ) |
|
| 46 |
+ assert Vault(phrase=self.phrase).generate(b'google') == Vault( |
|
| 47 |
+ phrase=self.phrase |
|
| 48 |
+ ).generate(bytearray(b'google')) |
|
| 48 | 49 |
|
| 49 | 50 |
def test_210_nonstandard_length(self): |
| 50 | 51 |
assert ( |
| 51 |
- Vault(phrase=self.phrase, length=4).generate('google')
|
|
| 52 |
- == b'xDFu' |
|
| 52 |
+ Vault(phrase=self.phrase, length=4).generate('google') == b'xDFu'
|
|
| 53 | 53 |
) |
| 54 | 54 |
|
| 55 | 55 |
def test_211_repetition_limit(self): |
| 56 | 56 |
assert ( |
| 57 |
- Vault(phrase=b'', length=24, symbol=0, number=0, |
|
| 58 |
- repeat=1).generate('asd') ==
|
|
| 59 |
- b'IVTDzACftqopUXqDHPkuCIhV' |
|
| 57 |
+ Vault( |
|
| 58 |
+ phrase=b'', length=24, symbol=0, number=0, repeat=1 |
|
| 59 |
+ ).generate('asd')
|
|
| 60 |
+ == b'IVTDzACftqopUXqDHPkuCIhV' |
|
| 60 | 61 |
) |
| 61 | 62 |
|
| 62 | 63 |
def test_212_without_symbols(self): |
| 63 | 64 |
assert ( |
| 64 |
- Vault(phrase=self.phrase, symbol=0).generate('google') ==
|
|
| 65 |
- b'XZ4wRe0bZCazbljCaMqR' |
|
| 65 |
+ Vault(phrase=self.phrase, symbol=0).generate('google')
|
|
| 66 |
+ == b'XZ4wRe0bZCazbljCaMqR' |
|
| 66 | 67 |
) |
| 67 | 68 |
|
| 68 | 69 |
def test_213_no_numbers(self): |
| 69 | 70 |
assert ( |
| 70 |
- Vault(phrase=self.phrase, number=0).generate('google') ==
|
|
| 71 |
- b'_*$TVH.%^aZl(LUeOT?>' |
|
| 71 |
+ Vault(phrase=self.phrase, number=0).generate('google')
|
|
| 72 |
+ == b'_*$TVH.%^aZl(LUeOT?>' |
|
| 72 | 73 |
) |
| 73 | 74 |
|
| 74 | 75 |
def test_214_no_lowercase_letters(self): |
| 75 | 76 |
assert ( |
| 76 |
- Vault(phrase=self.phrase, lower=0).generate('google') ==
|
|
| 77 |
- b':{?)+7~@OA:L]!0E$)(+'
|
|
| 77 |
+ Vault(phrase=self.phrase, lower=0).generate('google')
|
|
| 78 |
+ == b':{?)+7~@OA:L]!0E$)(+'
|
|
| 78 | 79 |
) |
| 79 | 80 |
|
| 80 | 81 |
def test_215_at_least_5_digits(self): |
| 81 | 82 |
assert ( |
| 82 |
- Vault(phrase=self.phrase, length=8, number=5) |
|
| 83 |
- .generate('songkick') == b'i0908.7['
|
|
| 83 |
+ Vault(phrase=self.phrase, length=8, number=5).generate('songkick')
|
|
| 84 |
+ == b'i0908.7[' |
|
| 84 | 85 |
) |
| 85 | 86 |
|
| 86 | 87 |
def test_216_lots_of_spaces(self): |
| 87 | 88 |
assert ( |
| 88 |
- Vault(phrase=self.phrase, space=12) |
|
| 89 |
- .generate('songkick') == b' c 6 Bq % 5fR '
|
|
| 89 |
+ Vault(phrase=self.phrase, space=12).generate('songkick')
|
|
| 90 |
+ == b' c 6 Bq % 5fR ' |
|
| 90 | 91 |
) |
| 91 | 92 |
|
| 92 | 93 |
def test_217_all_character_classes(self): |
| 93 | 94 |
assert ( |
| 94 |
- Vault(phrase=self.phrase, lower=2, upper=2, number=1, |
|
| 95 |
- space=3, dash=2, symbol=1) |
|
| 96 |
- .generate('google') == b': : fv_wqt>a-4w1S R'
|
|
| 95 |
+ Vault( |
|
| 96 |
+ phrase=self.phrase, |
|
| 97 |
+ lower=2, |
|
| 98 |
+ upper=2, |
|
| 99 |
+ number=1, |
|
| 100 |
+ space=3, |
|
| 101 |
+ dash=2, |
|
| 102 |
+ symbol=1, |
|
| 103 |
+ ).generate('google')
|
|
| 104 |
+ == b': : fv_wqt>a-4w1S R' |
|
| 97 | 105 |
) |
| 98 | 106 |
|
| 99 | 107 |
def test_218_only_numbers_and_very_high_repetition_limit(self): |
| 100 |
- generated = Vault(phrase=b'', length=40, lower=0, upper=0, space=0, |
|
| 101 |
- dash=0, symbol=0, repeat=4).generate('abcdef')
|
|
| 102 |
- forbidden_substrings = {b'0000', b'1111', b'2222', b'3333', b'4444',
|
|
| 103 |
- b'5555', b'6666', b'7777', b'8888', b'9999'} |
|
| 108 |
+ generated = Vault( |
|
| 109 |
+ phrase=b'', |
|
| 110 |
+ length=40, |
|
| 111 |
+ lower=0, |
|
| 112 |
+ upper=0, |
|
| 113 |
+ space=0, |
|
| 114 |
+ dash=0, |
|
| 115 |
+ symbol=0, |
|
| 116 |
+ repeat=4, |
|
| 117 |
+ ).generate('abcdef')
|
|
| 118 |
+ forbidden_substrings = {
|
|
| 119 |
+ b'0000', |
|
| 120 |
+ b'1111', |
|
| 121 |
+ b'2222', |
|
| 122 |
+ b'3333', |
|
| 123 |
+ b'4444', |
|
| 124 |
+ b'5555', |
|
| 125 |
+ b'6666', |
|
| 126 |
+ b'7777', |
|
| 127 |
+ b'8888', |
|
| 128 |
+ b'9999', |
|
| 129 |
+ } |
|
| 104 | 130 |
for substring in forbidden_substrings: |
| 105 | 131 |
assert substring not in generated |
| 106 | 132 |
|
| 107 | 133 |
def test_219_very_limited_character_set(self): |
| 108 |
- generated = Vault(phrase=b'', length=24, lower=0, upper=0, |
|
| 109 |
- space=0, symbol=0).generate('testing')
|
|
| 134 |
+ generated = Vault( |
|
| 135 |
+ phrase=b'', length=24, lower=0, upper=0, space=0, symbol=0 |
|
| 136 |
+ ).generate('testing')
|
|
| 110 | 137 |
assert generated == b'763252593304946694588866' |
| 111 | 138 |
|
| 112 | 139 |
def test_220_character_set_subtraction(self): |
| 113 | 140 |
assert Vault._subtract(b'be', b'abcdef') == bytearray(b'acdf') |
| 114 | 141 |
|
| 115 |
- @pytest.mark.parametrize(['length', 'settings', 'entropy'], [ |
|
| 142 |
+ @pytest.mark.parametrize( |
|
| 143 |
+ ['length', 'settings', 'entropy'], |
|
| 144 |
+ [ |
|
| 116 | 145 |
(20, {}, math.log2(math.factorial(20)) + 20 * math.log2(94)),
|
| 117 | 146 |
( |
| 118 | 147 |
20, |
| 119 | 148 |
{'upper': 0, 'number': 0, 'space': 0, 'symbol': 0},
|
| 120 |
- math.log2(math.factorial(20)) + 20 * math.log2(26) |
|
| 149 |
+ math.log2(math.factorial(20)) + 20 * math.log2(26), |
|
| 121 | 150 |
), |
| 122 | 151 |
(0, {}, float('-inf')),
|
| 123 |
- (0, {'lower': 0, 'number': 0, 'space': 0, 'symbol': 0}, float('-inf')),
|
|
| 152 |
+ ( |
|
| 153 |
+ 0, |
|
| 154 |
+ {'lower': 0, 'number': 0, 'space': 0, 'symbol': 0},
|
|
| 155 |
+ float('-inf'),
|
|
| 156 |
+ ), |
|
| 124 | 157 |
(1, {}, math.log2(94)),
|
| 125 | 158 |
(1, {'upper': 0, 'lower': 0, 'number': 0, 'symbol': 0}, 0.0),
|
| 126 |
- ]) |
|
| 159 |
+ ], |
|
| 160 |
+ ) |
|
| 127 | 161 |
def test_221_entropy( |
| 128 | 162 |
self, length: int, settings: dict[str, int], entropy: int |
| 129 | 163 |
) -> None: |
| ... | ... |
@@ -131,38 +165,54 @@ class TestVault: |
| 131 | 165 |
assert math.isclose(v._entropy(), entropy) |
| 132 | 166 |
assert v._estimate_sufficient_hash_length() > 0 |
| 133 | 167 |
if math.isfinite(entropy) and entropy: |
| 134 |
- assert ( |
|
| 135 |
- v._estimate_sufficient_hash_length(1.0) == |
|
| 136 |
- math.ceil(entropy / 8) |
|
| 168 |
+ assert v._estimate_sufficient_hash_length(1.0) == math.ceil( |
|
| 169 |
+ entropy / 8 |
|
| 137 | 170 |
) |
| 138 | 171 |
assert v._estimate_sufficient_hash_length(8.0) >= entropy |
| 139 | 172 |
|
| 140 | 173 |
def test_222_hash_length_estimation(self) -> None: |
| 141 |
- v = Vault(phrase=self.phrase, lower=0, upper=0, number=0, |
|
| 142 |
- symbol=0, space=1, length=1) |
|
| 174 |
+ v = Vault( |
|
| 175 |
+ phrase=self.phrase, |
|
| 176 |
+ lower=0, |
|
| 177 |
+ upper=0, |
|
| 178 |
+ number=0, |
|
| 179 |
+ symbol=0, |
|
| 180 |
+ space=1, |
|
| 181 |
+ length=1, |
|
| 182 |
+ ) |
|
| 143 | 183 |
assert v._entropy() == 0.0 |
| 144 | 184 |
assert v._estimate_sufficient_hash_length() > 0 |
| 145 | 185 |
|
| 146 |
- @pytest.mark.parametrize(['service', 'expected'], [ |
|
| 186 |
+ @pytest.mark.parametrize( |
|
| 187 |
+ ['service', 'expected'], |
|
| 188 |
+ [ |
|
| 147 | 189 |
(b'google', google_phrase), |
| 148 | 190 |
('twitter', twitter_phrase),
|
| 149 |
- ]) |
|
| 191 |
+ ], |
|
| 192 |
+ ) |
|
| 150 | 193 |
def test_223_hash_length_expansion( |
| 151 | 194 |
self, monkeypatch: Any, service: str | bytes, expected: bytes |
| 152 | 195 |
) -> None: |
| 153 | 196 |
v = Vault(phrase=self.phrase) |
| 154 |
- monkeypatch.setattr(v, |
|
| 197 |
+ monkeypatch.setattr( |
|
| 198 |
+ v, |
|
| 155 | 199 |
'_estimate_sufficient_hash_length', |
| 156 |
- lambda *args, **kwargs: 1) # noqa: ARG005 |
|
| 200 |
+ lambda *args, **kwargs: 1, # noqa: ARG005 |
|
| 201 |
+ ) |
|
| 157 | 202 |
assert v._estimate_sufficient_hash_length() < len(self.phrase) |
| 158 | 203 |
assert v.generate(service) == expected |
| 159 | 204 |
|
| 160 |
- @pytest.mark.parametrize(['s', 'raises'], [ |
|
| 161 |
- ('ñ', True), ('Düsseldorf', True),
|
|
| 162 |
- ('liberté, egalité, fraternité', True), ('ASCII', False),
|
|
| 205 |
+ @pytest.mark.parametrize( |
|
| 206 |
+ ['s', 'raises'], |
|
| 207 |
+ [ |
|
| 208 |
+ ('ñ', True),
|
|
| 209 |
+ ('Düsseldorf', True),
|
|
| 210 |
+ ('liberté, egalité, fraternité', True),
|
|
| 211 |
+ ('ASCII', False),
|
|
| 163 | 212 |
(b'D\xc3\xbcsseldorf', False), |
| 164 | 213 |
(bytearray([2, 3, 5, 7, 11, 13]), False), |
| 165 |
- ]) |
|
| 214 |
+ ], |
|
| 215 |
+ ) |
|
| 166 | 216 |
def test_224_binary_strings( |
| 167 | 217 |
self, s: str | bytes | bytearray, raises: bool |
| 168 | 218 |
) -> None: |
| ... | ... |
@@ -181,15 +231,22 @@ class TestVault: |
| 181 | 231 |
assert binstr(binstr(s)) == bytes(s) |
| 182 | 232 |
|
| 183 | 233 |
def test_310_too_many_symbols(self): |
| 184 |
- with pytest.raises(ValueError, |
|
| 185 |
- match='requested passphrase length too short'): |
|
| 234 |
+ with pytest.raises( |
|
| 235 |
+ ValueError, match='requested passphrase length too short' |
|
| 236 |
+ ): |
|
| 186 | 237 |
Vault(phrase=self.phrase, symbol=100) |
| 187 | 238 |
|
| 188 | 239 |
def test_311_no_viable_characters(self): |
| 189 |
- with pytest.raises(ValueError, |
|
| 190 |
- match='no allowed characters left'): |
|
| 191 |
- Vault(phrase=self.phrase, lower=0, upper=0, number=0, |
|
| 192 |
- space=0, dash=0, symbol=0) |
|
| 240 |
+ with pytest.raises(ValueError, match='no allowed characters left'): |
|
| 241 |
+ Vault( |
|
| 242 |
+ phrase=self.phrase, |
|
| 243 |
+ lower=0, |
|
| 244 |
+ upper=0, |
|
| 245 |
+ number=0, |
|
| 246 |
+ space=0, |
|
| 247 |
+ dash=0, |
|
| 248 |
+ symbol=0, |
|
| 249 |
+ ) |
|
| 193 | 250 |
|
| 194 | 251 |
def test_320_character_set_subtraction_duplicate(self): |
| 195 | 252 |
with pytest.raises(ValueError, match='duplicate characters'): |
| ... | ... |
@@ -199,9 +256,9 @@ class TestVault: |
| 199 | 256 |
|
| 200 | 257 |
def test_322_hash_length_estimation(self) -> None: |
| 201 | 258 |
v = Vault(phrase=self.phrase) |
| 202 |
- with pytest.raises(ValueError, |
|
| 203 |
- match='invalid safety factor'): |
|
| 259 |
+ with pytest.raises(ValueError, match='invalid safety factor'): |
|
| 204 | 260 |
assert v._estimate_sufficient_hash_length(-1.0) |
| 205 |
- with pytest.raises(TypeError, |
|
| 206 |
- match='invalid safety factor: not a float'): |
|
| 261 |
+ with pytest.raises( |
|
| 262 |
+ TypeError, match='invalid safety factor: not a float' |
|
| 263 |
+ ): |
|
| 207 | 264 |
assert v._estimate_sufficient_hash_length(None) # type: ignore |
| ... | ... |
@@ -56,68 +58,91 @@ class OptionCombination(NamedTuple): |
| 56 | 58 |
|
| 57 | 59 |
|
| 58 | 60 |
PASSWORD_GENERATION_OPTIONS: list[tuple[str, ...]] = [ |
| 59 |
- ('--phrase',), ('--key',), ('--length', '20'), ('--repeat', '20'),
|
|
| 60 |
- ('--lower', '1'), ('--upper', '1'), ('--number', '1'),
|
|
| 61 |
- ('--space', '1'), ('--dash', '1'), ('--symbol', '1')
|
|
| 61 |
+ ('--phrase',),
|
|
| 62 |
+ ('--key',),
|
|
| 63 |
+ ('--length', '20'),
|
|
| 64 |
+ ('--repeat', '20'),
|
|
| 65 |
+ ('--lower', '1'),
|
|
| 66 |
+ ('--upper', '1'),
|
|
| 67 |
+ ('--number', '1'),
|
|
| 68 |
+ ('--space', '1'),
|
|
| 69 |
+ ('--dash', '1'),
|
|
| 70 |
+ ('--symbol', '1'),
|
|
| 62 | 71 |
] |
| 63 | 72 |
CONFIGURATION_OPTIONS: list[tuple[str, ...]] = [ |
| 64 |
- ('--notes',), ('--config',), ('--delete',), ('--delete-globals',),
|
|
| 65 |
- ('--clear',)
|
|
| 73 |
+ ('--notes',),
|
|
| 74 |
+ ('--config',),
|
|
| 75 |
+ ('--delete',),
|
|
| 76 |
+ ('--delete-globals',),
|
|
| 77 |
+ ('--clear',),
|
|
| 66 | 78 |
] |
| 67 | 79 |
CONFIGURATION_COMMANDS: list[tuple[str, ...]] = [ |
| 68 |
- ('--notes',), ('--delete',), ('--delete-globals',), ('--clear',)
|
|
| 69 |
-] |
|
| 70 |
-STORAGE_OPTIONS: list[tuple[str, ...]] = [ |
|
| 71 |
- ('--export', '-'), ('--import', '-')
|
|
| 80 |
+ ('--notes',),
|
|
| 81 |
+ ('--delete',),
|
|
| 82 |
+ ('--delete-globals',),
|
|
| 83 |
+ ('--clear',),
|
|
| 72 | 84 |
] |
| 85 |
+STORAGE_OPTIONS: list[tuple[str, ...]] = [('--export', '-'), ('--import', '-')]
|
|
| 73 | 86 |
INCOMPATIBLE: dict[tuple[str, ...], IncompatibleConfiguration] = {
|
| 74 | 87 |
('--phrase',): IncompatibleConfiguration(
|
| 75 | 88 |
[('--key',), *CONFIGURATION_COMMANDS, *STORAGE_OPTIONS],
|
| 76 |
- True, DUMMY_PASSPHRASE), |
|
| 89 |
+ True, |
|
| 90 |
+ DUMMY_PASSPHRASE, |
|
| 91 |
+ ), |
|
| 77 | 92 |
('--key',): IncompatibleConfiguration(
|
| 78 |
- CONFIGURATION_COMMANDS + STORAGE_OPTIONS, |
|
| 79 |
- True, DUMMY_PASSPHRASE), |
|
| 93 |
+ CONFIGURATION_COMMANDS + STORAGE_OPTIONS, True, DUMMY_PASSPHRASE |
|
| 94 |
+ ), |
|
| 80 | 95 |
('--length', '20'): IncompatibleConfiguration(
|
| 81 |
- CONFIGURATION_COMMANDS + STORAGE_OPTIONS, |
|
| 82 |
- True, DUMMY_PASSPHRASE), |
|
| 96 |
+ CONFIGURATION_COMMANDS + STORAGE_OPTIONS, True, DUMMY_PASSPHRASE |
|
| 97 |
+ ), |
|
| 83 | 98 |
('--repeat', '20'): IncompatibleConfiguration(
|
| 84 |
- CONFIGURATION_COMMANDS + STORAGE_OPTIONS, |
|
| 85 |
- True, DUMMY_PASSPHRASE), |
|
| 99 |
+ CONFIGURATION_COMMANDS + STORAGE_OPTIONS, True, DUMMY_PASSPHRASE |
|
| 100 |
+ ), |
|
| 86 | 101 |
('--lower', '1'): IncompatibleConfiguration(
|
| 87 |
- CONFIGURATION_COMMANDS + STORAGE_OPTIONS, |
|
| 88 |
- True, DUMMY_PASSPHRASE), |
|
| 102 |
+ CONFIGURATION_COMMANDS + STORAGE_OPTIONS, True, DUMMY_PASSPHRASE |
|
| 103 |
+ ), |
|
| 89 | 104 |
('--upper', '1'): IncompatibleConfiguration(
|
| 90 |
- CONFIGURATION_COMMANDS + STORAGE_OPTIONS, |
|
| 91 |
- True, DUMMY_PASSPHRASE), |
|
| 105 |
+ CONFIGURATION_COMMANDS + STORAGE_OPTIONS, True, DUMMY_PASSPHRASE |
|
| 106 |
+ ), |
|
| 92 | 107 |
('--number', '1'): IncompatibleConfiguration(
|
| 93 |
- CONFIGURATION_COMMANDS + STORAGE_OPTIONS, |
|
| 94 |
- True, DUMMY_PASSPHRASE), |
|
| 108 |
+ CONFIGURATION_COMMANDS + STORAGE_OPTIONS, True, DUMMY_PASSPHRASE |
|
| 109 |
+ ), |
|
| 95 | 110 |
('--space', '1'): IncompatibleConfiguration(
|
| 96 |
- CONFIGURATION_COMMANDS + STORAGE_OPTIONS, |
|
| 97 |
- True, DUMMY_PASSPHRASE), |
|
| 111 |
+ CONFIGURATION_COMMANDS + STORAGE_OPTIONS, True, DUMMY_PASSPHRASE |
|
| 112 |
+ ), |
|
| 98 | 113 |
('--dash', '1'): IncompatibleConfiguration(
|
| 99 |
- CONFIGURATION_COMMANDS + STORAGE_OPTIONS, |
|
| 100 |
- True, DUMMY_PASSPHRASE), |
|
| 114 |
+ CONFIGURATION_COMMANDS + STORAGE_OPTIONS, True, DUMMY_PASSPHRASE |
|
| 115 |
+ ), |
|
| 101 | 116 |
('--symbol', '1'): IncompatibleConfiguration(
|
| 102 |
- CONFIGURATION_COMMANDS + STORAGE_OPTIONS, |
|
| 103 |
- True, DUMMY_PASSPHRASE), |
|
| 117 |
+ CONFIGURATION_COMMANDS + STORAGE_OPTIONS, True, DUMMY_PASSPHRASE |
|
| 118 |
+ ), |
|
| 104 | 119 |
('--notes',): IncompatibleConfiguration(
|
| 105 |
- [('--config',), ('--delete',), ('--delete-globals',),
|
|
| 106 |
- ('--clear',), *STORAGE_OPTIONS],
|
|
| 107 |
- True, None), |
|
| 120 |
+ [ |
|
| 121 |
+ ('--config',),
|
|
| 122 |
+ ('--delete',),
|
|
| 123 |
+ ('--delete-globals',),
|
|
| 124 |
+ ('--clear',),
|
|
| 125 |
+ *STORAGE_OPTIONS, |
|
| 126 |
+ ], |
|
| 127 |
+ True, |
|
| 128 |
+ None, |
|
| 129 |
+ ), |
|
| 108 | 130 |
('--config', '-p'): IncompatibleConfiguration(
|
| 109 |
- [('--delete',), ('--delete-globals',),
|
|
| 110 |
- ('--clear',), *STORAGE_OPTIONS],
|
|
| 111 |
- None, DUMMY_PASSPHRASE), |
|
| 131 |
+ [('--delete',), ('--delete-globals',), ('--clear',), *STORAGE_OPTIONS],
|
|
| 132 |
+ None, |
|
| 133 |
+ DUMMY_PASSPHRASE, |
|
| 134 |
+ ), |
|
| 112 | 135 |
('--delete',): IncompatibleConfiguration(
|
| 113 |
- [('--delete-globals',), ('--clear',), *STORAGE_OPTIONS], True, None),
|
|
| 136 |
+ [('--delete-globals',), ('--clear',), *STORAGE_OPTIONS], True, None
|
|
| 137 |
+ ), |
|
| 114 | 138 |
('--delete-globals',): IncompatibleConfiguration(
|
| 115 |
- [('--clear',), *STORAGE_OPTIONS], False, None),
|
|
| 139 |
+ [('--clear',), *STORAGE_OPTIONS], False, None
|
|
| 140 |
+ ), |
|
| 116 | 141 |
('--clear',): IncompatibleConfiguration(STORAGE_OPTIONS, False, None),
|
| 117 | 142 |
('--export', '-'): IncompatibleConfiguration(
|
| 118 |
- [('--import', '-')], False, None),
|
|
| 119 |
- ('--import', '-'): IncompatibleConfiguration(
|
|
| 120 |
- [], False, None), |
|
| 143 |
+ [('--import', '-')], False, None
|
|
| 144 |
+ ), |
|
| 145 |
+ ('--import', '-'): IncompatibleConfiguration([], False, None),
|
|
| 121 | 146 |
} |
| 122 | 147 |
SINGLES: dict[tuple[str, ...], SingleConfiguration] = {
|
| 123 | 148 |
('--phrase',): SingleConfiguration(True, DUMMY_PASSPHRASE, True),
|
| ... | ... |
@@ -143,37 +168,50 @@ config: IncompatibleConfiguration | SingleConfiguration |
| 143 | 168 |
for opt, config in INCOMPATIBLE.items(): |
| 144 | 169 |
for opt2 in config.other_options: |
| 145 | 170 |
INTERESTING_OPTION_COMBINATIONS.extend([ |
| 146 |
- OptionCombination(options=list(opt + opt2), incompatible=True, |
|
| 171 |
+ OptionCombination( |
|
| 172 |
+ options=list(opt + opt2), |
|
| 173 |
+ incompatible=True, |
|
| 147 | 174 |
needs_service=config.needs_service, |
| 148 |
- input=config.input, check_success=False), |
|
| 149 |
- OptionCombination(options=list(opt2 + opt), incompatible=True, |
|
| 175 |
+ input=config.input, |
|
| 176 |
+ check_success=False, |
|
| 177 |
+ ), |
|
| 178 |
+ OptionCombination( |
|
| 179 |
+ options=list(opt2 + opt), |
|
| 180 |
+ incompatible=True, |
|
| 150 | 181 |
needs_service=config.needs_service, |
| 151 |
- input=config.input, check_success=False) |
|
| 182 |
+ input=config.input, |
|
| 183 |
+ check_success=False, |
|
| 184 |
+ ), |
|
| 152 | 185 |
]) |
| 153 | 186 |
for opt, config in SINGLES.items(): |
| 154 | 187 |
INTERESTING_OPTION_COMBINATIONS.append( |
| 155 |
- OptionCombination(options=list(opt), incompatible=False, |
|
| 188 |
+ OptionCombination( |
|
| 189 |
+ options=list(opt), |
|
| 190 |
+ incompatible=False, |
|
| 156 | 191 |
needs_service=config.needs_service, |
| 157 | 192 |
input=config.input, |
| 158 |
- check_success=config.check_success)) |
|
| 193 |
+ check_success=config.check_success, |
|
| 194 |
+ ) |
|
| 195 |
+ ) |
|
| 159 | 196 |
|
| 160 | 197 |
|
| 161 | 198 |
class TestCLI: |
| 162 | 199 |
def test_200_help_output(self): |
| 163 | 200 |
runner = click.testing.CliRunner(mix_stderr=False) |
| 164 |
- result = runner.invoke(cli.derivepassphrase, ['--help'], |
|
| 165 |
- catch_exceptions=False) |
|
| 166 |
- assert result.exit_code == 0 |
|
| 167 |
- assert 'Password generation:\n' in result.output, ( |
|
| 168 |
- 'Option groups not respected in help text.' |
|
| 169 |
- ) |
|
| 170 |
- assert 'Use NUMBER=0, e.g. "--symbol 0"' in result.output, ( |
|
| 171 |
- 'Option group epilog not printed.' |
|
| 201 |
+ result = runner.invoke( |
|
| 202 |
+ cli.derivepassphrase, ['--help'], catch_exceptions=False |
|
| 172 | 203 |
) |
| 204 |
+ assert result.exit_code == 0 |
|
| 205 |
+ assert ( |
|
| 206 |
+ 'Password generation:\n' in result.output |
|
| 207 |
+ ), 'Option groups not respected in help text.' |
|
| 208 |
+ assert ( |
|
| 209 |
+ 'Use NUMBER=0, e.g. "--symbol 0"' in result.output |
|
| 210 |
+ ), 'Option group epilog not printed.' |
|
| 173 | 211 |
|
| 174 |
- @pytest.mark.parametrize('charset_name',
|
|
| 175 |
- ['lower', 'upper', 'number', 'space', |
|
| 176 |
- 'dash', 'symbol']) |
|
| 212 |
+ @pytest.mark.parametrize( |
|
| 213 |
+ 'charset_name', ['lower', 'upper', 'number', 'space', 'dash', 'symbol'] |
|
| 214 |
+ ) |
|
| 177 | 215 |
def test_201_disable_character_set( |
| 178 | 216 |
self, monkeypatch: Any, charset_name: str |
| 179 | 217 |
) -> None: |
| ... | ... |
@@ -181,15 +219,18 @@ class TestCLI: |
| 181 | 219 |
option = f'--{charset_name}'
|
| 182 | 220 |
charset = dpp.Vault._CHARSETS[charset_name].decode('ascii')
|
| 183 | 221 |
runner = click.testing.CliRunner(mix_stderr=False) |
| 184 |
- result = runner.invoke(cli.derivepassphrase, |
|
| 222 |
+ result = runner.invoke( |
|
| 223 |
+ cli.derivepassphrase, |
|
| 185 | 224 |
[option, '0', '-p', DUMMY_SERVICE], |
| 186 |
- input=DUMMY_PASSPHRASE, catch_exceptions=False) |
|
| 187 |
- assert result.exit_code == 0, ( |
|
| 188 |
- f'program died unexpectedly with exit code {result.exit_code}'
|
|
| 189 |
- ) |
|
| 190 |
- assert not result.stderr_bytes, ( |
|
| 191 |
- f'program barfed on stderr: {result.stderr_bytes!r}'
|
|
| 225 |
+ input=DUMMY_PASSPHRASE, |
|
| 226 |
+ catch_exceptions=False, |
|
| 192 | 227 |
) |
| 228 |
+ assert ( |
|
| 229 |
+ result.exit_code == 0 |
|
| 230 |
+ ), f'program died unexpectedly with exit code {result.exit_code}'
|
|
| 231 |
+ assert ( |
|
| 232 |
+ not result.stderr_bytes |
|
| 233 |
+ ), f'program barfed on stderr: {result.stderr_bytes!r}'
|
|
| 193 | 234 |
for c in charset: |
| 194 | 235 |
assert c not in result.stdout, ( |
| 195 | 236 |
f'derived password contains forbidden character {c!r}: '
|
| ... | ... |
@@ -199,15 +240,18 @@ class TestCLI: |
| 199 | 240 |
def test_202_disable_repetition(self, monkeypatch: Any) -> None: |
| 200 | 241 |
monkeypatch.setattr(cli, '_prompt_for_passphrase', tests.auto_prompt) |
| 201 | 242 |
runner = click.testing.CliRunner(mix_stderr=False) |
| 202 |
- result = runner.invoke(cli.derivepassphrase, |
|
| 243 |
+ result = runner.invoke( |
|
| 244 |
+ cli.derivepassphrase, |
|
| 203 | 245 |
['--repeat', '0', '-p', DUMMY_SERVICE], |
| 204 |
- input=DUMMY_PASSPHRASE, catch_exceptions=False) |
|
| 205 |
- assert result.exit_code == 0, ( |
|
| 206 |
- f'program died unexpectedly with exit code {result.exit_code}'
|
|
| 207 |
- ) |
|
| 208 |
- assert not result.stderr_bytes, ( |
|
| 209 |
- f'program barfed on stderr: {result.stderr_bytes!r}'
|
|
| 246 |
+ input=DUMMY_PASSPHRASE, |
|
| 247 |
+ catch_exceptions=False, |
|
| 210 | 248 |
) |
| 249 |
+ assert ( |
|
| 250 |
+ result.exit_code == 0 |
|
| 251 |
+ ), f'program died unexpectedly with exit code {result.exit_code}'
|
|
| 252 |
+ assert ( |
|
| 253 |
+ not result.stderr_bytes |
|
| 254 |
+ ), f'program barfed on stderr: {result.stderr_bytes!r}'
|
|
| 211 | 255 |
passphrase = result.stdout.rstrip('\r\n')
|
| 212 | 256 |
for i in range(len(passphrase) - 1): |
| 213 | 257 |
assert passphrase[i : i + 1] != passphrase[i + 1 : i + 2], ( |
| ... | ... |
@@ -215,391 +259,497 @@ class TestCLI: |
| 215 | 259 |
f'at position {i}: {result.stdout!r}'
|
| 216 | 260 |
) |
| 217 | 261 |
|
| 218 |
- @pytest.mark.parametrize('config', [
|
|
| 219 |
- pytest.param({'global': {'key': DUMMY_KEY1_B64},
|
|
| 220 |
- 'services': {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS}},
|
|
| 221 |
- id='global'), |
|
| 222 |
- pytest.param({'global': {'phrase': DUMMY_PASSPHRASE.rstrip(b'\n')
|
|
| 223 |
- .decode('ASCII')},
|
|
| 224 |
- 'services': {DUMMY_SERVICE: {'key': DUMMY_KEY1_B64,
|
|
| 225 |
- **DUMMY_CONFIG_SETTINGS}}}, |
|
| 226 |
- id='service'), |
|
| 227 |
- ]) |
|
| 262 |
+ @pytest.mark.parametrize( |
|
| 263 |
+ 'config', |
|
| 264 |
+ [ |
|
| 265 |
+ pytest.param( |
|
| 266 |
+ {
|
|
| 267 |
+ 'global': {'key': DUMMY_KEY1_B64},
|
|
| 268 |
+ 'services': {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS},
|
|
| 269 |
+ }, |
|
| 270 |
+ id='global', |
|
| 271 |
+ ), |
|
| 272 |
+ pytest.param( |
|
| 273 |
+ {
|
|
| 274 |
+ 'global': {
|
|
| 275 |
+ 'phrase': DUMMY_PASSPHRASE.rstrip(b'\n').decode( |
|
| 276 |
+ 'ASCII' |
|
| 277 |
+ ) |
|
| 278 |
+ }, |
|
| 279 |
+ 'services': {
|
|
| 280 |
+ DUMMY_SERVICE: {
|
|
| 281 |
+ 'key': DUMMY_KEY1_B64, |
|
| 282 |
+ **DUMMY_CONFIG_SETTINGS, |
|
| 283 |
+ } |
|
| 284 |
+ }, |
|
| 285 |
+ }, |
|
| 286 |
+ id='service', |
|
| 287 |
+ ), |
|
| 288 |
+ ], |
|
| 289 |
+ ) |
|
| 228 | 290 |
def test_204a_key_from_config( |
| 229 |
- self, monkeypatch: Any, config: dpp.types.VaultConfig, |
|
| 291 |
+ self, |
|
| 292 |
+ monkeypatch: Any, |
|
| 293 |
+ config: dpp.types.VaultConfig, |
|
| 230 | 294 |
) -> None: |
| 231 | 295 |
runner = click.testing.CliRunner(mix_stderr=False) |
| 232 |
- with tests.isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
| 233 |
- config=config): |
|
| 234 |
- monkeypatch.setattr(dpp.Vault, 'phrase_from_key', |
|
| 235 |
- tests.phrase_from_key) |
|
| 236 |
- result = runner.invoke(cli.derivepassphrase, [DUMMY_SERVICE], |
|
| 237 |
- catch_exceptions=False) |
|
| 238 |
- assert (result.exit_code, result.stderr_bytes) == (0, b''), ( |
|
| 239 |
- 'program exited with failure' |
|
| 296 |
+ with tests.isolated_config( |
|
| 297 |
+ monkeypatch=monkeypatch, runner=runner, config=config |
|
| 298 |
+ ): |
|
| 299 |
+ monkeypatch.setattr( |
|
| 300 |
+ dpp.Vault, 'phrase_from_key', tests.phrase_from_key |
|
| 301 |
+ ) |
|
| 302 |
+ result = runner.invoke( |
|
| 303 |
+ cli.derivepassphrase, [DUMMY_SERVICE], catch_exceptions=False |
|
| 240 | 304 |
) |
| 305 |
+ assert (result.exit_code, result.stderr_bytes) == ( |
|
| 306 |
+ 0, |
|
| 307 |
+ b'', |
|
| 308 |
+ ), 'program exited with failure' |
|
| 241 | 309 |
assert ( |
| 242 | 310 |
result.stdout_bytes.rstrip(b'\n') != DUMMY_RESULT_PASSPHRASE |
| 243 |
- ), ( |
|
| 244 |
- 'program generated unexpected result (phrase instead of key)' |
|
| 245 |
- ) |
|
| 246 |
- assert result.stdout_bytes.rstrip(b'\n') == DUMMY_RESULT_KEY1, ( |
|
| 247 |
- 'program generated unexpected result (wrong settings?)' |
|
| 248 |
- ) |
|
| 311 |
+ ), 'program generated unexpected result (phrase instead of key)' |
|
| 312 |
+ assert ( |
|
| 313 |
+ result.stdout_bytes.rstrip(b'\n') == DUMMY_RESULT_KEY1 |
|
| 314 |
+ ), 'program generated unexpected result (wrong settings?)' |
|
| 249 | 315 |
|
| 250 | 316 |
def test_204b_key_from_command_line(self, monkeypatch: Any) -> None: |
| 251 | 317 |
runner = click.testing.CliRunner(mix_stderr=False) |
| 252 |
- with tests.isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
| 253 |
- config={'services': {DUMMY_SERVICE:
|
|
| 254 |
- DUMMY_CONFIG_SETTINGS}}): |
|
| 255 |
- monkeypatch.setattr(cli, '_get_suitable_ssh_keys', |
|
| 256 |
- tests.suitable_ssh_keys) |
|
| 257 |
- monkeypatch.setattr(dpp.Vault, 'phrase_from_key', |
|
| 258 |
- tests.phrase_from_key) |
|
| 259 |
- result = runner.invoke(cli.derivepassphrase, |
|
| 318 |
+ with tests.isolated_config( |
|
| 319 |
+ monkeypatch=monkeypatch, |
|
| 320 |
+ runner=runner, |
|
| 321 |
+ config={'services': {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS}},
|
|
| 322 |
+ ): |
|
| 323 |
+ monkeypatch.setattr( |
|
| 324 |
+ cli, '_get_suitable_ssh_keys', tests.suitable_ssh_keys |
|
| 325 |
+ ) |
|
| 326 |
+ monkeypatch.setattr( |
|
| 327 |
+ dpp.Vault, 'phrase_from_key', tests.phrase_from_key |
|
| 328 |
+ ) |
|
| 329 |
+ result = runner.invoke( |
|
| 330 |
+ cli.derivepassphrase, |
|
| 260 | 331 |
['-k', DUMMY_SERVICE], |
| 261 |
- input=b'1\n', catch_exceptions=False) |
|
| 332 |
+ input=b'1\n', |
|
| 333 |
+ catch_exceptions=False, |
|
| 334 |
+ ) |
|
| 262 | 335 |
assert result.exit_code == 0, 'program exited with failure' |
| 263 | 336 |
assert result.stdout_bytes, 'program output expected' |
| 264 | 337 |
last_line = result.stdout_bytes.splitlines(True)[-1] |
| 265 |
- assert last_line.rstrip(b'\n') != DUMMY_RESULT_PASSPHRASE, ( |
|
| 266 |
- 'program generated unexpected result (phrase instead of key)' |
|
| 267 |
- ) |
|
| 268 |
- assert last_line.rstrip(b'\n') == DUMMY_RESULT_KEY1, ( |
|
| 269 |
- 'program generated unexpected result (wrong settings?)' |
|
| 270 |
- ) |
|
| 338 |
+ assert ( |
|
| 339 |
+ last_line.rstrip(b'\n') != DUMMY_RESULT_PASSPHRASE |
|
| 340 |
+ ), 'program generated unexpected result (phrase instead of key)' |
|
| 341 |
+ assert ( |
|
| 342 |
+ last_line.rstrip(b'\n') == DUMMY_RESULT_KEY1 |
|
| 343 |
+ ), 'program generated unexpected result (wrong settings?)' |
|
| 271 | 344 |
|
| 272 | 345 |
def test_205_service_phrase_if_key_in_global_config( |
| 273 |
- self, monkeypatch: Any, |
|
| 346 |
+ self, |
|
| 347 |
+ monkeypatch: Any, |
|
| 274 | 348 |
) -> None: |
| 275 | 349 |
runner = click.testing.CliRunner(mix_stderr=False) |
| 276 | 350 |
with tests.isolated_config( |
| 277 |
- monkeypatch=monkeypatch, runner=runner, |
|
| 351 |
+ monkeypatch=monkeypatch, |
|
| 352 |
+ runner=runner, |
|
| 278 | 353 |
config={
|
| 279 | 354 |
'global': {'key': DUMMY_KEY1_B64},
|
| 280 | 355 |
'services': {
|
| 281 | 356 |
DUMMY_SERVICE: {
|
| 282 |
- 'phrase': DUMMY_PASSPHRASE.rstrip(b'\n') |
|
| 283 |
- .decode('ASCII'),
|
|
| 284 |
- **DUMMY_CONFIG_SETTINGS}}} |
|
| 357 |
+ 'phrase': DUMMY_PASSPHRASE.rstrip(b'\n').decode( |
|
| 358 |
+ 'ASCII' |
|
| 359 |
+ ), |
|
| 360 |
+ **DUMMY_CONFIG_SETTINGS, |
|
| 361 |
+ } |
|
| 362 |
+ }, |
|
| 363 |
+ }, |
|
| 285 | 364 |
): |
| 286 |
- result = runner.invoke(cli.derivepassphrase, [DUMMY_SERVICE], |
|
| 287 |
- catch_exceptions=False) |
|
| 365 |
+ result = runner.invoke( |
|
| 366 |
+ cli.derivepassphrase, [DUMMY_SERVICE], catch_exceptions=False |
|
| 367 |
+ ) |
|
| 288 | 368 |
assert result.exit_code == 0, 'program exited with failure' |
| 289 | 369 |
assert result.stdout_bytes, 'program output expected' |
| 290 | 370 |
last_line = result.stdout_bytes.splitlines(True)[-1] |
| 291 |
- assert last_line.rstrip(b'\n') != DUMMY_RESULT_KEY1, ( |
|
| 292 |
- 'program generated unexpected result (key instead of phrase)' |
|
| 293 |
- ) |
|
| 294 |
- assert last_line.rstrip(b'\n') == DUMMY_RESULT_PASSPHRASE, ( |
|
| 295 |
- 'program generated unexpected result (wrong settings?)' |
|
| 296 |
- ) |
|
| 371 |
+ assert ( |
|
| 372 |
+ last_line.rstrip(b'\n') != DUMMY_RESULT_KEY1 |
|
| 373 |
+ ), 'program generated unexpected result (key instead of phrase)' |
|
| 374 |
+ assert ( |
|
| 375 |
+ last_line.rstrip(b'\n') == DUMMY_RESULT_PASSPHRASE |
|
| 376 |
+ ), 'program generated unexpected result (wrong settings?)' |
|
| 297 | 377 |
|
| 298 |
- @pytest.mark.parametrize('option',
|
|
| 299 |
- ['--lower', '--upper', '--number', |
|
| 300 |
- '--space', '--dash', '--symbol', |
|
| 301 |
- '--repeat', '--length']) |
|
| 378 |
+ @pytest.mark.parametrize( |
|
| 379 |
+ 'option', |
|
| 380 |
+ [ |
|
| 381 |
+ '--lower', |
|
| 382 |
+ '--upper', |
|
| 383 |
+ '--number', |
|
| 384 |
+ '--space', |
|
| 385 |
+ '--dash', |
|
| 386 |
+ '--symbol', |
|
| 387 |
+ '--repeat', |
|
| 388 |
+ '--length', |
|
| 389 |
+ ], |
|
| 390 |
+ ) |
|
| 302 | 391 |
def test_210_invalid_argument_range(self, option: str) -> None: |
| 303 | 392 |
runner = click.testing.CliRunner(mix_stderr=False) |
| 304 | 393 |
value: str | int |
| 305 | 394 |
for value in '-42', 'invalid': |
| 306 |
- result = runner.invoke(cli.derivepassphrase, |
|
| 307 |
- [option, cast(str, value), '-p', |
|
| 308 |
- DUMMY_SERVICE], |
|
| 395 |
+ result = runner.invoke( |
|
| 396 |
+ cli.derivepassphrase, |
|
| 397 |
+ [option, cast(str, value), '-p', DUMMY_SERVICE], |
|
| 309 | 398 |
input=DUMMY_PASSPHRASE, |
| 310 |
- catch_exceptions=False) |
|
| 311 |
- assert result.exit_code > 0, ( |
|
| 312 |
- 'program unexpectedly succeeded' |
|
| 313 |
- ) |
|
| 314 |
- assert result.stderr_bytes, ( |
|
| 315 |
- 'program did not print any error message' |
|
| 316 |
- ) |
|
| 317 |
- assert b'Error: Invalid value' in result.stderr_bytes, ( |
|
| 318 |
- 'program did not print the expected error message' |
|
| 399 |
+ catch_exceptions=False, |
|
| 319 | 400 |
) |
| 401 |
+ assert result.exit_code > 0, 'program unexpectedly succeeded' |
|
| 402 |
+ assert ( |
|
| 403 |
+ result.stderr_bytes |
|
| 404 |
+ ), 'program did not print any error message' |
|
| 405 |
+ assert ( |
|
| 406 |
+ b'Error: Invalid value' in result.stderr_bytes |
|
| 407 |
+ ), 'program did not print the expected error message' |
|
| 320 | 408 |
|
| 321 | 409 |
@pytest.mark.parametrize( |
| 322 | 410 |
['options', 'service', 'input', 'check_success'], |
| 323 |
- [(o.options, o.needs_service, o.input, o.check_success) |
|
| 324 |
- for o in INTERESTING_OPTION_COMBINATIONS if not o.incompatible], |
|
| 411 |
+ [ |
|
| 412 |
+ (o.options, o.needs_service, o.input, o.check_success) |
|
| 413 |
+ for o in INTERESTING_OPTION_COMBINATIONS |
|
| 414 |
+ if not o.incompatible |
|
| 415 |
+ ], |
|
| 325 | 416 |
) |
| 326 | 417 |
def test_211_service_needed( |
| 327 |
- self, monkeypatch: Any, options: list[str], |
|
| 328 |
- service: bool | None, input: bytes | None, check_success: bool, |
|
| 418 |
+ self, |
|
| 419 |
+ monkeypatch: Any, |
|
| 420 |
+ options: list[str], |
|
| 421 |
+ service: bool | None, |
|
| 422 |
+ input: bytes | None, |
|
| 423 |
+ check_success: bool, |
|
| 329 | 424 |
) -> None: |
| 330 | 425 |
monkeypatch.setattr(cli, '_prompt_for_passphrase', tests.auto_prompt) |
| 331 | 426 |
runner = click.testing.CliRunner(mix_stderr=False) |
| 332 |
- with tests.isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
| 333 |
- config={'global': {'phrase': 'abc'},
|
|
| 334 |
- 'services': {}}):
|
|
| 335 |
- result = runner.invoke(cli.derivepassphrase, |
|
| 336 |
- options if service |
|
| 337 |
- else [*options, DUMMY_SERVICE], |
|
| 338 |
- input=input, catch_exceptions=False) |
|
| 339 |
- if service is not None: |
|
| 340 |
- assert result.exit_code > 0, ( |
|
| 341 |
- 'program unexpectedly succeeded' |
|
| 342 |
- ) |
|
| 343 |
- assert result.stderr_bytes, ( |
|
| 344 |
- 'program did not print any error message' |
|
| 427 |
+ with tests.isolated_config( |
|
| 428 |
+ monkeypatch=monkeypatch, |
|
| 429 |
+ runner=runner, |
|
| 430 |
+ config={'global': {'phrase': 'abc'}, 'services': {}},
|
|
| 431 |
+ ): |
|
| 432 |
+ result = runner.invoke( |
|
| 433 |
+ cli.derivepassphrase, |
|
| 434 |
+ options if service else [*options, DUMMY_SERVICE], |
|
| 435 |
+ input=input, |
|
| 436 |
+ catch_exceptions=False, |
|
| 345 | 437 |
) |
| 346 |
- err_msg = (b' requires a SERVICE' if service |
|
| 347 |
- else b' does not take a SERVICE argument') |
|
| 348 |
- assert err_msg in result.stderr_bytes, ( |
|
| 349 |
- 'program did not print the expected error message' |
|
| 438 |
+ if service is not None: |
|
| 439 |
+ assert result.exit_code > 0, 'program unexpectedly succeeded' |
|
| 440 |
+ assert ( |
|
| 441 |
+ result.stderr_bytes |
|
| 442 |
+ ), 'program did not print any error message' |
|
| 443 |
+ err_msg = ( |
|
| 444 |
+ b' requires a SERVICE' |
|
| 445 |
+ if service |
|
| 446 |
+ else b' does not take a SERVICE argument' |
|
| 350 | 447 |
) |
| 448 |
+ assert ( |
|
| 449 |
+ err_msg in result.stderr_bytes |
|
| 450 |
+ ), 'program did not print the expected error message' |
|
| 351 | 451 |
else: |
| 352 |
- assert (result.exit_code, result.stderr_bytes) == (0, b''), ( |
|
| 353 |
- 'program unexpectedly failed' |
|
| 354 |
- ) |
|
| 452 |
+ assert (result.exit_code, result.stderr_bytes) == ( |
|
| 453 |
+ 0, |
|
| 454 |
+ b'', |
|
| 455 |
+ ), 'program unexpectedly failed' |
|
| 355 | 456 |
if check_success: |
| 356 |
- with tests.isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
| 357 |
- config={'global': {'phrase': 'abc'},
|
|
| 358 |
- 'services': {}}):
|
|
| 359 |
- monkeypatch.setattr(cli, '_prompt_for_passphrase', |
|
| 360 |
- tests.auto_prompt) |
|
| 361 |
- result = runner.invoke(cli.derivepassphrase, |
|
| 362 |
- [*options, DUMMY_SERVICE] |
|
| 363 |
- if service else options, |
|
| 364 |
- input=input, catch_exceptions=False) |
|
| 365 |
- assert (result.exit_code, result.stderr_bytes) == (0, b''), ( |
|
| 366 |
- 'program unexpectedly failed' |
|
| 457 |
+ with tests.isolated_config( |
|
| 458 |
+ monkeypatch=monkeypatch, |
|
| 459 |
+ runner=runner, |
|
| 460 |
+ config={'global': {'phrase': 'abc'}, 'services': {}},
|
|
| 461 |
+ ): |
|
| 462 |
+ monkeypatch.setattr( |
|
| 463 |
+ cli, '_prompt_for_passphrase', tests.auto_prompt |
|
| 464 |
+ ) |
|
| 465 |
+ result = runner.invoke( |
|
| 466 |
+ cli.derivepassphrase, |
|
| 467 |
+ [*options, DUMMY_SERVICE] if service else options, |
|
| 468 |
+ input=input, |
|
| 469 |
+ catch_exceptions=False, |
|
| 367 | 470 |
) |
| 471 |
+ assert (result.exit_code, result.stderr_bytes) == ( |
|
| 472 |
+ 0, |
|
| 473 |
+ b'', |
|
| 474 |
+ ), 'program unexpectedly failed' |
|
| 368 | 475 |
|
| 369 | 476 |
@pytest.mark.parametrize( |
| 370 | 477 |
['options', 'service'], |
| 371 |
- [(o.options, o.needs_service) |
|
| 372 |
- for o in INTERESTING_OPTION_COMBINATIONS if o.incompatible], |
|
| 478 |
+ [ |
|
| 479 |
+ (o.options, o.needs_service) |
|
| 480 |
+ for o in INTERESTING_OPTION_COMBINATIONS |
|
| 481 |
+ if o.incompatible |
|
| 482 |
+ ], |
|
| 373 | 483 |
) |
| 374 | 484 |
def test_212_incompatible_options( |
| 375 |
- self, options: list[str], service: bool | None, |
|
| 485 |
+ self, |
|
| 486 |
+ options: list[str], |
|
| 487 |
+ service: bool | None, |
|
| 376 | 488 |
) -> None: |
| 377 | 489 |
runner = click.testing.CliRunner(mix_stderr=False) |
| 378 |
- result = runner.invoke(cli.derivepassphrase, |
|
| 379 |
- [*options, DUMMY_SERVICE] if service |
|
| 380 |
- else options, |
|
| 381 |
- input=DUMMY_PASSPHRASE, catch_exceptions=False) |
|
| 382 |
- assert result.exit_code > 0, ( |
|
| 383 |
- 'program unexpectedly succeeded' |
|
| 384 |
- ) |
|
| 385 |
- assert result.stderr_bytes, ( |
|
| 386 |
- 'program did not print any error message' |
|
| 387 |
- ) |
|
| 388 |
- assert b'mutually exclusive with ' in result.stderr_bytes, ( |
|
| 389 |
- 'program did not print the expected error message' |
|
| 490 |
+ result = runner.invoke( |
|
| 491 |
+ cli.derivepassphrase, |
|
| 492 |
+ [*options, DUMMY_SERVICE] if service else options, |
|
| 493 |
+ input=DUMMY_PASSPHRASE, |
|
| 494 |
+ catch_exceptions=False, |
|
| 390 | 495 |
) |
| 496 |
+ assert result.exit_code > 0, 'program unexpectedly succeeded' |
|
| 497 |
+ assert result.stderr_bytes, 'program did not print any error message' |
|
| 498 |
+ assert ( |
|
| 499 |
+ b'mutually exclusive with ' in result.stderr_bytes |
|
| 500 |
+ ), 'program did not print the expected error message' |
|
| 391 | 501 |
|
| 392 | 502 |
def test_213_import_bad_config_not_vault_config( |
| 393 |
- self, monkeypatch: Any, |
|
| 503 |
+ self, |
|
| 504 |
+ monkeypatch: Any, |
|
| 394 | 505 |
) -> None: |
| 395 | 506 |
runner = click.testing.CliRunner(mix_stderr=False) |
| 396 |
- with tests.isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
| 397 |
- config={'services': {}}):
|
|
| 398 |
- result = runner.invoke(cli.derivepassphrase, ['--import', '-'], |
|
| 399 |
- input=b'null', catch_exceptions=False) |
|
| 400 |
- assert result.exit_code > 0, ( |
|
| 401 |
- 'program unexpectedly succeeded' |
|
| 402 |
- ) |
|
| 403 |
- assert result.stderr_bytes, ( |
|
| 404 |
- 'program did not print any error message' |
|
| 405 |
- ) |
|
| 406 |
- assert b'not a valid config' in result.stderr_bytes, ( |
|
| 407 |
- 'program did not print the expected error message' |
|
| 507 |
+ with tests.isolated_config( |
|
| 508 |
+ monkeypatch=monkeypatch, runner=runner, config={'services': {}}
|
|
| 509 |
+ ): |
|
| 510 |
+ result = runner.invoke( |
|
| 511 |
+ cli.derivepassphrase, |
|
| 512 |
+ ['--import', '-'], |
|
| 513 |
+ input=b'null', |
|
| 514 |
+ catch_exceptions=False, |
|
| 408 | 515 |
) |
| 516 |
+ assert result.exit_code > 0, 'program unexpectedly succeeded' |
|
| 517 |
+ assert ( |
|
| 518 |
+ result.stderr_bytes |
|
| 519 |
+ ), 'program did not print any error message' |
|
| 520 |
+ assert ( |
|
| 521 |
+ b'not a valid config' in result.stderr_bytes |
|
| 522 |
+ ), 'program did not print the expected error message' |
|
| 409 | 523 |
|
| 410 | 524 |
def test_213a_import_bad_config_not_json_data( |
| 411 |
- self, monkeypatch: Any, |
|
| 525 |
+ self, |
|
| 526 |
+ monkeypatch: Any, |
|
| 412 | 527 |
) -> None: |
| 413 | 528 |
runner = click.testing.CliRunner(mix_stderr=False) |
| 414 |
- with tests.isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
| 415 |
- config={'services': {}}):
|
|
| 416 |
- result = runner.invoke(cli.derivepassphrase, ['--import', '-'], |
|
| 529 |
+ with tests.isolated_config( |
|
| 530 |
+ monkeypatch=monkeypatch, runner=runner, config={'services': {}}
|
|
| 531 |
+ ): |
|
| 532 |
+ result = runner.invoke( |
|
| 533 |
+ cli.derivepassphrase, |
|
| 534 |
+ ['--import', '-'], |
|
| 417 | 535 |
input=b'This string is not valid JSON.', |
| 418 |
- catch_exceptions=False) |
|
| 419 |
- assert result.exit_code > 0, ( |
|
| 420 |
- 'program unexpectedly succeeded' |
|
| 421 |
- ) |
|
| 422 |
- assert result.stderr_bytes, ( |
|
| 423 |
- 'program did not print any error message' |
|
| 424 |
- ) |
|
| 425 |
- assert b'cannot decode JSON' in result.stderr_bytes, ( |
|
| 426 |
- 'program did not print the expected error message' |
|
| 536 |
+ catch_exceptions=False, |
|
| 427 | 537 |
) |
| 538 |
+ assert result.exit_code > 0, 'program unexpectedly succeeded' |
|
| 539 |
+ assert ( |
|
| 540 |
+ result.stderr_bytes |
|
| 541 |
+ ), 'program did not print any error message' |
|
| 542 |
+ assert ( |
|
| 543 |
+ b'cannot decode JSON' in result.stderr_bytes |
|
| 544 |
+ ), 'program did not print the expected error message' |
|
| 428 | 545 |
|
| 429 | 546 |
def test_213b_import_bad_config_not_a_file( |
| 430 |
- self, monkeypatch: Any, |
|
| 547 |
+ self, |
|
| 548 |
+ monkeypatch: Any, |
|
| 431 | 549 |
) -> None: |
| 432 | 550 |
runner = click.testing.CliRunner(mix_stderr=False) |
| 433 | 551 |
# `isolated_config` validates the configuration. So, to pass an |
| 434 | 552 |
# actual broken configuration, we must open the configuration file |
| 435 | 553 |
# ourselves afterwards, inside the context. |
| 436 |
- with tests.isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
| 437 |
- config={'services': {}}):
|
|
| 438 |
- with open(cli._config_filename(), 'w', |
|
| 439 |
- encoding='UTF-8') as outfile: |
|
| 554 |
+ with tests.isolated_config( |
|
| 555 |
+ monkeypatch=monkeypatch, runner=runner, config={'services': {}}
|
|
| 556 |
+ ): |
|
| 557 |
+ with open( |
|
| 558 |
+ cli._config_filename(), 'w', encoding='UTF-8' |
|
| 559 |
+ ) as outfile: |
|
| 440 | 560 |
print('This string is not valid JSON.', file=outfile)
|
| 441 | 561 |
dname = os.path.dirname(cli._config_filename()) |
| 442 | 562 |
result = runner.invoke( |
| 443 | 563 |
cli.derivepassphrase, |
| 444 | 564 |
['--import', os.fsdecode(dname)], |
| 445 |
- catch_exceptions=False) |
|
| 446 |
- assert result.exit_code > 0, ( |
|
| 447 |
- 'program unexpectedly succeeded' |
|
| 448 |
- ) |
|
| 449 |
- assert result.stderr_bytes, ( |
|
| 450 |
- 'program did not print any error message' |
|
| 565 |
+ catch_exceptions=False, |
|
| 451 | 566 |
) |
| 567 |
+ assert result.exit_code > 0, 'program unexpectedly succeeded' |
|
| 568 |
+ assert ( |
|
| 569 |
+ result.stderr_bytes |
|
| 570 |
+ ), 'program did not print any error message' |
|
| 452 | 571 |
# Don't test the actual error message, because it is subject to |
| 453 | 572 |
# locale settings. TODO: find a way anyway. |
| 454 | 573 |
|
| 455 | 574 |
def test_214_export_settings_no_stored_settings( |
| 456 |
- self, monkeypatch: Any, |
|
| 575 |
+ self, |
|
| 576 |
+ monkeypatch: Any, |
|
| 457 | 577 |
) -> None: |
| 458 | 578 |
runner = click.testing.CliRunner(mix_stderr=False) |
| 459 |
- with tests.isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
| 460 |
- config={'services': {}}):
|
|
| 579 |
+ with tests.isolated_config( |
|
| 580 |
+ monkeypatch=monkeypatch, runner=runner, config={'services': {}}
|
|
| 581 |
+ ): |
|
| 461 | 582 |
with contextlib.suppress(FileNotFoundError): |
| 462 | 583 |
os.remove(cli._config_filename()) |
| 463 |
- result = runner.invoke(cli.derivepassphrase, ['--export', '-'], |
|
| 464 |
- catch_exceptions=False) |
|
| 465 |
- assert (result.exit_code, result.stderr_bytes) == (0, b''), ( |
|
| 466 |
- 'program exited with failure' |
|
| 584 |
+ result = runner.invoke( |
|
| 585 |
+ cli.derivepassphrase, ['--export', '-'], catch_exceptions=False |
|
| 467 | 586 |
) |
| 587 |
+ assert (result.exit_code, result.stderr_bytes) == ( |
|
| 588 |
+ 0, |
|
| 589 |
+ b'', |
|
| 590 |
+ ), 'program exited with failure' |
|
| 468 | 591 |
|
| 469 | 592 |
def test_214a_export_settings_bad_stored_config( |
| 470 |
- self, monkeypatch: Any, |
|
| 593 |
+ self, |
|
| 594 |
+ monkeypatch: Any, |
|
| 471 | 595 |
) -> None: |
| 472 | 596 |
runner = click.testing.CliRunner(mix_stderr=False) |
| 473 |
- with tests.isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
| 474 |
- config={}):
|
|
| 475 |
- result = runner.invoke(cli.derivepassphrase, ['--export', '-'], |
|
| 476 |
- input=b'null', catch_exceptions=False) |
|
| 477 |
- assert result.exit_code > 0, ( |
|
| 478 |
- 'program unexpectedly succeeded' |
|
| 479 |
- ) |
|
| 480 |
- assert result.stderr_bytes, ( |
|
| 481 |
- 'program did not print any error message' |
|
| 482 |
- ) |
|
| 483 |
- assert b'cannot load config' in result.stderr_bytes, ( |
|
| 484 |
- 'program did not print the expected error message' |
|
| 597 |
+ with tests.isolated_config( |
|
| 598 |
+ monkeypatch=monkeypatch, runner=runner, config={}
|
|
| 599 |
+ ): |
|
| 600 |
+ result = runner.invoke( |
|
| 601 |
+ cli.derivepassphrase, |
|
| 602 |
+ ['--export', '-'], |
|
| 603 |
+ input=b'null', |
|
| 604 |
+ catch_exceptions=False, |
|
| 485 | 605 |
) |
| 606 |
+ assert result.exit_code > 0, 'program unexpectedly succeeded' |
|
| 607 |
+ assert ( |
|
| 608 |
+ result.stderr_bytes |
|
| 609 |
+ ), 'program did not print any error message' |
|
| 610 |
+ assert ( |
|
| 611 |
+ b'cannot load config' in result.stderr_bytes |
|
| 612 |
+ ), 'program did not print the expected error message' |
|
| 486 | 613 |
|
| 487 | 614 |
def test_214b_export_settings_not_a_file( |
| 488 |
- self, monkeypatch: Any, |
|
| 615 |
+ self, |
|
| 616 |
+ monkeypatch: Any, |
|
| 489 | 617 |
) -> None: |
| 490 | 618 |
runner = click.testing.CliRunner(mix_stderr=False) |
| 491 |
- with tests.isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
| 492 |
- config={'services': {}}):
|
|
| 619 |
+ with tests.isolated_config( |
|
| 620 |
+ monkeypatch=monkeypatch, runner=runner, config={'services': {}}
|
|
| 621 |
+ ): |
|
| 493 | 622 |
with contextlib.suppress(FileNotFoundError): |
| 494 | 623 |
os.remove(cli._config_filename()) |
| 495 | 624 |
os.makedirs(cli._config_filename()) |
| 496 |
- result = runner.invoke(cli.derivepassphrase, ['--export', '-'], |
|
| 497 |
- input=b'null', catch_exceptions=False) |
|
| 498 |
- assert result.exit_code > 0, ( |
|
| 499 |
- 'program unexpectedly succeeded' |
|
| 500 |
- ) |
|
| 501 |
- assert result.stderr_bytes, ( |
|
| 502 |
- 'program did not print any error message' |
|
| 503 |
- ) |
|
| 504 |
- assert b'cannot load config' in result.stderr_bytes, ( |
|
| 505 |
- 'program did not print the expected error message' |
|
| 625 |
+ result = runner.invoke( |
|
| 626 |
+ cli.derivepassphrase, |
|
| 627 |
+ ['--export', '-'], |
|
| 628 |
+ input=b'null', |
|
| 629 |
+ catch_exceptions=False, |
|
| 506 | 630 |
) |
| 631 |
+ assert result.exit_code > 0, 'program unexpectedly succeeded' |
|
| 632 |
+ assert ( |
|
| 633 |
+ result.stderr_bytes |
|
| 634 |
+ ), 'program did not print any error message' |
|
| 635 |
+ assert ( |
|
| 636 |
+ b'cannot load config' in result.stderr_bytes |
|
| 637 |
+ ), 'program did not print the expected error message' |
|
| 507 | 638 |
|
| 508 | 639 |
def test_214c_export_settings_target_not_a_file( |
| 509 |
- self, monkeypatch: Any, |
|
| 640 |
+ self, |
|
| 641 |
+ monkeypatch: Any, |
|
| 510 | 642 |
) -> None: |
| 511 | 643 |
runner = click.testing.CliRunner(mix_stderr=False) |
| 512 |
- with tests.isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
| 513 |
- config={'services': {}}):
|
|
| 644 |
+ with tests.isolated_config( |
|
| 645 |
+ monkeypatch=monkeypatch, runner=runner, config={'services': {}}
|
|
| 646 |
+ ): |
|
| 514 | 647 |
dname = os.path.dirname(cli._config_filename()) |
| 515 |
- result = runner.invoke(cli.derivepassphrase, |
|
| 648 |
+ result = runner.invoke( |
|
| 649 |
+ cli.derivepassphrase, |
|
| 516 | 650 |
['--export', os.fsdecode(dname)], |
| 517 |
- input=b'null', catch_exceptions=False) |
|
| 518 |
- assert result.exit_code > 0, ( |
|
| 519 |
- 'program unexpectedly succeeded' |
|
| 520 |
- ) |
|
| 521 |
- assert result.stderr_bytes, ( |
|
| 522 |
- 'program did not print any error message' |
|
| 523 |
- ) |
|
| 524 |
- assert b'cannot write config' in result.stderr_bytes, ( |
|
| 525 |
- 'program did not print the expected error message' |
|
| 651 |
+ input=b'null', |
|
| 652 |
+ catch_exceptions=False, |
|
| 526 | 653 |
) |
| 654 |
+ assert result.exit_code > 0, 'program unexpectedly succeeded' |
|
| 655 |
+ assert ( |
|
| 656 |
+ result.stderr_bytes |
|
| 657 |
+ ), 'program did not print any error message' |
|
| 658 |
+ assert ( |
|
| 659 |
+ b'cannot write config' in result.stderr_bytes |
|
| 660 |
+ ), 'program did not print the expected error message' |
|
| 527 | 661 |
|
| 528 | 662 |
def test_220_edit_notes_successfully(self, monkeypatch: Any) -> None: |
| 529 |
- edit_result = ''' |
|
| 663 |
+ edit_result = """ |
|
| 530 | 664 |
|
| 531 | 665 |
# - - - - - >8 - - - - - >8 - - - - - >8 - - - - - >8 - - - - - |
| 532 | 666 |
contents go here |
| 533 |
-''' |
|
| 667 |
+""" |
|
| 534 | 668 |
runner = click.testing.CliRunner(mix_stderr=False) |
| 535 |
- with tests.isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
| 536 |
- config={'global': {'phrase': 'abc'},
|
|
| 537 |
- 'services': {}}):
|
|
| 538 |
- monkeypatch.setattr(click, 'edit', |
|
| 539 |
- lambda *a, **kw: edit_result) # noqa: ARG005 |
|
| 540 |
- result = runner.invoke(cli.derivepassphrase, ['--notes', 'sv'], |
|
| 541 |
- catch_exceptions=False) |
|
| 542 |
- assert (result.exit_code, result.stderr_bytes) == (0, b''), ( |
|
| 543 |
- 'program exited with failure' |
|
| 669 |
+ with tests.isolated_config( |
|
| 670 |
+ monkeypatch=monkeypatch, |
|
| 671 |
+ runner=runner, |
|
| 672 |
+ config={'global': {'phrase': 'abc'}, 'services': {}},
|
|
| 673 |
+ ): |
|
| 674 |
+ monkeypatch.setattr(click, 'edit', lambda *a, **kw: edit_result) # noqa: ARG005 |
|
| 675 |
+ result = runner.invoke( |
|
| 676 |
+ cli.derivepassphrase, ['--notes', 'sv'], catch_exceptions=False |
|
| 544 | 677 |
) |
| 678 |
+ assert (result.exit_code, result.stderr_bytes) == ( |
|
| 679 |
+ 0, |
|
| 680 |
+ b'', |
|
| 681 |
+ ), 'program exited with failure' |
|
| 545 | 682 |
with open(cli._config_filename(), encoding='UTF-8') as infile: |
| 546 | 683 |
config = json.load(infile) |
| 547 |
- assert config == {'global': {'phrase': 'abc'},
|
|
| 548 |
- 'services': {'sv': {'notes':
|
|
| 549 |
- 'contents go here'}}} |
|
| 684 |
+ assert config == {
|
|
| 685 |
+ 'global': {'phrase': 'abc'},
|
|
| 686 |
+ 'services': {'sv': {'notes': 'contents go here'}},
|
|
| 687 |
+ } |
|
| 550 | 688 |
|
| 551 | 689 |
def test_221_edit_notes_noop(self, monkeypatch: Any) -> None: |
| 552 | 690 |
runner = click.testing.CliRunner(mix_stderr=False) |
| 553 |
- with tests.isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
| 554 |
- config={'global': {'phrase': 'abc'},
|
|
| 555 |
- 'services': {}}):
|
|
| 556 |
- monkeypatch.setattr(click, 'edit', |
|
| 557 |
- lambda *a, **kw: None) # noqa: ARG005 |
|
| 558 |
- result = runner.invoke(cli.derivepassphrase, ['--notes', 'sv'], |
|
| 559 |
- catch_exceptions=False) |
|
| 560 |
- assert (result.exit_code, result.stderr_bytes) == (0, b''), ( |
|
| 561 |
- 'program exited with failure' |
|
| 691 |
+ with tests.isolated_config( |
|
| 692 |
+ monkeypatch=monkeypatch, |
|
| 693 |
+ runner=runner, |
|
| 694 |
+ config={'global': {'phrase': 'abc'}, 'services': {}},
|
|
| 695 |
+ ): |
|
| 696 |
+ monkeypatch.setattr(click, 'edit', lambda *a, **kw: None) # noqa: ARG005 |
|
| 697 |
+ result = runner.invoke( |
|
| 698 |
+ cli.derivepassphrase, ['--notes', 'sv'], catch_exceptions=False |
|
| 562 | 699 |
) |
| 700 |
+ assert (result.exit_code, result.stderr_bytes) == ( |
|
| 701 |
+ 0, |
|
| 702 |
+ b'', |
|
| 703 |
+ ), 'program exited with failure' |
|
| 563 | 704 |
with open(cli._config_filename(), encoding='UTF-8') as infile: |
| 564 | 705 |
config = json.load(infile) |
| 565 | 706 |
assert config == {'global': {'phrase': 'abc'}, 'services': {}}
|
| 566 | 707 |
|
| 567 | 708 |
def test_222_edit_notes_marker_removed(self, monkeypatch: Any) -> None: |
| 568 | 709 |
runner = click.testing.CliRunner(mix_stderr=False) |
| 569 |
- with tests.isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
| 570 |
- config={'global': {'phrase': 'abc'},
|
|
| 571 |
- 'services': {}}):
|
|
| 572 |
- monkeypatch.setattr(click, 'edit', |
|
| 573 |
- lambda *a, **kw: 'long\ntext') # noqa: ARG005 |
|
| 574 |
- result = runner.invoke(cli.derivepassphrase, ['--notes', 'sv'], |
|
| 575 |
- catch_exceptions=False) |
|
| 576 |
- assert (result.exit_code, result.stderr_bytes) == (0, b''), ( |
|
| 577 |
- 'program exited with failure' |
|
| 710 |
+ with tests.isolated_config( |
|
| 711 |
+ monkeypatch=monkeypatch, |
|
| 712 |
+ runner=runner, |
|
| 713 |
+ config={'global': {'phrase': 'abc'}, 'services': {}},
|
|
| 714 |
+ ): |
|
| 715 |
+ monkeypatch.setattr(click, 'edit', lambda *a, **kw: 'long\ntext') # noqa: ARG005 |
|
| 716 |
+ result = runner.invoke( |
|
| 717 |
+ cli.derivepassphrase, ['--notes', 'sv'], catch_exceptions=False |
|
| 578 | 718 |
) |
| 719 |
+ assert (result.exit_code, result.stderr_bytes) == ( |
|
| 720 |
+ 0, |
|
| 721 |
+ b'', |
|
| 722 |
+ ), 'program exited with failure' |
|
| 579 | 723 |
with open(cli._config_filename(), encoding='UTF-8') as infile: |
| 580 | 724 |
config = json.load(infile) |
| 581 |
- assert config == {'global': {'phrase': 'abc'},
|
|
| 582 |
- 'services': {'sv': {'notes': 'long\ntext'}}}
|
|
| 725 |
+ assert config == {
|
|
| 726 |
+ 'global': {'phrase': 'abc'},
|
|
| 727 |
+ 'services': {'sv': {'notes': 'long\ntext'}},
|
|
| 728 |
+ } |
|
| 583 | 729 |
|
| 584 | 730 |
def test_223_edit_notes_abort(self, monkeypatch: Any) -> None: |
| 585 | 731 |
runner = click.testing.CliRunner(mix_stderr=False) |
| 586 |
- with tests.isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
| 587 |
- config={'global': {'phrase': 'abc'},
|
|
| 588 |
- 'services': {}}):
|
|
| 589 |
- monkeypatch.setattr(click, 'edit', |
|
| 590 |
- lambda *a, **kw: '\n\n') # noqa: ARG005 |
|
| 591 |
- result = runner.invoke(cli.derivepassphrase, ['--notes', 'sv'], |
|
| 592 |
- catch_exceptions=False) |
|
| 732 |
+ with tests.isolated_config( |
|
| 733 |
+ monkeypatch=monkeypatch, |
|
| 734 |
+ runner=runner, |
|
| 735 |
+ config={'global': {'phrase': 'abc'}, 'services': {}},
|
|
| 736 |
+ ): |
|
| 737 |
+ monkeypatch.setattr(click, 'edit', lambda *a, **kw: '\n\n') # noqa: ARG005 |
|
| 738 |
+ result = runner.invoke( |
|
| 739 |
+ cli.derivepassphrase, ['--notes', 'sv'], catch_exceptions=False |
|
| 740 |
+ ) |
|
| 593 | 741 |
assert result.exit_code != 0, 'program unexpectedly succeeded' |
| 594 | 742 |
assert result.stderr_bytes is not None |
| 595 |
- assert b'user aborted request' in result.stderr_bytes, ( |
|
| 596 |
- 'expected error message missing' |
|
| 597 |
- ) |
|
| 743 |
+ assert ( |
|
| 744 |
+ b'user aborted request' in result.stderr_bytes |
|
| 745 |
+ ), 'expected error message missing' |
|
| 598 | 746 |
with open(cli._config_filename(), encoding='UTF-8') as infile: |
| 599 | 747 |
config = json.load(infile) |
| 600 | 748 |
assert config == {'global': {'phrase': 'abc'}, 'services': {}}
|
| 601 | 749 |
|
| 602 |
- @pytest.mark.parametrize(['command_line', 'input', 'result_config'], [ |
|
| 750 |
+ @pytest.mark.parametrize( |
|
| 751 |
+ ['command_line', 'input', 'result_config'], |
|
| 752 |
+ [ |
|
| 603 | 753 |
( |
| 604 | 754 |
['--phrase'], |
| 605 | 755 |
b'my passphrase\n', |
| ... | ... |
@@ -613,44 +763,66 @@ contents go here |
| 613 | 763 |
( |
| 614 | 764 |
['--phrase', 'sv'], |
| 615 | 765 |
b'my passphrase\n', |
| 616 |
- {'global': {'phrase': 'abc'},
|
|
| 617 |
- 'services': {'sv': {'phrase': 'my passphrase'}}},
|
|
| 766 |
+ {
|
|
| 767 |
+ 'global': {'phrase': 'abc'},
|
|
| 768 |
+ 'services': {'sv': {'phrase': 'my passphrase'}},
|
|
| 769 |
+ }, |
|
| 618 | 770 |
), |
| 619 | 771 |
( |
| 620 | 772 |
['--key', 'sv'], |
| 621 | 773 |
b'1\n', |
| 622 |
- {'global': {'phrase': 'abc'},
|
|
| 623 |
- 'services': {'sv': {'key': DUMMY_KEY1_B64}}},
|
|
| 774 |
+ {
|
|
| 775 |
+ 'global': {'phrase': 'abc'},
|
|
| 776 |
+ 'services': {'sv': {'key': DUMMY_KEY1_B64}},
|
|
| 777 |
+ }, |
|
| 624 | 778 |
), |
| 625 | 779 |
( |
| 626 | 780 |
['--key', '--length', '15', 'sv'], |
| 627 | 781 |
b'1\n', |
| 628 |
- {'global': {'phrase': 'abc'},
|
|
| 629 |
- 'services': {'sv': {'key': DUMMY_KEY1_B64, 'length': 15}}},
|
|
| 782 |
+ {
|
|
| 783 |
+ 'global': {'phrase': 'abc'},
|
|
| 784 |
+ 'services': {'sv': {'key': DUMMY_KEY1_B64, 'length': 15}},
|
|
| 785 |
+ }, |
|
| 630 | 786 |
), |
| 631 |
- ]) |
|
| 787 |
+ ], |
|
| 788 |
+ ) |
|
| 632 | 789 |
def test_224_store_config_good( |
| 633 |
- self, monkeypatch: Any, command_line: list[str], input: bytes, |
|
| 790 |
+ self, |
|
| 791 |
+ monkeypatch: Any, |
|
| 792 |
+ command_line: list[str], |
|
| 793 |
+ input: bytes, |
|
| 634 | 794 |
result_config: Any, |
| 635 | 795 |
) -> None: |
| 636 | 796 |
runner = click.testing.CliRunner(mix_stderr=False) |
| 637 |
- with tests.isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
| 638 |
- config={'global': {'phrase': 'abc'},
|
|
| 639 |
- 'services': {}}):
|
|
| 640 |
- monkeypatch.setattr(cli, '_get_suitable_ssh_keys', |
|
| 641 |
- tests.suitable_ssh_keys) |
|
| 642 |
- result = runner.invoke(cli.derivepassphrase, |
|
| 797 |
+ with tests.isolated_config( |
|
| 798 |
+ monkeypatch=monkeypatch, |
|
| 799 |
+ runner=runner, |
|
| 800 |
+ config={'global': {'phrase': 'abc'}, 'services': {}},
|
|
| 801 |
+ ): |
|
| 802 |
+ monkeypatch.setattr( |
|
| 803 |
+ cli, '_get_suitable_ssh_keys', tests.suitable_ssh_keys |
|
| 804 |
+ ) |
|
| 805 |
+ result = runner.invoke( |
|
| 806 |
+ cli.derivepassphrase, |
|
| 643 | 807 |
['--config', *command_line], |
| 644 |
- catch_exceptions=False, input=input) |
|
| 808 |
+ catch_exceptions=False, |
|
| 809 |
+ input=input, |
|
| 810 |
+ ) |
|
| 645 | 811 |
assert result.exit_code == 0, 'program exited with failure' |
| 646 | 812 |
with open(cli._config_filename(), encoding='UTF-8') as infile: |
| 647 | 813 |
config = json.load(infile) |
| 648 |
- assert config == result_config, ( |
|
| 649 |
- 'stored config does not match expectation' |
|
| 650 |
- ) |
|
| 814 |
+ assert ( |
|
| 815 |
+ config == result_config |
|
| 816 |
+ ), 'stored config does not match expectation' |
|
| 651 | 817 |
|
| 652 |
- @pytest.mark.parametrize(['command_line', 'input', 'err_text'], [ |
|
| 653 |
- ([], b'', b'cannot update global settings without actual settings'), |
|
| 818 |
+ @pytest.mark.parametrize( |
|
| 819 |
+ ['command_line', 'input', 'err_text'], |
|
| 820 |
+ [ |
|
| 821 |
+ ( |
|
| 822 |
+ [], |
|
| 823 |
+ b'', |
|
| 824 |
+ b'cannot update global settings without actual settings', |
|
| 825 |
+ ), |
|
| 654 | 826 |
( |
| 655 | 827 |
['sv'], |
| 656 | 828 |
b'', |
| ... | ... |
@@ -658,65 +830,84 @@ contents go here |
| 658 | 830 |
), |
| 659 | 831 |
(['--phrase', 'sv'], b'', b'no passphrase given'), |
| 660 | 832 |
(['--key'], b'', b'no valid SSH key selected'), |
| 661 |
- ]) |
|
| 833 |
+ ], |
|
| 834 |
+ ) |
|
| 662 | 835 |
def test_225_store_config_fail( |
| 663 |
- self, monkeypatch: Any, command_line: list[str], |
|
| 664 |
- input: bytes, err_text: bytes, |
|
| 836 |
+ self, |
|
| 837 |
+ monkeypatch: Any, |
|
| 838 |
+ command_line: list[str], |
|
| 839 |
+ input: bytes, |
|
| 840 |
+ err_text: bytes, |
|
| 665 | 841 |
) -> None: |
| 666 | 842 |
runner = click.testing.CliRunner(mix_stderr=False) |
| 667 |
- with tests.isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
| 668 |
- config={'global': {'phrase': 'abc'},
|
|
| 669 |
- 'services': {}}):
|
|
| 670 |
- monkeypatch.setattr(cli, '_get_suitable_ssh_keys', |
|
| 671 |
- tests.suitable_ssh_keys) |
|
| 672 |
- result = runner.invoke(cli.derivepassphrase, |
|
| 843 |
+ with tests.isolated_config( |
|
| 844 |
+ monkeypatch=monkeypatch, |
|
| 845 |
+ runner=runner, |
|
| 846 |
+ config={'global': {'phrase': 'abc'}, 'services': {}},
|
|
| 847 |
+ ): |
|
| 848 |
+ monkeypatch.setattr( |
|
| 849 |
+ cli, '_get_suitable_ssh_keys', tests.suitable_ssh_keys |
|
| 850 |
+ ) |
|
| 851 |
+ result = runner.invoke( |
|
| 852 |
+ cli.derivepassphrase, |
|
| 673 | 853 |
['--config', *command_line], |
| 674 |
- catch_exceptions=False, input=input) |
|
| 854 |
+ catch_exceptions=False, |
|
| 855 |
+ input=input, |
|
| 856 |
+ ) |
|
| 675 | 857 |
assert result.exit_code != 0, 'program unexpectedly succeeded?!' |
| 676 | 858 |
assert result.stderr_bytes is not None |
| 677 |
- assert err_text in result.stderr_bytes, ( |
|
| 678 |
- 'expected error message missing' |
|
| 679 |
- ) |
|
| 859 |
+ assert ( |
|
| 860 |
+ err_text in result.stderr_bytes |
|
| 861 |
+ ), 'expected error message missing' |
|
| 680 | 862 |
|
| 681 | 863 |
def test_225a_store_config_fail_manual_no_ssh_key_selection( |
| 682 |
- self, monkeypatch: Any, |
|
| 864 |
+ self, |
|
| 865 |
+ monkeypatch: Any, |
|
| 683 | 866 |
) -> None: |
| 684 | 867 |
runner = click.testing.CliRunner(mix_stderr=False) |
| 685 |
- with tests.isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
| 686 |
- config={'global': {'phrase': 'abc'},
|
|
| 687 |
- 'services': {}}):
|
|
| 868 |
+ with tests.isolated_config( |
|
| 869 |
+ monkeypatch=monkeypatch, |
|
| 870 |
+ runner=runner, |
|
| 871 |
+ config={'global': {'phrase': 'abc'}, 'services': {}},
|
|
| 872 |
+ ): |
|
| 688 | 873 |
custom_error = 'custom error message' |
| 874 |
+ |
|
| 689 | 875 |
def raiser(): |
| 690 | 876 |
raise RuntimeError(custom_error) |
| 877 |
+ |
|
| 691 | 878 |
monkeypatch.setattr(cli, '_select_ssh_key', raiser) |
| 692 |
- result = runner.invoke(cli.derivepassphrase, |
|
| 879 |
+ result = runner.invoke( |
|
| 880 |
+ cli.derivepassphrase, |
|
| 693 | 881 |
['--key', '--config'], |
| 694 |
- catch_exceptions=False) |
|
| 882 |
+ catch_exceptions=False, |
|
| 883 |
+ ) |
|
| 695 | 884 |
assert result.exit_code != 0, 'program unexpectedly succeeded' |
| 696 | 885 |
assert result.stderr_bytes is not None |
| 697 |
- assert custom_error.encode() in result.stderr_bytes, ( |
|
| 698 |
- 'expected error message missing' |
|
| 699 |
- ) |
|
| 886 |
+ assert ( |
|
| 887 |
+ custom_error.encode() in result.stderr_bytes |
|
| 888 |
+ ), 'expected error message missing' |
|
| 700 | 889 |
|
| 701 | 890 |
def test_226_no_arguments(self) -> None: |
| 702 | 891 |
runner = click.testing.CliRunner(mix_stderr=False) |
| 703 |
- result = runner.invoke(cli.derivepassphrase, [], |
|
| 704 |
- catch_exceptions=False) |
|
| 892 |
+ result = runner.invoke( |
|
| 893 |
+ cli.derivepassphrase, [], catch_exceptions=False |
|
| 894 |
+ ) |
|
| 705 | 895 |
assert result.exit_code != 0, 'program unexpectedly succeeded' |
| 706 | 896 |
assert result.stderr_bytes is not None |
| 707 |
- assert b'SERVICE is required' in result.stderr_bytes, ( |
|
| 708 |
- 'expected error message missing' |
|
| 709 |
- ) |
|
| 897 |
+ assert ( |
|
| 898 |
+ b'SERVICE is required' in result.stderr_bytes |
|
| 899 |
+ ), 'expected error message missing' |
|
| 710 | 900 |
|
| 711 | 901 |
def test_226a_no_passphrase_or_key(self) -> None: |
| 712 | 902 |
runner = click.testing.CliRunner(mix_stderr=False) |
| 713 |
- result = runner.invoke(cli.derivepassphrase, [DUMMY_SERVICE], |
|
| 714 |
- catch_exceptions=False) |
|
| 903 |
+ result = runner.invoke( |
|
| 904 |
+ cli.derivepassphrase, [DUMMY_SERVICE], catch_exceptions=False |
|
| 905 |
+ ) |
|
| 715 | 906 |
assert result.exit_code != 0, 'program unexpectedly succeeded' |
| 716 | 907 |
assert result.stderr_bytes is not None |
| 717 |
- assert b'no passphrase or key given' in result.stderr_bytes, ( |
|
| 718 |
- 'expected error message missing' |
|
| 719 |
- ) |
|
| 908 |
+ assert ( |
|
| 909 |
+ b'no passphrase or key given' in result.stderr_bytes |
|
| 910 |
+ ), 'expected error message missing' |
|
| 720 | 911 |
|
| 721 | 912 |
|
| 722 | 913 |
class TestCLIUtils: |
| ... | ... |
@@ -724,9 +914,10 @@ class TestCLIUtils: |
| 724 | 914 |
def test_100_save_bad_config(self, monkeypatch: Any) -> None: |
| 725 | 915 |
runner = click.testing.CliRunner() |
| 726 | 916 |
with ( |
| 727 |
- tests.isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
| 728 |
- config={}),
|
|
| 729 |
- pytest.raises(ValueError, match='Invalid vault config') |
|
| 917 |
+ tests.isolated_config( |
|
| 918 |
+ monkeypatch=monkeypatch, runner=runner, config={}
|
|
| 919 |
+ ), |
|
| 920 |
+ pytest.raises(ValueError, match='Invalid vault config'), |
|
| 730 | 921 |
): |
| 731 | 922 |
cli._save_config(None) # type: ignore |
| 732 | 923 |
|
| ... | ... |
@@ -746,11 +936,15 @@ class TestCLIUtils: |
| 746 | 936 |
'Spam, bacon, sausage and spam', |
| 747 | 937 |
'Spam, egg, spam, spam, bacon and spam', |
| 748 | 938 |
'Spam, spam, spam, egg and spam', |
| 749 |
- ('Spam, spam, spam, spam, spam, spam, baked beans, '
|
|
| 750 |
- 'spam, spam, spam and spam'), |
|
| 751 |
- ('Lobster thermidor aux crevettes with a mornay sauce '
|
|
| 939 |
+ ( |
|
| 940 |
+ 'Spam, spam, spam, spam, spam, spam, baked beans, ' |
|
| 941 |
+ 'spam, spam, spam and spam' |
|
| 942 |
+ ), |
|
| 943 |
+ ( |
|
| 944 |
+ 'Lobster thermidor aux crevettes with a mornay sauce ' |
|
| 752 | 945 |
'garnished with truffle paté, brandy ' |
| 753 |
- 'and a fried egg on top and spam'), |
|
| 946 |
+ 'and a fried egg on top and spam' |
|
| 947 |
+ ), |
|
| 754 | 948 |
] |
| 755 | 949 |
index = cli._prompt_for_selection(items, heading=heading) |
| 756 | 950 |
click.echo('A fine choice: ', nl=False)
|
| ... | ... |
@@ -759,7 +954,9 @@ class TestCLIUtils: |
| 759 | 954 |
runner = click.testing.CliRunner(mix_stderr=True) |
| 760 | 955 |
result = runner.invoke(driver, [], input='9') |
| 761 | 956 |
assert result.exit_code == 0, 'driver program failed' |
| 762 |
- assert result.stdout == '''\ |
|
| 957 |
+ assert ( |
|
| 958 |
+ result.stdout |
|
| 959 |
+ == """\ |
|
| 763 | 960 |
Our menu: |
| 764 | 961 |
[1] Egg and bacon |
| 765 | 962 |
[2] Egg, sausage and bacon |
| ... | ... |
@@ -774,11 +971,15 @@ Our menu: |
| 774 | 971 |
Your selection? (1-10, leave empty to abort): 9 |
| 775 | 972 |
A fine choice: Spam, spam, spam, spam, spam, spam, baked beans, spam, spam, spam and spam |
| 776 | 973 |
(Note: Vikings strictly optional.) |
| 777 |
-''', 'driver program produced unexpected output' # noqa: E501 |
|
| 778 |
- result = runner.invoke(driver, ['--heading='], input='', |
|
| 779 |
- catch_exceptions=True) |
|
| 974 |
+""" # noqa: E501 |
|
| 975 |
+ ), 'driver program produced unexpected output' |
|
| 976 |
+ result = runner.invoke( |
|
| 977 |
+ driver, ['--heading='], input='', catch_exceptions=True |
|
| 978 |
+ ) |
|
| 780 | 979 |
assert result.exit_code > 0, 'driver program succeeded?!' |
| 781 |
- assert result.stdout == '''\ |
|
| 980 |
+ assert ( |
|
| 981 |
+ result.stdout |
|
| 982 |
+ == """\ |
|
| 782 | 983 |
[1] Egg and bacon |
| 783 | 984 |
[2] Egg, sausage and bacon |
| 784 | 985 |
[3] Egg and spam |
| ... | ... |
@@ -789,13 +990,12 @@ A fine choice: Spam, spam, spam, spam, spam, spam, baked beans, spam, spam, spam |
| 789 | 990 |
[8] Spam, spam, spam, egg and spam |
| 790 | 991 |
[9] Spam, spam, spam, spam, spam, spam, baked beans, spam, spam, spam and spam |
| 791 | 992 |
[10] Lobster thermidor aux crevettes with a mornay sauce garnished with truffle paté, brandy and a fried egg on top and spam |
| 792 |
-Your selection? (1-10, leave empty to abort): \n''', ( # noqa: E501 |
|
| 793 |
- 'driver program produced unexpected output' |
|
| 794 |
- ) |
|
| 795 |
- assert isinstance(result.exception, IndexError), ( |
|
| 796 |
- 'driver program did not raise IndexError?!' |
|
| 797 |
- ) |
|
| 798 |
- |
|
| 993 |
+Your selection? (1-10, leave empty to abort):\x20 |
|
| 994 |
+""" # noqa: E501 |
|
| 995 |
+ ), 'driver program produced unexpected output' |
|
| 996 |
+ assert isinstance( |
|
| 997 |
+ result.exception, IndexError |
|
| 998 |
+ ), 'driver program did not raise IndexError?!' |
|
| 799 | 999 |
|
| 800 | 1000 |
def test_102_prompt_for_selection_single(self) -> None: |
| 801 | 1001 |
@click.command() |
| ... | ... |
@@ -803,42 +1003,52 @@ Your selection? (1-10, leave empty to abort): \n''', ( # noqa: E501 |
| 803 | 1003 |
@click.argument('prompt')
|
| 804 | 1004 |
def driver(item, prompt): |
| 805 | 1005 |
try: |
| 806 |
- cli._prompt_for_selection([item], heading='', |
|
| 807 |
- single_choice_prompt=prompt) |
|
| 1006 |
+ cli._prompt_for_selection( |
|
| 1007 |
+ [item], heading='', single_choice_prompt=prompt |
|
| 1008 |
+ ) |
|
| 808 | 1009 |
except IndexError: |
| 809 | 1010 |
click.echo('Boo.')
|
| 810 | 1011 |
raise |
| 811 | 1012 |
else: |
| 812 | 1013 |
click.echo('Great!')
|
| 1014 |
+ |
|
| 813 | 1015 |
runner = click.testing.CliRunner(mix_stderr=True) |
| 814 |
- result = runner.invoke(driver, |
|
| 815 |
- ['Will replace with spam. Confirm, y/n?'], |
|
| 816 |
- input='y') |
|
| 1016 |
+ result = runner.invoke( |
|
| 1017 |
+ driver, ['Will replace with spam. Confirm, y/n?'], input='y' |
|
| 1018 |
+ ) |
|
| 817 | 1019 |
assert result.exit_code == 0, 'driver program failed' |
| 818 |
- assert result.stdout == '''\ |
|
| 1020 |
+ assert ( |
|
| 1021 |
+ result.stdout |
|
| 1022 |
+ == """\ |
|
| 819 | 1023 |
[1] baked beans |
| 820 | 1024 |
Will replace with spam. Confirm, y/n? y |
| 821 | 1025 |
Great! |
| 822 |
-''', 'driver program produced unexpected output' |
|
| 823 |
- result = runner.invoke(driver, |
|
| 824 |
- ['Will replace with spam, okay? ' |
|
| 825 |
- '(Please say "y" or "n".)'], |
|
| 826 |
- input='') |
|
| 1026 |
+""" |
|
| 1027 |
+ ), 'driver program produced unexpected output' |
|
| 1028 |
+ result = runner.invoke( |
|
| 1029 |
+ driver, |
|
| 1030 |
+ ['Will replace with spam, okay? ' '(Please say "y" or "n".)'], |
|
| 1031 |
+ input='', |
|
| 1032 |
+ ) |
|
| 827 | 1033 |
assert result.exit_code > 0, 'driver program succeeded?!' |
| 828 |
- assert result.stdout == '''\ |
|
| 1034 |
+ assert ( |
|
| 1035 |
+ result.stdout |
|
| 1036 |
+ == """\ |
|
| 829 | 1037 |
[1] baked beans |
| 830 | 1038 |
Will replace with spam, okay? (Please say "y" or "n".):\x20 |
| 831 | 1039 |
Boo. |
| 832 |
-''', 'driver program produced unexpected output' |
|
| 833 |
- assert isinstance(result.exception, IndexError), ( |
|
| 834 |
- 'driver program did not raise IndexError?!' |
|
| 835 |
- ) |
|
| 836 |
- |
|
| 1040 |
+""" |
|
| 1041 |
+ ), 'driver program produced unexpected output' |
|
| 1042 |
+ assert isinstance( |
|
| 1043 |
+ result.exception, IndexError |
|
| 1044 |
+ ), 'driver program did not raise IndexError?!' |
|
| 837 | 1045 |
|
| 838 | 1046 |
def test_103_prompt_for_passphrase(self, monkeypatch: Any) -> None: |
| 839 |
- monkeypatch.setattr(click, 'prompt', |
|
| 840 |
- lambda *a, **kw: |
|
| 841 |
- json.dumps({'args': a, 'kwargs': kw}))
|
|
| 1047 |
+ monkeypatch.setattr( |
|
| 1048 |
+ click, |
|
| 1049 |
+ 'prompt', |
|
| 1050 |
+ lambda *a, **kw: json.dumps({'args': a, 'kwargs': kw}),
|
|
| 1051 |
+ ) |
|
| 842 | 1052 |
res = json.loads(cli._prompt_for_passphrase()) |
| 843 | 1053 |
err_msg = 'missing arguments to passphrase prompt' |
| 844 | 1054 |
assert 'args' in res, err_msg |
| ... | ... |
@@ -849,48 +1059,70 @@ Boo. |
| 849 | 1059 |
assert res['kwargs'].get('err'), err_msg
|
| 850 | 1060 |
assert res['kwargs'].get('hide_input'), err_msg
|
| 851 | 1061 |
|
| 852 |
- |
|
| 853 |
- @pytest.mark.parametrize(['command_line', 'config', 'result_config'], [ |
|
| 854 |
- (['--delete-globals'], |
|
| 855 |
- {'global': {'phrase': 'abc'}, 'services': {}}, {'services': {}}),
|
|
| 856 |
- (['--delete', DUMMY_SERVICE], |
|
| 857 |
- {'global': {'phrase': 'abc'},
|
|
| 858 |
- 'services': {DUMMY_SERVICE: {'notes': '...'}}},
|
|
| 859 |
- {'global': {'phrase': 'abc'}, 'services': {}}),
|
|
| 860 |
- (['--clear'], |
|
| 861 |
- {'global': {'phrase': 'abc'},
|
|
| 862 |
- 'services': {DUMMY_SERVICE: {'notes': '...'}}},
|
|
| 863 |
- {'services': {}}),
|
|
| 864 |
- ]) |
|
| 1062 |
+ @pytest.mark.parametrize( |
|
| 1063 |
+ ['command_line', 'config', 'result_config'], |
|
| 1064 |
+ [ |
|
| 1065 |
+ ( |
|
| 1066 |
+ ['--delete-globals'], |
|
| 1067 |
+ {'global': {'phrase': 'abc'}, 'services': {}},
|
|
| 1068 |
+ {'services': {}},
|
|
| 1069 |
+ ), |
|
| 1070 |
+ ( |
|
| 1071 |
+ ['--delete', DUMMY_SERVICE], |
|
| 1072 |
+ {
|
|
| 1073 |
+ 'global': {'phrase': 'abc'},
|
|
| 1074 |
+ 'services': {DUMMY_SERVICE: {'notes': '...'}},
|
|
| 1075 |
+ }, |
|
| 1076 |
+ {'global': {'phrase': 'abc'}, 'services': {}},
|
|
| 1077 |
+ ), |
|
| 1078 |
+ ( |
|
| 1079 |
+ ['--clear'], |
|
| 1080 |
+ {
|
|
| 1081 |
+ 'global': {'phrase': 'abc'},
|
|
| 1082 |
+ 'services': {DUMMY_SERVICE: {'notes': '...'}},
|
|
| 1083 |
+ }, |
|
| 1084 |
+ {'services': {}},
|
|
| 1085 |
+ ), |
|
| 1086 |
+ ], |
|
| 1087 |
+ ) |
|
| 865 | 1088 |
def test_203_repeated_config_deletion( |
| 866 |
- self, monkeypatch: Any, command_line: list[str], |
|
| 867 |
- config: dpp.types.VaultConfig, result_config: dpp.types.VaultConfig, |
|
| 1089 |
+ self, |
|
| 1090 |
+ monkeypatch: Any, |
|
| 1091 |
+ command_line: list[str], |
|
| 1092 |
+ config: dpp.types.VaultConfig, |
|
| 1093 |
+ result_config: dpp.types.VaultConfig, |
|
| 868 | 1094 |
) -> None: |
| 869 | 1095 |
runner = click.testing.CliRunner(mix_stderr=False) |
| 870 | 1096 |
for start_config in [config, result_config]: |
| 871 |
- with tests.isolated_config(monkeypatch=monkeypatch, |
|
| 872 |
- runner=runner, config=start_config): |
|
| 873 |
- result = runner.invoke(cli.derivepassphrase, command_line, |
|
| 874 |
- catch_exceptions=False) |
|
| 875 |
- assert (result.exit_code, result.stderr_bytes) == (0, b''), ( |
|
| 876 |
- 'program exited with failure' |
|
| 1097 |
+ with tests.isolated_config( |
|
| 1098 |
+ monkeypatch=monkeypatch, runner=runner, config=start_config |
|
| 1099 |
+ ): |
|
| 1100 |
+ result = runner.invoke( |
|
| 1101 |
+ cli.derivepassphrase, command_line, catch_exceptions=False |
|
| 877 | 1102 |
) |
| 1103 |
+ assert (result.exit_code, result.stderr_bytes) == ( |
|
| 1104 |
+ 0, |
|
| 1105 |
+ b'', |
|
| 1106 |
+ ), 'program exited with failure' |
|
| 878 | 1107 |
with open(cli._config_filename(), encoding='UTF-8') as infile: |
| 879 | 1108 |
config_readback = json.load(infile) |
| 880 | 1109 |
assert config_readback == result_config |
| 881 | 1110 |
|
| 882 |
- |
|
| 883 | 1111 |
def test_204_phrase_from_key_manually(self) -> None: |
| 884 | 1112 |
assert ( |
| 885 |
- dpp.Vault(phrase=DUMMY_PHRASE_FROM_KEY1, **DUMMY_CONFIG_SETTINGS) |
|
| 886 |
- .generate(DUMMY_SERVICE) == DUMMY_RESULT_KEY1 |
|
| 1113 |
+ dpp.Vault( |
|
| 1114 |
+ phrase=DUMMY_PHRASE_FROM_KEY1, **DUMMY_CONFIG_SETTINGS |
|
| 1115 |
+ ).generate(DUMMY_SERVICE) |
|
| 1116 |
+ == DUMMY_RESULT_KEY1 |
|
| 887 | 1117 |
) |
| 888 | 1118 |
|
| 889 |
- |
|
| 890 |
- @pytest.mark.parametrize(['vfunc', 'input'], [ |
|
| 1119 |
+ @pytest.mark.parametrize( |
|
| 1120 |
+ ['vfunc', 'input'], |
|
| 1121 |
+ [ |
|
| 891 | 1122 |
(cli._validate_occurrence_constraint, 20), |
| 892 | 1123 |
(cli._validate_length, 20), |
| 893 |
- ]) |
|
| 1124 |
+ ], |
|
| 1125 |
+ ) |
|
| 894 | 1126 |
def test_210a_validate_constraints_manually( |
| 895 | 1127 |
self, |
| 896 | 1128 |
vfunc: Callable[[click.Context, click.Parameter, Any], int | None], |
| ... | ... |
@@ -904,10 +1135,13 @@ Boo. |
| 904 | 1135 |
@tests.skip_if_no_agent |
| 905 | 1136 |
@pytest.mark.parametrize('conn_hint', ['none', 'socket', 'client'])
|
| 906 | 1137 |
def test_227_get_suitable_ssh_keys( |
| 907 |
- self, monkeypatch: Any, conn_hint: str, |
|
| 1138 |
+ self, |
|
| 1139 |
+ monkeypatch: Any, |
|
| 1140 |
+ conn_hint: str, |
|
| 908 | 1141 |
) -> None: |
| 909 |
- monkeypatch.setattr(ssh_agent_client.SSHAgentClient, |
|
| 910 |
- 'list_keys', tests.list_keys) |
|
| 1142 |
+ monkeypatch.setattr( |
|
| 1143 |
+ ssh_agent_client.SSHAgentClient, 'list_keys', tests.list_keys |
|
| 1144 |
+ ) |
|
| 911 | 1145 |
hint: ssh_agent_client.SSHAgentClient | socket.socket | None |
| 912 | 1146 |
match conn_hint: |
| 913 | 1147 |
case 'client': |
| ... | ... |
@@ -10,47 +10,88 @@ from typing_extensions import Any |
| 10 | 10 |
import derivepassphrase.types |
| 11 | 11 |
|
| 12 | 12 |
|
| 13 |
-@pytest.mark.parametrize(['obj', 'comment'], [ |
|
| 13 |
+@pytest.mark.parametrize( |
|
| 14 |
+ ['obj', 'comment'], |
|
| 15 |
+ [ |
|
| 14 | 16 |
(None, 'not a dict'), |
| 15 | 17 |
({}, 'missing required keys'),
|
| 16 | 18 |
({'global': None, 'services': {}}, 'bad config value: global'),
|
| 17 |
- ({'global': {'key': 123}, 'services': {}},
|
|
| 18 |
- 'bad config value: global.key'), |
|
| 19 |
- ({'global': {'phrase': 'abc', 'key': '...'}, 'services': {}},
|
|
| 20 |
- 'incompatible config values: global.key and global.phrase'), |
|
| 19 |
+ ( |
|
| 20 |
+ {'global': {'key': 123}, 'services': {}},
|
|
| 21 |
+ 'bad config value: global.key', |
|
| 22 |
+ ), |
|
| 23 |
+ ( |
|
| 24 |
+ {'global': {'phrase': 'abc', 'key': '...'}, 'services': {}},
|
|
| 25 |
+ 'incompatible config values: global.key and global.phrase', |
|
| 26 |
+ ), |
|
| 21 | 27 |
({'services': None}, 'bad config value: services'),
|
| 22 | 28 |
({'services': {2: {}}}, 'bad config value: services."2"'),
|
| 23 | 29 |
({'services': {'2': 2}}, 'bad config value: services."2"'),
|
| 24 |
- ({'services': {'sv': {'notes': False}}},
|
|
| 25 |
- 'bad config value: services.sv.notes'), |
|
| 30 |
+ ( |
|
| 31 |
+ {'services': {'sv': {'notes': False}}},
|
|
| 32 |
+ 'bad config value: services.sv.notes', |
|
| 33 |
+ ), |
|
| 26 | 34 |
({'services': {'sv': {'notes': 'blah blah blah'}}}, ''),
|
| 27 |
- ({'services': {'sv': {'length': '200'}}},
|
|
| 28 |
- 'bad config value: services.sv.length'), |
|
| 29 |
- ({'services': {'sv': {'length': 0.5}}},
|
|
| 30 |
- 'bad config value: services.sv.length'), |
|
| 31 |
- ({'services': {'sv': {'length': -10}}},
|
|
| 32 |
- 'bad config value: services.sv.length'), |
|
| 33 |
- ({'services': {'sv': {'upper': -10}}},
|
|
| 34 |
- 'bad config value: services.sv.upper'), |
|
| 35 |
- ({'global': {'phrase': 'my secret phrase'},
|
|
| 36 |
- 'services': {'sv': {'length': 10}}},
|
|
| 37 |
- ''), |
|
| 35 |
+ ( |
|
| 36 |
+ {'services': {'sv': {'length': '200'}}},
|
|
| 37 |
+ 'bad config value: services.sv.length', |
|
| 38 |
+ ), |
|
| 39 |
+ ( |
|
| 40 |
+ {'services': {'sv': {'length': 0.5}}},
|
|
| 41 |
+ 'bad config value: services.sv.length', |
|
| 42 |
+ ), |
|
| 43 |
+ ( |
|
| 44 |
+ {'services': {'sv': {'length': -10}}},
|
|
| 45 |
+ 'bad config value: services.sv.length', |
|
| 46 |
+ ), |
|
| 47 |
+ ( |
|
| 48 |
+ {'services': {'sv': {'upper': -10}}},
|
|
| 49 |
+ 'bad config value: services.sv.upper', |
|
| 50 |
+ ), |
|
| 51 |
+ ( |
|
| 52 |
+ {
|
|
| 53 |
+ 'global': {'phrase': 'my secret phrase'},
|
|
| 54 |
+ 'services': {'sv': {'length': 10}},
|
|
| 55 |
+ }, |
|
| 56 |
+ '', |
|
| 57 |
+ ), |
|
| 38 | 58 |
({'services': {'sv': {'length': 10, 'phrase': '...'}}}, ''),
|
| 39 | 59 |
({'services': {'sv': {'length': 10, 'key': '...'}}}, ''),
|
| 40 | 60 |
({'services': {'sv': {'upper': 10, 'key': '...'}}}, ''),
|
| 41 |
- ({'services': {'sv': {'phrase': 'abc', 'key': '...'}}},
|
|
| 42 |
- 'incompatible config values: services.sv.key and services.sv.phrase'), |
|
| 43 |
- ({'global': {'phrase': 'abc'},
|
|
| 44 |
- 'services': {'sv': {'phrase': 'abc', 'length': 10}}}, ''),
|
|
| 45 |
- ({'global': {'key': '...'},
|
|
| 46 |
- 'services': {'sv': {'phrase': 'abc', 'length': 10}}}, ''),
|
|
| 47 |
- ({'global': {'key': '...'},
|
|
| 48 |
- 'services': {'sv1': {'phrase': 'abc', 'length': 10, 'upper': 1},
|
|
| 49 |
- 'sv2': {'length': 10, 'repeat': 1, 'lower': 1}}}, ''),
|
|
| 50 |
-]) |
|
| 61 |
+ ( |
|
| 62 |
+ {'services': {'sv': {'phrase': 'abc', 'key': '...'}}},
|
|
| 63 |
+ 'incompatible config values: services.sv.key and services.sv.phrase', # noqa: E501 |
|
| 64 |
+ ), |
|
| 65 |
+ ( |
|
| 66 |
+ {
|
|
| 67 |
+ 'global': {'phrase': 'abc'},
|
|
| 68 |
+ 'services': {'sv': {'phrase': 'abc', 'length': 10}},
|
|
| 69 |
+ }, |
|
| 70 |
+ '', |
|
| 71 |
+ ), |
|
| 72 |
+ ( |
|
| 73 |
+ {
|
|
| 74 |
+ 'global': {'key': '...'},
|
|
| 75 |
+ 'services': {'sv': {'phrase': 'abc', 'length': 10}},
|
|
| 76 |
+ }, |
|
| 77 |
+ '', |
|
| 78 |
+ ), |
|
| 79 |
+ ( |
|
| 80 |
+ {
|
|
| 81 |
+ 'global': {'key': '...'},
|
|
| 82 |
+ 'services': {
|
|
| 83 |
+ 'sv1': {'phrase': 'abc', 'length': 10, 'upper': 1},
|
|
| 84 |
+ 'sv2': {'length': 10, 'repeat': 1, 'lower': 1},
|
|
| 85 |
+ }, |
|
| 86 |
+ }, |
|
| 87 |
+ '', |
|
| 88 |
+ ), |
|
| 89 |
+ ], |
|
| 90 |
+) |
|
| 51 | 91 |
def test_200_is_vault_config(obj: Any, comment: str) -> None: |
| 52 | 92 |
is_vault_config = derivepassphrase.types.is_vault_config |
| 53 | 93 |
assert is_vault_config(obj) == (not comment), ( |
| 54 |
- 'failed to complain about: ' + comment if comment |
|
| 94 |
+ 'failed to complain about: ' + comment |
|
| 95 |
+ if comment |
|
| 55 | 96 |
else 'failed on valid example' |
| 56 | 97 |
) |
| ... | ... |
@@ -12,53 +12,107 @@ import sequin |
| 12 | 12 |
|
| 13 | 13 |
|
| 14 | 14 |
class TestStaticFunctionality: |
| 15 |
- |
|
| 16 |
- @pytest.mark.parametrize(['sequence', 'base', 'expected'], [ |
|
| 15 |
+ @pytest.mark.parametrize( |
|
| 16 |
+ ['sequence', 'base', 'expected'], |
|
| 17 |
+ [ |
|
| 17 | 18 |
([1, 2, 3, 4, 5, 6], 10, 123456), |
| 18 | 19 |
([1, 2, 3, 4, 5, 6], 100, 10203040506), |
| 19 | 20 |
([0, 0, 1, 4, 9, 7], 10, 1497), |
| 20 | 21 |
([1, 0, 0, 1, 0, 0, 0, 0], 2, 144), |
| 21 | 22 |
([1, 7, 5, 5], 8, 0o1755), |
| 22 |
- ]) |
|
| 23 |
+ ], |
|
| 24 |
+ ) |
|
| 23 | 25 |
def test_200_big_endian_number(self, sequence, base, expected): |
| 24 | 26 |
assert ( |
| 25 | 27 |
sequin.Sequin._big_endian_number(sequence, base=base) |
| 26 | 28 |
) == expected |
| 27 | 29 |
|
| 28 | 30 |
@pytest.mark.parametrize( |
| 29 |
- ['exc_type', 'exc_pattern', 'sequence', 'base'], [ |
|
| 31 |
+ ['exc_type', 'exc_pattern', 'sequence', 'base'], |
|
| 32 |
+ [ |
|
| 30 | 33 |
(ValueError, 'invalid base 3 digit:', [-1], 3), |
| 31 | 34 |
(ValueError, 'invalid base:', [0], 1), |
| 32 | 35 |
(TypeError, 'not an integer:', [0.0, 1.0, 0.0, 1.0], 2), |
| 33 |
- ] |
|
| 36 |
+ ], |
|
| 34 | 37 |
) |
| 35 |
- def test_300_big_endian_number_exceptions(self, exc_type, exc_pattern, |
|
| 36 |
- sequence, base): |
|
| 38 |
+ def test_300_big_endian_number_exceptions( |
|
| 39 |
+ self, exc_type, exc_pattern, sequence, base |
|
| 40 |
+ ): |
|
| 37 | 41 |
with pytest.raises(exc_type, match=exc_pattern): |
| 38 | 42 |
sequin.Sequin._big_endian_number(sequence, base=base) |
| 39 | 43 |
|
| 40 |
-class TestSequin: |
|
| 41 | 44 |
|
| 42 |
- @pytest.mark.parametrize(['sequence', 'is_bitstring', 'expected'], [ |
|
| 43 |
- ([1, 0, 0, 1, 0, 1], False, [0, 0, 0, 0, 0, 0, 0, 1, |
|
| 44 |
- 0, 0, 0, 0, 0, 0, 0, 0, |
|
| 45 |
- 0, 0, 0, 0, 0, 0, 0, 0, |
|
| 46 |
- 0, 0, 0, 0, 0, 0, 0, 1, |
|
| 47 |
- 0, 0, 0, 0, 0, 0, 0, 0, |
|
| 48 |
- 0, 0, 0, 0, 0, 0, 0, 1]), |
|
| 45 |
+class TestSequin: |
|
| 46 |
+ @pytest.mark.parametrize( |
|
| 47 |
+ ['sequence', 'is_bitstring', 'expected'], |
|
| 48 |
+ [ |
|
| 49 |
+ ( |
|
| 50 |
+ [1, 0, 0, 1, 0, 1], |
|
| 51 |
+ False, |
|
| 52 |
+ [ |
|
| 53 |
+ 0, |
|
| 54 |
+ 0, |
|
| 55 |
+ 0, |
|
| 56 |
+ 0, |
|
| 57 |
+ 0, |
|
| 58 |
+ 0, |
|
| 59 |
+ 0, |
|
| 60 |
+ 1, |
|
| 61 |
+ 0, |
|
| 62 |
+ 0, |
|
| 63 |
+ 0, |
|
| 64 |
+ 0, |
|
| 65 |
+ 0, |
|
| 66 |
+ 0, |
|
| 67 |
+ 0, |
|
| 68 |
+ 0, |
|
| 69 |
+ 0, |
|
| 70 |
+ 0, |
|
| 71 |
+ 0, |
|
| 72 |
+ 0, |
|
| 73 |
+ 0, |
|
| 74 |
+ 0, |
|
| 75 |
+ 0, |
|
| 76 |
+ 0, |
|
| 77 |
+ 0, |
|
| 78 |
+ 0, |
|
| 79 |
+ 0, |
|
| 80 |
+ 0, |
|
| 81 |
+ 0, |
|
| 82 |
+ 0, |
|
| 83 |
+ 0, |
|
| 84 |
+ 1, |
|
| 85 |
+ 0, |
|
| 86 |
+ 0, |
|
| 87 |
+ 0, |
|
| 88 |
+ 0, |
|
| 89 |
+ 0, |
|
| 90 |
+ 0, |
|
| 91 |
+ 0, |
|
| 92 |
+ 0, |
|
| 93 |
+ 0, |
|
| 94 |
+ 0, |
|
| 95 |
+ 0, |
|
| 96 |
+ 0, |
|
| 97 |
+ 0, |
|
| 98 |
+ 0, |
|
| 99 |
+ 0, |
|
| 100 |
+ 1, |
|
| 101 |
+ ], |
|
| 102 |
+ ), |
|
| 49 | 103 |
([1, 0, 0, 1, 0, 1], True, [1, 0, 0, 1, 0, 1]), |
| 50 |
- (b'OK', False, [0, 1, 0, 0, 1, 1, 1, 1, |
|
| 51 |
- 0, 1, 0, 0, 1, 0, 1, 1]), |
|
| 52 |
- ('OK', False, [0, 1, 0, 0, 1, 1, 1, 1,
|
|
| 53 |
- 0, 1, 0, 0, 1, 0, 1, 1]), |
|
| 54 |
- ]) |
|
| 104 |
+ (b'OK', False, [0, 1, 0, 0, 1, 1, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1]), |
|
| 105 |
+ ('OK', False, [0, 1, 0, 0, 1, 1, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1]),
|
|
| 106 |
+ ], |
|
| 107 |
+ ) |
|
| 55 | 108 |
def test_200_constructor(self, sequence, is_bitstring, expected): |
| 56 | 109 |
seq = sequin.Sequin(sequence, is_bitstring=is_bitstring) |
| 57 | 110 |
assert seq.bases == {2: collections.deque(expected)}
|
| 58 | 111 |
|
| 59 | 112 |
def test_201_generating(self): |
| 60 |
- seq = sequin.Sequin([1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1], |
|
| 61 |
- is_bitstring=True) |
|
| 113 |
+ seq = sequin.Sequin( |
|
| 114 |
+ [1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1], is_bitstring=True |
|
| 115 |
+ ) |
|
| 62 | 116 |
assert seq.generate(1) == 0 |
| 63 | 117 |
assert seq.generate(5) == 3 |
| 64 | 118 |
assert seq.generate(5) == 3 |
| ... | ... |
@@ -67,21 +121,24 @@ class TestSequin: |
| 67 | 121 |
seq.generate(5) |
| 68 | 122 |
with pytest.raises(sequin.SequinExhaustedError): |
| 69 | 123 |
seq.generate(1) |
| 70 |
- seq = sequin.Sequin([1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1], |
|
| 71 |
- is_bitstring=True) |
|
| 124 |
+ seq = sequin.Sequin( |
|
| 125 |
+ [1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1], is_bitstring=True |
|
| 126 |
+ ) |
|
| 72 | 127 |
with pytest.raises(ValueError, match='invalid target range'): |
| 73 | 128 |
seq.generate(0) |
| 74 | 129 |
|
| 75 | 130 |
def test_210_internal_generating(self): |
| 76 |
- seq = sequin.Sequin([1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1], |
|
| 77 |
- is_bitstring=True) |
|
| 131 |
+ seq = sequin.Sequin( |
|
| 132 |
+ [1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1], is_bitstring=True |
|
| 133 |
+ ) |
|
| 78 | 134 |
assert seq._generate_inner(5) == 3 |
| 79 | 135 |
assert seq._generate_inner(5) == 3 |
| 80 | 136 |
assert seq._generate_inner(5) == 1 |
| 81 | 137 |
assert seq._generate_inner(5) == 5 |
| 82 | 138 |
assert seq._generate_inner(1) == 0 |
| 83 |
- seq = sequin.Sequin([1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1], |
|
| 84 |
- is_bitstring=True) |
|
| 139 |
+ seq = sequin.Sequin( |
|
| 140 |
+ [1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1], is_bitstring=True |
|
| 141 |
+ ) |
|
| 85 | 142 |
assert seq._generate_inner(1) == 0 |
| 86 | 143 |
with pytest.raises(ValueError, match='invalid target range'): |
| 87 | 144 |
seq._generate_inner(0) |
| ... | ... |
@@ -90,8 +147,9 @@ class TestSequin: |
| 90 | 147 |
|
| 91 | 148 |
def test_211_shifting(self): |
| 92 | 149 |
seq = sequin.Sequin([1, 0, 1, 0, 0, 1, 0, 0, 0, 1], is_bitstring=True) |
| 93 |
- assert seq.bases == {2: collections.deque([
|
|
| 94 |
- 1, 0, 1, 0, 0, 1, 0, 0, 0, 1])} |
|
| 150 |
+ assert seq.bases == {
|
|
| 151 |
+ 2: collections.deque([1, 0, 1, 0, 0, 1, 0, 0, 0, 1]) |
|
| 152 |
+ } |
|
| 95 | 153 |
|
| 96 | 154 |
assert seq._all_or_nothing_shift(3) == (1, 0, 1) |
| 97 | 155 |
assert seq._all_or_nothing_shift(3) == (0, 0, 1) |
| ... | ... |
@@ -106,13 +164,17 @@ class TestSequin: |
| 106 | 164 |
@pytest.mark.parametrize( |
| 107 | 165 |
['sequence', 'is_bitstring', 'exc_type', 'exc_pattern'], |
| 108 | 166 |
[ |
| 109 |
- ([0, 1, 2, 3, 4, 5, 6, 7], True, |
|
| 110 |
- ValueError, 'sequence item out of range'), |
|
| 111 |
- ('こんにちは。', False,
|
|
| 112 |
- ValueError, 'sequence item out of range'), |
|
| 113 |
- ] |
|
| 167 |
+ ( |
|
| 168 |
+ [0, 1, 2, 3, 4, 5, 6, 7], |
|
| 169 |
+ True, |
|
| 170 |
+ ValueError, |
|
| 171 |
+ 'sequence item out of range', |
|
| 172 |
+ ), |
|
| 173 |
+ ('こんにちは。', False, ValueError, 'sequence item out of range'),
|
|
| 174 |
+ ], |
|
| 114 | 175 |
) |
| 115 |
- def test_300_constructor_exceptions(self, sequence, is_bitstring, |
|
| 116 |
- exc_type, exc_pattern): |
|
| 176 |
+ def test_300_constructor_exceptions( |
|
| 177 |
+ self, sequence, is_bitstring, exc_type, exc_pattern |
|
| 178 |
+ ): |
|
| 117 | 179 |
with pytest.raises(exc_type, match=exc_pattern): |
| 118 | 180 |
sequin.Sequin(sequence, is_bitstring=is_bitstring) |
| ... | ... |
@@ -24,10 +24,13 @@ import tests |
| 24 | 24 |
|
| 25 | 25 |
|
| 26 | 26 |
class TestStaticFunctionality: |
| 27 |
- |
|
| 28 |
- @pytest.mark.parametrize(['public_key', 'public_key_data'], |
|
| 29 |
- [(val['public_key'], val['public_key_data']) |
|
| 30 |
- for val in tests.SUPPORTED_KEYS.values()]) |
|
| 27 |
+ @pytest.mark.parametrize( |
|
| 28 |
+ ['public_key', 'public_key_data'], |
|
| 29 |
+ [ |
|
| 30 |
+ (val['public_key'], val['public_key_data']) |
|
| 31 |
+ for val in tests.SUPPORTED_KEYS.values() |
|
| 32 |
+ ], |
|
| 33 |
+ ) |
|
| 31 | 34 |
def test_100_key_decoding(self, public_key, public_key_data): |
| 32 | 35 |
keydata = base64.b64decode(public_key.split(None, 2)[1]) |
| 33 | 36 |
assert ( |
| ... | ... |
@@ -37,77 +40,101 @@ class TestStaticFunctionality: |
| 37 | 40 |
def test_200_constructor_no_running_agent(self, monkeypatch): |
| 38 | 41 |
monkeypatch.delenv('SSH_AUTH_SOCK', raising=False)
|
| 39 | 42 |
sock = socket.socket(family=socket.AF_UNIX) |
| 40 |
- with pytest.raises(KeyError, |
|
| 41 |
- match='SSH_AUTH_SOCK environment variable'): |
|
| 43 |
+ with pytest.raises( |
|
| 44 |
+ KeyError, match='SSH_AUTH_SOCK environment variable' |
|
| 45 |
+ ): |
|
| 42 | 46 |
ssh_agent_client.SSHAgentClient(socket=sock) |
| 43 | 47 |
|
| 44 |
- @pytest.mark.parametrize(['input', 'expected'], [ |
|
| 48 |
+ @pytest.mark.parametrize( |
|
| 49 |
+ ['input', 'expected'], |
|
| 50 |
+ [ |
|
| 45 | 51 |
(16777216, b'\x01\x00\x00\x00'), |
| 46 |
- ]) |
|
| 52 |
+ ], |
|
| 53 |
+ ) |
|
| 47 | 54 |
def test_210_uint32(self, input, expected): |
| 48 | 55 |
uint32 = ssh_agent_client.SSHAgentClient.uint32 |
| 49 | 56 |
assert uint32(input) == expected |
| 50 | 57 |
|
| 51 |
- @pytest.mark.parametrize(['input', 'expected'], [ |
|
| 58 |
+ @pytest.mark.parametrize( |
|
| 59 |
+ ['input', 'expected'], |
|
| 60 |
+ [ |
|
| 52 | 61 |
(b'ssh-rsa', b'\x00\x00\x00\x07ssh-rsa'), |
| 53 | 62 |
(b'ssh-ed25519', b'\x00\x00\x00\x0bssh-ed25519'), |
| 54 | 63 |
( |
| 55 | 64 |
ssh_agent_client.SSHAgentClient.string(b'ssh-ed25519'), |
| 56 | 65 |
b'\x00\x00\x00\x0f\x00\x00\x00\x0bssh-ed25519', |
| 57 | 66 |
), |
| 58 |
- ]) |
|
| 67 |
+ ], |
|
| 68 |
+ ) |
|
| 59 | 69 |
def test_211_string(self, input, expected): |
| 60 | 70 |
string = ssh_agent_client.SSHAgentClient.string |
| 61 | 71 |
assert bytes(string(input)) == expected |
| 62 | 72 |
|
| 63 |
- @pytest.mark.parametrize(['input', 'expected'], [ |
|
| 73 |
+ @pytest.mark.parametrize( |
|
| 74 |
+ ['input', 'expected'], |
|
| 75 |
+ [ |
|
| 64 | 76 |
(b'\x00\x00\x00\x07ssh-rsa', b'ssh-rsa'), |
| 65 | 77 |
( |
| 66 | 78 |
ssh_agent_client.SSHAgentClient.string(b'ssh-ed25519'), |
| 67 | 79 |
b'ssh-ed25519', |
| 68 | 80 |
), |
| 69 |
- ]) |
|
| 81 |
+ ], |
|
| 82 |
+ ) |
|
| 70 | 83 |
def test_212_unstring(self, input, expected): |
| 71 | 84 |
unstring = ssh_agent_client.SSHAgentClient.unstring |
| 72 | 85 |
unstring_prefix = ssh_agent_client.SSHAgentClient.unstring_prefix |
| 73 | 86 |
assert bytes(unstring(input)) == expected |
| 74 |
- assert tuple( |
|
| 75 |
- bytes(x) for x in unstring_prefix(input) |
|
| 76 |
- ) == (expected, b'') |
|
| 87 |
+ assert tuple(bytes(x) for x in unstring_prefix(input)) == ( |
|
| 88 |
+ expected, |
|
| 89 |
+ b'', |
|
| 90 |
+ ) |
|
| 77 | 91 |
|
| 78 |
- @pytest.mark.parametrize(['value', 'exc_type', 'exc_pattern'], [ |
|
| 92 |
+ @pytest.mark.parametrize( |
|
| 93 |
+ ['value', 'exc_type', 'exc_pattern'], |
|
| 94 |
+ [ |
|
| 79 | 95 |
(10000000000000000, OverflowError, 'int too big to convert'), |
| 80 | 96 |
(-1, OverflowError, "can't convert negative int to unsigned"), |
| 81 |
- ]) |
|
| 97 |
+ ], |
|
| 98 |
+ ) |
|
| 82 | 99 |
def test_310_uint32_exceptions(self, value, exc_type, exc_pattern): |
| 83 | 100 |
uint32 = ssh_agent_client.SSHAgentClient.uint32 |
| 84 | 101 |
with pytest.raises(exc_type, match=exc_pattern): |
| 85 | 102 |
uint32(value) |
| 86 | 103 |
|
| 87 |
- @pytest.mark.parametrize(['input', 'exc_type', 'exc_pattern'], [ |
|
| 104 |
+ @pytest.mark.parametrize( |
|
| 105 |
+ ['input', 'exc_type', 'exc_pattern'], |
|
| 106 |
+ [ |
|
| 88 | 107 |
('some string', TypeError, 'invalid payload type'),
|
| 89 |
- ]) |
|
| 108 |
+ ], |
|
| 109 |
+ ) |
|
| 90 | 110 |
def test_311_string_exceptions(self, input, exc_type, exc_pattern): |
| 91 | 111 |
string = ssh_agent_client.SSHAgentClient.string |
| 92 | 112 |
with pytest.raises(exc_type, match=exc_pattern): |
| 93 | 113 |
string(input) |
| 94 | 114 |
|
| 95 | 115 |
@pytest.mark.parametrize( |
| 96 |
- ['input', 'exc_type', 'exc_pattern', 'has_trailer', 'parts'], [ |
|
| 116 |
+ ['input', 'exc_type', 'exc_pattern', 'has_trailer', 'parts'], |
|
| 117 |
+ [ |
|
| 97 | 118 |
(b'ssh', ValueError, 'malformed SSH byte string', False, None), |
| 98 | 119 |
( |
| 99 | 120 |
b'\x00\x00\x00\x08ssh-rsa', |
| 100 |
- ValueError, 'malformed SSH byte string', |
|
| 101 |
- False, None, |
|
| 121 |
+ ValueError, |
|
| 122 |
+ 'malformed SSH byte string', |
|
| 123 |
+ False, |
|
| 124 |
+ None, |
|
| 102 | 125 |
), |
| 103 | 126 |
( |
| 104 | 127 |
b'\x00\x00\x00\x04XXX trailing text', |
| 105 |
- ValueError, 'malformed SSH byte string', |
|
| 106 |
- True, (b'XXX ', b'trailing text'), |
|
| 128 |
+ ValueError, |
|
| 129 |
+ 'malformed SSH byte string', |
|
| 130 |
+ True, |
|
| 131 |
+ (b'XXX ', b'trailing text'), |
|
| 107 | 132 |
), |
| 108 |
- ]) |
|
| 109 |
- def test_312_unstring_exceptions(self, input, exc_type, exc_pattern, |
|
| 110 |
- has_trailer, parts): |
|
| 133 |
+ ], |
|
| 134 |
+ ) |
|
| 135 |
+ def test_312_unstring_exceptions( |
|
| 136 |
+ self, input, exc_type, exc_pattern, has_trailer, parts |
|
| 137 |
+ ): |
|
| 111 | 138 |
unstring = ssh_agent_client.SSHAgentClient.unstring |
| 112 | 139 |
unstring_prefix = ssh_agent_client.SSHAgentClient.unstring_prefix |
| 113 | 140 |
with pytest.raises(exc_type, match=exc_pattern): |
| ... | ... |
@@ -118,22 +145,26 @@ class TestStaticFunctionality: |
| 118 | 145 |
with pytest.raises(exc_type, match=exc_pattern): |
| 119 | 146 |
unstring_prefix(input) |
| 120 | 147 |
|
| 148 |
+ |
|
| 121 | 149 |
@tests.skip_if_no_agent |
| 122 | 150 |
class TestAgentInteraction: |
| 123 |
- |
|
| 124 |
- @pytest.mark.parametrize(['keytype', 'data_dict'], |
|
| 125 |
- list(tests.SUPPORTED_KEYS.items())) |
|
| 151 |
+ @pytest.mark.parametrize( |
|
| 152 |
+ ['keytype', 'data_dict'], list(tests.SUPPORTED_KEYS.items()) |
|
| 153 |
+ ) |
|
| 126 | 154 |
def test_200_sign_data_via_agent(self, keytype, data_dict): |
| 127 | 155 |
del keytype # Unused. |
| 128 | 156 |
private_key = data_dict['private_key'] |
| 129 | 157 |
try: |
| 130 |
- _ = subprocess.run(['ssh-add', '-t', '30', '-q', '-'], |
|
| 131 |
- input=private_key, check=True, |
|
| 132 |
- capture_output=True) |
|
| 158 |
+ _ = subprocess.run( |
|
| 159 |
+ ['ssh-add', '-t', '30', '-q', '-'], |
|
| 160 |
+ input=private_key, |
|
| 161 |
+ check=True, |
|
| 162 |
+ capture_output=True, |
|
| 163 |
+ ) |
|
| 133 | 164 |
except subprocess.CalledProcessError as e: |
| 134 | 165 |
pytest.skip( |
| 135 |
- f"uploading test key: {e!r}, stdout={e.stdout!r}, "
|
|
| 136 |
- f"stderr={e.stderr!r}"
|
|
| 166 |
+ f'uploading test key: {e!r}, stdout={e.stdout!r}, '
|
|
| 167 |
+ f'stderr={e.stderr!r}'
|
|
| 137 | 168 |
) |
| 138 | 169 |
else: |
| 139 | 170 |
try: |
| ... | ... |
@@ -141,37 +172,48 @@ class TestAgentInteraction: |
| 141 | 172 |
except OSError: # pragma: no cover |
| 142 | 173 |
pytest.skip('communication error with the SSH agent')
|
| 143 | 174 |
with client: |
| 144 |
- key_comment_pairs = {bytes(k): bytes(c)
|
|
| 145 |
- for k, c in client.list_keys()} |
|
| 175 |
+ key_comment_pairs = {
|
|
| 176 |
+ bytes(k): bytes(c) for k, c in client.list_keys() |
|
| 177 |
+ } |
|
| 146 | 178 |
public_key_data = data_dict['public_key_data'] |
| 147 | 179 |
expected_signature = data_dict['expected_signature'] |
| 148 | 180 |
derived_passphrase = data_dict['derived_passphrase'] |
| 149 | 181 |
if public_key_data not in key_comment_pairs: # pragma: no cover |
| 150 | 182 |
pytest.skip('prerequisite SSH key not loaded')
|
| 151 |
- signature = bytes(client.sign( |
|
| 152 |
- payload=derivepassphrase.Vault._UUID, key=public_key_data)) |
|
| 183 |
+ signature = bytes( |
|
| 184 |
+ client.sign( |
|
| 185 |
+ payload=derivepassphrase.Vault._UUID, key=public_key_data |
|
| 186 |
+ ) |
|
| 187 |
+ ) |
|
| 153 | 188 |
assert signature == expected_signature, 'SSH signature mismatch' |
| 154 |
- signature2 = bytes(client.sign( |
|
| 155 |
- payload=derivepassphrase.Vault._UUID, key=public_key_data)) |
|
| 189 |
+ signature2 = bytes( |
|
| 190 |
+ client.sign( |
|
| 191 |
+ payload=derivepassphrase.Vault._UUID, key=public_key_data |
|
| 192 |
+ ) |
|
| 193 |
+ ) |
|
| 156 | 194 |
assert signature2 == expected_signature, 'SSH signature mismatch' |
| 157 | 195 |
assert ( |
| 158 |
- derivepassphrase.Vault.phrase_from_key(public_key_data) == |
|
| 159 |
- derived_passphrase |
|
| 196 |
+ derivepassphrase.Vault.phrase_from_key(public_key_data) |
|
| 197 |
+ == derived_passphrase |
|
| 160 | 198 |
), 'SSH signature mismatch' |
| 161 | 199 |
|
| 162 |
- @pytest.mark.parametrize(['keytype', 'data_dict'], |
|
| 163 |
- list(tests.UNSUITABLE_KEYS.items())) |
|
| 200 |
+ @pytest.mark.parametrize( |
|
| 201 |
+ ['keytype', 'data_dict'], list(tests.UNSUITABLE_KEYS.items()) |
|
| 202 |
+ ) |
|
| 164 | 203 |
def test_201_sign_data_via_agent_unsupported(self, keytype, data_dict): |
| 165 | 204 |
del keytype # Unused. |
| 166 | 205 |
private_key = data_dict['private_key'] |
| 167 | 206 |
try: |
| 168 |
- _ = subprocess.run(['ssh-add', '-t', '30', '-q', '-'], |
|
| 169 |
- input=private_key, check=True, |
|
| 170 |
- capture_output=True) |
|
| 207 |
+ _ = subprocess.run( |
|
| 208 |
+ ['ssh-add', '-t', '30', '-q', '-'], |
|
| 209 |
+ input=private_key, |
|
| 210 |
+ check=True, |
|
| 211 |
+ capture_output=True, |
|
| 212 |
+ ) |
|
| 171 | 213 |
except subprocess.CalledProcessError as e: # pragma: no cover |
| 172 | 214 |
pytest.skip( |
| 173 |
- f"uploading test key: {e!r}, stdout={e.stdout!r}, "
|
|
| 174 |
- f"stderr={e.stderr!r}"
|
|
| 215 |
+ f'uploading test key: {e!r}, stdout={e.stdout!r}, '
|
|
| 216 |
+ f'stderr={e.stderr!r}'
|
|
| 175 | 217 |
) |
| 176 | 218 |
else: |
| 177 | 219 |
try: |
| ... | ... |
@@ -179,16 +221,23 @@ class TestAgentInteraction: |
| 179 | 221 |
except OSError: # pragma: no cover |
| 180 | 222 |
pytest.skip('communication error with the SSH agent')
|
| 181 | 223 |
with client: |
| 182 |
- key_comment_pairs = {bytes(k): bytes(c)
|
|
| 183 |
- for k, c in client.list_keys()} |
|
| 224 |
+ key_comment_pairs = {
|
|
| 225 |
+ bytes(k): bytes(c) for k, c in client.list_keys() |
|
| 226 |
+ } |
|
| 184 | 227 |
public_key_data = data_dict['public_key_data'] |
| 185 | 228 |
_ = data_dict['expected_signature'] |
| 186 | 229 |
if public_key_data not in key_comment_pairs: # pragma: no cover |
| 187 | 230 |
pytest.skip('prerequisite SSH key not loaded')
|
| 188 |
- signature = bytes(client.sign( |
|
| 189 |
- payload=derivepassphrase.Vault._UUID, key=public_key_data)) |
|
| 190 |
- signature2 = bytes(client.sign( |
|
| 191 |
- payload=derivepassphrase.Vault._UUID, key=public_key_data)) |
|
| 231 |
+ signature = bytes( |
|
| 232 |
+ client.sign( |
|
| 233 |
+ payload=derivepassphrase.Vault._UUID, key=public_key_data |
|
| 234 |
+ ) |
|
| 235 |
+ ) |
|
| 236 |
+ signature2 = bytes( |
|
| 237 |
+ client.sign( |
|
| 238 |
+ payload=derivepassphrase.Vault._UUID, key=public_key_data |
|
| 239 |
+ ) |
|
| 240 |
+ ) |
|
| 192 | 241 |
assert signature != signature2, 'SSH signature repeatable?!' |
| 193 | 242 |
with pytest.raises(ValueError, match='unsuitable SSH key'): |
| 194 | 243 |
derivepassphrase.Vault.phrase_from_key(public_key_data) |
| ... | ... |
@@ -207,20 +256,32 @@ class TestAgentInteraction: |
| 207 | 256 |
@pytest.mark.parametrize(['key', 'single'], list(_params())) |
| 208 | 257 |
def test_210_ssh_key_selector(self, monkeypatch, key, single): |
| 209 | 258 |
def key_is_suitable(key: bytes): |
| 210 |
- return key in {v['public_key_data']
|
|
| 211 |
- for v in tests.SUPPORTED_KEYS.values()} |
|
| 259 |
+ return key in {
|
|
| 260 |
+ v['public_key_data'] for v in tests.SUPPORTED_KEYS.values() |
|
| 261 |
+ } |
|
| 262 |
+ |
|
| 212 | 263 |
if single: |
| 213 |
- monkeypatch.setattr(ssh_agent_client.SSHAgentClient, |
|
| 214 |
- 'list_keys', tests.list_keys_singleton) |
|
| 215 |
- keys = [pair.key for pair in tests.list_keys_singleton() |
|
| 216 |
- if key_is_suitable(pair.key)] |
|
| 264 |
+ monkeypatch.setattr( |
|
| 265 |
+ ssh_agent_client.SSHAgentClient, |
|
| 266 |
+ 'list_keys', |
|
| 267 |
+ tests.list_keys_singleton, |
|
| 268 |
+ ) |
|
| 269 |
+ keys = [ |
|
| 270 |
+ pair.key |
|
| 271 |
+ for pair in tests.list_keys_singleton() |
|
| 272 |
+ if key_is_suitable(pair.key) |
|
| 273 |
+ ] |
|
| 217 | 274 |
index = '1' |
| 218 | 275 |
text = 'Use this key? yes\n' |
| 219 | 276 |
else: |
| 220 |
- monkeypatch.setattr(ssh_agent_client.SSHAgentClient, |
|
| 221 |
- 'list_keys', tests.list_keys) |
|
| 222 |
- keys = [pair.key for pair in tests.list_keys() |
|
| 223 |
- if key_is_suitable(pair.key)] |
|
| 277 |
+ monkeypatch.setattr( |
|
| 278 |
+ ssh_agent_client.SSHAgentClient, 'list_keys', tests.list_keys |
|
| 279 |
+ ) |
|
| 280 |
+ keys = [ |
|
| 281 |
+ pair.key |
|
| 282 |
+ for pair in tests.list_keys() |
|
| 283 |
+ if key_is_suitable(pair.key) |
|
| 284 |
+ ] |
|
| 224 | 285 |
index = str(1 + keys.index(key)) |
| 225 | 286 |
n = len(keys) |
| 226 | 287 |
text = f'Your selection? (1-{n}, leave empty to abort): {index}\n'
|
| ... | ... |
@@ -232,31 +293,36 @@ class TestAgentInteraction: |
| 232 | 293 |
click.echo(base64.standard_b64encode(key).decode('ASCII'))
|
| 233 | 294 |
|
| 234 | 295 |
runner = click.testing.CliRunner(mix_stderr=True) |
| 235 |
- result = runner.invoke(driver, [], |
|
| 296 |
+ result = runner.invoke( |
|
| 297 |
+ driver, |
|
| 298 |
+ [], |
|
| 236 | 299 |
input=('yes\n' if single else f'{index}\n'),
|
| 237 |
- catch_exceptions=True) |
|
| 238 |
- assert result.stdout.startswith('Suitable SSH keys:\n'), (
|
|
| 239 |
- 'missing expected output' |
|
| 300 |
+ catch_exceptions=True, |
|
| 240 | 301 |
) |
| 302 |
+ assert result.stdout.startswith( |
|
| 303 |
+ 'Suitable SSH keys:\n' |
|
| 304 |
+ ), 'missing expected output' |
|
| 241 | 305 |
assert text in result.stdout, 'missing expected output' |
| 242 |
- assert ( |
|
| 243 |
- result.stdout.endswith(f'\n{b64_key}\n')
|
|
| 306 |
+ assert result.stdout.endswith( |
|
| 307 |
+ f'\n{b64_key}\n'
|
|
| 244 | 308 |
), 'missing expected output' |
| 245 | 309 |
assert result.exit_code == 0, 'driver program failed?!' |
| 246 | 310 |
|
| 247 | 311 |
del _params |
| 248 | 312 |
|
| 249 | 313 |
def test_300_constructor_bad_running_agent(self, monkeypatch): |
| 250 |
- monkeypatch.setenv('SSH_AUTH_SOCK',
|
|
| 251 |
- os.environ['SSH_AUTH_SOCK'] + '~') |
|
| 314 |
+ monkeypatch.setenv('SSH_AUTH_SOCK', os.environ['SSH_AUTH_SOCK'] + '~')
|
|
| 252 | 315 |
sock = socket.socket(family=socket.AF_UNIX) |
| 253 | 316 |
with pytest.raises(OSError): # noqa: PT011 |
| 254 | 317 |
ssh_agent_client.SSHAgentClient(socket=sock) |
| 255 | 318 |
|
| 256 |
- @pytest.mark.parametrize('response', [
|
|
| 319 |
+ @pytest.mark.parametrize( |
|
| 320 |
+ 'response', |
|
| 321 |
+ [ |
|
| 257 | 322 |
b'\x00\x00', |
| 258 | 323 |
b'\x00\x00\x00\x1f some bytes missing', |
| 259 |
- ]) |
|
| 324 |
+ ], |
|
| 325 |
+ ) |
|
| 260 | 326 |
def test_310_truncated_server_response(self, monkeypatch, response): |
| 261 | 327 |
client = ssh_agent_client.SSHAgentClient() |
| 262 | 328 |
response_stream = io.BytesIO(response) |
| ... | ... |
@@ -282,13 +351,17 @@ class TestAgentInteraction: |
| 282 | 351 |
ssh_agent_client.TrailingDataError, |
| 283 | 352 |
'Overlong response', |
| 284 | 353 |
), |
| 285 |
- ] |
|
| 354 |
+ ], |
|
| 286 | 355 |
) |
| 287 |
- def test_320_list_keys_error_responses(self, monkeypatch, response_code, |
|
| 288 |
- response, exc_type, exc_pattern): |
|
| 356 |
+ def test_320_list_keys_error_responses( |
|
| 357 |
+ self, monkeypatch, response_code, response, exc_type, exc_pattern |
|
| 358 |
+ ): |
|
| 289 | 359 |
client = ssh_agent_client.SSHAgentClient() |
| 290 |
- monkeypatch.setattr(client, 'request', |
|
| 291 |
- lambda *a, **kw: (response_code, response)) # noqa: ARG005 |
|
| 360 |
+ monkeypatch.setattr( |
|
| 361 |
+ client, |
|
| 362 |
+ 'request', |
|
| 363 |
+ lambda *a, **kw: (response_code, response), # noqa: ARG005 |
|
| 364 |
+ ) |
|
| 292 | 365 |
with pytest.raises(exc_type, match=exc_pattern): |
| 293 | 366 |
client.list_keys() |
| 294 | 367 |
|
| ... | ... |
@@ -309,16 +382,19 @@ class TestAgentInteraction: |
| 309 | 382 |
(255, b''), |
| 310 | 383 |
RuntimeError, |
| 311 | 384 |
'signing data failed:', |
| 385 |
+ ), |
|
| 386 |
+ ], |
|
| 312 | 387 |
) |
| 313 |
- ] |
|
| 314 |
- ) |
|
| 315 |
- def test_330_sign_error_responses(self, monkeypatch, key, check, |
|
| 316 |
- response, exc_type, exc_pattern): |
|
| 388 |
+ def test_330_sign_error_responses( |
|
| 389 |
+ self, monkeypatch, key, check, response, exc_type, exc_pattern |
|
| 390 |
+ ): |
|
| 317 | 391 |
client = ssh_agent_client.SSHAgentClient() |
| 318 | 392 |
monkeypatch.setattr(client, 'request', lambda a, b: response) # noqa: ARG005 |
| 319 | 393 |
KeyCommentPair = ssh_agent_client.types.KeyCommentPair # noqa: N806 |
| 320 |
- loaded_keys = [KeyCommentPair(v['public_key_data'], b'no comment') |
|
| 321 |
- for v in tests.SUPPORTED_KEYS.values()] |
|
| 394 |
+ loaded_keys = [ |
|
| 395 |
+ KeyCommentPair(v['public_key_data'], b'no comment') |
|
| 396 |
+ for v in tests.SUPPORTED_KEYS.values() |
|
| 397 |
+ ] |
|
| 322 | 398 |
monkeypatch.setattr(client, 'list_keys', lambda: loaded_keys) |
| 323 | 399 |
with pytest.raises(exc_type, match=exc_pattern): |
| 324 | 400 |
client.sign(key, b'abc', check_if_key_loaded=check) |
| 325 | 401 |