Split the top-level `tests` module into subpackages
Marco Ricci

Marco Ricci commited on 2025-08-08 22:58:18
Zeige 15 geänderte Dateien mit 3928 Einfügungen und 3620 Löschungen.


Split the top-level `tests` module into a pair of subpackages
`tests.data` and `tests.machinery`, grouping them by import
requirements.  This "stratifies" the data, and (in some cases) reduces
the amount of necessary support code while also (always) forcing us to
carefully consider the scope and the prerequisites of every test support
datum or function.

As a practical example of the benefits of this stratification, the
`test_key_signatures_and_outputs.py` script can get away with importing
only the lowest stratum `tests.data`, and thus does not need `pytest` or
`hypothesis` installed to run.

There are five strata, defined by the five subordinate modules in the
`tests` package: `tests.data`, `tests.data.callables`,
`tests.machinery`, `tests.machinery.pytest` and
`tests.machinery.hypothesis`.  Each module may import from modules
earlier in the list, but not later.  The `tests.data.callables`
submodule and the `tests.machinery` subpackage differ in that the
`test.data.callables` are still expected to be functional and useful
even without a testing system, the latter not necessarily.
... ...
@@ -28,7 +28,7 @@ from __future__ import annotations
28 28
 import argparse
29 29
 import textwrap
30 30
 
31
-import tests
31
+import tests.data
32 32
 
33 33
 from derivepassphrase import _types, ssh_agent, vault  # noqa: PLC2701
34 34
 
... ...
@@ -39,9 +39,9 @@ def try_key(
39 39
     /,
40 40
     *,
41 41
     deterministic_signature_class: (
42
-        tests.SSHTestKeyDeterministicSignatureClass
43
-    ) = tests.SSHTestKeyDeterministicSignatureClass.RFC_6979,
44
-) -> tests.SSHTestKey | None:
42
+        tests.data.SSHTestKeyDeterministicSignatureClass
43
+    ) = tests.data.SSHTestKeyDeterministicSignatureClass.RFC_6979,
44
+) -> tests.data.SSHTestKey | None:
45 45
     """Query a signature and derived passphrase for the named key, if possible.
46 46
 
47 47
     Args:
... ...
@@ -66,7 +66,7 @@ def try_key(
66 66
         `None` if no such signature augmentation could be performed.
67 67
 
68 68
     """  # noqa: E501,RUF002
69
-    key = tests.ALL_KEYS[keyname]
69
+    key = tests.data.ALL_KEYS[keyname]
70 70
     if not vault.Vault.is_suitable_ssh_key(key.public_key_data, client=client):
71 71
         return None
72 72
     signature: bytes
... ...
@@ -82,18 +82,18 @@ def try_key(
82 82
         return None
83 83
     expected_signatures = dict(key.expected_signatures)
84 84
     signature_class = (
85
-        tests.SSHTestKeyDeterministicSignatureClass.SPEC
85
+        tests.data.SSHTestKeyDeterministicSignatureClass.SPEC
86 86
         if vault.Vault.is_suitable_ssh_key(key.public_key_data)
87 87
         else deterministic_signature_class
88 88
     )
89 89
     expected_signatures[signature_class] = (
90
-        tests.SSHTestKeyDeterministicSignature(
90
+        tests.data.SSHTestKeyDeterministicSignature(
91 91
             signature=signature,
92 92
             derived_passphrase=derived_passphrase,
93 93
             signature_class=signature_class,
94 94
         )
95 95
     )
96
-    return tests.SSHTestKey(
96
+    return tests.data.SSHTestKey(
97 97
         public_key=key.public_key,
98 98
         public_key_data=key.public_key_data,
99 99
         private_key=key.private_key,
... ...
@@ -102,7 +102,7 @@ def try_key(
102 102
     )
103 103
 
104 104
 
105
-def format_key(key: tests.SSHTestKey) -> str:
105
+def format_key(key: tests.data.SSHTestKey) -> str:
106 106
     """Return a formatted SSH test key."""
107 107
     ascii_printables = range(32, 127)
108 108
     ascii_whitespace = {ord(" "), ord("\n"), ord("\t"), ord("\r"), ord("\f")}
... ...
@@ -145,7 +145,7 @@ def format_key(key: tests.SSHTestKey) -> str:
145 145
             ])
146 146
             if (
147 147
                 sig.signature_class
148
-                != tests.SSHTestKeyDeterministicSignatureClass.SPEC
148
+                != tests.data.SSHTestKeyDeterministicSignatureClass.SPEC
149 149
             ):
150 150
                 expected_signature_lines.append(
151 151
                     f"        signature_class={sig.signature_class!s},\n"
... ...
@@ -168,16 +168,16 @@ def main(argv: list[str] | None = None) -> None:
168 168
         "--rfc-6979",
169 169
         action="store_const",
170 170
         dest="deterministic_signature_class",
171
-        const=tests.SSHTestKeyDeterministicSignatureClass.RFC_6979,
172
-        default=tests.SSHTestKeyDeterministicSignatureClass.RFC_6979,
171
+        const=tests.data.SSHTestKeyDeterministicSignatureClass.RFC_6979,
172
+        default=tests.data.SSHTestKeyDeterministicSignatureClass.RFC_6979,
173 173
         help="assume RFC 6979 signatures for deterministic DSA",
174 174
     )
175 175
     group.add_argument(
176 176
         "--pageant-068-080",
177 177
         action="store_const",
178 178
         dest="deterministic_signature_class",
179
-        const=tests.SSHTestKeyDeterministicSignatureClass.Pageant_068_080,
180
-        default=tests.SSHTestKeyDeterministicSignatureClass.RFC_6979,
179
+        const=tests.data.SSHTestKeyDeterministicSignatureClass.Pageant_068_080,
180
+        default=tests.data.SSHTestKeyDeterministicSignatureClass.RFC_6979,
181 181
         help="assume Pageant 0.68-0.80 signatures for deterministic DSA",
182 182
     )
183 183
     ap.add_argument(
... ...
@@ -189,7 +189,7 @@ def main(argv: list[str] | None = None) -> None:
189 189
     )
190 190
     args = ap.parse_args(args=argv)
191 191
     if not args.keynames:
192
-        args.keynames = list(tests.ALL_KEYS.keys())
192
+        args.keynames = list(tests.data.ALL_KEYS.keys())
193 193
     with ssh_agent.SSHAgentClient.ensure_agent_subcontext() as client:
194 194
         for keyname in args.keynames:
195 195
             key = try_key(
... ...
@@ -2,3120 +2,53 @@
2 2
 #
3 3
 # SPDX-License-Identifier: Zlib
4 4
 
5
-from __future__ import annotations
5
+"""The `derivepassphrase` test suite.
6 6
 
7
-import base64
8
-import contextlib
9
-import copy
10
-import enum
11
-import errno
12
-import importlib.util
13
-import json
14
-import logging
15
-import os
16
-import pathlib
17
-import re
18
-import shlex
19
-import stat
20
-import sys
21
-import tempfile
22
-import types
23
-import zipfile
24
-from typing import TYPE_CHECKING, TypedDict
7
+Overview
8
+========
25 9
 
26
-import click.testing
27
-import hypothesis
28
-import pytest
29
-from hypothesis import strategies
30
-from typing_extensions import NamedTuple, assert_never, overload
10
+The `derivepassphrase` test suite contains testing support code and the
11
+actual unit- and integration-test functions.
31 12
 
32
-from derivepassphrase import _types, cli, ssh_agent, vault
33
-from derivepassphrase._internals import cli_helpers, cli_machinery
34
-from derivepassphrase.ssh_agent import socketprovider
13
+Layout (test functions)
14
+=======================
35 15
 
36
-__all__ = ()
16
+`derivepassphrase` uses `pytest`, `coverage` and `hypothesis` for
17
+testing.  Tests are auto-discovered using `pytest`'s test collection
18
+machinery and configuration system, and may use `hypothesis` for
19
+parametrization.
37 20
 
38
-if TYPE_CHECKING:
39
-    import socket
40
-    from collections.abc import Callable, Iterator, Mapping, Sequence
41
-    from contextlib import AbstractContextManager
42
-    from typing import IO, NotRequired
21
+Layout (support code)
22
+=====================
43 23
 
44
-    from typing_extensions import Any, Buffer, Self
24
+The testing support code is divided into five modules in two
25
+subpackages: `tests.data`, `tests.data.callables`, `tests.machinery`,
26
+`tests.machinery.pytest` and `tests.machinery.hypothesis`.  They have
27
+strict import requirements: any module in this list may only import data
28
+and functionality from modules earlier in this list.
45 29
 
30
+  * The package `tests.data` includes static test data, types, and
31
+    associated lightweight machinery (think: accessors, categorization
32
+    functions, and the like).
46 33
 
47
-class SSHTestKeyDeterministicSignatureClass(str, enum.Enum):
48
-    """The class of a deterministic signature from an SSH test key.
34
+  * The package `tests.data.callables` includes functions that operate
35
+    on test data, or that alternatively implement stubbed versions of
36
+    proper `derivepassphrase` functionality.  This includes test
37
+    doubles, if they do not depend on the presence of certain test
38
+    machinery.  Data is only included in this module (instead of in
39
+    `tests.data`) if it depends on the functions in this module.
49 40
 
50
-    Attributes:
51
-        SPEC:
52
-            A deterministic signature directly implied by the
53
-            specification of the signature algorithm.
54
-        RFC_6979:
55
-            A deterministic signature as specified by RFC 6979.  Only
56
-            used with DSA and ECDSA keys (that aren't also EdDSA keys).
57
-        Pageant_068_080:
58
-            A deterministic signature as specified by Pageant 0.68.
59
-            Only used with DSA and ECDSA keys (that aren't also EdDSA
60
-            keys), and only used with Pageant from 0.68 up to and
61
-            including 0.80.
41
+  * The package `tests.machinery` includes data and functions that is
42
+    not specific to any test system, but which nonetheless is of little
43
+    value outside of such a system.  This includes "fakes", i.e.
44
+    reasonably complete reimplementations of existing `derivepassphrase`
45
+    functionality, for comparison or "switching out" purposes.
62 46
 
63
-            Usage of this signature class together with an ECDSA NIST
64
-            P-521 key [turned out to leak enough information per
65
-            signature to quickly compromise the entire private key
66
-            (CVE-2024-31497)][PUTTY_CVE_2024_31497], so newer Pageant
67
-            versions abandon this signature class in favor of RFC 6979.
47
+  * The package `tests.machinery.pytest` includes `pytest`-specific data
48
+    and functions, such as marks and parametrization sets.
68 49
 
69
-            [PUTTY_CVE_2024_31497]: https://www.chiark.greenend.org.uk/~sgtatham/putty/wishlist/vuln-p521-bias.html
50
+  * The package `tests.machinery.hypothesis` includes
51
+    `hypothesis`-specific data and functions, such as `hypothesis`
52
+    strategies.
70 53
 
71 54
 """
72
-
73
-    SPEC = enum.auto()
74
-    """"""
75
-    RFC_6979 = enum.auto()
76
-    """"""
77
-    Pageant_068_080 = enum.auto()
78
-    """"""
79
-
80
-
81
-class SSHTestKeyDeterministicSignature(NamedTuple):
82
-    """An SSH test key deterministic signature.
83
-
84
-    Attributes:
85
-        signature:
86
-            The binary signature of the [vault UUID][vault.Vault.UUID]
87
-            under this signature class.
88
-        derived_passphrase:
89
-            The equivalent master passphrase derived from this
90
-            signature.
91
-        signature_class:
92
-            The [signature
93
-            class][SSHTestKeyDeterministicSignatureClass].
94
-
95
-    """
96
-
97
-    signature: bytes
98
-    """"""
99
-    derived_passphrase: bytes
100
-    """"""
101
-    signature_class: SSHTestKeyDeterministicSignatureClass = (
102
-        SSHTestKeyDeterministicSignatureClass.SPEC
103
-    )
104
-    """"""
105
-
106
-
107
-class SSHTestKey(NamedTuple):
108
-    """An SSH test key.
109
-
110
-    Attributes:
111
-        public_key:
112
-            The SSH public key string, as used e.g. by OpenSSH's
113
-            `authorized_keys` file.  Includes a comment.
114
-        public_key_data:
115
-            The SSH protocol wire format of the public key.
116
-        private_key:
117
-            A base64 encoded representation of the private key, in
118
-            OpenSSH's v1 private key format.
119
-        private_key_blob:
120
-            The SSH protocol wire format of the private key.
121
-        expected_signatures:
122
-            A mapping of deterministic signature classes to the
123
-            expected, deterministic signature (of that class) of the
124
-            vault UUID for this key, together with the respective
125
-            "equivalent master passphrase" derived from this signature.
126
-
127
-    """
128
-
129
-    public_key: bytes
130
-    """"""
131
-    public_key_data: bytes
132
-    """"""
133
-    private_key: bytes
134
-    """"""
135
-    private_key_blob: bytes
136
-    """"""
137
-    expected_signatures: Mapping[
138
-        SSHTestKeyDeterministicSignatureClass, SSHTestKeyDeterministicSignature
139
-    ]
140
-    """"""
141
-
142
-    def is_suitable(
143
-        self,
144
-        *,
145
-        client: ssh_agent.SSHAgentClient | None = None,
146
-    ) -> bool:
147
-        """Return if this key is suitable for use with vault.
148
-
149
-        Args:
150
-            client:
151
-                An optional SSH agent client to check for additional
152
-                deterministic key types. If not given, assume no such
153
-                types.
154
-
155
-        """
156
-        return vault.Vault.is_suitable_ssh_key(
157
-            self.public_key_data, client=client
158
-        )
159
-
160
-
161
-class ValidationSettings(NamedTuple):
162
-    """Validation settings for [`VaultTestConfig`][]s.
163
-
164
-    Attributes:
165
-        allow_unknown_settings:
166
-            See [`_types.validate_vault_config`][].
167
-
168
-    """
169
-
170
-    allow_unknown_settings: bool
171
-    """"""
172
-
173
-
174
-class VaultTestConfig(NamedTuple):
175
-    """A (not necessarily valid) sample vault config, plus metadata.
176
-
177
-    Attributes:
178
-        config:
179
-            The actual configuration object.  Usually a [`dict`][].
180
-        comment:
181
-            An explanatory comment for what is wrong with this config,
182
-            or empty if the config is valid.  This is intended as
183
-            a debugging message to be shown to the user (e.g. when an
184
-            assertion fails), not as an error message to
185
-            programmatically match against.
186
-        validation_settings:
187
-            See [`_types.validate_vault_config`][].
188
-
189
-    """
190
-
191
-    config: Any
192
-    """"""
193
-    comment: str
194
-    """"""
195
-    validation_settings: ValidationSettings | None
196
-    """"""
197
-
198
-
199
-TEST_CONFIGS: list[VaultTestConfig] = [
200
-    VaultTestConfig(None, "not a dict", None),
201
-    VaultTestConfig({}, "missing required keys", None),
202
-    VaultTestConfig(
203
-        {"global": None, "services": {}}, "bad config value: global", None
204
-    ),
205
-    VaultTestConfig(
206
-        {"global": {"key": 123}, "services": {}},
207
-        "bad config value: global.key",
208
-        None,
209
-    ),
210
-    VaultTestConfig(
211
-        {"global": {"phrase": "abc", "key": "..."}, "services": {}},
212
-        "",
213
-        None,
214
-    ),
215
-    VaultTestConfig({"services": None}, "bad config value: services", None),
216
-    VaultTestConfig(
217
-        {"services": {"1": {}, 2: {}}}, 'bad config value: services."2"', None
218
-    ),
219
-    VaultTestConfig(
220
-        {"services": {"1": {}, "2": 2}}, 'bad config value: services."2"', None
221
-    ),
222
-    VaultTestConfig(
223
-        {"services": {"sv": {"notes": ["sentinel", "list"]}}},
224
-        "bad config value: services.sv.notes",
225
-        None,
226
-    ),
227
-    VaultTestConfig(
228
-        {"services": {"sv": {"notes": "blah blah blah"}}}, "", None
229
-    ),
230
-    VaultTestConfig(
231
-        {"services": {"sv": {"length": "200"}}},
232
-        "bad config value: services.sv.length",
233
-        None,
234
-    ),
235
-    VaultTestConfig(
236
-        {"services": {"sv": {"length": 0.5}}},
237
-        "bad config value: services.sv.length",
238
-        None,
239
-    ),
240
-    VaultTestConfig(
241
-        {"services": {"sv": {"length": ["sentinel", "list"]}}},
242
-        "bad config value: services.sv.length",
243
-        None,
244
-    ),
245
-    VaultTestConfig(
246
-        {"services": {"sv": {"length": -10}}},
247
-        "bad config value: services.sv.length",
248
-        None,
249
-    ),
250
-    VaultTestConfig(
251
-        {"services": {"sv": {"lower": "10"}}},
252
-        "bad config value: services.sv.lower",
253
-        None,
254
-    ),
255
-    VaultTestConfig(
256
-        {"services": {"sv": {"upper": -10}}},
257
-        "bad config value: services.sv.upper",
258
-        None,
259
-    ),
260
-    VaultTestConfig(
261
-        {"services": {"sv": {"number": ["sentinel", "list"]}}},
262
-        "bad config value: services.sv.number",
263
-        None,
264
-    ),
265
-    VaultTestConfig(
266
-        {
267
-            "global": {"phrase": "my secret phrase"},
268
-            "services": {"sv": {"length": 10}},
269
-        },
270
-        "",
271
-        None,
272
-    ),
273
-    VaultTestConfig(
274
-        {"services": {"sv": {"length": 10, "phrase": "..."}}}, "", None
275
-    ),
276
-    VaultTestConfig(
277
-        {"services": {"sv": {"length": 10, "key": "..."}}}, "", None
278
-    ),
279
-    VaultTestConfig(
280
-        {"services": {"sv": {"upper": 10, "key": "..."}}}, "", None
281
-    ),
282
-    VaultTestConfig(
283
-        {"services": {"sv": {"phrase": "abc", "key": "..."}}}, "", None
284
-    ),
285
-    VaultTestConfig(
286
-        {
287
-            "global": {"phrase": "abc"},
288
-            "services": {"sv": {"phrase": "abc", "length": 10}},
289
-        },
290
-        "",
291
-        None,
292
-    ),
293
-    VaultTestConfig(
294
-        {
295
-            "global": {"key": "..."},
296
-            "services": {"sv": {"phrase": "abc", "length": 10}},
297
-        },
298
-        "",
299
-        None,
300
-    ),
301
-    VaultTestConfig(
302
-        {
303
-            "global": {"key": "..."},
304
-            "services": {"sv": {"phrase": "abc", "key": "...", "length": 10}},
305
-        },
306
-        "",
307
-        None,
308
-    ),
309
-    VaultTestConfig(
310
-        {
311
-            "global": {"key": "..."},
312
-            "services": {
313
-                "sv1": {"phrase": "abc", "length": 10, "upper": 1},
314
-                "sv2": {"length": 10, "repeat": 1, "lower": 1},
315
-            },
316
-        },
317
-        "",
318
-        None,
319
-    ),
320
-    VaultTestConfig(
321
-        {
322
-            "global": {"key": "...", "unicode_normalization_form": "NFC"},
323
-            "services": {
324
-                "sv1": {"phrase": "abc", "length": 10, "upper": 1},
325
-                "sv2": {"length": 10, "repeat": 1, "lower": 1},
326
-            },
327
-        },
328
-        "",
329
-        None,
330
-    ),
331
-    VaultTestConfig(
332
-        {
333
-            "global": {"key": "...", "unicode_normalization_form": True},
334
-            "services": {},
335
-        },
336
-        "bad config value: global.unicode_normalization_form",
337
-        None,
338
-    ),
339
-    VaultTestConfig(
340
-        {
341
-            "global": {"key": "...", "unicode_normalization_form": "NFC"},
342
-            "services": {
343
-                "sv1": {"phrase": "abc", "length": 10, "upper": 1},
344
-                "sv2": {"length": 10, "repeat": 1, "lower": 1},
345
-            },
346
-        },
347
-        "",
348
-        ValidationSettings(True),
349
-    ),
350
-    VaultTestConfig(
351
-        {
352
-            "global": {"key": "...", "unicode_normalization_form": "NFC"},
353
-            "services": {
354
-                "sv1": {"phrase": "abc", "length": 10, "upper": 1},
355
-                "sv2": {"length": 10, "repeat": 1, "lower": 1},
356
-            },
357
-        },
358
-        "extension/unknown key: .global.unicode_normalization_form",
359
-        ValidationSettings(False),
360
-    ),
361
-    VaultTestConfig(
362
-        {
363
-            "global": {"key": "...", "unknown_key": True},
364
-            "services": {
365
-                "sv1": {"phrase": "abc", "length": 10, "upper": 1},
366
-                "sv2": {"length": 10, "repeat": 1, "lower": 1},
367
-            },
368
-        },
369
-        "",
370
-        ValidationSettings(True),
371
-    ),
372
-    VaultTestConfig(
373
-        {
374
-            "global": {"key": "...", "unknown_key": True},
375
-            "services": {
376
-                "sv1": {"phrase": "abc", "length": 10, "upper": 1},
377
-                "sv2": {"length": 10, "repeat": 1, "lower": 1},
378
-            },
379
-        },
380
-        "unknown key: .global.unknown_key",
381
-        ValidationSettings(False),
382
-    ),
383
-    VaultTestConfig(
384
-        {
385
-            "global": {"key": "..."},
386
-            "services": {
387
-                "sv1": {"phrase": "abc", "length": 10, "upper": 1},
388
-                "sv2": {
389
-                    "length": 10,
390
-                    "repeat": 1,
391
-                    "lower": 1,
392
-                    "unknown_key": True,
393
-                },
394
-            },
395
-        },
396
-        "unknown key: .services.sv2.unknown_key",
397
-        ValidationSettings(False),
398
-    ),
399
-    VaultTestConfig(
400
-        {
401
-            "global": {"key": "...", "unicode_normalization_form": "NFC"},
402
-            "services": {
403
-                "sv1": {"phrase": "abc", "length": 10, "upper": 1},
404
-                "sv2": {
405
-                    "length": 10,
406
-                    "repeat": 1,
407
-                    "lower": 1,
408
-                    "unknown_key": True,
409
-                },
410
-            },
411
-        },
412
-        "",
413
-        ValidationSettings(True),
414
-    ),
415
-    VaultTestConfig(
416
-        {
417
-            "global": {"key": "...", "unicode_normalization_form": "NFC"},
418
-            "services": {
419
-                "sv1": {"phrase": "abc", "length": 10, "upper": 1},
420
-                "sv2": {
421
-                    "length": 10,
422
-                    "repeat": 1,
423
-                    "lower": 1,
424
-                    "unknown_key": True,
425
-                },
426
-            },
427
-        },
428
-        "",
429
-        ValidationSettings(True),
430
-    ),
431
-    VaultTestConfig(
432
-        {
433
-            "global": {"key": "...", "unicode_normalization_form": "NFC"},
434
-            "services": {
435
-                "sv1": {"phrase": "abc", "length": 10, "upper": 1},
436
-                "sv2": {
437
-                    "length": 10,
438
-                    "repeat": 1,
439
-                    "lower": 1,
440
-                    "unknown_key": True,
441
-                },
442
-            },
443
-        },
444
-        "",
445
-        ValidationSettings(True),
446
-    ),
447
-]
448
-"""The master list of test configurations for vault."""
449
-
450
-
451
-def is_valid_test_config(conf: VaultTestConfig, /) -> bool:
452
-    """Return true if the test config is valid.
453
-
454
-    Args:
455
-        conf: The test config to check.
456
-
457
-    """
458
-    return not conf.comment and conf.validation_settings in {
459
-        None,
460
-        (True,),
461
-    }
462
-
463
-
464
-def _test_config_ids(val: VaultTestConfig) -> Any:  # pragma: no cover
465
-    """pytest id function for VaultTestConfig objects."""
466
-    assert isinstance(val, VaultTestConfig)
467
-    return val[1] or (val[0], val[1], val[2])
468
-
469
-
470
-@strategies.composite
471
-def vault_full_service_config(draw: strategies.DrawFn) -> dict[str, int]:
472
-    """Hypothesis strategy for full vault service configurations.
473
-
474
-    Returns a sample configuration with restrictions on length, repeat
475
-    count, and all character classes, while ensuring the settings are
476
-    not obviously unsatisfiable.
477
-
478
-    Args:
479
-        draw:
480
-            The `draw` function, as provided for by hypothesis.
481
-
482
-    """
483
-    repeat = draw(strategies.integers(min_value=0, max_value=10))
484
-    lower = draw(strategies.integers(min_value=0, max_value=10))
485
-    upper = draw(strategies.integers(min_value=0, max_value=10))
486
-    number = draw(strategies.integers(min_value=0, max_value=10))
487
-    space = draw(strategies.integers(min_value=0, max_value=repeat))
488
-    dash = draw(strategies.integers(min_value=0, max_value=10))
489
-    symbol = draw(strategies.integers(min_value=0, max_value=10))
490
-    length = draw(
491
-        strategies.integers(
492
-            min_value=max(1, lower + upper + number + space + dash + symbol),
493
-            max_value=70,
494
-        )
495
-    )
496
-    hypothesis.assume(lower + upper + number + dash + symbol > 0)
497
-    hypothesis.assume(lower + upper + number + space + symbol > 0)
498
-    hypothesis.assume(repeat >= space)
499
-    return {
500
-        "lower": lower,
501
-        "upper": upper,
502
-        "number": number,
503
-        "space": space,
504
-        "dash": dash,
505
-        "symbol": symbol,
506
-        "repeat": repeat,
507
-        "length": length,
508
-    }
509
-
510
-
511
-def is_smudgable_vault_test_config(conf: VaultTestConfig) -> bool:
512
-    """Check whether this vault test config can be effectively smudged.
513
-
514
-    A "smudged" test config is one where falsy values (in the JavaScript
515
-    sense) can be replaced by other falsy values without changing the
516
-    meaning of the config.
517
-
518
-    Args:
519
-        conf: A test config to check.
520
-
521
-    Returns:
522
-        True if the test config can be smudged, False otherwise.
523
-
524
-    """
525
-    config = conf.config
526
-    return bool(
527
-        isinstance(config, dict)
528
-        and ("global" not in config or isinstance(config["global"], dict))
529
-        and ("services" in config and isinstance(config["services"], dict))
530
-        and all(isinstance(x, dict) for x in config["services"].values())
531
-        and (config["services"] or config.get("global"))
532
-    )
533
-
534
-
535
-@strategies.composite
536
-def smudged_vault_test_config(
537
-    draw: strategies.DrawFn,
538
-    config: Any = strategies.sampled_from(TEST_CONFIGS).filter(  # noqa: B008
539
-        is_smudgable_vault_test_config
540
-    ),
541
-) -> Any:
542
-    """Hypothesis strategy to replace falsy values with other falsy values.
543
-
544
-    Uses [`_types.js_truthiness`][] internally, which is tested
545
-    separately by
546
-    [`tests.test_derivepassphrase_types.test_100_js_truthiness`][].
547
-
548
-    Args:
549
-        draw:
550
-            The `draw` function, as provided for by hypothesis.
551
-        config:
552
-            A strategy which generates [`VaultTestConfig`][] objects.
553
-
554
-    Returns:
555
-        A new [`VaultTestConfig`][] where some falsy values have been
556
-        replaced or added.
557
-
558
-    """
559
-
560
-    falsy = (None, False, 0, 0.0, "", float("nan"))
561
-    falsy_no_str = (None, False, 0, 0.0, float("nan"))
562
-    falsy_no_zero = (None, False, "", float("nan"))
563
-    conf = draw(config)
564
-    hypothesis.assume(is_smudgable_vault_test_config(conf))
565
-    obj = copy.deepcopy(conf.config)
566
-    services: list[dict[str, Any]] = list(obj["services"].values())
567
-    if "global" in obj:
568
-        services.append(obj["global"])
569
-    assert all(isinstance(x, dict) for x in services), (
570
-        "is_smudgable_vault_test_config guard failed to "
571
-        "ensure each settings dict is a dict"
572
-    )
573
-    for service in services:
574
-        for key in ("phrase",):
575
-            value = service.get(key)
576
-            if not _types.js_truthiness(value) and value != "":
577
-                service[key] = draw(strategies.sampled_from(falsy_no_str))
578
-        for key in (
579
-            "notes",
580
-            "key",
581
-            "length",
582
-            "repeat",
583
-        ):
584
-            value = service.get(key)
585
-            if not _types.js_truthiness(value):
586
-                service[key] = draw(strategies.sampled_from(falsy))
587
-        for key in (
588
-            "lower",
589
-            "upper",
590
-            "number",
591
-            "space",
592
-            "dash",
593
-            "symbol",
594
-        ):
595
-            value = service.get(key)
596
-            if not _types.js_truthiness(value) and value != 0:
597
-                service[key] = draw(strategies.sampled_from(falsy_no_zero))
598
-    hypothesis.assume(obj != conf.config)
599
-    return VaultTestConfig(obj, conf.comment, conf.validation_settings)
600
-
601
-
602
-class KnownSSHAgent(str, enum.Enum):
603
-    """Known SSH agents.
604
-
605
-    Attributes:
606
-        UNKNOWN (str):
607
-            Not a known agent, or not known statically.
608
-        Pageant (str):
609
-            The agent from Simon Tatham's PuTTY suite.
610
-        OpenSSHAgent (str):
611
-            The agent from OpenBSD's OpenSSH suite.
612
-        StubbedSSHAgent (str):
613
-            The stubbed, fake agent pseudo-socket defined in this test
614
-            suite.
615
-
616
-    """
617
-
618
-    UNKNOWN = "(unknown)"
619
-    """"""
620
-    Pageant = "Pageant"
621
-    """"""
622
-    OpenSSHAgent = "OpenSSHAgent"
623
-    """"""
624
-    StubbedSSHAgent = "StubbedSSHAgent"
625
-    """"""
626
-
627
-
628
-class SpawnedSSHAgentInfo(NamedTuple):
629
-    """Info about a spawned SSH agent, as provided by some fixtures.
630
-
631
-    Differs from [`RunningSSHAgentInfo`][] in that this info object
632
-    already provides a functional client connected to the agent, but not
633
-    the address.
634
-
635
-    Attributes:
636
-        agent_type:
637
-            The agent's type.
638
-        client:
639
-            An SSH agent client connected to this agent.
640
-        isolated:
641
-            Whether this agent was spawned specifically for this test
642
-            suite, with attempts to isolate it from the user.  If false,
643
-            then the user may be interacting with the agent externally,
644
-            meaning e.g. keys other than the test keys may be visible in
645
-            this agent.
646
-
647
-    """
648
-
649
-    agent_type: KnownSSHAgent
650
-    """"""
651
-    client: ssh_agent.SSHAgentClient
652
-    """"""
653
-    isolated: bool
654
-    """"""
655
-
656
-
657
-class RunningSSHAgentInfo(NamedTuple):
658
-    """Info about a running SSH agent, as provided by some fixtures.
659
-
660
-    Differs from [`SpawnedSSHAgentInfo`][] in that this info object
661
-    provides only an address of the agent, not a functional client
662
-    already connected to it.  The running SSH agent may or may not be
663
-    isolated.
664
-
665
-    Attributes:
666
-        socket:
667
-            A socket address to connect to the agent.
668
-        agent_type:
669
-            The agent's type.
670
-
671
-    """
672
-
673
-    socket: str | type[_types.SSHAgentSocket]
674
-    """"""
675
-    agent_type: KnownSSHAgent
676
-    """"""
677
-
678
-    def require_external_address(self) -> str:  # pragma: no cover
679
-        if not isinstance(self.socket, str):
680
-            pytest.skip(
681
-                reason="This test requires a real, externally resolvable "
682
-                "address for the SSH agent socket."
683
-            )
684
-        return self.socket
685
-
686
-
687
-ALL_KEYS: Mapping[str, SSHTestKey] = {
688
-    "ed25519": SSHTestKey(
689
-        private_key=rb"""-----BEGIN OPENSSH PRIVATE KEY-----
690
-b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
691
-QyNTUxOQAAACCBeIFoJtYCSF8P/zJIb+TBMIncHGpFBgnpCQ/7whJpdgAAAKDweO7H8Hju
692
-xwAAAAtzc2gtZWQyNTUxOQAAACCBeIFoJtYCSF8P/zJIb+TBMIncHGpFBgnpCQ/7whJpdg
693
-AAAEAbM/A869nkWZbe2tp3Dm/L6gitvmpH/aRZt8sBII3ExYF4gWgm1gJIXw//Mkhv5MEw
694
-idwcakUGCekJD/vCEml2AAAAG3Rlc3Qga2V5IHdpdGhvdXQgcGFzc3BocmFzZQEC
695
------END OPENSSH PRIVATE KEY-----
696
-""",
697
-        private_key_blob=bytes.fromhex("""
698
-            00 00 00 0b 73 73 68 2d 65 64 32 35 35 31 39
699
-            00 00 00 20
700
-            81 78 81 68 26 d6 02 48 5f 0f ff 32 48 6f e4 c1
701
-            30 89 dc 1c 6a 45 06 09 e9 09 0f fb c2 12 69 76
702
-            00 00 00 40
703
-            1b 33 f0 3c eb d9 e4 59 96 de da da 77 0e 6f cb
704
-            ea 08 ad be 6a 47 fd a4 59 b7 cb 01 20 8d c4 c5
705
-            81 78 81 68 26 d6 02 48 5f 0f ff 32 48 6f e4 c1
706
-            30 89 dc 1c 6a 45 06 09 e9 09 0f fb c2 12 69 76
707
-            00 00 00 1b 74 65 73 74 20 6b 65 79 20 77 69 74
708
-            68 6f 75 74 20 70 61 73 73 70 68 72 61 73 65
709
-"""),
710
-        public_key=rb"""ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIF4gWgm1gJIXw//Mkhv5MEwidwcakUGCekJD/vCEml2 test key without passphrase
711
-""",
712
-        public_key_data=bytes.fromhex("""
713
-            00 00 00 0b 73 73 68 2d 65 64 32 35 35 31 39
714
-            00 00 00 20
715
-            81 78 81 68 26 d6 02 48 5f 0f ff 32 48 6f e4 c1
716
-            30 89 dc 1c 6a 45 06 09 e9 09 0f fb c2 12 69 76
717
-"""),
718
-        expected_signatures={
719
-            SSHTestKeyDeterministicSignatureClass.SPEC: SSHTestKeyDeterministicSignature(
720
-                signature=bytes.fromhex("""
721
-                    00 00 00 0b 73 73 68 2d 65 64 32 35 35 31 39
722
-                    00 00 00 40
723
-                    f0 98 19 80 6c 1a 97 d5 26 03 6e cc e3 65 8f 86
724
-                    66 07 13 19 13 09 21 33 33 f9 e4 36 53 1d af fd
725
-                    0d 08 1f ec f8 73 9b 8c 5f 55 39 16 7c 53 54 2c
726
-                    1e 52 bb 30 ed 7f 89 e2 2f 69 51 55 d8 9e a6 02
727
-"""),
728
-                derived_passphrase=rb"""8JgZgGwal9UmA27M42WPhmYHExkTCSEzM/nkNlMdr/0NCB/s+HObjF9VORZ8U1QsHlK7MO1/ieIvaVFV2J6mAg==""",
729
-            ),
730
-        },
731
-    ),
732
-    # Currently only supported by PuTTY (which is deficient in other
733
-    # niceties of the SSH agent and the agent's client).
734
-    "ed448": SSHTestKey(
735
-        private_key=rb"""-----BEGIN OPENSSH PRIVATE KEY-----
736
-b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAASgAAAAlz
737
-c2gtZWQ0NDgAAAA54vZy009Wu8wExjvEb3hqtLz1GO/+d5vmGUbErWQ4AUO9mYLT
738
-zHJHc2m4s+yWzP29Cc3EcxizLG8AAAAA8BdhfCcXYXwnAAAACXNzaC1lZDQ0OAAA
739
-ADni9nLTT1a7zATGO8RveGq0vPUY7/53m+YZRsStZDgBQ72ZgtPMckdzabiz7JbM
740
-/b0JzcRzGLMsbwAAAAByM7GIMRvWJB3YD6SIpAF2uudX4ozZe0X917wPwiBrs373
741
-9TM1n94Nib6hrxGNmCk2iBQDe2KALPgA4vZy009Wu8wExjvEb3hqtLz1GO/+d5vm
742
-GUbErWQ4AUO9mYLTzHJHc2m4s+yWzP29Cc3EcxizLG8AAAAAG3Rlc3Qga2V5IHdp
743
-dGhvdXQgcGFzc3BocmFzZQECAwQFBgcICQ==
744
------END OPENSSH PRIVATE KEY-----
745
-""",
746
-        private_key_blob=bytes.fromhex("""
747
-            00 00 00 09 73 73 68 2d 65 64 34 34 38
748
-            00 00 00 39 e2 f6 72 d3 4f 56 bb cc 04
749
-            c6 3b c4 6f 78 6a b4 bc f5 18 ef fe 77 9b e6 19
750
-            46 c4 ad 64 38 01 43 bd 99 82 d3 cc 72 47 73 69
751
-            b8 b3 ec 96 cc fd bd 09 cd c4 73 18 b3 2c 6f 00
752
-            00 00 00 72 33 b1
753
-            88 31 1b d6 24 1d d8 0f a4 88 a4 01 76 ba e7 57
754
-            e2 8c d9 7b 45 fd d7 bc 0f c2 20 6b b3 7e f7 f5
755
-            33 35 9f de 0d 89 be a1 af 11 8d 98 29 36 88 14
756
-            03 7b 62 80 2c f8 00 e2 f6 72 d3 4f 56 bb cc 04
757
-            c6 3b c4 6f 78 6a b4 bc f5 18 ef fe 77 9b e6 19
758
-            46 c4 ad 64 38 01 43 bd 99 82 d3 cc 72 47 73 69
759
-            b8 b3 ec 96 cc fd bd 09 cd c4 73 18 b3 2c 6f 00
760
-            00 00 00 1b 74 65 73 74 20 6b 65 79 20 77 69
761
-            74 68 6f 75 74 20 70 61 73 73 70 68 72 61 73 65
762
-"""),
763
-        public_key=rb"""ssh-ed448 AAAACXNzaC1lZDQ0OAAAADni9nLTT1a7zATGO8RveGq0vPUY7/53m+YZRsStZDgBQ72ZgtPMckdzabiz7JbM/b0JzcRzGLMsbwA= test key without passphrase
764
-""",
765
-        public_key_data=bytes.fromhex("""
766
-            00 00 00 09 73 73 68 2d 65 64 34 34 38
767
-            00 00 00 39 e2 f6 72 d3 4f 56 bb cc 04
768
-            c6 3b c4 6f 78 6a b4 bc f5 18 ef fe 77 9b e6 19
769
-            46 c4 ad 64 38 01 43 bd 99 82 d3 cc 72 47 73 69
770
-            b8 b3 ec 96 cc fd bd 09 cd c4 73 18 b3 2c 6f 00
771
-        """),
772
-        expected_signatures={
773
-            SSHTestKeyDeterministicSignatureClass.SPEC: SSHTestKeyDeterministicSignature(
774
-                signature=bytes.fromhex("""
775
-                    00 00 00 09 73 73 68 2d 65 64 34 34 38
776
-                    00 00 00 72 06 86
777
-                    f4 64 a4 a6 ba d9 c3 22 c4 93 49 99 fc 11 de 67
778
-                    97 08 f2 d8 b7 3c 2c 13 e7 c5 1c 1e 92 a6 0e d8
779
-                    2f 6d 81 03 82 00 e3 72 e4 32 6d 72 d2 6d 32 84
780
-                    3f cc a9 1e 57 2c 00 9a b3 99 de 45 da ce 2e d1
781
-                    db e5 89 f3 35 be 24 58 90 c6 ca 04 f0 db 88 80
782
-                    db bd 77 7c 80 20 7f 3a 48 61 f6 1f ae a9 5e 53
783
-                    7b e0 9d 93 1e ea dc eb b5 cd 56 4c ea 8f 08 00
784
-"""),
785
-                derived_passphrase=rb"""Bob0ZKSmutnDIsSTSZn8Ed5nlwjy2Lc8LBPnxRwekqYO2C9tgQOCAONy5DJtctJtMoQ/zKkeVywAmrOZ3kXazi7R2+WJ8zW+JFiQxsoE8NuIgNu9d3yAIH86SGH2H66pXlN74J2THurc67XNVkzqjwgA""",
786
-            ),
787
-        },
788
-    ),
789
-    "rsa": SSHTestKey(
790
-        private_key=rb"""-----BEGIN OPENSSH PRIVATE KEY-----
791
-b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
792
-NhAAAAAwEAAQAAAYEAsaHu6Xs4cVsuDSNJlMCqoPVgmDgEviI8TfXmHKqX3JkIqI3LsvV7
793
-Ijf8WCdTveEq7CkuZhImtsR52AOEVAoU8mDXDNr+nJ5wUPzf1UIaRjDe0lcXW4SlF01hQs
794
-G4wYDuqxshwelraB/L3e0zhD7fjYHF8IbFsqGlFHWEwOtlfhhfbxJsTGguLm4A8/gdEJD5
795
-2rkqDcZpIXCHtJbCzW9aQpWcs/PDw5ylwl/3dB7jfxyfrGz4O3QrzsqhWEsip97mOmwl6q
796
-CHbq8V8x9zu89D/H+bG5ijqxhijbjcVUW3lZfw/97gy9J6rG31HNar5H8GycLTFwuCFepD
797
-mTEpNgQLKoe8ePIEPq4WHhFUovBdwlrOByUKKqxreyvWt5gkpTARz+9Lt8OjBO3rpqK8sZ
798
-VKH3sE3de2RJM3V9PJdmZSs2b8EFK3PsUGdlMPM9pn1uk4uIItKWBmooOynuD8Ll6aPwuW
799
-AFn3l8nLLyWdrmmEYzHWXiRjQJxy1Bi5AbHMOWiPAAAFkDPkuBkz5LgZAAAAB3NzaC1yc2
800
-EAAAGBALGh7ul7OHFbLg0jSZTAqqD1YJg4BL4iPE315hyql9yZCKiNy7L1eyI3/FgnU73h
801
-KuwpLmYSJrbEedgDhFQKFPJg1wza/pyecFD839VCGkYw3tJXF1uEpRdNYULBuMGA7qsbIc
802
-Hpa2gfy93tM4Q+342BxfCGxbKhpRR1hMDrZX4YX28SbExoLi5uAPP4HRCQ+dq5Kg3GaSFw
803
-h7SWws1vWkKVnLPzw8OcpcJf93Qe438cn6xs+Dt0K87KoVhLIqfe5jpsJeqgh26vFfMfc7
804
-vPQ/x/mxuYo6sYYo243FVFt5WX8P/e4MvSeqxt9RzWq+R/BsnC0xcLghXqQ5kxKTYECyqH
805
-vHjyBD6uFh4RVKLwXcJazgclCiqsa3sr1reYJKUwEc/vS7fDowTt66aivLGVSh97BN3Xtk
806
-STN1fTyXZmUrNm/BBStz7FBnZTDzPaZ9bpOLiCLSlgZqKDsp7g/C5emj8LlgBZ95fJyy8l
807
-na5phGMx1l4kY0CcctQYuQGxzDlojwAAAAMBAAEAAAF/cNVYT+Om4x9+SItcz5bOByGIOj
808
-yWUH8f9rRjnr5ILuwabIDgvFaVG+xM1O1hWADqzMnSEcknHRkTYEsqYPykAtxFvjOFEh70
809
-6qRUJ+fVZkqRGEaI3oWyWKTOhcCIYImtONvb0LOv/HQ2H2AXCoeqjST1qr/xSuljBtcB8u
810
-wxs3EqaO1yU7QoZpDcMX9plH7Rmc9nNfZcgrnktPk2deX2+Y/A5tzdVgG1IeqYp6CBMLNM
811
-uhL0OPdDehgBoDujx+rhkZ1gpo1wcULIM94NL7VSHBPX0Lgh9T+3j1HVP+YnMAvhfOvfct
812
-LlbJ06+TYGRAMuF2LPCAZM/m0FEyAurRgWxAjLXm+4kp2GAJXlw82deDkQ+P8cHNT6s9ZH
813
-R5YSy3lpZ35594ZMOLR8KqVvhgJGF6i9019BiF91SDxjE+sp6dNGfN8W+64tHdDv2a0Mso
814
-+8Qjyx7sTpi++EjLU8Iy73/e4B8qbXMyheyA/UUfgMtNKShh6sLlrD9h2Sm9RFTuEAAADA
815
-Jh3u7WfnjhhKZYbAW4TsPNXDMrB0/t7xyAQgFmko7JfESyrJSLg1cO+QMOiDgD7zuQ9RSp
816
-NIKdPsnIna5peh979mVjb2HgnikjyJECmBpLdwZKhX7MnIvgKw5lnQXHboEtWCa1N58l7f
817
-srzwbi9pFUuUp9dShXNffmlUCjDRsVLbK5C6+iaIQyCWFYK8mc6dpNkIoPKf+Xg+EJCIFQ
818
-oITqeu30Gc1+M+fdZc2ghq0b6XLthh/uHEry8b68M5KglMAAAAwQDw1i+IdcvPV/3u/q9O
819
-/kzLpKO3tbT89sc1zhjZsDNjDAGluNr6n38iq/XYRZu7UTL9BG+EgFVfIUV7XsYT5e+BPf
820
-13VS94rzZ7maCsOlULX+VdMO2zBucHIoec9RUlRZrfB21B2W7YGMhbpoa5lN3lKJQ7afHo
821
-dXZUMp0cTFbOmbzJgSzO2/NE7BhVwmvcUzTDJGMMKuxBO6w99YKDKRKm0PNLFDz26rWm9L
822
-dNS2MVfVuPMTpzT26HQG4pFageq9cAAADBALzRBXdZF8kbSBa5MTUBVTTzgKQm1C772gJ8
823
-T01DJEXZsVtOv7mUC1/m/by6Hk4tPyvDBuGj9hHq4N7dPqGutHb1q5n0ADuoQjRW7BXw5Q
824
-vC2EAD91xexdorIA5BgXU+qltBqzzBVzVtF7+jOZOjfzOlaTX9I5I5veyeTaTxZj1XXUzi
825
-btBNdMEJJp7ifucYmoYAAwE7K+VlWagDEK2y8Mte9y9E+N0uO2j+h85sQt/UIb2iE/vhcg
826
-Bgp6142WnSCQAAABt0ZXN0IGtleSB3aXRob3V0IHBhc3NwaHJhc2UB
827
------END OPENSSH PRIVATE KEY-----
828
-""",
829
-        private_key_blob=bytes.fromhex("""
830
-            00 00 00 07 73 73 68 2d 72 73 61
831
-            00 00 01 81 00
832
-            b1 a1 ee e9 7b 38 71 5b 2e 0d 23 49 94 c0 aa a0
833
-            f5 60 98 38 04 be 22 3c 4d f5 e6 1c aa 97 dc 99
834
-            08 a8 8d cb b2 f5 7b 22 37 fc 58 27 53 bd e1 2a
835
-            ec 29 2e 66 12 26 b6 c4 79 d8 03 84 54 0a 14 f2
836
-            60 d7 0c da fe 9c 9e 70 50 fc df d5 42 1a 46 30
837
-            de d2 57 17 5b 84 a5 17 4d 61 42 c1 b8 c1 80 ee
838
-            ab 1b 21 c1 e9 6b 68 1f cb dd ed 33 84 3e df 8d
839
-            81 c5 f0 86 c5 b2 a1 a5 14 75 84 c0 eb 65 7e 18
840
-            5f 6f 12 6c 4c 68 2e 2e 6e 00 f3 f8 1d 10 90 f9
841
-            da b9 2a 0d c6 69 21 70 87 b4 96 c2 cd 6f 5a 42
842
-            95 9c b3 f3 c3 c3 9c a5 c2 5f f7 74 1e e3 7f 1c
843
-            9f ac 6c f8 3b 74 2b ce ca a1 58 4b 22 a7 de e6
844
-            3a 6c 25 ea a0 87 6e af 15 f3 1f 73 bb cf 43 fc
845
-            7f 9b 1b 98 a3 ab 18 62 8d b8 dc 55 45 b7 95 97
846
-            f0 ff de e0 cb d2 7a ac 6d f5 1c d6 ab e4 7f 06
847
-            c9 c2 d3 17 0b 82 15 ea 43 99 31 29 36 04 0b 2a
848
-            87 bc 78 f2 04 3e ae 16 1e 11 54 a2 f0 5d c2 5a
849
-            ce 07 25 0a 2a ac 6b 7b 2b d6 b7 98 24 a5 30 11
850
-            cf ef 4b b7 c3 a3 04 ed eb a6 a2 bc b1 95 4a 1f
851
-            7b 04 dd d7 b6 44 93 37 57 d3 c9 76 66 52 b3 66
852
-            fc 10 52 b7 3e c5 06 76 53 0f 33 da 67 d6 e9 38
853
-            b8 82 2d 29 60 66 a2 83 b2 9e e0 fc 2e 5e 9a 3f
854
-            0b 96 00 59 f7 97 c9 cb 2f 25 9d ae 69 84 63 31
855
-            d6 5e 24 63 40 9c 72 d4 18 b9 01 b1 cc 39 68 8f
856
-            00 00 00 03 01 00 01
857
-            00 00 01 7f
858
-            70 d5 58 4f e3 a6 e3 1f 7e 48 8b 5c cf 96 ce
859
-            07 21 88 3a 3c 96 50 7f 1f f6 b4 63 9e be 48 2e
860
-            ec 1a 6c 80 e0 bc 56 95 1b ec 4c d4 ed 61 58 00
861
-            ea cc c9 d2 11 c9 27 1d 19 13 60 4b 2a 60 fc a4
862
-            02 dc 45 be 33 85 12 1e f4 ea a4 54 27 e7 d5 66
863
-            4a 91 18 46 88 de 85 b2 58 a4 ce 85 c0 88 60 89
864
-            ad 38 db db d0 b3 af fc 74 36 1f 60 17 0a 87 aa
865
-            8d 24 f5 aa bf f1 4a e9 63 06 d7 01 f2 ec 31 b3
866
-            71 2a 68 ed 72 53 b4 28 66 90 dc 31 7f 69 94 7e
867
-            d1 99 cf 67 35 f6 5c 82 b9 e4 b4 f9 36 75 e5 f6
868
-            f9 8f c0 e6 dc dd 56 01 b5 21 ea 98 a7 a0 81 30
869
-            b3 4c ba 12 f4 38 f7 43 7a 18 01 a0 3b a3 c7 ea
870
-            e1 91 9d 60 a6 8d 70 71 42 c8 33 de 0d 2f b5 52
871
-            1c 13 d7 d0 b8 21 f5 3f b7 8f 51 d5 3f e6 27 30
872
-            0b e1 7c eb df 72 d2 e5 6c 9d 3a f9 36 06 44 03
873
-            2e 17 62 cf 08 06 4c fe 6d 05 13 20 2e ad 18 16
874
-            c4 08 cb 5e 6f b8 92 9d 86 00 95 e5 c3 cd 9d 78
875
-            39 10 f8 ff 1c 1c d4 fa b3 d6 47 47 96 12 cb 79
876
-            69 67 7e 79 f7 86 4c 38 b4 7c 2a a5 6f 86 02 46
877
-            17 a8 bd d3 5f 41 88 5f 75 48 3c 63 13 eb 29 e9
878
-            d3 46 7c df 16 fb ae 2d 1d d0 ef d9 ad 0c b2 8f
879
-            bc 42 3c b1 ee c4 e9 8b ef 84 8c b5 3c 23 2e f7
880
-            fd ee 01 f2 a6 d7 33 28 5e c8 0f d4 51 f8 0c b4
881
-            d2 92 86 1e ac 2e 5a c3 f6 1d 92 9b d4 45 4e e1
882
-            00 00 00 c0
883
-            26 1d ee ed 67 e7 8e 18 4a 65 86 c0 5b 84 ec 3c
884
-            d5 c3 32 b0 74 fe de f1 c8 04 20 16 69 28 ec 97
885
-            c4 4b 2a c9 48 b8 35 70 ef 90 30 e8 83 80 3e f3
886
-            b9 0f 51 4a 93 48 29 d3 ec 9c 89 da e6 97 a1 f7
887
-            bf 66 56 36 f6 1e 09 e2 92 3c 89 10 29 81 a4 b7
888
-            70 64 a8 57 ec c9 c8 be 02 b0 e6 59 d0 5c 76 e8
889
-            12 d5 82 6b 53 79 f2 5e df b2 bc f0 6e 2f 69 15
890
-            4b 94 a7 d7 52 85 73 5f 7e 69 54 0a 30 d1 b1 52
891
-            db 2b 90 ba fa 26 88 43 20 96 15 82 bc 99 ce 9d
892
-            a4 d9 08 a0 f2 9f f9 78 3e 10 90 88 15 0a 08 4e
893
-            a7 ae df 41 9c d7 e3 3e 7d d6 5c da 08 6a d1 be
894
-            97 2e d8 61 fe e1 c4 af 2f 1b eb c3 39 2a 09 4c
895
-            00 00 00 c1 00
896
-            f0 d6 2f 88 75 cb cf 57 fd ee fe af 4e fe 4c cb
897
-            a4 a3 b7 b5 b4 fc f6 c7 35 ce 18 d9 b0 33 63 0c
898
-            01 a5 b8 da fa 9f 7f 22 ab f5 d8 45 9b bb 51 32
899
-            fd 04 6f 84 80 55 5f 21 45 7b 5e c6 13 e5 ef 81
900
-            3d fd 77 55 2f 78 af 36 7b 99 a0 ac 3a 55 0b 5f
901
-            e5 5d 30 ed b3 06 e7 07 22 87 9c f5 15 25 45 9a
902
-            df 07 6d 41 d9 6e d8 18 c8 5b a6 86 b9 94 dd e5
903
-            28 94 3b 69 f1 e8 75 76 54 32 9d 1c 4c 56 ce 99
904
-            bc c9 81 2c ce db f3 44 ec 18 55 c2 6b dc 53 34
905
-            c3 24 63 0c 2a ec 41 3b ac 3d f5 82 83 29 12 a6
906
-            d0 f3 4b 14 3c f6 ea b5 a6 f4 b7 4d 4b 63 15 7d
907
-            5b 8f 31 3a 73 4f 6e 87 40 6e 29 15 a8 1e ab d7
908
-            00 00 00 c1 00
909
-            bc d1 05 77 59 17 c9 1b 48 16 b9 31 35 01 55 34
910
-            f3 80 a4 26 d4 2e fb da 02 7c 4f 4d 43 24 45 d9
911
-            b1 5b 4e bf b9 94 0b 5f e6 fd bc ba 1e 4e 2d 3f
912
-            2b c3 06 e1 a3 f6 11 ea e0 de dd 3e a1 ae b4 76
913
-            f5 ab 99 f4 00 3b a8 42 34 56 ec 15 f0 e5 0b c2
914
-            d8 40 03 f7 5c 5e c5 da 2b 20 0e 41 81 75 3e aa
915
-            5b 41 ab 3c c1 57 35 6d 17 bf a3 39 93 a3 7f 33
916
-            a5 69 35 fd 23 92 39 bd ec 9e 4d a4 f1 66 3d 57
917
-            5d 4c e2 6e d0 4d 74 c1 09 26 9e e2 7e e7 18 9a
918
-            86 00 03 01 3b 2b e5 65 59 a8 03 10 ad b2 f0 cb
919
-            5e f7 2f 44 f8 dd 2e 3b 68 fe 87 ce 6c 42 df d4
920
-            21 bd a2 13 fb e1 72 00 60 a7 ad 78 d9 69 d2 09
921
-            00 00 00 1b 74 65 73 74 20 6b 65 79 20 77 69
922
-            74 68 6f 75 74 20 70 61 73 73 70 68 72 61 73 65
923
-"""),
924
-        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
925
-""",
926
-        public_key_data=bytes.fromhex("""
927
-            00 00 00 07 73 73 68 2d 72 73 61
928
-            00 00 00 03 01 00 01
929
-            00 00 01 81 00
930
-            b1 a1 ee e9 7b 38 71 5b 2e 0d 23 49 94 c0 aa a0
931
-            f5 60 98 38 04 be 22 3c 4d f5 e6 1c aa 97 dc 99
932
-            08 a8 8d cb b2 f5 7b 22 37 fc 58 27 53 bd e1 2a
933
-            ec 29 2e 66 12 26 b6 c4 79 d8 03 84 54 0a 14 f2
934
-            60 d7 0c da fe 9c 9e 70 50 fc df d5 42 1a 46 30
935
-            de d2 57 17 5b 84 a5 17 4d 61 42 c1 b8 c1 80 ee
936
-            ab 1b 21 c1 e9 6b 68 1f cb dd ed 33 84 3e df 8d
937
-            81 c5 f0 86 c5 b2 a1 a5 14 75 84 c0 eb 65 7e 18
938
-            5f 6f 12 6c 4c 68 2e 2e 6e 00 f3 f8 1d 10 90 f9
939
-            da b9 2a 0d c6 69 21 70 87 b4 96 c2 cd 6f 5a 42
940
-            95 9c b3 f3 c3 c3 9c a5 c2 5f f7 74 1e e3 7f 1c
941
-            9f ac 6c f8 3b 74 2b ce ca a1 58 4b 22 a7 de e6
942
-            3a 6c 25 ea a0 87 6e af 15 f3 1f 73 bb cf 43 fc
943
-            7f 9b 1b 98 a3 ab 18 62 8d b8 dc 55 45 b7 95 97
944
-            f0 ff de e0 cb d2 7a ac 6d f5 1c d6 ab e4 7f 06
945
-            c9 c2 d3 17 0b 82 15 ea 43 99 31 29 36 04 0b 2a
946
-            87 bc 78 f2 04 3e ae 16 1e 11 54 a2 f0 5d c2 5a
947
-            ce 07 25 0a 2a ac 6b 7b 2b d6 b7 98 24 a5 30 11
948
-            cf ef 4b b7 c3 a3 04 ed eb a6 a2 bc b1 95 4a 1f
949
-            7b 04 dd d7 b6 44 93 37 57 d3 c9 76 66 52 b3 66
950
-            fc 10 52 b7 3e c5 06 76 53 0f 33 da 67 d6 e9 38
951
-            b8 82 2d 29 60 66 a2 83 b2 9e e0 fc 2e 5e 9a 3f
952
-            0b 96 00 59 f7 97 c9 cb 2f 25 9d ae 69 84 63 31
953
-            d6 5e 24 63 40 9c 72 d4 18 b9 01 b1 cc 39 68 8f
954
-"""),
955
-        expected_signatures={
956
-            SSHTestKeyDeterministicSignatureClass.SPEC: SSHTestKeyDeterministicSignature(
957
-                signature=bytes.fromhex("""
958
-                    00 00 00 07 73 73 68 2d 72 73 61
959
-                    00 00 01 80
960
-                    a2 10 7c 2e f6 bb 53 a8 74 2a a1 19 99 ad 81 be
961
-                    79 9c ed d6 9d 09 4e 6e c5 18 48 33 90 77 99 68
962
-                    f7 9e 03 5a cd 4e 18 eb 89 7d 85 a2 ee ae 4a 92
963
-                    f6 6f ce b9 fe 86 7f 2a 6b 31 da 6e 1a fe a2 a5
964
-                    88 b8 44 7f a1 76 73 b3 ec 75 b5 d0 a6 b9 15 97
965
-                    65 09 13 7d 94 21 d1 fb 5d 0f 8b 23 04 77 c2 c3
966
-                    55 22 b1 a0 09 8a f5 38 2a d6 7f 1b 87 29 a0 25
967
-                    d3 25 6f cb 64 61 07 98 dc 14 c5 84 f8 92 24 5e
968
-                    50 11 6b 49 e5 f0 cc 29 cb 29 a9 19 d8 a7 71 1f
969
-                    91 0b 05 b1 01 4b c2 5f 00 a5 b6 21 bf f8 2c 9d
970
-                    67 9b 47 3b 0a 49 6b 79 2d fc 1d ec 0c b0 e5 27
971
-                    22 d5 a9 f8 d3 c3 f9 df 48 68 e9 fb ef 3c dc 26
972
-                    bf cf ea 29 43 01 a6 e3 c5 51 95 f4 66 6d 8a 55
973
-                    e2 47 ec e8 30 45 4c ae 47 e7 c9 a4 21 8b 64 ba
974
-                    b6 88 f6 21 f8 73 b9 cb 11 a1 78 75 92 c6 5a e5
975
-                    64 fe ed 42 d9 95 99 e6 2b 6f 3c 16 3c 28 74 a4
976
-                    72 2f 0d 3f 2c 33 67 aa 35 19 8e e7 b5 11 2f b3
977
-                    f7 6a c5 02 e2 6f a3 42 e3 62 19 99 03 ea a5 20
978
-                    e7 a1 e3 bc c8 06 a3 b5 7c d6 76 5d df 6f 60 46
979
-                    83 2a 08 00 d6 d3 d9 a4 c1 41 8c f8 60 56 45 81
980
-                    da 3b a2 16 1f 9e 4e 75 83 17 da c3 53 c3 3e 19
981
-                    a4 1b bc d2 29 b8 78 61 2b 78 e6 b1 52 b0 d5 ec
982
-                    de 69 2c 48 62 d9 fd d1 9b 6b b0 49 db d3 ff 38
983
-                    e7 10 d9 2d ce 9f 0d 5e 09 7b 37 d2 7b c3 bf ce
984
-"""),
985
-                derived_passphrase=rb"""ohB8Lva7U6h0KqEZma2Bvnmc7dadCU5uxRhIM5B3mWj3ngNazU4Y64l9haLurkqS9m/Ouf6GfyprMdpuGv6ipYi4RH+hdnOz7HW10Ka5FZdlCRN9lCHR+10PiyMEd8LDVSKxoAmK9Tgq1n8bhymgJdMlb8tkYQeY3BTFhPiSJF5QEWtJ5fDMKcspqRnYp3EfkQsFsQFLwl8ApbYhv/gsnWebRzsKSWt5Lfwd7Ayw5Sci1an408P530ho6fvvPNwmv8/qKUMBpuPFUZX0Zm2KVeJH7OgwRUyuR+fJpCGLZLq2iPYh+HO5yxGheHWSxlrlZP7tQtmVmeYrbzwWPCh0pHIvDT8sM2eqNRmO57URL7P3asUC4m+jQuNiGZkD6qUg56HjvMgGo7V81nZd329gRoMqCADW09mkwUGM+GBWRYHaO6IWH55OdYMX2sNTwz4ZpBu80im4eGEreOaxUrDV7N5pLEhi2f3Rm2uwSdvT/zjnENktzp8NXgl7N9J7w7/O""",
986
-            ),
987
-        },
988
-    ),
989
-    "dsa1024": SSHTestKey(
990
-        private_key=rb"""-----BEGIN OPENSSH PRIVATE KEY-----
991
-b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABsQAAAAdzc2gtZH
992
-NzAAAAgQC7KAZXqBGNVLBQPrcMYAoNW54BhD8aIhe7BDWYzJcsaMt72VKSkguZ8+XR7nRa
993
-0C/ZsBi+uJp0dpxy9ZMTOWX4u5YPMeQcXEdGExZIfimGqSOAsy6fCld2IfJZJZExcCmhe9
994
-Ssjsd3YSAPJRluOXFQc95MZoR5hMwlIDD8QzrE7QAAABUA99nOZOgd7aHMVGoXpUEBcn7H
995
-ossAAACALr2Ag3hxM3rKdxzVUw8fX0VVPXO+3+Kr8hGe0Kc/7NwVaBVL1GQ8fenBuWynpA
996
-UbH0wo3h1wkB/8hX6p+S8cnu5rIBlUuVNwLw/bIYohK98LfqTYK/V+g6KD+8m34wvEiXZm
997
-qywY54n2bksch1Nqvj/tNpLzExSx/XS0kSM1aigAAACAbQNRPcVEuGDrEcf+xg5tgAejPX
998
-BPXr/Jss+Chk64km3mirMYjAWyWYtVcgT+7hOYxtYRin8LyMLqKRmqa0Q5UrvDfChgLhvs
999
-G9YSb/Mpw5qm8PiHSafwhkaz/te3+8hKogqoe7sd+tCF06IpJr5k70ACiNtRGqssNF8Elr
1000
-l1efYAAAH4swlfVrMJX1YAAAAHc3NoLWRzcwAAAIEAuygGV6gRjVSwUD63DGAKDVueAYQ/
1001
-GiIXuwQ1mMyXLGjLe9lSkpILmfPl0e50WtAv2bAYvriadHaccvWTEzll+LuWDzHkHFxHRh
1002
-MWSH4phqkjgLMunwpXdiHyWSWRMXApoXvUrI7Hd2EgDyUZbjlxUHPeTGaEeYTMJSAw/EM6
1003
-xO0AAAAVAPfZzmToHe2hzFRqF6VBAXJ+x6LLAAAAgC69gIN4cTN6yncc1VMPH19FVT1zvt
1004
-/iq/IRntCnP+zcFWgVS9RkPH3pwblsp6QFGx9MKN4dcJAf/IV+qfkvHJ7uayAZVLlTcC8P
1005
-2yGKISvfC36k2Cv1foOig/vJt+MLxIl2ZqssGOeJ9m5LHIdTar4/7TaS8xMUsf10tJEjNW
1006
-ooAAAAgG0DUT3FRLhg6xHH/sYObYAHoz1wT16/ybLPgoZOuJJt5oqzGIwFslmLVXIE/u4T
1007
-mMbWEYp/C8jC6ikZqmtEOVK7w3woYC4b7BvWEm/zKcOapvD4h0mn8IZGs/7Xt/vISqIKqH
1008
-u7HfrQhdOiKSa+ZO9AAojbURqrLDRfBJa5dXn2AAAAFQDJHfenj4EJ9WkehpdJatPBlqCW
1009
-0gAAABt0ZXN0IGtleSB3aXRob3V0IHBhc3NwaHJhc2UBAgMEBQYH
1010
------END OPENSSH PRIVATE KEY-----
1011
-""",
1012
-        private_key_blob=bytes.fromhex("""
1013
-            00 00 00 07 73 73 68 2d 64 73 73
1014
-            00 00 00 81 00
1015
-            bb 28 06 57 a8 11 8d 54 b0 50 3e b7 0c 60 0a 0d
1016
-            5b 9e 01 84 3f 1a 22 17 bb 04 35 98 cc 97 2c 68
1017
-            cb 7b d9 52 92 92 0b 99 f3 e5 d1 ee 74 5a d0 2f
1018
-            d9 b0 18 be b8 9a 74 76 9c 72 f5 93 13 39 65 f8
1019
-            bb 96 0f 31 e4 1c 5c 47 46 13 16 48 7e 29 86 a9
1020
-            23 80 b3 2e 9f 0a 57 76 21 f2 59 25 91 31 70 29
1021
-            a1 7b d4 ac 8e c7 77 61 20 0f 25 19 6e 39 71 50
1022
-            73 de 4c 66 84 79 84 cc 25 20 30 fc 43 3a c4 ed
1023
-            00 00 00 15 00 f7 d9 ce 64
1024
-            e8 1d ed a1 cc 54 6a 17 a5 41 01 72 7e c7 a2 cb
1025
-            00 00 00 80
1026
-            2e bd 80 83 78 71 33 7a ca 77 1c d5 53 0f 1f 5f
1027
-            45 55 3d 73 be df e2 ab f2 11 9e d0 a7 3f ec dc
1028
-            15 68 15 4b d4 64 3c 7d e9 c1 b9 6c a7 a4 05 1b
1029
-            1f 4c 28 de 1d 70 90 1f fc 85 7e a9 f9 2f 1c 9e
1030
-            ee 6b 20 19 54 b9 53 70 2f 0f db 21 8a 21 2b df
1031
-            0b 7e a4 d8 2b f5 7e 83 a2 83 fb c9 b7 e3 0b c4
1032
-            89 76 66 ab 2c 18 e7 89 f6 6e 4b 1c 87 53 6a be
1033
-            3f ed 36 92 f3 13 14 b1 fd 74 b4 91 23 35 6a 28
1034
-            00 00 00 80
1035
-            6d 03 51 3d c5 44 b8 60 eb 11 c7 fe c6 0e 6d 80
1036
-            07 a3 3d 70 4f 5e bf c9 b2 cf 82 86 4e b8 92 6d
1037
-            e6 8a b3 18 8c 05 b2 59 8b 55 72 04 fe ee 13 98
1038
-            c6 d6 11 8a 7f 0b c8 c2 ea 29 19 aa 6b 44 39 52
1039
-            bb c3 7c 28 60 2e 1b ec 1b d6 12 6f f3 29 c3 9a
1040
-            a6 f0 f8 87 49 a7 f0 86 46 b3 fe d7 b7 fb c8 4a
1041
-            a2 0a a8 7b bb 1d fa d0 85 d3 a2 29 26 be 64 ef
1042
-            40 02 88 db 51 1a ab 2c 34 5f 04 96 b9 75 79 f6
1043
-            00 00 00 15 00 c9 1d f7 a7
1044
-            8f 81 09 f5 69 1e 86 97 49 6a d3 c1 96 a0 96 d2
1045
-            00 00 00 1b 74 65 73 74 20 6b 65 79 20 77 69
1046
-            74 68 6f 75 74 20 70 61 73 73 70 68 72 61 73 65
1047
-"""),
1048
-        public_key=rb"""ssh-dss AAAAB3NzaC1kc3MAAACBALsoBleoEY1UsFA+twxgCg1bngGEPxoiF7sENZjMlyxoy3vZUpKSC5nz5dHudFrQL9mwGL64mnR2nHL1kxM5Zfi7lg8x5BxcR0YTFkh+KYapI4CzLp8KV3Yh8lklkTFwKaF71KyOx3dhIA8lGW45cVBz3kxmhHmEzCUgMPxDOsTtAAAAFQD32c5k6B3tocxUahelQQFyfseiywAAAIAuvYCDeHEzesp3HNVTDx9fRVU9c77f4qvyEZ7Qpz/s3BVoFUvUZDx96cG5bKekBRsfTCjeHXCQH/yFfqn5Lxye7msgGVS5U3AvD9shiiEr3wt+pNgr9X6DooP7ybfjC8SJdmarLBjnifZuSxyHU2q+P+02kvMTFLH9dLSRIzVqKAAAAIBtA1E9xUS4YOsRx/7GDm2AB6M9cE9ev8myz4KGTriSbeaKsxiMBbJZi1VyBP7uE5jG1hGKfwvIwuopGaprRDlSu8N8KGAuG+wb1hJv8ynDmqbw+IdJp/CGRrP+17f7yEqiCqh7ux360IXToikmvmTvQAKI21Eaqyw0XwSWuXV59g== test key without passphrase
1049
-""",
1050
-        public_key_data=bytes.fromhex("""
1051
-            00 00 00 07 73 73 68 2d 64 73 73
1052
-            00 00 00 81 00
1053
-            bb 28 06 57 a8 11 8d 54 b0 50 3e b7 0c 60 0a 0d
1054
-            5b 9e 01 84 3f 1a 22 17 bb 04 35 98 cc 97 2c 68
1055
-            cb 7b d9 52 92 92 0b 99 f3 e5 d1 ee 74 5a d0 2f
1056
-            d9 b0 18 be b8 9a 74 76 9c 72 f5 93 13 39 65 f8
1057
-            bb 96 0f 31 e4 1c 5c 47 46 13 16 48 7e 29 86 a9
1058
-            23 80 b3 2e 9f 0a 57 76 21 f2 59 25 91 31 70 29
1059
-            a1 7b d4 ac 8e c7 77 61 20 0f 25 19 6e 39 71 50
1060
-            73 de 4c 66 84 79 84 cc 25 20 30 fc 43 3a c4 ed
1061
-            00 00 00 15 00 f7 d9 ce 64
1062
-            e8 1d ed a1 cc 54 6a 17 a5 41 01 72 7e c7 a2 cb
1063
-            00 00 00 80
1064
-            2e bd 80 83 78 71 33 7a ca 77 1c d5 53 0f 1f 5f
1065
-            45 55 3d 73 be df e2 ab f2 11 9e d0 a7 3f ec dc
1066
-            15 68 15 4b d4 64 3c 7d e9 c1 b9 6c a7 a4 05 1b
1067
-            1f 4c 28 de 1d 70 90 1f fc 85 7e a9 f9 2f 1c 9e
1068
-            ee 6b 20 19 54 b9 53 70 2f 0f db 21 8a 21 2b df
1069
-            0b 7e a4 d8 2b f5 7e 83 a2 83 fb c9 b7 e3 0b c4
1070
-            89 76 66 ab 2c 18 e7 89 f6 6e 4b 1c 87 53 6a be
1071
-            3f ed 36 92 f3 13 14 b1 fd 74 b4 91 23 35 6a 28
1072
-            00 00 00 80
1073
-            6d 03 51 3d c5 44 b8 60 eb 11 c7 fe c6 0e 6d 80
1074
-            07 a3 3d 70 4f 5e bf c9 b2 cf 82 86 4e b8 92 6d
1075
-            e6 8a b3 18 8c 05 b2 59 8b 55 72 04 fe ee 13 98
1076
-            c6 d6 11 8a 7f 0b c8 c2 ea 29 19 aa 6b 44 39 52
1077
-            bb c3 7c 28 60 2e 1b ec 1b d6 12 6f f3 29 c3 9a
1078
-            a6 f0 f8 87 49 a7 f0 86 46 b3 fe d7 b7 fb c8 4a
1079
-            a2 0a a8 7b bb 1d fa d0 85 d3 a2 29 26 be 64 ef
1080
-            40 02 88 db 51 1a ab 2c 34 5f 04 96 b9 75 79 f6
1081
-"""),
1082
-        expected_signatures={
1083
-            SSHTestKeyDeterministicSignatureClass.RFC_6979: SSHTestKeyDeterministicSignature(
1084
-                signature=bytes.fromhex("""
1085
-                    00 00 00 07 73 73 68 2d 64 73 73
1086
-                    00 00 00 28 11 5f 4d 13 c2 ee 61 97
1087
-                    1e f6 23 14 3b 2b dd cf 06 c0 71 13 cc ac 34 19
1088
-                    ad 36 8d 79 aa 25 fb 5e 4f ea fe 6b 5b fa 57 42
1089
-"""),
1090
-                derived_passphrase=rb"""EV9NE8LuYZce9iMUOyvdzwbAcRPMrDQZrTaNeaol+15P6v5rW/pXQg==""",
1091
-                signature_class=SSHTestKeyDeterministicSignatureClass.RFC_6979,
1092
-            ),
1093
-            SSHTestKeyDeterministicSignatureClass.Pageant_068_080: SSHTestKeyDeterministicSignature(
1094
-                signature=bytes.fromhex("""
1095
-                    00 00 00 07 73 73 68 2d 64 73 73
1096
-                    00 00 00 28 0b f7 a8 ab 89 f5 b6 c4
1097
-                    1c 9b 78 2c 46 35 69 e2 88 b7 eb 55 37 48 7f 6d
1098
-                    49 a1 e6 de 58 1a 04 eb e6 28 99 0e 3c fd 3b 48
1099
-"""),
1100
-                derived_passphrase=rb"""C/eoq4n1tsQcm3gsRjVp4oi361U3SH9tSaHm3lgaBOvmKJkOPP07SA==""",
1101
-                signature_class=SSHTestKeyDeterministicSignatureClass.Pageant_068_080,
1102
-            ),
1103
-        },
1104
-    ),
1105
-    "ecdsa256": SSHTestKey(
1106
-        private_key=rb"""-----BEGIN OPENSSH PRIVATE KEY-----
1107
-b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS
1108
-1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQTLbU0zDwsk2Dvp+VYIrsNVf5gWwz2S
1109
-3SZ8TbxiQRkpnGSVqyIoHJOJc+NQItAa7xlJ/8Z6gfz57Z3apUkaMJm6AAAAuKeY+YinmP
1110
-mIAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBMttTTMPCyTYO+n5
1111
-Vgiuw1V/mBbDPZLdJnxNvGJBGSmcZJWrIigck4lz41Ai0BrvGUn/xnqB/PntndqlSRowmb
1112
-oAAAAhAKIl/3n0pKVIxpZkXTGtii782Qr4yIcvHdpxjO/QsIqKAAAAG3Rlc3Qga2V5IHdp
1113
-dGhvdXQgcGFzc3BocmFzZQECAwQ=
1114
------END OPENSSH PRIVATE KEY-----
1115
-""",
1116
-        private_key_blob=bytes.fromhex("""
1117
-            00 00 00 13 65 63 64
1118
-            73 61 2d 73 68 61 32 2d 6e 69 73 74 70 32 35 36
1119
-            00 00 00 08 6e 69 73 74 70 32 35 36
1120
-            00 00 00 41 04
1121
-            cb 6d 4d 33 0f 0b 24 d8 3b e9 f9 56 08 ae c3 55
1122
-            7f 98 16 c3 3d 92 dd 26 7c 4d bc 62 41 19 29 9c
1123
-            64 95 ab 22 28 1c 93 89 73 e3 50 22 d0 1a ef 19
1124
-            49 ff c6 7a 81 fc f9 ed 9d da a5 49 1a 30 99 ba
1125
-            00 00 00 21 00
1126
-            a2 25 ff 79 f4 a4 a5 48 c6 96 64 5d 31 ad 8a 2e
1127
-            fc d9 0a f8 c8 87 2f 1d da 71 8c ef d0 b0 8a 8a
1128
-            00 00 00 1b 74 65 73 74 20 6b 65 79 20 77 69
1129
-            74 68 6f 75 74 20 70 61 73 73 70 68 72 61 73 65
1130
-"""),
1131
-        public_key=rb"""ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBMttTTMPCyTYO+n5Vgiuw1V/mBbDPZLdJnxNvGJBGSmcZJWrIigck4lz41Ai0BrvGUn/xnqB/PntndqlSRowmbo= test key without passphrase
1132
-""",
1133
-        public_key_data=bytes.fromhex("""
1134
-            00 00 00 13 65 63 64
1135
-            73 61 2d 73 68 61 32 2d 6e 69 73 74 70 32 35 36
1136
-            00 00 00 08 6e 69 73 74 70 32 35 36
1137
-            00 00 00 41 04
1138
-            cb 6d 4d 33 0f 0b 24 d8 3b e9 f9 56 08 ae c3 55
1139
-            7f 98 16 c3 3d 92 dd 26 7c 4d bc 62 41 19 29 9c
1140
-            64 95 ab 22 28 1c 93 89 73 e3 50 22 d0 1a ef 19
1141
-            49 ff c6 7a 81 fc f9 ed 9d da a5 49 1a 30 99 ba
1142
-"""),
1143
-        expected_signatures={
1144
-            SSHTestKeyDeterministicSignatureClass.RFC_6979: SSHTestKeyDeterministicSignature(
1145
-                signature=bytes.fromhex("""
1146
-                    00 00 00 13 65 63 64
1147
-                    73 61 2d 73 68 61 32 2d 6e 69 73 74 70 32 35 36
1148
-                    00 00 00 49
1149
-                    00 00 00 20
1150
-                    22 ad 23 8a 9c 5d ca 4e ea 73 e7 29 77 ab a8 b2
1151
-                    2e 01 d8 de 11 ae c9 b3 57 ce d5 84 9c 85 73 eb
1152
-                    00 00 00 21 00
1153
-                    9b 1a cb dd 45 89 f0 37 95 9c a2 d8 ac c3 f7 71
1154
-                    55 33 50 86 9e cb 3a 95 e4 68 80 1a 9d d6 d5 bc
1155
-"""),
1156
-                derived_passphrase=rb"""AAAAICKtI4qcXcpO6nPnKXerqLIuAdjeEa7Js1fO1YSchXPrAAAAIQCbGsvdRYnwN5Wcotisw/dxVTNQhp7LOpXkaIAandbVvA==""",
1157
-                signature_class=SSHTestKeyDeterministicSignatureClass.RFC_6979,
1158
-            ),
1159
-            SSHTestKeyDeterministicSignatureClass.Pageant_068_080: SSHTestKeyDeterministicSignature(
1160
-                signature=bytes.fromhex("""
1161
-                    00 00 00 13 65 63 64
1162
-                    73 61 2d 73 68 61 32 2d 6e 69 73 74 70 32 35 36
1163
-                    00 00 00 49
1164
-                    00 00 00 21 00
1165
-                    b7 9e 4f ec ec 9b 77 dd 12 d9 43 a2 f5 bf b5 34
1166
-                    91 e0 89 44 e6 20 48 36 fa 75 22 77 86 38 de 21
1167
-                    00 00 00 20
1168
-                    3f d8 04 0f fa f5 bc d2 26 e0 4c 0c 77 5d 0e 08
1169
-                    ec 30 04 8e 42 58 41 96 f6 7e 4f d2 14 39 f4 87
1170
-"""),
1171
-                derived_passphrase=rb"""AAAAIQC3nk/s7Jt33RLZQ6L1v7U0keCJROYgSDb6dSJ3hjjeIQAAACA/2AQP+vW80ibgTAx3XQ4I7DAEjkJYQZb2fk/SFDn0hw==""",
1172
-                signature_class=SSHTestKeyDeterministicSignatureClass.Pageant_068_080,
1173
-            ),
1174
-        },
1175
-    ),
1176
-    "ecdsa384": SSHTestKey(
1177
-        private_key=rb"""-----BEGIN OPENSSH PRIVATE KEY-----
1178
-b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAiAAAABNlY2RzYS
1179
-1zaGEyLW5pc3RwMzg0AAAACG5pc3RwMzg0AAAAYQSgkOjkAvq7v5vHuj3KBL4/EAWcn5hZ
1180
-DyKcbyV0eBMGFq7hKXQlZqIahLVqeMR0QqmkxNJ2rly2VHcXneq3vZ+9fIsWCOdYk5WP3N
1181
-ZPzv911Xn7wbEkC7QndD5zKlm4pBUAAADomhj+IZoY/iEAAAATZWNkc2Etc2hhMi1uaXN0
1182
-cDM4NAAAAAhuaXN0cDM4NAAAAGEEoJDo5AL6u7+bx7o9ygS+PxAFnJ+YWQ8inG8ldHgTBh
1183
-au4Sl0JWaiGoS1anjEdEKppMTSdq5ctlR3F53qt72fvXyLFgjnWJOVj9zWT87/ddV5+8Gx
1184
-JAu0J3Q+cypZuKQVAAAAMQD5sTy8p+B1cn/DhOmXquui1BcxvASqzzevkBlbQoBa73y04B
1185
-2OdqVOVRkwZWRROz0AAAAbdGVzdCBrZXkgd2l0aG91dCBwYXNzcGhyYXNlAQIDBA==
1186
------END OPENSSH PRIVATE KEY-----
1187
-""",
1188
-        private_key_blob=bytes.fromhex("""
1189
-            00 00 00 13 65 63 64
1190
-            73 61 2d 73 68 61 32 2d 6e 69 73 74 70 33 38 34
1191
-            00 00 00 08 6e 69 73 74 70 33 38 34
1192
-            00 00 00 61 04
1193
-            a0 90 e8 e4 02 fa bb bf 9b c7 ba 3d ca 04 be 3f
1194
-            10 05 9c 9f 98 59 0f 22 9c 6f 25 74 78 13 06 16
1195
-            ae e1 29 74 25 66 a2 1a 84 b5 6a 78 c4 74 42 a9
1196
-            a4 c4 d2 76 ae 5c b6 54 77 17 9d ea b7 bd 9f bd
1197
-            7c 8b 16 08 e7 58 93 95 8f dc d6 4f ce ff 75 d5
1198
-            79 fb c1 b1 24 0b b4 27 74 3e 73 2a 59 b8 a4 15
1199
-            00 00 00 31 00
1200
-            f9 b1 3c bc a7 e0 75 72 7f c3 84 e9 97 aa eb a2
1201
-            d4 17 31 bc 04 aa cf 37 af 90 19 5b 42 80 5a ef
1202
-            7c b4 e0 1d 8e 76 a5 4e 55 19 30 65 64 51 3b 3d
1203
-            00 00 00 1b 74 65 73 74 20 6b 65 79 20 77 69
1204
-            74 68 6f 75 74 20 70 61 73 73 70 68 72 61 73 65
1205
-"""),
1206
-        public_key=rb"""ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBKCQ6OQC+ru/m8e6PcoEvj8QBZyfmFkPIpxvJXR4EwYWruEpdCVmohqEtWp4xHRCqaTE0nauXLZUdxed6re9n718ixYI51iTlY/c1k/O/3XVefvBsSQLtCd0PnMqWbikFQ== test key without passphrase
1207
-""",
1208
-        public_key_data=bytes.fromhex("""
1209
-            00 00 00 13 65 63 64
1210
-            73 61 2d 73 68 61 32 2d 6e 69 73 74 70 33 38 34
1211
-            00 00 00 08 6e 69 73 74 70 33 38 34
1212
-            00 00 00 61 04
1213
-            a0 90 e8 e4 02 fa bb bf 9b c7 ba 3d ca 04 be 3f
1214
-            10 05 9c 9f 98 59 0f 22 9c 6f 25 74 78 13 06 16
1215
-            ae e1 29 74 25 66 a2 1a 84 b5 6a 78 c4 74 42 a9
1216
-            a4 c4 d2 76 ae 5c b6 54 77 17 9d ea b7 bd 9f bd
1217
-            7c 8b 16 08 e7 58 93 95 8f dc d6 4f ce ff 75 d5
1218
-            79 fb c1 b1 24 0b b4 27 74 3e 73 2a 59 b8 a4 15
1219
-"""),
1220
-        expected_signatures={
1221
-            SSHTestKeyDeterministicSignatureClass.RFC_6979: SSHTestKeyDeterministicSignature(
1222
-                signature=bytes.fromhex("""
1223
-                    00 00 00 13 65 63 64
1224
-                    73 61 2d 73 68 61 32 2d 6e 69 73 74 70 33 38 34
1225
-                    00 00 00 68
1226
-                    00 00 00 30
1227
-                    78 e1 a8 f5 8c d2 7a 21 e5 a2 ca e6 d0 1a 19 f8
1228
-                    3a 1c 39 7e 71 a0 e6 7e 93 83 49 95 05 01 d0 3e
1229
-                    23 22 cd 09 63 7f 7c 6c b0 97 44 6d 7e 48 39 87
1230
-                    00 00 00 30
1231
-                    10 ee 85 51 77 2b 91 2c e9 42 79 66 59 8a a2 c0
1232
-                    d2 c8 8a 8f 2f 8f 33 87 9e 12 54 e4 da 02 f9 e7
1233
-                    95 f5 82 6f 82 2b 38 6d 6e 5d 17 15 ac 12 e7 62
1234
-"""),
1235
-                derived_passphrase=rb"""AAAAMHjhqPWM0noh5aLK5tAaGfg6HDl+caDmfpODSZUFAdA+IyLNCWN/fGywl0Rtfkg5hwAAADAQ7oVRdyuRLOlCeWZZiqLA0siKjy+PM4eeElTk2gL555X1gm+CKzhtbl0XFawS52I=""",
1236
-                signature_class=SSHTestKeyDeterministicSignatureClass.RFC_6979,
1237
-            ),
1238
-            SSHTestKeyDeterministicSignatureClass.Pageant_068_080: SSHTestKeyDeterministicSignature(
1239
-                signature=bytes.fromhex("""
1240
-                    00 00 00 13 65 63 64
1241
-                    73 61 2d 73 68 61 32 2d 6e 69 73 74 70 33 38 34
1242
-                    00 00 00 69
1243
-                    00 00 00 30
1244
-                    4b 3e b7 22 c2 87 77 6d e0 3e f5 05 75 36 b6 0f
1245
-                    cd 9f a4 49 c7 48 ef 76 fd ea 4b 49 e3 b1 f2 22
1246
-                    d5 41 22 d7 96 b2 29 70 ff bb 81 97 27 e2 35 60
1247
-                    00 00 00 31 00
1248
-                    c8 a4 d8 62 fe f2 a6 63 97 98 08 c7 39 24 b2 55
1249
-                    0a b8 e7 79 ab a6 62 96 3e cc ea 73 e2 fb dc 46
1250
-                    d6 25 b9 c8 0c e8 3e 33 91 51 78 25 a8 c5 46 85
1251
-"""),
1252
-                derived_passphrase=rb"""AAAAMEs+tyLCh3dt4D71BXU2tg/Nn6RJx0jvdv3qS0njsfIi1UEi15ayKXD/u4GXJ+I1YAAAADEAyKTYYv7ypmOXmAjHOSSyVQq453mrpmKWPszqc+L73EbWJbnIDOg+M5FReCWoxUaF""",
1253
-                signature_class=SSHTestKeyDeterministicSignatureClass.Pageant_068_080,
1254
-            ),
1255
-        },
1256
-    ),
1257
-    "ecdsa521": SSHTestKey(
1258
-        private_key=rb"""-----BEGIN OPENSSH PRIVATE KEY-----
1259
-b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAArAAAABNlY2RzYS
1260
-1zaGEyLW5pc3RwNTIxAAAACG5pc3RwNTIxAAAAhQQASVOdwDznmlcGqiLvFtYeVtrAEiVz
1261
-iIfsL7jEM8Utu/m8WSkPFQtjwqdFw+WfZ0mi6qMbEFgi/ELzZSKVteCSbcMAhqAkOMFKiD
1262
-u4bxvsM6bT02Ru7q2yT41ySyGhUD0QySBnI6Ckt/wnQ1TEpj8zDKiRErxs9e6QLGElNRkz
1263
-LPMs+mMAAAEY2FXeh9hV3ocAAAATZWNkc2Etc2hhMi1uaXN0cDUyMQAAAAhuaXN0cDUyMQ
1264
-AAAIUEAElTncA855pXBqoi7xbWHlbawBIlc4iH7C+4xDPFLbv5vFkpDxULY8KnRcPln2dJ
1265
-ouqjGxBYIvxC82UilbXgkm3DAIagJDjBSog7uG8b7DOm09Nkbu6tsk+NckshoVA9EMkgZy
1266
-OgpLf8J0NUxKY/MwyokRK8bPXukCxhJTUZMyzzLPpjAAAAQSFqUmKK7lGQzxT6GKZSLDju
1267
-U3otwLYnuj+/5AdzuB/zotu95UdFv9I2DNXzd9E4WAyz6IqBBNcsMkxrzHAdqsYDAAAAG3
1268
-Rlc3Qga2V5IHdpdGhvdXQgcGFzc3BocmFzZQ==
1269
------END OPENSSH PRIVATE KEY-----
1270
-""",
1271
-        private_key_blob=bytes.fromhex("""
1272
-            00 00 00 13 65 63 64
1273
-            73 61 2d 73 68 61 32 2d 6e 69 73 74 70 35 32 31
1274
-            00 00 00 08 6e 69 73 74 70 35 32 31
1275
-            00 00 00 85 04 00 49 53 9d
1276
-            c0 3c e7 9a 57 06 aa 22 ef 16 d6 1e 56 da c0 12
1277
-            25 73 88 87 ec 2f b8 c4 33 c5 2d bb f9 bc 59 29
1278
-            0f 15 0b 63 c2 a7 45 c3 e5 9f 67 49 a2 ea a3 1b
1279
-            10 58 22 fc 42 f3 65 22 95 b5 e0 92 6d c3 00 86
1280
-            a0 24 38 c1 4a 88 3b b8 6f 1b ec 33 a6 d3 d3 64
1281
-            6e ee ad b2 4f 8d 72 4b 21 a1 50 3d 10 c9 20 67
1282
-            23 a0 a4 b7 fc 27 43 54 c4 a6 3f 33 0c a8 91 12
1283
-            bc 6c f5 ee 90 2c 61 25 35 19 33 2c f3 2c fa 63
1284
-            00 00 00 41 21
1285
-            6a 52 62 8a ee 51 90 cf 14 fa 18 a6 52 2c 38 ee
1286
-            53 7a 2d c0 b6 27 ba 3f bf e4 07 73 b8 1f f3 a2
1287
-            db bd e5 47 45 bf d2 36 0c d5 f3 77 d1 38 58 0c
1288
-            b3 e8 8a 81 04 d7 2c 32 4c 6b cc 70 1d aa c6 03
1289
-            00 00 00 1b 74 65 73 74 20 6b 65 79 20 77 69
1290
-            74 68 6f 75 74 20 70 61 73 73 70 68 72 61 73 65
1291
-"""),
1292
-        public_key=rb"""ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBABJU53APOeaVwaqIu8W1h5W2sASJXOIh+wvuMQzxS27+bxZKQ8VC2PCp0XD5Z9nSaLqoxsQWCL8QvNlIpW14JJtwwCGoCQ4wUqIO7hvG+wzptPTZG7urbJPjXJLIaFQPRDJIGcjoKS3/CdDVMSmPzMMqJESvGz17pAsYSU1GTMs8yz6Yw== test key without passphrase
1293
-""",
1294
-        public_key_data=bytes.fromhex("""
1295
-            00 00 00 13 65 63 64
1296
-            73 61 2d 73 68 61 32 2d 6e 69 73 74 70 35 32 31
1297
-            00 00 00 08 6e 69 73 74 70 35 32 31
1298
-            00 00 00 85 04 00 49 53 9d
1299
-            c0 3c e7 9a 57 06 aa 22 ef 16 d6 1e 56 da c0 12
1300
-            25 73 88 87 ec 2f b8 c4 33 c5 2d bb f9 bc 59 29
1301
-            0f 15 0b 63 c2 a7 45 c3 e5 9f 67 49 a2 ea a3 1b
1302
-            10 58 22 fc 42 f3 65 22 95 b5 e0 92 6d c3 00 86
1303
-            a0 24 38 c1 4a 88 3b b8 6f 1b ec 33 a6 d3 d3 64
1304
-            6e ee ad b2 4f 8d 72 4b 21 a1 50 3d 10 c9 20 67
1305
-            23 a0 a4 b7 fc 27 43 54 c4 a6 3f 33 0c a8 91 12
1306
-            bc 6c f5 ee 90 2c 61 25 35 19 33 2c f3 2c fa 63
1307
-"""),
1308
-        expected_signatures={
1309
-            SSHTestKeyDeterministicSignatureClass.RFC_6979: SSHTestKeyDeterministicSignature(
1310
-                signature=bytes.fromhex("""
1311
-                    00 00 00 13 65 63 64
1312
-                    73 61 2d 73 68 61 32 2d 6e 69 73 74 70 35 32 31
1313
-                    00 00 00 8b
1314
-                    00 00 00 42 01 d8
1315
-                    ea c2 1e 55 c6 9e dd 4b 00 ed 1b 93 19 cc 9b 74
1316
-                    27 44 c0 c0 e3 5b 3d 81 15 00 12 cc 07 89 54 97
1317
-                    ec 60 42 ad e6 40 c1 c6 5f c0 1b c3 0a 8e 58 6e
1318
-                    da 3f a9 57 90 04 79 46 1d 48 bb 19 67 e9 65 19
1319
-                    00 00 00 41 7d
1320
-                    58 e0 2e d7 86 2e 36 8c 1a 44 23 af 19 e7 51 97
1321
-                    bb fb 32 90 a1 35 bb 88 d7 b5 22 37 b3 99 ba e4
1322
-                    a7 9d 2d 56 14 0a f5 68 f5 cc 38 84 e9 b6 c6 71
1323
-                    7a 3b 87 e7 7a b1 37 e7 1d e6 80 96 d1 a6 1e bc
1324
-"""),
1325
-                derived_passphrase=rb"""AAAAQgHY6sIeVcae3UsA7RuTGcybdCdEwMDjWz2BFQASzAeJVJfsYEKt5kDBxl/AG8MKjlhu2j+pV5AEeUYdSLsZZ+llGQAAAEF9WOAu14YuNowaRCOvGedRl7v7MpChNbuI17UiN7OZuuSnnS1WFAr1aPXMOITptsZxejuH53qxN+cd5oCW0aYevA==""",
1326
-                signature_class=SSHTestKeyDeterministicSignatureClass.RFC_6979,
1327
-            ),
1328
-            SSHTestKeyDeterministicSignatureClass.Pageant_068_080: SSHTestKeyDeterministicSignature(
1329
-                signature=bytes.fromhex("""
1330
-                    00 00 00 13 65 63 64
1331
-                    73 61 2d 73 68 61 32 2d 6e 69 73 74 70 35 32 31
1332
-                    00 00 00 8c
1333
-                    00 00 00 42 01 ce
1334
-                    fe 9d 66 b6 01 76 2e 86 c2 ab 68 62 73 44 05 23
1335
-                    fd d1 79 07 fc 45 f5 c0 83 36 88 61 d4 04 79 90
1336
-                    b0 ef 8b 3c b5 55 0e cc 26 6b a0 3e 6a 04 48 ca
1337
-                    e4 6a a5 a0 cf 91 5f 71 6f 37 9a 0f 6b a9 fb 9b
1338
-                    00 00 00 42 01 6d
1339
-                    21 77 c6 13 fa ea ac de 90 19 24 5a d2 61 39 d9
1340
-                    66 9b 86 1a 41 04 58 a2 9b b8 93 b6 6f 82 23 f2
1341
-                    01 23 c7 ff 5a d3 86 95 0f da 28 f9 3b e3 9c 27
1342
-                    e7 b2 d7 66 4e 5f 38 36 4c 8c be 76 4e fa 0a 2d
1343
-"""),
1344
-                derived_passphrase=rb"""AAAAQgHO/p1mtgF2LobCq2hic0QFI/3ReQf8RfXAgzaIYdQEeZCw74s8tVUOzCZroD5qBEjK5GqloM+RX3FvN5oPa6n7mwAAAEIBbSF3xhP66qzekBkkWtJhOdlmm4YaQQRYopu4k7ZvgiPyASPH/1rThpUP2ij5O+OcJ+ey12ZOXzg2TIy+dk76Ci0=""",
1345
-                signature_class=SSHTestKeyDeterministicSignatureClass.Pageant_068_080,
1346
-            ),
1347
-        },
1348
-    ),
1349
-}
1350
-"""The master list of SSH test keys."""
1351
-SUPPORTED_KEYS: Mapping[str, SSHTestKey] = {
1352
-    k: v for k, v in ALL_KEYS.items() if v.is_suitable()
1353
-}
1354
-"""The subset of SSH test keys suitable for use with vault."""
1355
-UNSUITABLE_KEYS: Mapping[str, SSHTestKey] = {
1356
-    k: v for k, v in ALL_KEYS.items() if not v.is_suitable()
1357
-}
1358
-"""The subset of SSH test keys not suitable for use with vault."""
1359
-
1360
-DUMMY_SERVICE = "service1"
1361
-"""A standard/sample service name."""
1362
-DUMMY_PASSPHRASE = "my secret passphrase"
1363
-"""A standard/sample passphrase."""
1364
-DUMMY_KEY1 = SUPPORTED_KEYS["ed25519"].public_key_data
1365
-"""A sample universally supported SSH test key (in wire format)."""
1366
-DUMMY_KEY1_B64 = base64.standard_b64encode(DUMMY_KEY1).decode("ASCII")
1367
-"""
1368
-A sample universally supported SSH test key (in `authorized_keys` format).
1369
-"""
1370
-DUMMY_KEY2 = SUPPORTED_KEYS["rsa"].public_key_data
1371
-"""A second supported SSH test key (in wire format)."""
1372
-DUMMY_KEY2_B64 = base64.standard_b64encode(DUMMY_KEY2).decode("ASCII")
1373
-"""A second supported SSH test key (in `authorized_keys` format)."""
1374
-DUMMY_KEY3 = SUPPORTED_KEYS["ed448"].public_key_data
1375
-"""A third supported SSH test key (in wire format)."""
1376
-DUMMY_KEY3_B64 = base64.standard_b64encode(DUMMY_KEY3).decode("ASCII")
1377
-"""A third supported SSH test key (in `authorized_keys` format)."""
1378
-DUMMY_CONFIG_SETTINGS = {
1379
-    "length": 10,
1380
-    "upper": 1,
1381
-    "lower": 1,
1382
-    "repeat": 5,
1383
-    "number": 1,
1384
-    "space": 1,
1385
-    "dash": 1,
1386
-    "symbol": 1,
1387
-}
1388
-"""Sample vault settings."""
1389
-DUMMY_RESULT_PASSPHRASE = b".2V_QJkd o"
1390
-"""
1391
-The passphrase derived from [`DUMMY_SERVICE`][] using [`DUMMY_PASSPHRASE`][].
1392
-"""
1393
-DUMMY_RESULT_KEY1 = b"E<b<{ -7iG"
1394
-"""
1395
-The passphrase derived from [`DUMMY_SERVICE`][] using [`DUMMY_KEY1`][].
1396
-"""
1397
-DUMMY_PHRASE_FROM_KEY1_RAW = (
1398
-    b"\x00\x00\x00\x0bssh-ed25519"
1399
-    b"\x00\x00\x00@\xf0\x98\x19\x80l\x1a\x97\xd5&\x03n"
1400
-    b"\xcc\xe3e\x8f\x86f\x07\x13\x19\x13\t!33\xf9\xe46S"
1401
-    b"\x1d\xaf\xfd\r\x08\x1f\xec\xf8s\x9b\x8c_U9\x16|ST,"
1402
-    b"\x1eR\xbb0\xed\x7f\x89\xe2/iQU\xd8\x9e\xa6\x02"
1403
-)
1404
-"""
1405
-The "equivalent master passphrase" derived from [`DUMMY_KEY1`][] (raw format).
1406
-"""
1407
-DUMMY_PHRASE_FROM_KEY1 = b"8JgZgGwal9UmA27M42WPhmYHExkTCSEzM/nkNlMdr/0NCB/s+HObjF9VORZ8U1QsHlK7MO1/ieIvaVFV2J6mAg=="
1408
-"""
1409
-The "equivalent master passphrase" derived from [`DUMMY_KEY1`][] (in base64).
1410
-"""
1411
-
1412
-VAULT_MASTER_KEY = "vault key"
1413
-"""
1414
-The storage passphrase used to encrypt all sample vault native configurations.
1415
-"""
1416
-VAULT_V02_CONFIG = "P7xeh5y4jmjpJ2pFq4KUcTVoaE9ZOEkwWmpVTURSSWQxbGt6emN4aFE4eFM3anVPbDRNTGpOLzY3eDF5aE1YTm5LNWh5Q1BwWTMwM3M5S083MWRWRFlmOXNqSFJNcStGMWFOS3c2emhiOUNNenZYTmNNMnZxaUErdlRoOGF2ZHdGT1ZLNTNLOVJQcU9jWmJrR3g5N09VcVBRZ0ZnSFNUQy9HdFVWWnFteVhRVkY3MHNBdnF2ZWFEbFBseWRGelE1c3BFTnVUckRQdWJSL29wNjFxd2Y2ZVpob3VyVzRod3FKTElTenJ1WTZacTJFOFBtK3BnVzh0QWVxcWtyWFdXOXYyenNQeFNZbWt1MDU2Vm1kVGtISWIxWTBpcWRFbyswUVJudVVhZkVlNVpGWDA4WUQ2Q2JTWW81SnlhQ2Zxa3cxNmZoQjJES0Uyd29rNXpSck5iWVBrVmEwOXFya1NpMi9saU5LL3F0M3N3MjZKekNCem9ER2svWkZ0SUJLdmlHRno0VlQzQ3pqZTBWcTM3YmRiNmJjTkhqUHZoQ0NxMW1ldW1XOFVVK3pQMEtUMkRMVGNvNHFlOG40ck5KcGhsYXg1b1VzZ1NYU1B2T3RXdEkwYzg4NWE3YWUzOWI1MDI0MThhMWZjODQ3MDA2OTJmNDQ0MDkxNGFiNmRlMGQ2YjZiNjI5NGMwN2IwMmI4MGZi"
1417
-"""
1418
-A sample vault native configuration, in v0.2 format, encoded in base64
1419
-and encrypted with [`VAULT_MASTER_KEY`][].
1420
-"""
1421
-VAULT_V02_CONFIG_DATA = {
1422
-    "global": {
1423
-        "phrase": DUMMY_PASSPHRASE.rstrip("\n"),
1424
-    },
1425
-    "services": {
1426
-        "(meta)": {
1427
-            "notes": "This config was originally in v0.2 format.",
1428
-        },
1429
-        DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy(),
1430
-    },
1431
-}
1432
-"""
1433
-The plaintext contents (a vault native configuration) stored in
1434
-[`VAULT_V02_CONFIG`][].
1435
-"""
1436
-VAULT_V03_CONFIG = "sBPBrr8BFHPxSJkV/A53zk9zwDQHFxLe6UIusCVvzFQre103pcj5xxmE11lMTA0U2QTYjkhRXKkH5WegSmYpAnzReuRsYZlWWp6N4kkubf+twZ9C3EeggPm7as2Af4TICHVbX4uXpIHeQJf9y1OtqrO+SRBrgPBzgItoxsIxebxVKgyvh1CZQOSkn7BIzt9xKhDng3ubS4hQ91fB0QCumlldTbUl8tj4Xs5JbvsSlUMxRlVzZ0OgAOrSsoWELXmsp6zXFa9K6wIuZa4wQuMLQFHiA64JO1CR3I+rviWCeMlbTOuJNx6vMB5zotKJqA2hIUpN467TQ9vI4g/QTo40m5LT2EQKbIdTvBQAzcV4lOcpr5Lqt4LHED5mKvm/4YfpuuT3I3XCdWfdG5SB7ciiB4Go+xQdddy3zZMiwm1fEwIB8XjFf2cxoJdccLQ2yxf+9diedBP04EsMHrvxKDhQ7/vHl7xF2MMFTDKl3WFd23vvcjpR1JgNAKYprG/e1p/7"
1437
-"""
1438
-A sample vault native configuration, in v0.3 format, encoded in base64
1439
-and encrypted with [`VAULT_MASTER_KEY`][].
1440
-"""
1441
-VAULT_V03_CONFIG_DATA = {
1442
-    "global": {
1443
-        "phrase": DUMMY_PASSPHRASE.rstrip("\n"),
1444
-    },
1445
-    "services": {
1446
-        "(meta)": {
1447
-            "notes": "This config was originally in v0.3 format.",
1448
-        },
1449
-        DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy(),
1450
-    },
1451
-}
1452
-"""
1453
-The plaintext contents (a vault native configuration) stored in
1454
-[`VAULT_V03_CONFIG`][].
1455
-"""
1456
-VAULT_STOREROOM_CONFIG_ZIPPED = b"""
1457
-UEsDBBQAAAAIAJ1WGVnTVFGT0gAAAOYAAAAFAAAALmtleXMFwclSgzAAANC7n9GrBzBldcYDE5Al
1458
-EKbFAvGWklBAtqYsBcd/973fw8LFox76w/vb34tzhD5OATeEAk6tJ6Fbp3WrvkJO7l0KIjtxCLfY
1459
-ORm8ScEDPbNkyVwGLmZNTuQzXPMl/GnLO0I2PmUhRcxSj2Iy6PUy57up4thL6zndYwtyORpyCTGy
1460
-ibbjIeq/K/9atsHkl680nwsKFVk1i97gbGhG4gC5CMS8aUx8uebuToRCDsAT61UQVp0yEjw1bhm1
1461
-6UPWzM2wyfMGMyY1ox5HH/9QSwMEFAAAAAgAnVYZWd1pX+EFAwAA1AMAAAIAAAAwMA3ON7abQAAA
1462
-wP4fwy0FQUR3ZASLYEkCOnKOEtHPd7e7KefPr71YP800/vqN//3hAywvUaCcTYb6TbKS/kYcVnvG
1463
-wGA5N8ksjpFNCu5BZGu953GdoVnOfN6PNXoluWOS2JzO23ELNJ2m9nDn0uDhwC39VHJT1pQdejIw
1464
-CovQTEWmBH53FJufhNSZKQG5s1fMcw9hqn3NbON6wRDquOjLe/tqWkG1yiQDSF5Ail8Wd2UaA7vo
1465
-40QorG1uOBU7nPlDx/cCTDpSqwTZDkkAt6Zy9RT61NUZqHSMIgKMerj3njXOK+1q5sA/upSGvMrN
1466
-7/JpSEhcmu7GDvQJ8TyLos6vPCSmxO6RRG3X4BLpqHkTgeqHz+YDZwTV+6y5dvSmTSsCP5uPCmi+
1467
-7r9irZ1m777iL2R8NFH0QDIo1GFsy1NrUvWq4TGuvVIbkHrML5mFdR6ajNhRjL/6//1crYAMLHxo
1468
-qkjGz2Wck2dmRd96mFFAfdQ1/BqDgi6X/KRwHL9VmhpdjcKJhuE04xLYgTCyKLv8TkFfseNAbN3N
1469
-7KvVW7QVF97W50pzXzy3Ea3CatNQkJ1DnkR0vc0dsHd1Zr0o1acUaAa65B2yjYXCk3TFlMo9TNce
1470
-OWBXzJrpaZ4N7bscdwCF9XYesSMpxBDpwyCIVyJ8tHZVf/iS4pE6u+XgvD42yef+ujhM/AyboqPk
1471
-sFNV/XoNpmWIySdkTMmwu72q1GfPqr01ze/TzCVrCe0KkFcZhe77jrLPOnRCIarF2c9MMHNfmguU
1472
-A0tJ8HodQb/zehL6C9KSiNWfG+NlK1Dro1sGKhiJETLMFru272CNlwQJmzTHuKAXuUvJmQCfmLfL
1473
-EPrxoE08fu+v6DKnSopnG8GTkbscPZ+K5q2kC6m7pCizKO1sLKG7fMBRnJxnel/vmpY2lFCB4ADy
1474
-no+dvqBl6z3X/ji9AFXC9X8HRd+8u57OS1zV4OhiVd7hMy1U8F5qbIBms+FS6QbL9NhIb2lFN4VO
1475
-3+ITZz1sPJBl68ZgJWOV6O4F5cAHGKl/UEsDBBQAAAAIAJ1WGVn9pqLBygEAACsCAAACAAAAMDMN
1476
-z8mWa0AAANB9f0ZvLZQhyDsnC0IMJShDBTuzJMZoktLn/ft79w/u7/dWvZb7OHz/Yf5+yYUBMTNK
1477
-RrCI1xIQs67d6yI6bM75waX0gRLdKMGyC5O2SzBLs57V4+bqxo5xI2DraLTVeniUXLxkLyjRnC4u
1478
-24Vp+7p+ppt9DlVNNZp7rskQDOe47mbgViNeE5oXpg/oDgTcfQYNvt8V0OoyKbIiNymOW/mB3hze
1479
-D1EHqTWQvFZB5ANGpLMM0U10xWYAClzuVJXKm/n/8JgVaobY38IjzxXyk4iPkQUuYtws73Kan871
1480
-R3mZa7/j0pO6Wu0LuoV+czp9yZEH/SU42lCgjEsZ9Mny3tHaF09QWU4oB7HI+LBhKnFJ9c0bHEky
1481
-OooHgzgTIa0y8fbpst30PEUwfUAS+lYzPXG3y+QUiy5nrJFPb0IwESd9gIIOVSfZK63wvD5ueoxj
1482
-O9bn2gutSFT6GO17ibguhXtItAjPbZWfyyQqHRyeBcpT7qbzQ6H1Of5clEqVdNcetAg8ZMKoWTbq
1483
-/vSSQ2lpkEqT0tEQo7zwKBzeB37AysB5hhDCPn1gUTER6d+1S4dzwO7HhDf9kG+3botig2Xm1Dz9
1484
-A1BLAwQUAAAACACdVhlZs14oCcgBAAArAgAAAgAAADA5BcHJkqIwAADQe39GXz2wE5gqDxAGQRZF
1485
-QZZbDIFG2YwIga7593nv93sm9N0M/fcf4d+XcUlVE+kvustz3BU7FjHOaW+u6TRsfNKzLh74mO1w
1486
-IXUlM/2sGKKuY5sYrW5N+oGqit2zLBYv57mFvH/S8pWGYDGzUnU1CdTL3B4Yix+Hk8E/+m0cSi2E
1487
-dnAibw1brWVXM++8iYcUg84TMbJXntFYCyrNw1NF+008I02PeH4C8oDID6fIoKvsw3p7WJJ/I9Yp
1488
-a6oJzlJiP5JGxRxZPj50N6EMtzNB+tZoIGxgtOFVpiJ05yMQFztY6I6LKIgvXW/s919GIjGshqdM
1489
-XVPFxaKG4p9Iux/xazf48FY8O7SMmbQC1VsXIYo+7eSpIY67VzrCoh41wXPklOWS6CV8RR/JBSqq
1490
-8lHkcz8L21lMCOrVR1Cs0ls4HLIhUkqr9YegTJ67VM7xevUsgOI7BkPDldiulRgX+sdPheCyCacu
1491
-e7/b/nk0SXWF7ZBxsR1awYqwkFKz41/1bZDsETsmd8n1DHycGIvRULv3yYhKcvWQ4asAMhP1ks5k
1492
-AgOcrM+JFvpYA86Ja8HCqCg8LihEI1e7+m8F71Lpavv/UEsDBBQAAAAIAJ1WGVnKO2Ji+AEAAGsC
1493
-AAACAAAAMWENx7dyo0AAANDen+GWAonMzbggLsJakgGBOhBLlGBZsjz373eve7+fKyJTM/Sff85/
1494
-P5QMwMFfAWipfXwvFPWU582cd3t7JVV5pBV0Y1clL4eKUd0w1m1M5JrkgW5PlfpOVedgABSe4zPY
1495
-LnSIZVuen5Eua9QY8lQ7rxW7YIqeajhgLfL54BIcY90fd8ANixlcM8V23Z03U35Txba0BbSguc0f
1496
-NRF83cWp+7rOYgNO9wWLs915oQmWAqAtqRYCiWlgAtxYFg0MnNS4/G80FvFmQTh0cjwcF1xEVPeW
1497
-l72ky84PEA0QMgRtQW+HXWtE0/vQTtNKzvNqPfrGZCldL5nk9PWhhPEQ/azyW11bz2eB+aM0g0r7
1498
-0/5YkO9er10YonsBT1rEn0lfBXDHwtwbxG2bdqELTuEtX2+OEih7K43rN2EvpXX47azaNpe/drIz
1499
-wgAdhpfZ/mZwaGFX0c7r5HCTnroNRi5Bx/vu7m1A7Nt1dix4Gl/aPLCWQzpwmdIMJDiqD1RGpc5v
1500
-+pDLrpfhZOVhLjAPSQ0V7mm/XNSca8oIsDjwdvR438RQCU56mrlypklS4/tJAe0JZNZIgBmJszjG
1501
-AFbsmNYTJ9GmULB9lXmTWmrME592S285iWU5SsJcE1s+3oQw9QrvWB+e3bGAd9e+VFmFqr6+/gFQ
1502
-SwECHgMUAAAACACdVhlZ01RRk9IAAADmAAAABQAAAAAAAAABAAAApIEAAAAALmtleXNQSwECHgMU
1503
-AAAACACdVhlZ3Wlf4QUDAADUAwAAAgAAAAAAAAABAAAApIH1AAAAMDBQSwECHgMUAAAACACdVhlZ
1504
-/aaiwcoBAAArAgAAAgAAAAAAAAABAAAApIEaBAAAMDNQSwECHgMUAAAACACdVhlZs14oCcgBAAAr
1505
-AgAAAgAAAAAAAAABAAAApIEEBgAAMDlQSwECHgMUAAAACACdVhlZyjtiYvgBAABrAgAAAgAAAAAA
1506
-AAABAAAApIHsBwAAMWFQSwUGAAAAAAUABQDzAAAABAoAAAAA
1507
-"""
1508
-"""
1509
-A sample vault native configuration, in storeroom format, encrypted with
1510
-[`VAULT_MASTER_KEY`][].  The configuration is compressed (zip archive)
1511
-and then encoded in base64.
1512
-"""
1513
-VAULT_STOREROOM_CONFIG_DATA = {
1514
-    "global": {
1515
-        "phrase": DUMMY_PASSPHRASE.rstrip("\n"),
1516
-    },
1517
-    "services": {
1518
-        "(meta)": {
1519
-            "notes": "This config was originally in storeroom format.",
1520
-        },
1521
-        DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy(),
1522
-    },
1523
-}
1524
-"""
1525
-The parsed vault configuration stored in
1526
-[`VAULT_STOREROOM_CONFIG_ZIPPED`][].
1527
-"""
1528
-
1529
-VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED_JAVASCRIPT_SOURCE = """
1530
-// Executed in the top-level directory of the vault project code, in Node.js.
1531
-const storeroom = require('storeroom')
1532
-const Store = require('./lib/store.js')
1533
-let store = new Store(storeroom.createFileAdapter('./broken-dir'), 'vault key')
1534
-await store._storeroom.put('/services/array/', ['entry1','entry2'])
1535
-// The resulting "broken-dir" was then zipped manually.
1536
-"""
1537
-"""
1538
-The JavaScript source for the script that generated the storeroom
1539
-archive in [`VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED`][].
1540
-"""
1541
-VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED = b"""
1542
-UEsDBBQAAgAIAHijH1kjc0ql0gAAAOYAAAAFAAAALmtleXMFwclygjAAANB7P8Mrh7LIYmd6oGxC
1543
-HKwTJJgbNpBKCpGAhNTpv/e952ZpxHTjw+bN+HuJJABEikvHecD0pLgpgYKWjue0CZGk19mKF+4f
1544
-0AoLrXKh+ckk13nmxVk/KFE28eEHkBgJTISvRUVMQ0N5aRapLgWs/M7NSXV7qs0s2aIEstUG5FHv
1545
-fo/HKjpdUJMGK86vs2rOJFGyrx9ZK4iWW+LefwSTYxhYOlWpb0PpgXsV4dHNTz5skcJqpPUudZf9
1546
-jCFD0vxChL6ajm0P0prY+z9QSwMEFAACAAgAeKMfWX4L7vDYAQAAPwIAAAIAAAAwNQXByZKiMAAA
1547
-0Ht/Rl85sIR1qvqAouxbJAG8kWYxgCKICEzNv897f7+XanrR4fH9h//3pVdF8qmVeWjW+STwSbak
1548
-4e3CS00h2AcrQIcghm0lOcrLdJfuaOFqg5zEsW9lTbJMtIId5ezNGM9jPKaxeriXXm45pGuHCwFP
1549
-/gmcXKWGeU3sHfj93iIf6p0xrfQIGGJOvayKjzypUqb99Bllo9IwNP2FZjxmBWDw0NRzJrxr/4Qj
1550
-qp4ted4f91ZaR8+64C0BJBzDngElJEFLdA2WBcip2R/VZIG219WT3JlkbFrYSjhHWeb47igytTpo
1551
-USPjEJWVol0cVpD6iX1/mGM2BpHAFa+fLx3trXgbXaVmjyZVzUKDh/XqnovnLs529UGYCAdj8Xnx
1552
-vWwfWclm5uIB8cHbElx6G82Zs8RQnkDsyGVDbNaMOO7lMQF7o1Uy7Q9GuSWcFMK4KBAbcwm4l8RY
1553
-+2ema46H3/S31IW1LOFpoZxjwyBS69dWS7/ulVxJfbuydMvZMeWpmerjUHnKaQdumibSeSOXh+zg
1554
-XU6w6SsKAjHWXCTjRehWmyNnI7z3+epr1RzUlnDcUMiYQ/seaNefgNx4jIbOw92FC2hxnZOJupK9
1555
-M1WVdH3+8x9QSwMEFAACAAgAeKMfWUXRU2i7AQAAFwIAAAIAAAAxYQ3QyZZjUAAA0H19Rm2zCGLs
1556
-c2rxzDMxBTtTEA8hnqlO/3v3/YT7+71W86cdh+8/+N8vUMGNNAjWlNHgsyBlwCpgBd/a2rrW0qwg
1557
-p/CmvT4PTpwjHztJ2T10Jc2Fc8O7eHQb9MawAbxSKscxFAjz5wnJviaOMT5kEIZS+ibU6GgqU61P
1558
-lbeYRIiNCfK1VeHMFCpUhZ1ipnh50kux5N2jph5aMvc+HOR3lQgx9MJpMzQ2oNxSfEm7wZ5s0GYb
1559
-Bgy2xwaEMXNRnbzlbijZJi0M7yXNKS7nS1uFMtsapEc204YOBbOY4VK6L/9jS2ez56ybGkQPfn6+
1560
-QCwTqvkR5ieuRhF0zcoPLld+OUlI0RfEPnYHKEG7gtSya/Z1Hh77Xq4ytJHdr7WmXt7BUFA8Sffm
1561
-obXI31UOyVNLW0y4WMKDWq+atKGbU5BDUayoITMqvCteAZfJvnR4kZftMaFEG5ln7ptpdzpl10m3
1562
-G2rgUwTjPBJKomnOtJpdwm1tXm6IMPQ6IPy7oMDC5JjrmxAPXwdPnY/i07Go6EKSYjbkj8vdj/BR
1563
-rAMe2wnzdJaRhKv8kPVG1VqNdzm6xLb/Cf8AUEsDBBQAAgAIAHijH1kaCPeauQEAABcCAAACAAAA
1564
-MWUFwTmyokAAAND8H+OnBAKyTpVBs8iOIG2zZM0OigJCg07N3ee9v7+kmt/d6/n7h/n3AyJEvoaD
1565
-gtd8f4RxATnaHVeGNjyuolVVL+mY8Tms5ldfgYseNYMzRYJj3+i3iUgqlT5D1r7j1Bh5qVzi14X0
1566
-jpuH7DBKeeot2jWI5mPubptvV567pX2U3OC6ccxWmyo2Dd3ehUkbPP4uiDgWDZzFg/fFETIawMng
1567
-ahWHB2cfc2bM2kugNhWLS4peUBp36UWqMpF6+sLeUxAVZ24u08MDNMpNk81VDgiftnfBTBBhBGm0
1568
-RNpzxMMOPnCx3RRFgttiJTydfkB9MeZ9pvxP9jUm/fndQfJI83CsBxcEWhbjzlEparc3VS2s4LjR
1569
-3Xafw3HLSlPqylHOWK2vc2ZJoObwqrCaFRg7kz1+z08SGu8pe0EHaII6FSxL7VM+rfVgpc1045Ut
1570
-6ayCQ0TwRL5m4oMYkZbFnivCBTY3Cdji2SQ+gh8m3A6YkFxXUH0Vz9Is8JZaLFyi24GjyZZ9rGuk
1571
-Y6w53oLyTF/fSzG24ghCDZ6pOgB5qyfk4z2mUmH7pwxNCoHZ1oaxeTSn039QSwECHgMUAAIACAB4
1572
-ox9ZI3NKpdIAAADmAAAABQAAAAAAAAABAAAApIEAAAAALmtleXNQSwECHgMUAAIACAB4ox9Zfgvu
1573
-8NgBAAA/AgAAAgAAAAAAAAABAAAApIH1AAAAMDVQSwECHgMUAAIACAB4ox9ZRdFTaLsBAAAXAgAA
1574
-AgAAAAAAAAABAAAApIHtAgAAMWFQSwECHgMUAAIACAB4ox9ZGgj3mrkBAAAXAgAAAgAAAAAAAAAB
1575
-AAAApIHIBAAAMWVQSwUGAAAAAAQABADDAAAAoQYAAAAA
1576
-"""
1577
-"""
1578
-A sample corrupted storeroom archive, encrypted with
1579
-[`VAULT_MASTER_KEY`][].  The configuration is compressed (zip archive)
1580
-and then encoded in base64.
1581
-
1582
-The archive contains a directory `/services/array/` that claims to have
1583
-two child items 'entry1' and 'entry2', but no such child items are
1584
-present in the archive.  See
1585
-[`VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED_JAVASCRIPT_SOURCE`][] for
1586
-the exact script that created this archive.
1587
-"""
1588
-
1589
-VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED2_JAVASCRIPT_SOURCE = """
1590
-// Executed in the top-level directory of the vault project code, in Node.js.
1591
-const storeroom = require('storeroom')
1592
-const Store = require('./lib/store.js')
1593
-let store = new Store(storeroom.createFileAdapter('./broken-dir'), 'vault key')
1594
-await store._storeroom.put('/services/array/', 'not a directory index')
1595
-// The resulting "broken-dir" was then zipped manually.
1596
-"""
1597
-"""
1598
-The JavaScript source for the script that generated the storeroom
1599
-archive in [`VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED2`][].
1600
-"""
1601
-VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED2 = b"""
1602
-UEsDBAoAAAAAAM6NSVmrcHdV5gAAAOYAAAAFAAAALmtleXN7InZlcnNpb24iOjF9CkV3ZS9LZkJp
1603
-L0V0OUcrZmxYM3gxaFU4ZjE4YlE3S253bHoxN0IxSDE3cUhVOGdWK2RpWWY5MTdFZ0YrSStidEpZ
1604
-VXBzWVZVck45OC9uLzdsZnl2NUdGVEg2NWZxVy93YjlOc2MxeEZ4ck43Q3p4eTZ5MVAxZzFPb2VK
1605
-b0RZU3J6YXlwT0E2M3pidmk0ZTRiREMyNXhPTXl5NHBoMDFGeGdnQmpSNnpUcmR2UDk2UlZQd0I5
1606
-WitOZkZWZUlXT1NQN254ZFNYMGdFbkZ4SDBmWDkzNTFaTTZnPVBLAwQKAAAAAADOjUlZJg3/BhcC
1607
-AAAXAgAAAgAAADBieyJ2ZXJzaW9uIjoxfQpBVXJJMjNDQ2VpcW14cUZRMlV4SUpBaUoxNEtyUzh2
1608
-SXpIa2xROURBaFRlVHNFMmxPVUg4WUhTcUk1cXRGSHBqY3c1WkRkZmRtUlEwQXVGRjllY3lkam14
1609
-dDdUemRYLzNmNFUvTGlVV2dLRmQ1K1FEN3BlVlE1bWpqeHNlUEpHTDlhTWlKaGxSUVB4SmtUbjBx
1610
-U2poM1RUT0ZZbVAzV0JkdlUyWnF2RzhaSDk2cU1WcnZsQ0dMRmZTc2svVXlvcHZKdENONUVXcTRZ
1611
-SDUwNFNiejFIUVhWd2RjejlrS1BuR3J6SVA4ZmZtZnhXQ0U0TmtLb0ZPQXZuNkZvS3FZdGlGbFE9
1612
-PQpBVXBMUVMrMG9VeEZTeCtxbTB3SUtyM1MvTVJxYWJJTFlEUnY0aHlBMVE2TGR2Nlk0UmJ0enVz
1613
-NzRBc0cxbVhhenlRU2hlZVowdk0xM2ZyTFA4YlV0VHBaRyszNXF1eUhLM2NaWVJRZUxKM0JzejZz
1614
-b0xaQjNZTkpNenFxTTQrdzM1U0FZZ2lMU1NkN05NeWVrTHNhRUIzRDFOajlTRk85K3NGNEpFMWVL
1615
-UXpNMkltNk9qOUNVQjZUSTV3UitibksxN1BnY2RaeTZUMVRMWElVREVxcDg4dWdsWmRFTVcrNU9k
1616
-aE5ZbXEzZERWVWV4UnJpM1AwUmVBSi9KMGdJNkNoUUE9PVBLAwQKAAAAAADOjUlZTNfdphcCAAAX
1617
-AgAAAgAAADBmeyJ2ZXJzaW9uIjoxfQpBWVJqOVpIUktGUEVKOHM2YVY2TkRoTk5jQlZ5cGVYUmdz
1618
-cnBldFQ0cGhJRGROWFdGYzRia0daYkJxMngwRDFkcVNjYWk5UzEveDZ2K28zRE0rVEF2OVE3ZFVR
1619
-QWVKR3RmRkhJZDZxWW0ybEdNSnF5WTRNWm14aE9YdXliend0V3Q4Mnhvb041QTZNcWpINmxKQllD
1620
-UUN3ZEJjb3RER0EwRnlnVTEzeHV2WnIzT1puZnFFRGRqbzMxNkw5aExDN1RxMTYwUHpBOXJOSDMz
1621
-ZkNBcUhIVXZiYlFQQWErekw1d3dEN3FlWkY2MHdJaEwvRmk5L3JhNGJDcHZRNC9ORWpRd3c9PQpB
1622
-WWNGUDB1Y2xMMHh3ZDM2UXZXbm4wWXFsOU5WV0s3c05CMTdjdmM3N3VDZ0J2OE9XYkR5UHk5d05h
1623
-R2NQQzdzcVdZdHpZRlBHR0taVjhVUzA1YTVsV1BabDNGVFNuQXNtekxPelBlcFZxaitleDU3aEsx
1624
-QnV1bHkrUCtYQkE0YUtsaDM3c0RJL3I0UE1BVlJuMDNoSDJ5dEhDMW9PbjF0V1M5Q1NLV1pSMThh
1625
-djdTT0RBMVBNRnFYTmZKZVNTaVJiQ2htbDdOcFVLbjlXSGJZandybDlqN0JSdy9kWjhNQldCb3Ns
1626
-Nlc1dGZtdnJMVHhGRFBXYUgzSUp0T0czMEI1M3c9PVBLAwQKAAAAAADOjUlZn9rNID8CAAA/AgAA
1627
-AgAAADFkeyJ2ZXJzaW9uIjoxfQpBYWFBb3lqaGljVDZ4eXh1c0U0RVlDZCtxbE81Z0dEYTBNSFVS
1628
-MmgrSW9QMHV4UkY3b1BRS2czOHlQUEN3Ny9MYVJLQ0dQZ0RyZ2RpTWJTeUwzZ3ZNMFhseVpVMVBW
1629
-QVJvNEFETU9lbXgrOWhtS0hjQWNKMG5EeW5oSkhGYTYyb2xyQUNxekZzblhKNVBSeEVTVzVEbUh0
1630
-Ui9nRm5Wa1FvalhyVW4ybmpYMjVVanZQaXhlMU96Y0daMmQ0MjdVTGdnY1hqMkhSdjJiZldDNDUw
1631
-SGFXS3FDckZlYWlrQ2xkUUM2WGV3SkxZUjdvQUY3UjVha2ttK3M2MXNCRTVCaTg0QmJLWHluc1NG
1632
-ejE0TXFrd2JMK1VMYVk9CkFUT3dqTUFpa3Q4My9NTW5KRXQ2b3EyNFN4KzJKNDc2K2gyTmEzbHUr
1633
-MDg0cjlBT25aaUk0TmlYV0N1Q0lzakEzcTBwUHFJS1VXZHlPQW9uM2VHY0huZUppWUtVYllBaUJI
1634
-MVNmbnhQQkMzZkFMRklybkQ4Y0VqeGpPcUFUaTQ5dE1mRmtib0dNQ3dEdFY0V3NJL0tLUlRCOFd1
1635
-MnNXK2J0V3QzVWlvZG9ZeUVLTDk3ekNNemZqdGptejF4SDhHTXY5WDVnaG9NSW5RQVNvYlRreVZ4
1636
-dWo5YnlDazdNbU0vK21ZL3AwZE9oYVY0Nncwcm04UGlvWEtzdzR4bXB3ditDWC9PRXV3Uy9meDJT
1637
-Y0lOQnNuYVRiWT1QSwECHgMKAAAAAADOjUlZq3B3VeYAAADmAAAABQAAAAAAAAAAAAAApIEAAAAA
1638
-LmtleXNQSwECHgMKAAAAAADOjUlZJg3/BhcCAAAXAgAAAgAAAAAAAAAAAAAApIEJAQAAMGJQSwEC
1639
-HgMKAAAAAADOjUlZTNfdphcCAAAXAgAAAgAAAAAAAAAAAAAApIFAAwAAMGZQSwECHgMKAAAAAADO
1640
-jUlZn9rNID8CAAA/AgAAAgAAAAAAAAAAAAAApIF3BQAAMWRQSwUGAAAAAAQABADDAAAA1gcAAAAA
1641
-"""
1642
-"""
1643
-A sample corrupted storeroom archive, encrypted with
1644
-[`VAULT_MASTER_KEY`][].  The configuration is compressed (zip archive)
1645
-and then encoded in base64.
1646
-
1647
-The archive contains a directory `/services/array/` whose list of child
1648
-items does not adhere to the serialization format.  See
1649
-[`VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED2_JAVASCRIPT_SOURCE`][] for
1650
-the exact script that created this archive.
1651
-"""
1652
-
1653
-VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED3_JAVASCRIPT_SOURCE = """
1654
-// Executed in the top-level directory of the vault project code, in Node.js.
1655
-const storeroom = require('storeroom')
1656
-const Store = require('./lib/store.js')
1657
-let store = new Store(storeroom.createFileAdapter('./broken-dir'), 'vault key')
1658
-await store._storeroom.put('/services/array/', [null, 1, true, [], {}])
1659
-// The resulting "broken-dir" was then zipped manually.
1660
-"""
1661
-"""
1662
-The JavaScript source for the script that generated the storeroom
1663
-archive in [`VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED3`][].
1664
-"""
1665
-VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED3 = b"""
1666
-UEsDBAoAAAAAAEOPSVnVlcff5gAAAOYAAAAFAAAALmtleXN7InZlcnNpb24iOjF9CkV4dVBHUDBi
1667
-YkxrUVdvWnV5ZUJQRy8xdmM2MCt6MThOa3BsS09ydFAvUTVnQmxkYVpIOG10dTE5VWZFNGdGRGRj
1668
-eHJtWUd4eXZDZFNqcVlOaDh4cTlzM3VydkdRTWFwcnhtdlZGZUxoSW4zZnVlTDAweEk0ZmlLenZN
1669
-MmthUlRsNWNORGh3eUNlWVk4dzhBcXNhYjNyVWVsOEE0eVQ0cHU2d2tmQ3dTWUdqeG5HR29EcWJK
1670
-VnVJVWNpZVBEcU9PTzU2b0MyMG9lT01adFVkTUtxV28zYnFZPVBLAwQKAAAAAABDj0lZ77OVHxcC
1671
-AAAXAgAAAgAAADBjeyJ2ZXJzaW9uIjoxfQpBZllFQVVobEkyU2lZeGlrdWh0RzRNbUN3L1V2THBN
1672
-VVhwVlB0NlRwdzRyNGdocVJhbGZWZ0hxUHFtbTczSnltdFFrNnZnR2JRdUpiQmVlYjYwOHNrMGk4
1673
-ZFJVZjNwdlc2SnUyejljQkdwOG5mTFpTdlNad1lLN09UK2gzSDNDcmoxbXNicEZUcHVldW81NXc1
1674
-dGdYMnBuWXNWTVcrczdjaHEyMUIya2lIVEZrdGt1MXlaRzhPYkVUQjNCOFNGODVVbi9CUjFEMHJ1
1675
-ME9zOWl4ZWM2VmNTMitTZndtNnNtSlk2ZW9ZNTJzOGJNRGdYMndjQ0srREdkOEo2VWp0NG5OQVE9
1676
-PQpBUWlPRnRZcmJybWUycEwxRFpGT1BjU0RHOUN2cVkvbHhTWGIwaVJUdmtIWFc2bEtHL0p4RUtU
1677
-d3RTc0RTeDhsMTUvaHRmbWpOQ2tuTzhLVEFoKzhRQm5FbjZ0a2x5Y3BmeEIrTUxLRjFCM1Q1bjcv
1678
-T0VUMExMdmgxU2k1bnRRNXhTUHZZNWtXeUMyZjhXUXFZb3FSNU5JVENMeDV6dWNsQ3dGb2kvVXc4
1679
-OWNNWjM1MHBSbThzUktJbjJFeDUrQ1JwS3ZHdnBHbFJaTmk5VHZmVkNic1FCalR3MC9aeklTdzVQ
1680
-NW9BVWE2U1ExUVFnNHg4VUNkY0s2QUNLaFluY0d4TVE9PVBLAwQKAAAAAABDj0lZGk9LVj8CAAA/
1681
-AgAAAgAAADE0eyJ2ZXJzaW9uIjoxfQpBY1g2NVpMUWk4ck9pUlIyWGEwQlFHQVhQVWF2aHNJVGVY
1682
-c2dzRk9OUmFTRzJCQlg0SGxJRHpwRUd5aDUrZ2czZVRwWDFNOERua3pMeTVzcWRkMFpmK3padTgz
1683
-Qm52Y1JPREVIVDllUW91YUtPTWltdlRYanNuSXAxUHo5VGY1TlRkRjNJVTd2V1lhUDg4WTI5NG1i
1684
-c1VVL2RKVTZqZ3ZDbUw2cE1VZ28xUU12bGJnaVp3cDV1RDFQZXlrSXdKVWdJSEgxTEpnYi9xU2tW
1685
-c25leW1XY1RXR0NobzRvZGx3S2hJWmFCelhvNFhlN2U1V2I2VHA3Rkk5VUpVcmZIRTAvcVdrZUZE
1686
-VmxlazY3cUx3ZFZXcU9DdFk9CkFhSGR0QjhydmQ0U3N4ZmJ5eU1OOHIzZEoxeHA5NmFIRTQvalNi
1687
-Z05hZWttaDkyb2ROM1F4MUlqYXZsYVkxeEt1eFF3KzlwTHFIcTF5a1JSRjQzL2RVWGFIRk5UU0NX
1688
-OVFsdmd3KzMwa1ZhSEdXRllvbFRnRWE4djQ3b3VrbGlmc01PZGM0YVNKb2R4ZUFJcVc3Q1cwdDVR
1689
-b2RUbWREUXpqc3phZkQ4R2VOd2NFQjdGMHI2RzNoZEJlQndxd3Z6eENVYnpSUmU5bEQ3NjQ3RFp1
1690
-bEo1U3c4amlvV0paTW40NlZhV3BYUXk4UnNva3hHaW00WUpybUZIQ2JkVU9qSWJsUmQ1Z3VhUDNU
1691
-M0NxeHRPdC94b1BhOD1QSwMECgAAAAAAQ49JWVJM8QYXAgAAFwIAAAIAAAAxNnsidmVyc2lvbiI6
1692
-MX0KQVlCWDF6M21qUlQrand4M2FyNkFpemxnalJZbUM0ZHg5NkxVQVBTVHNMWXJKVHFtWnd5N0Jy
1693
-OFlCcElVamorMHdlT3lNaUtLVnFwaER3RXExNWFqUmlSZUVEQURTVHZwWmlLZUlnZjR5elUzZXNP
1694
-eDJ2U2J1bXhTK0swUGZVa2tsSy9TRmRiU3EvUHFMRjBDRTVCMXNyKzJLYTB2WlJmak94R3VFeFRD
1695
-RXozN0ZlWDNNR3NCNkhZVHEzaUJWcUR6NVB6eHpCWWM5Kyt6RitLS1RnMVp2NGRtRmVQTC9JSEY5
1696
-WnV6TWlqRXdCRkE3WnJ0dkRqd3ZYcWtsMVpsR0c4eUV3PT0KQVhUWkRLVnNleldpR1RMUVZqa2hX
1697
-bXBnK05MYlM0M2MxZEpvK2xGcC9yWUJYZkw3Wll5cGdjWE5IWXNzd01nc2VSSTAzNmt6bGZkdGNa
1698
-bTdiUUN6M2JuQmZ6ZlorZFFuT2Y5STVSU2l0QzB2UmsydkQrOFdwbmRPSzNucGY5S0VpWklOSzVq
1699
-TEZGTTJDTkNmQzBabXNRUlF3T0k2N3l5ZHhjVnFDMXBnWHV6QXRXamlsSUpnN0p6eUtsY3BJUGJu
1700
-SUc0UzRSUlhIdW1wZnpoeWFZWkd6T0FDamRSYTZIMWJxYkJkZXFaSHMvQXJvM25mVjdlbjhxSUE5
1701
-aVUrbnNweXFnPT1QSwECHgMKAAAAAABDj0lZ1ZXH3+YAAADmAAAABQAAAAAAAAAAAAAApIEAAAAA
1702
-LmtleXNQSwECHgMKAAAAAABDj0lZ77OVHxcCAAAXAgAAAgAAAAAAAAAAAAAApIEJAQAAMGNQSwEC
1703
-HgMKAAAAAABDj0lZGk9LVj8CAAA/AgAAAgAAAAAAAAAAAAAApIFAAwAAMTRQSwECHgMKAAAAAABD
1704
-j0lZUkzxBhcCAAAXAgAAAgAAAAAAAAAAAAAApIGfBQAAMTZQSwUGAAAAAAQABADDAAAA1gcAAAAA
1705
-"""
1706
-"""
1707
-A sample corrupted storeroom archive, encrypted with
1708
-[`VAULT_MASTER_KEY`][].  The configuration is compressed (zip archive)
1709
-and then encoded in base64.
1710
-
1711
-The archive contains a directory `/services/array/` whose list of child
1712
-items are not all valid item names.  See
1713
-[`VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED3_JAVASCRIPT_SOURCE`][] for
1714
-the exact script that created this archive.
1715
-"""
1716
-
1717
-VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED4_JAVASCRIPT_SOURCE = """
1718
-// Executed in the top-level directory of the vault project code, in Node.js.
1719
-const storeroom = require('storeroom')
1720
-const Store = require('./lib/store.js')
1721
-let store = new Store(storeroom.createFileAdapter('./broken-dir'), 'vault key')
1722
-await store._storeroom.put('/dir/subdir/', [])
1723
-await store._storeroom.put('/dir/', [])
1724
-// The resulting "broken-dir" was then zipped manually.
1725
-"""
1726
-"""
1727
-The JavaScript source for the script that generated the storeroom
1728
-archive in [`VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED4`][].
1729
-"""
1730
-VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED4 = b"""
1731
-UEsDBAoAAAAAAE+5SVloORS+5gAAAOYAAAAFAAAALmtleXN7InZlcnNpb24iOjF9CkV6dWRoNkRQ
1732
-YTlNSWFabHZ5TytVYTFuamhjV2hIaTFBU0lKYW5zcXBxVlA0blN2V0twUzdZOUc2bjFSbi8vUnVM
1733
-VitwcHp5SC9RQk83R0hFenNVMzdCUzFwUmVVeGhxUVlVTE56OXZvQ0crM1ZaL3VncU44dDJiU05m
1734
-Nyt5K3hiNng2aVlFUmNZYTJ0UkhzZVdIc0laTE9ha2lDb0lRVGV3cndwYjVMM2pnd0E3SXBzaDkz
1735
-QkxHSzM5dXNYNmo0R0I2WkRUeW5JcGk4V3JkbDhnWVZCN0tVPVBLAwQKAAAAAABPuUlZ663uUhcC
1736
-AAAXAgAAAgAAADAzeyJ2ZXJzaW9uIjoxfQpBV2wzS2gzd21ZSFVZZU1RR3BLSVowdVd1VXFna09h
1737
-YmRjNzNYYXVsZTNtVS9sN2Zvd1AyS21jbFp3ZDM5V3lYVzRTcEw4R0l4YStDZW51S3V0Wm5nb0FR
1738
-bWlnaUJUbkFaais5TENCcGNIWlZNY2RBVkgxKzBFNGpsanZ1UkVwZ0tPS05LZjRsTUl1QnZ4VmFB
1739
-ZkdwNHJYNEZ4MmpPSlk1Y3NQZzBBRFBoZVAwN29GWVQ3alorSUNEK1AxNGZPdWpwMGRUeDRrTDIy
1740
-LzlqalRDNXBCNVF5NW5iOUx3Zk5DUWViSUVpaTZpbU0vRmFrK1dtV05tMndqMERSTEc4RHY3ZkE9
1741
-PQpBU0c3NTNGTVVwWmxjK3E1YXRzcC93OUNqN2JPOFlpY24wZHg2UGloTmwzUS9WSjVVeGJmU3l0
1742
-ZDFDNDBRU2xXeTJqOTJDWUd3VER6eEdBMXVnb0FCYi9kTllTelVwbHJFb3BuUVphYXdsdTVwV2x0
1743
-Y1E5WTcveWN4S2E4b0JaaGY3RkFYcGo2c01wUW9zNzI5VFVabFd4UmI4VFRtN2FrVnR1OXcvYXlK
1744
-RS9reDh4ZUYxSGJlc3Q4N1IxTGg2ODd3dS9XVUN2ZjNXYXo1VjNnZWY0RnpUTXg0bkpqSlZOd0U0
1745
-SzAxUTlaVzQ0bmVvbExPUVI1MkZDeDZvbml3RW9tenc9PVBLAwQKAAAAAABPuUlZRXky4CsCAAAr
1746
-AgAAAgAAADEweyJ2ZXJzaW9uIjoxfQpBWmlYWVlvNUdCY2d5dkFRaGtyK2ZjUkdVSkdabDd2dE5w
1747
-T2Mrd1VzbXJhQWhRN3dKdlYraGhKcTlrcWNKQnBWU0gyUTBTTVVhb29iNjBJM1NYNUNtTkJRU2FH
1748
-M3prd0Y0T2F4TnpCZUh0NFlpaDd4Y3p2ak4xR0hISDJQYW0xam05K09ja3JLVmNMVURtNXRKb2ZC
1749
-Z1E4Q2NwMGZMVkdEaURjNWF0MjVMc2piQVcvNkZFSnJ5VVBHWis4UVdYRmlWMGdtVVZybVc3VUFy
1750
-dGhJQitWNTdZS1BORi95Nng2OU43UTFQbmp1cUczdlpybzljMEJ3d012NWoyc3BMMTJHcTdzTDZE
1751
-alB1d0dHbnB2MkVZQTFLbmc9CkFTdjQwUkgzRmxzbGVlU1NjRlZNRmh3dEx6eEYxK2xpcmxEL29X
1752
-alJLQ05qVWZhUVpJTWpqMWRoVkhOakNUTWhWZ1ZONkl3b04xTnFOMEV6cmdhaTFBWnNiMm9UczYw
1753
-QkI1UGh0U0hhQ2U2WllUeE1JemFPS2FIK0w2eHhtaXIrTlQxNTRXS0x5amJMams3MU1na3Nwa0Yy
1754
-WDBJMnlaWW5IUUM0bmdEL24yZzRtSVI2Q1hWL0JOUXNzeTBEeXdGLzN6eGRRYWw5cFBtVk1qYnFu
1755
-cHY5SFNqRTg4S25naVpBWFhJWU1OVGF2L3Q3Y3dEWGdNekhKTlU0Y2xnVUtIQVZ3QT09UEsDBAoA
1756
-AAAAAE+5SVkPfKx9FwIAABcCAAACAAAAMWR7InZlcnNpb24iOjF9CkFYbHNLRzQwZG5ibTJvcXdY
1757
-U2ZrSWp3Mmxpa0lDS3hVOXU3TU52VkZ1NEJ2R1FVVitSVVdsS3MxL25TSlBtM2U2OTRvVHdoeDFo
1758
-RFF3U0M5U0QvbXd5bnpjSTloUnRCUWVXMkVMOVU5L1ZGcHFsVWY3Z1ZOMHZ0ZWpXYnV4QnhsZlRD
1759
-Tys4SFBwU2Zaa2VOUld5R2JNdzBFSU9LTmxRYjk3OUF0c1g3THR0NytaTkJnakZHYkZxaHdwa3kx
1760
-WUNDVng1UmNZZ2tma2ZjWnVncGpzc1RzNVFvK1p3QXBEcDZ4V3JjSHMxUDhvNktBRzAwcjZZbkNM
1761
-N2ErU1dwZmVNTUJhZz09CkFadVF0cFZMWmVvb292NkdyQlpnb3B6VmRGUXBlK1h6QXZuZ2dPVnZM
1762
-VWtCYVF2akl5K1VLdXVUVlFoQ1JiMVp6dGZQL2dsNnoxOEsyZW5sQlo2bGJTZnoxTlBWeUVzYXB3
1763
-dDVpUVh4azd5UkJlZks1cFlsNTduUXlmcFZQbzlreFpnOVdHTkV3NVJ5MkExemhnNGl6TWxLRmJh
1764
-UjZFZ0FjQ3NFOXAveGRLa29ZNjhOUlZmNXJDM3lMQjc3ZWgyS1hCUld2WDNZcE9XdW00OGtsbmtI
1765
-akJjMFpiQmUrT3NZb3d5cXpoRFA2ZGQxRlFnMlFjK09vc3B4V0sycld4M01HZz09UEsBAh4DCgAA
1766
-AAAAT7lJWWg5FL7mAAAA5gAAAAUAAAAAAAAAAAAAAKSBAAAAAC5rZXlzUEsBAh4DCgAAAAAAT7lJ
1767
-Weut7lIXAgAAFwIAAAIAAAAAAAAAAAAAAKSBCQEAADAzUEsBAh4DCgAAAAAAT7lJWUV5MuArAgAA
1768
-KwIAAAIAAAAAAAAAAAAAAKSBQAMAADEwUEsBAh4DCgAAAAAAT7lJWQ98rH0XAgAAFwIAAAIAAAAA
1769
-AAAAAAAAAKSBiwUAADFkUEsFBgAAAAAEAAQAwwAAAMIHAAAAAA==
1770
-"""
1771
-"""
1772
-A sample corrupted storeroom archive, encrypted with
1773
-[`VAULT_MASTER_KEY`][].  The configuration is compressed (zip archive)
1774
-and then encoded in base64.
1775
-
1776
-The archive contains two directories `/dir/` and `/dir/subdir/`, where
1777
-`/dir/subdir/` is a correctly serialized directory, but `/dir/` does not
1778
-contain `/dir/subdir/` in its list of child items.  See
1779
-[`VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED4_JAVASCRIPT_SOURCE`][] for
1780
-the exact script that created this archive.
1781
-"""
1782
-
1783
-CANNOT_LOAD_CRYPTOGRAPHY = (
1784
-    "Cannot load the required Python module 'cryptography'."
1785
-)
1786
-"""
1787
-The expected `derivepassphrase` error message when the `cryptography`
1788
-module cannot be loaded, which is needed e.g. by the `export vault`
1789
-subcommands.
1790
-"""
1791
-
1792
-skip_if_cryptography_support = pytest.mark.skipif(
1793
-    importlib.util.find_spec("cryptography") is not None,
1794
-    reason='cryptography support available; cannot test "no support" scenario',
1795
-)
1796
-"""
1797
-A cached pytest mark to skip this test if cryptography support is
1798
-available.  Usually this means that the test targets
1799
-`derivepassphrase`'s fallback functionality, which is not available
1800
-whenever the primary functionality is.
1801
-"""
1802
-skip_if_no_cryptography_support = pytest.mark.skipif(
1803
-    importlib.util.find_spec("cryptography") is None,
1804
-    reason='no "cryptography" support',
1805
-)
1806
-"""
1807
-A cached pytest mark to skip this test if cryptography support is not
1808
-available.  Usually this means that the test targets the
1809
-`derivepassphrase export vault` subcommand, whose functionality depends
1810
-on cryptography support being available.
1811
-"""
1812
-skip_if_on_the_annoying_os = pytest.mark.skipif(
1813
-    sys.platform == "win32",
1814
-    reason="The Annoying OS behaves differently.",
1815
-)
1816
-"""
1817
-A cached pytest mark to skip this test if running on The Annoying
1818
-Operating System, a.k.a. Microsoft Windows.  Usually this is due to
1819
-unnecessary and stupid differences in the OS internals, and these
1820
-differences are deemed irreconcilable in the context of the decorated
1821
-test, so the test is to be skipped.
1822
-
1823
-See also:
1824
-    [`xfail_on_the_annoying_os`][]
1825
-
1826
-"""
1827
-skip_if_no_multiprocessing_support = pytest.mark.skipif(
1828
-    importlib.util.find_spec("multiprocessing") is None,
1829
-    reason='no "multiprocessing" support',
1830
-)
1831
-"""
1832
-A cached pytest mark to skip this test if multiprocessing support is not
1833
-available.  Usually this means that the test targets the concurrency
1834
-features of `derivepassphrase`, which is generally only possible to test
1835
-in separate processes because the testing machinery operates on
1836
-process-global state.
1837
-"""
1838
-
1839
-MIN_CONCURRENCY = 4
1840
-"""
1841
-The minimum amount of concurrent threads used for testing.
1842
-"""
1843
-
1844
-
1845
-def get_concurrency_limit() -> int:
1846
-    """Return the imposed limit on the number of concurrent threads.
1847
-
1848
-    We use [`os.process_cpu_count`][] as the limit on Python 3.13 and
1849
-    higher, and [`os.cpu_count`][] on Python 3.12 and below.  On
1850
-    Python 3.12 and below, we explicitly support the `PYTHON_CPU_COUNT`
1851
-    environment variable.  We guarantee at least [`MIN_CONCURRENCY`][]
1852
-    many threads in any case.
1853
-
1854
-    """  # noqa: RUF002
1855
-    result: int | None = None
1856
-    if sys.version_info >= (3, 13):
1857
-        result = os.process_cpu_count()
1858
-    else:
1859
-        with contextlib.suppress(KeyError, ValueError):
1860
-            result = result or int(os.environ["PYTHON_CPU_COUNT"], 10)
1861
-        with contextlib.suppress(AttributeError):
1862
-            result = result or len(os.sched_getaffinity(os.getpid()))
1863
-    return max(result if result is not None else 0, MIN_CONCURRENCY)
1864
-
1865
-
1866
-def get_concurrency_step_count(
1867
-    settings: hypothesis.settings | None = None,
1868
-) -> int:
1869
-    """Return the desired step count for concurrency-related tests.
1870
-
1871
-    This is the smaller of the [general concurrency
1872
-    limit][tests.get_concurrency_limit] and the step count from the
1873
-    current hypothesis settings.
1874
-
1875
-    Args:
1876
-        settings:
1877
-            The hypothesis settings for a specific tests.  If not given,
1878
-            then the current profile will be queried directly.
1879
-
1880
-    """
1881
-    if settings is None:  # pragma: no cover
1882
-        settings = hypothesis.settings()
1883
-    return min(get_concurrency_limit(), settings.stateful_step_count)
1884
-
1885
-
1886
-def xfail_on_the_annoying_os(
1887
-    f: Callable | None = None,
1888
-    /,
1889
-    *,
1890
-    reason: str = "",
1891
-) -> pytest.MarkDecorator | Any:  # pragma: no cover
1892
-    """Annotate a test which fails on The Annoying OS.
1893
-
1894
-    Annotate a test to indicate that it fails on The Annoying Operating
1895
-    System, a.k.a. Microsoft Windows.  Usually this is due to
1896
-    differences in the design of OS internals, and usually, these
1897
-    differences are both unnecessary and stupid.
1898
-
1899
-    Args:
1900
-        f:
1901
-            A callable to decorate.  If not given, return the pytest
1902
-            mark directly.
1903
-        reason:
1904
-            An optional, more detailed reason stating why this test
1905
-            fails on The Annoying OS.
1906
-
1907
-    Returns:
1908
-        The callable, marked as an expected failure on the Annoying OS,
1909
-        or alternatively a suitable pytest mark if no callable was
1910
-        passed.  The reason will begin with the phrase "The Annoying OS
1911
-        behaves differently.", and the optional detailed reason, if not
1912
-        empty, will follow.
1913
-
1914
-    """
1915
-    base_reason = "The Annoying OS behaves differently."
1916
-    full_reason = base_reason if not reason else f"{base_reason}  {reason}"
1917
-    mark = pytest.mark.xfail(
1918
-        sys.platform == "win32",
1919
-        reason=full_reason,
1920
-        raises=(AssertionError, hypothesis.errors.FailedHealthCheck),
1921
-        strict=True,
1922
-    )
1923
-    return mark if f is None else mark(f)
1924
-
1925
-
1926
-@socketprovider.SocketProvider.register("stub_agent")
1927
-class StubbedSSHAgentSocket:
1928
-    """A stubbed SSH agent presenting an [`_types.SSHAgentSocket`][]."""
1929
-
1930
-    _SOCKET_IS_CLOSED = "Socket is closed."
1931
-    _NO_FLAG_SUPPORT = "This stubbed SSH agent socket does not support flags."
1932
-    _PROTOCOL_VIOLATION = "SSH agent protocol violation."
1933
-    _INVALID_REQUEST = "Invalid request."
1934
-    _UNSUPPORTED_REQUEST = "Unsupported request."
1935
-
1936
-    HEADER_SIZE = 4
1937
-    CODE_SIZE = 1
1938
-
1939
-    KNOWN_EXTENSIONS = frozenset({
1940
-        "query",
1941
-        "list-extended@putty.projects.tartarus.org",
1942
-    })
1943
-    """Known and implemented protocol extensions."""
1944
-
1945
-    def __init__(self, *extensions: str) -> None:
1946
-        """Initialize the agent."""
1947
-        self.send_to_client = bytearray()
1948
-        """
1949
-        The buffered response to the client, read piecemeal by [`recv`][].
1950
-        """
1951
-        self.receive_from_client = bytearray()
1952
-        """The last request issued by the client."""
1953
-        self.closed = False
1954
-        """True if the connection is closed, false otherwise."""
1955
-        self.enabled_extensions = frozenset(extensions) & self.KNOWN_EXTENSIONS
1956
-        """
1957
-        Extensions actually enabled in this particular stubbed SSH agent.
1958
-        """
1959
-        self.try_rfc6979 = False
1960
-        """
1961
-        Attempt to issue DSA and ECDSA signatures according to RFC 6979?
1962
-        """
1963
-        self.try_pageant_068_080 = False
1964
-        """
1965
-        Attempt to issue DSA and ECDSA signatures as per Pageant 0.68–0.80?
1966
-        """  # noqa: RUF001
1967
-
1968
-    def __enter__(self) -> Self:
1969
-        """Return self."""
1970
-        return self
1971
-
1972
-    def __exit__(self, *args: object) -> None:
1973
-        """Mark the agent's socket as closed."""
1974
-        self.closed = True
1975
-
1976
-    def sendall(self, data: Buffer, flags: int = 0, /) -> None:
1977
-        """Send data to the SSH agent.
1978
-
1979
-        The signature, and behavior, is identical to
1980
-        [`socket.socket.sendall`][].  Upon successful sending, this
1981
-        agent will parse the request, call the appropriate handler, and
1982
-        buffer the result such that it can be read via [`recv`][], in
1983
-        accordance with the SSH agent protocol.
1984
-
1985
-        Args:
1986
-            data: Binary data to send to the agent.
1987
-            flags: Reserved.  Must be 0.
1988
-
1989
-        Returns:
1990
-            Nothing.  The result should be requested via [`recv`][], and
1991
-            interpreted in accordance with the SSH agent protocol.
1992
-
1993
-        Raises:
1994
-            AssertionError:
1995
-                The flags argument, if specified, must be 0.
1996
-            ValueError:
1997
-                The agent's socket is already closed.  No further
1998
-                requests can be sent.
1999
-
2000
-        """
2001
-        assert not flags, self._NO_FLAG_SUPPORT
2002
-        if self.closed:
2003
-            raise ValueError(self._SOCKET_IS_CLOSED)
2004
-        self.receive_from_client.extend(memoryview(data))
2005
-        try:
2006
-            self.parse_client_request_and_dispatch()
2007
-        except ValueError:
2008
-            payload = int.to_bytes(_types.SSH_AGENT.FAILURE.value, 1, "big")
2009
-            self.send_to_client.extend(int.to_bytes(len(payload), 4, "big"))
2010
-            self.send_to_client.extend(payload)
2011
-        finally:
2012
-            self.receive_from_client.clear()
2013
-
2014
-    def recv(self, count: int, flags: int = 0, /) -> bytes:
2015
-        """Read data from the SSH agent.
2016
-
2017
-        As per the SSH agent protocol, data is only available to be read
2018
-        immediately after a request via [`sendall`][].  Calls to
2019
-        [`recv`][] at other points in time that attempt to read data
2020
-        violate the protocol, and will fail.  Notwithstanding the last
2021
-        sentence, at any point in time, though pointless, it is
2022
-        additionally permissible to read 0 bytes from the agent, or any
2023
-        number of bytes from a closed socket.
2024
-
2025
-        Args:
2026
-            count:
2027
-                Number of bytes to read from the agent.
2028
-            flags:
2029
-                Reserved.  Must be 0.
2030
-
2031
-        Returns:
2032
-            (A chunk of) the SSH agent's response to the most recent
2033
-            request.  If reading 0 bytes, or if reading from a closed
2034
-            socket, the returned chunk is always an empty byte string.
2035
-
2036
-        Raises:
2037
-            AssertionError:
2038
-                The flags argument, if specified, must be 0.
2039
-
2040
-                Alternatively, `recv` was called when there was no
2041
-                response to be obtained, in violation of the SSH agent
2042
-                protocol.
2043
-
2044
-        """
2045
-        assert not flags, self._NO_FLAG_SUPPORT
2046
-        assert not count or self.closed or self.send_to_client, (
2047
-            self._PROTOCOL_VIOLATION
2048
-        )
2049
-        ret = bytes(self.send_to_client[:count])
2050
-        del self.send_to_client[:count]
2051
-        return ret
2052
-
2053
-    def parse_client_request_and_dispatch(self) -> None:
2054
-        """Parse the client request and call the matching handler.
2055
-
2056
-        This agent supports the
2057
-        [`SSH_AGENTC_REQUEST_IDENTITIES`][_types.SSH_AGENTC.REQUEST_IDENTITIES],
2058
-        [`SSH_AGENTC_SIGN_REQUEST`][_types.SSH_AGENTC.SIGN_REQUEST] and
2059
-        the [`SSH_AGENTC_EXTENSION`][_types.SSH_AGENTC.EXTENSION]
2060
-        request types.
2061
-
2062
-        """
2063
-
2064
-        if len(self.receive_from_client) < self.HEADER_SIZE + self.CODE_SIZE:
2065
-            raise ValueError(self._INVALID_REQUEST)
2066
-        target_header = ssh_agent.SSHAgentClient.uint32(
2067
-            len(self.receive_from_client) - self.HEADER_SIZE
2068
-        )
2069
-        if target_header != self.receive_from_client[: self.HEADER_SIZE]:
2070
-            raise ValueError(self._INVALID_REQUEST)
2071
-        code = _types.SSH_AGENTC(
2072
-            int.from_bytes(
2073
-                self.receive_from_client[
2074
-                    self.HEADER_SIZE : self.HEADER_SIZE + self.CODE_SIZE
2075
-                ],
2076
-                "big",
2077
-            )
2078
-        )
2079
-
2080
-        def is_enabled_extension(extension: str) -> bool:
2081
-            if (
2082
-                extension not in self.enabled_extensions
2083
-                or code != _types.SSH_AGENTC.EXTENSION
2084
-            ):
2085
-                return False
2086
-            string = ssh_agent.SSHAgentClient.string
2087
-            extension_marker = b"\x1b" + string(extension.encode("ascii"))
2088
-            return self.receive_from_client.startswith(extension_marker, 4)
2089
-
2090
-        result: Buffer | Iterator[int]
2091
-        if code == _types.SSH_AGENTC.REQUEST_IDENTITIES:
2092
-            result = self.request_identities(list_extended=False)
2093
-        elif code == _types.SSH_AGENTC.SIGN_REQUEST:
2094
-            result = self.sign()
2095
-        elif is_enabled_extension("query"):
2096
-            result = self.query_extensions()
2097
-        elif is_enabled_extension("list-extended@putty.projects.tartarus.org"):
2098
-            result = self.request_identities(list_extended=True)
2099
-        else:
2100
-            raise ValueError(self._UNSUPPORTED_REQUEST)
2101
-        self.send_to_client.extend(
2102
-            ssh_agent.SSHAgentClient.string(bytes(result))
2103
-        )
2104
-
2105
-    def query_extensions(self) -> Iterator[int]:
2106
-        """Answer an `SSH_AGENTC_EXTENSION` request.
2107
-
2108
-        Yields:
2109
-            The bytes payload of the response, without the protocol
2110
-            framing.  The payload is yielded byte by byte, as an
2111
-            iterable of 8-bit integers.
2112
-
2113
-        """
2114
-        yield _types.SSH_AGENT.EXTENSION_RESPONSE.value
2115
-        yield from ssh_agent.SSHAgentClient.string(b"query")
2116
-        extension_answers = [
2117
-            b"query",
2118
-            b"list-extended@putty.projects.tartarus.org",
2119
-        ]
2120
-        for a in extension_answers:
2121
-            yield from ssh_agent.SSHAgentClient.string(a)
2122
-
2123
-    def request_identities(
2124
-        self, *, list_extended: bool = False
2125
-    ) -> Iterator[int]:
2126
-        """Answer an `SSH_AGENTC_REQUEST_IDENTITIES` request.
2127
-
2128
-        Args:
2129
-            list_extended:
2130
-                If true, answer an `SSH_AGENTC_EXTENSION` request for
2131
-                the `list-extended@putty.projects.tartarus.org`
2132
-                extension. Otherwise, answer an
2133
-                `SSH_AGENTC_REQUEST_IDENTITIES` request.
2134
-
2135
-        Yields:
2136
-            The bytes payload of the response, without the protocol
2137
-            framing.  The payload is yielded byte by byte, as an
2138
-            iterable of 8-bit integers.
2139
-
2140
-        """
2141
-        if list_extended:
2142
-            yield _types.SSH_AGENT.SUCCESS.value
2143
-        else:
2144
-            yield _types.SSH_AGENT.IDENTITIES_ANSWER.value
2145
-        signature_classes = [
2146
-            SSHTestKeyDeterministicSignatureClass.SPEC,
2147
-        ]
2148
-        if (
2149
-            "list-extended@putty.projects.tartarus.org"
2150
-            in self.enabled_extensions
2151
-        ):
2152
-            signature_classes.append(
2153
-                SSHTestKeyDeterministicSignatureClass.RFC_6979
2154
-            )
2155
-        keys = [
2156
-            v
2157
-            for v in ALL_KEYS.values()
2158
-            if any(cls in v.expected_signatures for cls in signature_classes)
2159
-        ]
2160
-        yield from ssh_agent.SSHAgentClient.uint32(len(keys))
2161
-        for key in keys:
2162
-            yield from ssh_agent.SSHAgentClient.string(key.public_key_data)
2163
-            yield from ssh_agent.SSHAgentClient.string(
2164
-                b"test key without passphrase"
2165
-            )
2166
-            if list_extended:
2167
-                yield from ssh_agent.SSHAgentClient.string(
2168
-                    ssh_agent.SSHAgentClient.uint32(0)
2169
-                )
2170
-
2171
-    def sign(self) -> bytes:
2172
-        """Answer an `SSH_AGENTC_SIGN_REQUEST` request.
2173
-
2174
-        Returns:
2175
-            The bytes payload of the response, without the protocol
2176
-            framing.
2177
-
2178
-        """
2179
-        try_rfc6979 = (
2180
-            "list-extended@putty.projects.tartarus.org"
2181
-            in self.enabled_extensions
2182
-        )
2183
-        spec = SSHTestKeyDeterministicSignatureClass.SPEC
2184
-        rfc6979 = SSHTestKeyDeterministicSignatureClass.RFC_6979
2185
-        key_blob, rest = ssh_agent.SSHAgentClient.unstring_prefix(
2186
-            self.receive_from_client[self.HEADER_SIZE + self.CODE_SIZE :]
2187
-        )
2188
-        sign_data, rest = ssh_agent.SSHAgentClient.unstring_prefix(rest)
2189
-        if len(rest) != 4:
2190
-            raise ValueError(self._INVALID_REQUEST)
2191
-        flags = int.from_bytes(rest, "big")
2192
-        if flags:
2193
-            raise ValueError(self._UNSUPPORTED_REQUEST)
2194
-        if sign_data != vault.Vault.UUID:
2195
-            raise ValueError(self._UNSUPPORTED_REQUEST)
2196
-        for key in ALL_KEYS.values():
2197
-            if key.public_key_data == key_blob:
2198
-                if spec in key.expected_signatures:
2199
-                    return int.to_bytes(
2200
-                        _types.SSH_AGENT.SIGN_RESPONSE.value, 1, "big"
2201
-                    ) + ssh_agent.SSHAgentClient.string(
2202
-                        key.expected_signatures[spec].signature
2203
-                    )
2204
-                if try_rfc6979 and rfc6979 in key.expected_signatures:
2205
-                    return int.to_bytes(
2206
-                        _types.SSH_AGENT.SIGN_RESPONSE.value, 1, "big"
2207
-                    ) + ssh_agent.SSHAgentClient.string(
2208
-                        key.expected_signatures[rfc6979].signature
2209
-                    )
2210
-                raise ValueError(self._UNSUPPORTED_REQUEST)
2211
-        raise ValueError(self._UNSUPPORTED_REQUEST)
2212
-
2213
-
2214
-@socketprovider.SocketProvider.register("stub_with_address")
2215
-class StubbedSSHAgentSocketWithAddress(StubbedSSHAgentSocket):
2216
-    """A [`StubbedSSHAgentSocket`][] requiring a specific address."""
2217
-
2218
-    ADDRESS = "stub-ssh-agent:"
2219
-    """The correct address for connecting to this stubbed agent."""
2220
-
2221
-    def __init__(self, *extensions: str) -> None:
2222
-        """Initialize the agent, based on `SSH_AUTH_SOCK`.
2223
-
2224
-        Socket addresses of the form `stub-ssh-agent:<errno_value>` will
2225
-        raise an [`OSError`][] (or the respective subclass) with the
2226
-        specified [`errno`][] value.  For example,
2227
-        `stub-ssh-agent:EPERM` will raise a [`PermissionError`][].
2228
-
2229
-        Raises:
2230
-            KeyError:
2231
-                The `SSH_AUTH_SOCK` environment variable is not set.
2232
-            OSError:
2233
-                The address in `SSH_AUTH_SOCK` is unsuited.
2234
-
2235
-        """
2236
-        super().__init__(*extensions)
2237
-        try:
2238
-            orig_address = os.environ["SSH_AUTH_SOCK"]
2239
-        except KeyError as exc:
2240
-            msg = "SSH_AUTH_SOCK environment variable"
2241
-            raise KeyError(msg) from exc
2242
-        address = orig_address
2243
-        if not address.startswith(self.ADDRESS):
2244
-            address = self.ADDRESS + "ENOENT"
2245
-        errcode = address.removeprefix(self.ADDRESS)
2246
-        if errcode and not (
2247
-            errcode.startswith("E") and hasattr(errno, errcode)
2248
-        ):
2249
-            errcode = "EINVAL"
2250
-        if errcode:
2251
-            errno_val = getattr(errno, errcode)
2252
-            raise OSError(errno_val, os.strerror(errno_val), orig_address)
2253
-
2254
-
2255
-@socketprovider.SocketProvider.register(
2256
-    "stub_with_address_and_deterministic_dsa"
2257
-)
2258
-class StubbedSSHAgentSocketWithAddressAndDeterministicDSA(
2259
-    StubbedSSHAgentSocketWithAddress
2260
-):
2261
-    """A [`StubbedSSHAgentSocketWithAddress`][] supporting deterministic DSA."""
2262
-
2263
-    def __init__(self) -> None:
2264
-        """Initialize the agent.
2265
-
2266
-        Set the supported extensions, and try issuing RFC 6979 and
2267
-        Pageant 0.68–0.80 DSA/ECDSA signatures, if possible.  See the
2268
-        [superclass constructor][StubbedSSHAgentSocketWithAddress] for
2269
-        other details.
2270
-
2271
-        Raises:
2272
-            KeyError: See superclass.
2273
-            OSError: See superclass.
2274
-
2275
-        """  # noqa: RUF002
2276
-        super().__init__("query", "list-extended@putty.projects.tartarus.org")
2277
-        self.try_rfc6979 = True
2278
-        self.try_pageant_068_080 = True
2279
-
2280
-
2281
-def list_keys(self: Any = None) -> list[_types.SSHKeyCommentPair]:
2282
-    """Return a list of all SSH test keys, as key/comment pairs.
2283
-
2284
-    Intended as a monkeypatching replacement for
2285
-    [`ssh_agent.SSHAgentClient.list_keys`][].
2286
-
2287
-    """
2288
-    del self  # Unused.
2289
-    Pair = _types.SSHKeyCommentPair  # noqa: N806
2290
-    return [
2291
-        Pair(value.public_key_data, f"{key} test key".encode("ASCII"))
2292
-        for key, value in ALL_KEYS.items()
2293
-    ]
2294
-
2295
-
2296
-def sign(
2297
-    self: Any, key: bytes | bytearray, message: bytes | bytearray
2298
-) -> bytes:
2299
-    """Return the signature of `message` under `key`.
2300
-
2301
-    Can only handle keys in [`SUPPORTED_KEYS`][], and only the vault
2302
-    UUID as the message.
2303
-
2304
-    Intended as a monkeypatching replacement for
2305
-    [`ssh_agent.SSHAgentClient.sign`][].
2306
-
2307
-    """
2308
-    del self  # Unused.
2309
-    assert message == vault.Vault.UUID
2310
-    for value in SUPPORTED_KEYS.values():
2311
-        if value.public_key_data == key:  # pragma: no branch
2312
-            return value.expected_signatures[
2313
-                SSHTestKeyDeterministicSignatureClass.SPEC
2314
-            ].signature
2315
-    raise AssertionError
2316
-
2317
-
2318
-def list_keys_singleton(self: Any = None) -> list[_types.SSHKeyCommentPair]:
2319
-    """Return a singleton list of the first supported SSH test key.
2320
-
2321
-    The key is returned as a key/comment pair.
2322
-
2323
-    Intended as a monkeypatching replacement for
2324
-    [`ssh_agent.SSHAgentClient.list_keys`][].
2325
-
2326
-    """
2327
-    del self  # Unused.
2328
-    Pair = _types.SSHKeyCommentPair  # noqa: N806
2329
-    list1 = [
2330
-        Pair(value.public_key_data, f"{key} test key".encode("ASCII"))
2331
-        for key, value in SUPPORTED_KEYS.items()
2332
-    ]
2333
-    return list1[:1]
2334
-
2335
-
2336
-def suitable_ssh_keys(conn: Any) -> Iterator[_types.SSHKeyCommentPair]:
2337
-    """Return a two-item list of SSH test keys (key/comment pairs).
2338
-
2339
-    Intended as a monkeypatching replacement for
2340
-    `cli_machinery.get_suitable_ssh_keys` to better script and test the
2341
-    interactive key selection.  When used this way, `derivepassphrase`
2342
-    believes that only those two keys are loaded and suitable.
2343
-
2344
-    """
2345
-    del conn  # Unused.
2346
-    Pair = _types.SSHKeyCommentPair  # noqa: N806
2347
-    yield from [
2348
-        Pair(DUMMY_KEY1, b"no comment"),
2349
-        Pair(DUMMY_KEY2, b"a comment"),
2350
-    ]
2351
-
2352
-
2353
-def phrase_from_key(
2354
-    key: bytes,
2355
-    /,
2356
-    *,
2357
-    conn: ssh_agent.SSHAgentClient | socket.socket | None = None,
2358
-) -> bytes:
2359
-    """Return the "equivalent master passphrase" for key.
2360
-
2361
-    Only works for key [`DUMMY_KEY1`][].
2362
-
2363
-    Intended as a monkeypatching replacement for
2364
-    [`vault.Vault.phrase_from_key`][], bypassing communication with an
2365
-    actual SSH agent.
2366
-
2367
-    """
2368
-    del conn
2369
-    if key == DUMMY_KEY1:  # pragma: no branch
2370
-        return DUMMY_PHRASE_FROM_KEY1
2371
-    raise KeyError(key)  # pragma: no cover
2372
-
2373
-
2374
-def provider_entry_provider() -> _types.SSHAgentSocket:  # pragma: no cover
2375
-    """A pseudo provider for a [`_types.SSHAgentSocketProviderEntry`][]."""
2376
-    msg = "We are not supposed to be called!"
2377
-    raise AssertionError(msg)
2378
-
2379
-
2380
-provider_entry1 = _types.SSHAgentSocketProviderEntry(
2381
-    provider_entry_provider, "entry1", ("entry1a", "entry1b", "entry1c")
2382
-)
2383
-"""A sample [`_types.SSHAgentSocketProviderEntry`][]."""
2384
-
2385
-provider_entry2 = _types.SSHAgentSocketProviderEntry(
2386
-    provider_entry_provider, "entry2", ("entry2d", "entry2e")
2387
-)
2388
-"""A sample [`_types.SSHAgentSocketProviderEntry`][]."""
2389
-
2390
-posix_entry = _types.SSHAgentSocketProviderEntry(
2391
-    socketprovider.SocketProvider.resolve("posix"), "posix", ()
2392
-)
2393
-"""
2394
-The standard [`_types.SSHAgentSocketProviderEntry`][] for the UNIX
2395
-domain socket handler on POSIX systems.
2396
-"""
2397
-
2398
-the_annoying_os_entry = _types.SSHAgentSocketProviderEntry(
2399
-    socketprovider.SocketProvider.resolve("the_annoying_os"),
2400
-    "the_annoying_os",
2401
-    (),
2402
-)
2403
-"""
2404
-The standard [`_types.SSHAgentSocketProviderEntry`][] for the named pipe
2405
-handler on The Annoying Operating System.
2406
-"""
2407
-
2408
-faulty_entry_callable = _types.SSHAgentSocketProviderEntry(
2409
-    (),  # type: ignore[arg-type]
2410
-    "tuple",
2411
-    (),
2412
-)
2413
-"""
2414
-A faulty [`_types.SSHAgentSocketProviderEntry`][]: the indicated handler
2415
-is not a callable.
2416
-"""
2417
-
2418
-faulty_entry_name_exists = _types.SSHAgentSocketProviderEntry(
2419
-    socketprovider.SocketProvider.resolve("the_annoying_os"), "posix", ()
2420
-)
2421
-"""
2422
-A faulty [`_types.SSHAgentSocketProviderEntry`][]: the indicated handler
2423
-is already registered with a different callable.
2424
-"""
2425
-
2426
-faulty_entry_alias_exists = _types.SSHAgentSocketProviderEntry(
2427
-    socketprovider.SocketProvider.resolve("posix"),
2428
-    "posix",
2429
-    ("unix_domain", "the_annoying_os"),
2430
-)
2431
-"""
2432
-A faulty [`_types.SSHAgentSocketProviderEntry`][]: the alias is already
2433
-registered with a different callable.
2434
-"""
2435
-
2436
-
2437
-@contextlib.contextmanager
2438
-def faked_entry_point_list(  # noqa: C901
2439
-    additional_entry_points: Sequence[importlib.metadata.EntryPoint],
2440
-    remove_conflicting_entries: bool = False,
2441
-) -> Iterator[Sequence[str]]:
2442
-    """Yield a context where additional entry points are visible.
2443
-
2444
-    Args:
2445
-        additional_entry_points:
2446
-            A sequence of entry point objects that should additionally
2447
-            be visible.
2448
-        remove_conflicting_entries:
2449
-            If true, remove all names provided by the additional entry
2450
-            points, otherwise leave them untouched.
2451
-
2452
-    Yields:
2453
-        A sequence of registry names that are newly available within the
2454
-        context.
2455
-
2456
-    """
2457
-    true_entry_points = importlib.metadata.entry_points()
2458
-    additional_entry_points = list(additional_entry_points)
2459
-
2460
-    if sys.version_info >= (3, 12):
2461
-        new_entry_points = importlib.metadata.EntryPoints(
2462
-            list(true_entry_points) + additional_entry_points
2463
-        )
2464
-
2465
-        @overload
2466
-        def mangled_entry_points(
2467
-            *, group: None = None
2468
-        ) -> importlib.metadata.EntryPoints: ...
2469
-
2470
-        @overload
2471
-        def mangled_entry_points(
2472
-            *, group: str
2473
-        ) -> importlib.metadata.EntryPoints: ...
2474
-
2475
-        def mangled_entry_points(
2476
-            **params: Any,
2477
-        ) -> importlib.metadata.EntryPoints:
2478
-            return new_entry_points.select(**params)
2479
-
2480
-    elif sys.version_info >= (3, 10):
2481
-        # Compatibility concerns within importlib.metadata: depending on
2482
-        # whether the .select() API is used, the result is either the dict
2483
-        # of groups of points (as in < 3.10), or the EntryPoints iterable
2484
-        # (as in >= 3.12).  So our wrapper needs to duplicate that
2485
-        # interface.  FUN.
2486
-        new_entry_points_dict = {
2487
-            k: list(v) for k, v in true_entry_points.items()
2488
-        }
2489
-        for ep in additional_entry_points:
2490
-            new_entry_points_dict.setdefault(ep.group, []).append(ep)
2491
-        new_entry_points = importlib.metadata.EntryPoints([
2492
-            ep for group in new_entry_points_dict.values() for ep in group
2493
-        ])
2494
-
2495
-        @overload
2496
-        def mangled_entry_points(
2497
-            *, group: None = None
2498
-        ) -> dict[
2499
-            str,
2500
-            list[importlib.metadata.EntryPoint]
2501
-            | tuple[importlib.metadata.EntryPoint, ...],
2502
-        ]: ...
2503
-
2504
-        @overload
2505
-        def mangled_entry_points(
2506
-            *, group: str
2507
-        ) -> importlib.metadata.EntryPoints: ...
2508
-
2509
-        def mangled_entry_points(
2510
-            **params: Any,
2511
-        ) -> (
2512
-            importlib.metadata.EntryPoints
2513
-            | dict[
2514
-                str,
2515
-                list[importlib.metadata.EntryPoint]
2516
-                | tuple[importlib.metadata.EntryPoint, ...],
2517
-            ]
2518
-        ):
2519
-            return (
2520
-                new_entry_points.select(**params)
2521
-                if params
2522
-                else new_entry_points_dict
2523
-            )
2524
-
2525
-    else:
2526
-        new_entry_points: dict[
2527
-            str,
2528
-            list[importlib.metadata.EntryPoint]
2529
-            | tuple[importlib.metadata.EntryPoint, ...],
2530
-        ] = {
2531
-            group_name: list(group)
2532
-            for group_name, group in true_entry_points.items()
2533
-        }
2534
-        for ep in additional_entry_points:
2535
-            new_entry_points.setdefault(ep.group, [])
2536
-            new_entry_points[ep.group].append(ep)
2537
-        new_entry_points = {
2538
-            group_name: tuple(group)
2539
-            for group_name, group in new_entry_points.items()
2540
-        }
2541
-
2542
-        @overload
2543
-        def mangled_entry_points(
2544
-            *, group: None = None
2545
-        ) -> dict[str, tuple[importlib.metadata.EntryPoint, ...]]: ...
2546
-
2547
-        @overload
2548
-        def mangled_entry_points(
2549
-            *, group: str
2550
-        ) -> tuple[importlib.metadata.EntryPoint, ...]: ...
2551
-
2552
-        def mangled_entry_points(
2553
-            *, group: str | None = None
2554
-        ) -> (
2555
-            dict[str, tuple[importlib.metadata.EntryPoint, ...]]
2556
-            | tuple[importlib.metadata.EntryPoint, ...]
2557
-        ):
2558
-            return (
2559
-                new_entry_points.get(group, ())
2560
-                if group is not None
2561
-                else new_entry_points
2562
-            )
2563
-
2564
-    registry = socketprovider.SocketProvider.registry
2565
-    new_registry = registry.copy()
2566
-    keys = [ep.load().key for ep in additional_entry_points]
2567
-    aliases = [a for ep in additional_entry_points for a in ep.load().aliases]
2568
-    if remove_conflicting_entries:  # pragma: no cover [unused]
2569
-        for name in [*keys, *aliases]:
2570
-            new_registry.pop(name, None)
2571
-
2572
-    with pytest.MonkeyPatch.context() as monkeypatch:
2573
-        monkeypatch.setattr(
2574
-            socketprovider.SocketProvider, "registry", new_registry
2575
-        )
2576
-        monkeypatch.setattr(
2577
-            importlib.metadata, "entry_points", mangled_entry_points
2578
-        )
2579
-        yield (*keys, *aliases)
2580
-
2581
-
2582
-@contextlib.contextmanager
2583
-def isolated_config(
2584
-    monkeypatch: pytest.MonkeyPatch,
2585
-    runner: CliRunner,
2586
-    main_config_str: str | None = None,
2587
-) -> Iterator[None]:
2588
-    """Provide an isolated configuration setup, as a context.
2589
-
2590
-    This context manager sets up (and changes into) a temporary
2591
-    directory, which holds the user configuration specified in
2592
-    `main_config_str`, if any.  The manager also ensures that the
2593
-    environment variables `HOME` and `USERPROFILE` are set, and that
2594
-    `DERIVEPASSPHRASE_PATH` is unset.  Upon exiting the context, the
2595
-    changes are undone and the temporary directory is removed.
2596
-
2597
-    Args:
2598
-        monkeypatch:
2599
-            A monkeypatch fixture object.
2600
-        runner:
2601
-            A `click` CLI runner harness.
2602
-        main_config_str:
2603
-            Optional TOML file contents, to be used as the user
2604
-            configuration.
2605
-
2606
-    Returns:
2607
-        A context manager, without a return value.
2608
-
2609
-    """
2610
-    prog_name = cli_helpers.PROG_NAME
2611
-    env_name = prog_name.replace(" ", "_").upper() + "_PATH"
2612
-    # TODO(the-13th-letter): Rewrite using parenthesized with-statements.
2613
-    # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
2614
-    with contextlib.ExitStack() as stack:
2615
-        stack.enter_context(runner.isolated_filesystem())
2616
-        stack.enter_context(
2617
-            cli_machinery.StandardCLILogging.ensure_standard_logging()
2618
-        )
2619
-        stack.enter_context(
2620
-            cli_machinery.StandardCLILogging.ensure_standard_warnings_logging()
2621
-        )
2622
-        cwd = str(pathlib.Path.cwd().resolve())
2623
-        monkeypatch.setenv("HOME", cwd)
2624
-        monkeypatch.setenv("APPDATA", cwd)
2625
-        monkeypatch.setenv("LOCALAPPDATA", cwd)
2626
-        monkeypatch.delenv(env_name, raising=False)
2627
-        config_dir = cli_helpers.config_filename(subsystem=None)
2628
-        config_dir.mkdir(parents=True, exist_ok=True)
2629
-        if isinstance(main_config_str, str):
2630
-            cli_helpers.config_filename("user configuration").write_text(
2631
-                main_config_str, encoding="UTF-8"
2632
-            )
2633
-        try:
2634
-            yield
2635
-        finally:
2636
-            cli_helpers.config_filename("write lock").unlink(missing_ok=True)
2637
-
2638
-
2639
-@contextlib.contextmanager
2640
-def isolated_vault_config(
2641
-    monkeypatch: pytest.MonkeyPatch,
2642
-    runner: CliRunner,
2643
-    vault_config: Any,
2644
-    main_config_str: str | None = None,
2645
-) -> Iterator[None]:
2646
-    """Provide an isolated vault configuration setup, as a context.
2647
-
2648
-    Uses [`isolated_config`][] internally.  Beyond those actions, this
2649
-    manager also loads the specified vault configuration into the
2650
-    context.
2651
-
2652
-    Args:
2653
-        monkeypatch:
2654
-            A monkeypatch fixture object.
2655
-        runner:
2656
-            A `click` CLI runner harness.
2657
-        vault_config:
2658
-            A valid vault configuration, to be integrated into the
2659
-            context.
2660
-        main_config_str:
2661
-            Optional TOML file contents, to be used as the user
2662
-            configuration.
2663
-
2664
-    Returns:
2665
-        A context manager, without a return value.
2666
-
2667
-    """
2668
-    with isolated_config(
2669
-        monkeypatch=monkeypatch, runner=runner, main_config_str=main_config_str
2670
-    ):
2671
-        config_filename = cli_helpers.config_filename(subsystem="vault")
2672
-        with config_filename.open("w", encoding="UTF-8") as outfile:
2673
-            json.dump(vault_config, outfile)
2674
-        yield
2675
-
2676
-
2677
-@contextlib.contextmanager
2678
-def isolated_vault_exporter_config(
2679
-    monkeypatch: pytest.MonkeyPatch,
2680
-    runner: CliRunner,
2681
-    vault_config: str | bytes | None = None,
2682
-    vault_key: str | None = None,
2683
-) -> Iterator[None]:
2684
-    """Provide an isolated vault configuration setup, as a context.
2685
-
2686
-    Works similarly to [`isolated_config`][], except that no user
2687
-    configuration is accepted or integrated into the context.  This
2688
-    manager also accepts a serialized vault-native configuration and
2689
-    a vault encryption key to integrate into the context.
2690
-
2691
-    Args:
2692
-        monkeypatch:
2693
-            A monkeypatch fixture object.
2694
-        runner:
2695
-            A `click` CLI runner harness.
2696
-        vault_config:
2697
-            An optional serialized vault-native configuration, to be
2698
-            integrated into the context.  If a text string, then the
2699
-            contents are written to the file `.vault`.  If a byte
2700
-            string, then it is treated as base64-encoded zip file
2701
-            contents, which---once inside the `.vault` directory---will
2702
-            be extracted into the current directory.
2703
-        vault_key:
2704
-            An optional encryption key presumably for the stored
2705
-            vault-native configuration.  If given, then the environment
2706
-            variable `VAULT_KEY` will be populated with this key while
2707
-            the context is active.
2708
-
2709
-    Returns:
2710
-        A context manager, without a return value.
2711
-
2712
-    """
2713
-    # TODO(the-13th-letter): Remove the fallback implementation.
2714
-    # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.10
2715
-    if TYPE_CHECKING:
2716
-        chdir: Callable[..., AbstractContextManager]
2717
-    else:
2718
-        try:
2719
-            chdir = contextlib.chdir  # type: ignore[attr]
2720
-        except AttributeError:
2721
-
2722
-            @contextlib.contextmanager
2723
-            def chdir(
2724
-                newpath: str | bytes | os.PathLike,
2725
-            ) -> Iterator[None]:  # pragma: no branch
2726
-                oldpath = pathlib.Path.cwd().resolve()
2727
-                os.chdir(newpath)
2728
-                yield
2729
-                os.chdir(oldpath)
2730
-
2731
-    with runner.isolated_filesystem():
2732
-        cwd = str(pathlib.Path.cwd().resolve())
2733
-        monkeypatch.setenv("HOME", cwd)
2734
-        monkeypatch.setenv("USERPROFILE", cwd)
2735
-        monkeypatch.delenv(
2736
-            cli_helpers.PROG_NAME.replace(" ", "_").upper() + "_PATH",
2737
-            raising=False,
2738
-        )
2739
-        monkeypatch.delenv("VAULT_PATH", raising=False)
2740
-        monkeypatch.delenv("VAULT_KEY", raising=False)
2741
-        monkeypatch.delenv("LOGNAME", raising=False)
2742
-        monkeypatch.delenv("USER", raising=False)
2743
-        monkeypatch.delenv("USERNAME", raising=False)
2744
-        if vault_key is not None:
2745
-            monkeypatch.setenv("VAULT_KEY", vault_key)
2746
-        vault_config_path = pathlib.Path(".vault").resolve()
2747
-        # TODO(the-13th-letter): Rewrite using structural pattern matching.
2748
-        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
2749
-        if isinstance(vault_config, str):
2750
-            vault_config_path.write_text(f"{vault_config}\n", encoding="UTF-8")
2751
-        elif isinstance(vault_config, bytes):
2752
-            vault_config_path.mkdir(parents=True, mode=0o700, exist_ok=True)
2753
-            # TODO(the-13th-letter): Rewrite using parenthesized
2754
-            # with-statements.
2755
-            # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
2756
-            with contextlib.ExitStack() as stack:
2757
-                stack.enter_context(chdir(vault_config_path))
2758
-                tmpzipfile = stack.enter_context(
2759
-                    tempfile.NamedTemporaryFile(suffix=".zip")
2760
-                )
2761
-                for line in vault_config.splitlines():
2762
-                    tmpzipfile.write(base64.standard_b64decode(line))
2763
-                tmpzipfile.flush()
2764
-                tmpzipfile.seek(0, 0)
2765
-                with zipfile.ZipFile(tmpzipfile.file) as zipfileobj:
2766
-                    zipfileobj.extractall()
2767
-        elif vault_config is None:
2768
-            pass
2769
-        else:  # pragma: no cover
2770
-            assert_never(vault_config)
2771
-        try:
2772
-            yield
2773
-        finally:
2774
-            cli_helpers.config_filename("write lock").unlink(missing_ok=True)
2775
-
2776
-
2777
-def auto_prompt(*args: Any, **kwargs: Any) -> str:
2778
-    """Return [`DUMMY_PASSPHRASE`][].
2779
-
2780
-    Intended as a monkeypatching replacement for
2781
-    `cli.prompt_for_passphrase` to better script and test the
2782
-    interactive passphrase queries.
2783
-
2784
-    """
2785
-    del args, kwargs  # Unused.
2786
-    return DUMMY_PASSPHRASE
2787
-
2788
-
2789
-def make_file_readonly(
2790
-    pathname: str | bytes | os.PathLike[str],
2791
-    /,
2792
-    *,
2793
-    try_race_free_implementation: bool = True,
2794
-) -> None:
2795
-    """Mark a file as read-only.
2796
-
2797
-    On POSIX, this entails removing the write permission bits for user,
2798
-    group and other, and ensuring the read permission bit for user is
2799
-    set.
2800
-
2801
-    Unfortunately, The Annoying OS (a.k.a. Microsoft Windows) has its
2802
-    own rules: Set exactly(?) the read permission bit for user to make
2803
-    the file read-only, and set exactly(?) the write permission bit for
2804
-    user to make the file read/write; all other permission bit settings
2805
-    are ignored.
2806
-
2807
-    The cross-platform procedure therefore is:
2808
-
2809
-    1. Call `os.stat` on the file, noting the permission bits.
2810
-    2. Calculate the new permission bits POSIX-style.
2811
-    3. Call `os.chmod` with permission bit `stat.S_IREAD`.
2812
-    4. Call `os.chmod` with the correct POSIX-style permissions.
2813
-
2814
-    If the platform supports it, we use a file descriptor instead of
2815
-    a path name.  Otherwise, we use the same path name multiple times,
2816
-    and are susceptible to race conditions.
2817
-
2818
-    """
2819
-    fname: int | str | bytes | os.PathLike
2820
-    if try_race_free_implementation and {os.stat, os.chmod} <= os.supports_fd:
2821
-        # The Annoying OS (v11 at least) supports fstat and fchmod, but
2822
-        # does not support changing the file mode on file descriptors
2823
-        # for read-only files.
2824
-        fname = os.open(
2825
-            pathname,
2826
-            os.O_RDWR
2827
-            | getattr(os, "O_CLOEXEC", 0)
2828
-            | getattr(os, "O_NOCTTY", 0),
2829
-        )
2830
-    else:
2831
-        fname = pathname
2832
-    try:
2833
-        orig_mode = os.stat(fname).st_mode  # noqa: PTH116
2834
-        new_mode = (
2835
-            orig_mode & ~stat.S_IWUSR & ~stat.S_IWGRP & ~stat.S_IWOTH
2836
-            | stat.S_IREAD
2837
-        )
2838
-        os.chmod(fname, stat.S_IREAD)  # noqa: PTH101
2839
-        os.chmod(fname, new_mode)  # noqa: PTH101
2840
-    finally:
2841
-        if isinstance(fname, int):
2842
-            os.close(fname)
2843
-
2844
-
2845
-class ReadableResult(NamedTuple):
2846
-    """Helper class for formatting and testing click.testing.Result objects."""
2847
-
2848
-    exception: BaseException | None
2849
-    exit_code: int
2850
-    stdout: str
2851
-    stderr: str
2852
-
2853
-    def clean_exit(
2854
-        self, *, output: str = "", empty_stderr: bool = False
2855
-    ) -> bool:
2856
-        """Return whether the invocation exited cleanly.
2857
-
2858
-        Args:
2859
-            output:
2860
-                An expected output string.
2861
-
2862
-        """
2863
-        return (
2864
-            (
2865
-                not self.exception
2866
-                or (
2867
-                    isinstance(self.exception, SystemExit)
2868
-                    and self.exit_code == 0
2869
-                )
2870
-            )
2871
-            and (not output or output in self.stdout)
2872
-            and (not empty_stderr or not self.stderr)
2873
-        )
2874
-
2875
-    def error_exit(
2876
-        self,
2877
-        *,
2878
-        error: str | re.Pattern[str] | type[BaseException] = BaseException,
2879
-        record_tuples: Sequence[tuple[str, int, str]] = (),
2880
-    ) -> bool:
2881
-        """Return whether the invocation exited uncleanly.
2882
-
2883
-        Args:
2884
-            error:
2885
-                An expected error message, or an expected numeric error
2886
-                code, or an expected exception type.
2887
-
2888
-        """
2889
-
2890
-        def error_match(error: str | re.Pattern[str], line: str) -> bool:
2891
-            return (
2892
-                error in line
2893
-                if isinstance(error, str)
2894
-                else error.match(line) is not None
2895
-            )
2896
-
2897
-        # TODO(the-13th-letter): Rewrite using structural pattern matching.
2898
-        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
2899
-        if isinstance(error, type):
2900
-            return isinstance(self.exception, error)
2901
-        else:  # noqa: RET505
2902
-            assert isinstance(error, (str, re.Pattern))
2903
-            return (
2904
-                isinstance(self.exception, SystemExit)
2905
-                and self.exit_code > 0
2906
-                and (
2907
-                    not error
2908
-                    or any(
2909
-                        error_match(error, line)
2910
-                        for line in self.stderr.splitlines(True)
2911
-                    )
2912
-                    or error_emitted(error, record_tuples)
2913
-                )
2914
-            )
2915
-
2916
-
2917
-class CliRunner:
2918
-    """An abstracted CLI runner class.
2919
-
2920
-    Intended to provide similar functionality and scope as the
2921
-    [`click.testing.CliRunner`][] class, though not necessarily
2922
-    `click`-specific.  Also allows for seamless migration away from
2923
-    `click`, if/when we decide this.
2924
-
2925
-    """
2926
-
2927
-    _SUPPORTS_MIX_STDERR_ATTRIBUTE = not hasattr(click.testing, "StreamMixer")
2928
-    """
2929
-    True if and only if [`click.testing.CliRunner`][] supports the
2930
-    `mix_stderr` attribute.  It was removed in 8.2.0 in favor of the
2931
-    `click.testing.StreamMixer` class.
2932
-
2933
-    See also
2934
-    [`pallets/click#2523`](https://github.com/pallets/click/pull/2523).
2935
-    """
2936
-
2937
-    def __init__(
2938
-        self,
2939
-        *,
2940
-        mix_stderr: bool = False,
2941
-        color: bool | None = None,
2942
-    ) -> None:
2943
-        self.color = color
2944
-        self.mix_stderr = mix_stderr
2945
-
2946
-        class MixStderrAttribute(TypedDict):
2947
-            mix_stderr: NotRequired[bool]
2948
-
2949
-        mix_stderr_args: MixStderrAttribute = (
2950
-            {"mix_stderr": mix_stderr}
2951
-            if self._SUPPORTS_MIX_STDERR_ATTRIBUTE
2952
-            else {}
2953
-        )
2954
-        self.click_testing_clirunner = click.testing.CliRunner(
2955
-            **mix_stderr_args
2956
-        )
2957
-
2958
-    def invoke(
2959
-        self,
2960
-        cli: click.BaseCommand,
2961
-        args: Sequence[str] | str | None = None,
2962
-        input: str | bytes | IO[Any] | None = None,
2963
-        env: Mapping[str, str | None] | None = None,
2964
-        catch_exceptions: bool = True,
2965
-        color: bool | None = None,
2966
-        **extra: Any,
2967
-    ) -> ReadableResult:
2968
-        if color is None:  # pragma: no cover
2969
-            color = self.color if self.color is not None else False
2970
-        raw_result = self.click_testing_clirunner.invoke(
2971
-            cli,
2972
-            args=args,
2973
-            input=input,
2974
-            env=env,
2975
-            catch_exceptions=catch_exceptions,
2976
-            color=color,
2977
-            **extra,
2978
-        )
2979
-        # In 8.2.0, r.stdout is no longer a property aliasing the
2980
-        # `output` attribute, but rather the raw stdout value.
2981
-        try:
2982
-            stderr = raw_result.stderr
2983
-        except ValueError:
2984
-            stderr = raw_result.stdout
2985
-        return ReadableResult(
2986
-            raw_result.exception,
2987
-            raw_result.exit_code,
2988
-            (raw_result.stdout if not self.mix_stderr else raw_result.output)
2989
-            or "",
2990
-            stderr or "",
2991
-        )
2992
-        return ReadableResult.parse(raw_result)
2993
-
2994
-    def isolated_filesystem(
2995
-        self,
2996
-        temp_dir: str | os.PathLike[str] | None = None,
2997
-    ) -> AbstractContextManager[str]:
2998
-        return self.click_testing_clirunner.isolated_filesystem(
2999
-            temp_dir=temp_dir
3000
-        )
3001
-
3002
-
3003
-def parse_sh_export_line(line: str, *, env_name: str) -> str:
3004
-    """Parse the output of typical SSH agents' SSH_AUTH_SOCK lines.
3005
-
3006
-    Intentionally parses only a small subset of sh(1) syntax which works
3007
-    with current OpenSSH and PuTTY output.  We require exactly one
3008
-    variable setting, and one export instruction, both on the same line,
3009
-    and perhaps combined into one statement.  Terminating semicolons
3010
-    after each command are ignored.
3011
-
3012
-    Args:
3013
-        line:
3014
-            A line of sh(1) script to parse.
3015
-        env_name:
3016
-            The name of the environment variable to expect.
3017
-
3018
-    Returns:
3019
-        The parsed environment variable value.
3020
-
3021
-    Raises:
3022
-        ValueError:
3023
-            Cannot parse the sh script.  Perhaps it is too complex,
3024
-            perhaps it is malformed.
3025
-
3026
-    """
3027
-    line = line.rstrip("\r\n")
3028
-    shlex_parser = shlex.shlex(
3029
-        instream=line, posix=True, punctuation_chars=True
3030
-    )
3031
-    shlex_parser.whitespace = " \t"
3032
-    tokens = list(shlex_parser)
3033
-    orig_tokens = tokens.copy()
3034
-    if tokens[-1] == ";":
3035
-        tokens.pop()
3036
-    if tokens[-3:] == [";", "export", env_name]:
3037
-        tokens[-3:] = []
3038
-        tokens[:0] = ["export"]
3039
-    if not (
3040
-        len(tokens) == 2
3041
-        and tokens[0] == "export"
3042
-        and tokens[1].startswith(f"{env_name}=")
3043
-    ):
3044
-        msg = f"Cannot parse sh line: {orig_tokens!r} -> {tokens!r}"
3045
-        raise ValueError(msg)
3046
-    return tokens[1].split("=", 1)[1]
3047
-
3048
-
3049
-def message_emitted_factory(
3050
-    level: int,
3051
-    *,
3052
-    logger_name: str = cli.PROG_NAME,
3053
-) -> Callable[[str | re.Pattern[str], Sequence[tuple[str, int, str]]], bool]:
3054
-    """Return a function to test if a matching message was emitted.
3055
-
3056
-    Args:
3057
-        level: The level to match messages at.
3058
-        logger_name: The name of the logger to match against.
3059
-
3060
-    """
3061
-
3062
-    def message_emitted(
3063
-        text: str | re.Pattern[str],
3064
-        record_tuples: Sequence[tuple[str, int, str]],
3065
-    ) -> bool:
3066
-        """Return true if a matching message was emitted.
3067
-
3068
-        Args:
3069
-            text: Substring or pattern to match against.
3070
-            record_tuples: Items to match.
3071
-
3072
-        """
3073
-
3074
-        def check_record(record: tuple[str, int, str]) -> bool:
3075
-            if record[:2] != (logger_name, level):
3076
-                return False
3077
-            if isinstance(text, str):
3078
-                return text in record[2]
3079
-            return text.match(record[2]) is not None  # pragma: no cover
3080
-
3081
-        return any(map(check_record, record_tuples))
3082
-
3083
-    return message_emitted
3084
-
3085
-
3086
-# No need to assert debug messages as of yet.
3087
-info_emitted = message_emitted_factory(logging.INFO)
3088
-warning_emitted = message_emitted_factory(logging.WARNING)
3089
-deprecation_warning_emitted = message_emitted_factory(
3090
-    logging.WARNING, logger_name=f"{cli.PROG_NAME}.deprecation"
3091
-)
3092
-deprecation_info_emitted = message_emitted_factory(
3093
-    logging.INFO, logger_name=f"{cli.PROG_NAME}.deprecation"
3094
-)
3095
-error_emitted = message_emitted_factory(logging.ERROR)
3096
-
3097
-
3098
-class Parametrize(types.SimpleNamespace):
3099
-    VAULT_CONFIG_FORMATS_DATA = pytest.mark.parametrize(
3100
-        ["config", "format", "config_data"],
3101
-        [
3102
-            pytest.param(
3103
-                VAULT_V02_CONFIG,
3104
-                "v0.2",
3105
-                VAULT_V02_CONFIG_DATA,
3106
-                id="0.2",
3107
-            ),
3108
-            pytest.param(
3109
-                VAULT_V03_CONFIG,
3110
-                "v0.3",
3111
-                VAULT_V03_CONFIG_DATA,
3112
-                id="0.3",
3113
-            ),
3114
-            pytest.param(
3115
-                VAULT_STOREROOM_CONFIG_ZIPPED,
3116
-                "storeroom",
3117
-                VAULT_STOREROOM_CONFIG_DATA,
3118
-                id="storeroom",
3119
-            ),
3120
-        ],
3121
-    )
... ...
@@ -19,7 +19,9 @@ import hypothesis
19 19
 import packaging.version
20 20
 import pytest
21 21
 
22
-import tests
22
+import tests.data
23
+import tests.data.callables
24
+import tests.machinery
23 25
 from derivepassphrase import _types, ssh_agent
24 26
 
25 27
 if TYPE_CHECKING:
... ...
@@ -281,28 +283,28 @@ def spawn_noop(  # pragma: no cover [unused]
281 283
     """Placeholder function. Does nothing."""
282 284
 
283 285
 
284
-spawn_handlers: dict[str, tuple[str, SpawnFunc, tests.KnownSSHAgent]] = {
286
+spawn_handlers: dict[str, tuple[str, SpawnFunc, tests.data.KnownSSHAgent]] = {
285 287
     "pageant": (
286 288
         "pageant",
287 289
         spawn_pageant_on_posix,
288
-        tests.KnownSSHAgent.Pageant,
290
+        tests.data.KnownSSHAgent.Pageant,
289 291
     ),
290 292
     "ssh-agent": (
291 293
         "ssh-agent",
292 294
         spawn_openssh_agent_on_posix,
293
-        tests.KnownSSHAgent.OpenSSHAgent,
295
+        tests.data.KnownSSHAgent.OpenSSHAgent,
294 296
     ),
295 297
     "stub_agent": (
296 298
         "stub_agent",
297 299
         spawn_noop,
298
-        tests.KnownSSHAgent.StubbedSSHAgent,
300
+        tests.data.KnownSSHAgent.StubbedSSHAgent,
299 301
     ),
300 302
     "stub_agent_with_extensions": (
301 303
         "stub_agent_with_extensions",
302 304
         spawn_noop,
303
-        tests.KnownSSHAgent.StubbedSSHAgent,
305
+        tests.data.KnownSSHAgent.StubbedSSHAgent,
304 306
     ),
305
-    "(system)": ("(system)", spawn_noop, tests.KnownSSHAgent.UNKNOWN),
307
+    "(system)": ("(system)", spawn_noop, tests.data.KnownSSHAgent.UNKNOWN),
306 308
 }
307 309
 """
308 310
 The standard registry of agent spawning functions.
... ...
@@ -339,8 +341,8 @@ class CannotSpawnError(RuntimeError):
339 341
 def spawn_named_agent(
340 342
     exec_name: str,
341 343
     spawn_func: SpawnFunc,
342
-    agent_type: tests.KnownSSHAgent,
343
-) -> Iterator[tests.SpawnedSSHAgentInfo]:  # pragma: no cover [external]
344
+    agent_type: tests.data.KnownSSHAgent,
345
+) -> Iterator[tests.data.SpawnedSSHAgentInfo]:  # pragma: no cover [external]
344 346
     """Spawn the named SSH agent and check that it is operational.
345 347
 
346 348
     Using the correct agent-specific spawn function from the
... ...
@@ -396,7 +398,7 @@ def spawn_named_agent(
396 398
     with exit_stack:
397 399
         if (
398 400
             spawn_func is spawn_noop
399
-            and agent_type == tests.KnownSSHAgent.StubbedSSHAgent
401
+            and agent_type == tests.data.KnownSSHAgent.StubbedSSHAgent
400 402
         ):
401 403
             ssh_auth_sock = None
402 404
         elif spawn_func is spawn_noop:
... ...
@@ -412,7 +414,7 @@ def spawn_named_agent(
412 414
             assert proc.stdout is not None
413 415
             ssh_auth_sock_line = proc.stdout.readline()
414 416
             try:
415
-                ssh_auth_sock = tests.parse_sh_export_line(
417
+                ssh_auth_sock = tests.data.callables.parse_sh_export_line(
416 418
                     ssh_auth_sock_line, env_name="SSH_AUTH_SOCK"
417 419
                 )
418 420
             except ValueError:  # pragma: no cover [external]
... ...
@@ -433,7 +435,7 @@ def spawn_named_agent(
433 435
             )
434 436
         else:
435 437
             monkeypatch.setenv(
436
-                "SSH_AUTH_SOCK", tests.StubbedSSHAgentSocketWithAddress.ADDRESS
438
+                "SSH_AUTH_SOCK", tests.machinery.StubbedSSHAgentSocketWithAddress.ADDRESS
437 439
             )
438 440
             monkeypatch.setattr(
439 441
                 ssh_agent.SSHAgentClient,
... ...
@@ -444,9 +446,9 @@ def spawn_named_agent(
444 446
             )
445 447
             client = exit_stack.enter_context(
446 448
                 ssh_agent.SSHAgentClient.ensure_agent_subcontext(
447
-                    tests.StubbedSSHAgentSocketWithAddressAndDeterministicDSA()
449
+                    tests.machinery.StubbedSSHAgentSocketWithAddressAndDeterministicDSA()
448 450
                     if exec_name == "stub_agent_with_extensions"
449
-                    else tests.StubbedSSHAgentSocketWithAddress()
451
+                    else tests.machinery.StubbedSSHAgentSocketWithAddress()
450 452
                 )
451 453
             )
452 454
         # We sanity-test the connected SSH agent if it is not one of our
... ...
@@ -460,7 +462,7 @@ def spawn_named_agent(
460 462
         # agent is not one of our test agents, and if the check fails,
461 463
         # skip this agent.
462 464
         if (
463
-            agent_type != tests.KnownSSHAgent.StubbedSSHAgent
465
+            agent_type != tests.data.KnownSSHAgent.StubbedSSHAgent
464 466
         ):  # pragma: no cover [external]
465 467
             try:
466 468
                 client.list_keys()  # sanity test
... ...
@@ -471,7 +473,7 @@ def spawn_named_agent(
471 473
             ) as exc:  # pragma: no cover [failsafe]
472 474
                 msg = f'agent failed the "list keys" sanity test: {exc!r}'
473 475
                 raise CannotSpawnError(msg) from exc
474
-        yield tests.SpawnedSSHAgentInfo(
476
+        yield tests.data.SpawnedSSHAgentInfo(
475 477
             agent_type, client, spawn_func is not spawn_noop
476 478
         )
477 479
     assert os.environ.get("SSH_AUTH_SOCK", None) == startup_ssh_auth_sock, (
... ...
@@ -480,7 +482,7 @@ def spawn_named_agent(
480 482
 
481 483
 
482 484
 def is_agent_permitted(
483
-    agent_type: tests.KnownSSHAgent,
485
+    agent_type: tests.data.KnownSSHAgent,
484 486
 ) -> bool:  # pragma: no cover [external]
485 487
     """May the given SSH agent be spawned by the test harness?
486 488
 
... ...
@@ -498,11 +500,11 @@ def is_agent_permitted(
498 500
     """
499 501
     if not os.environ.get("PERMITTED_SSH_AGENTS"):
500 502
         return True
501
-    permitted_agents = {tests.KnownSSHAgent.StubbedSSHAgent}
503
+    permitted_agents = {tests.data.KnownSSHAgent.StubbedSSHAgent}
502 504
     permitted_agents.update({
503
-        tests.KnownSSHAgent(x)
505
+        tests.data.KnownSSHAgent(x)
504 506
         for x in os.environ["PERMITTED_SSH_AGENTS"].split(",")
505
-        if x in tests.KnownSSHAgent.__members__
507
+        if x in tests.data.KnownSSHAgent.__members__
506 508
     })
507 509
     return agent_type in permitted_agents
508 510
 
... ...
@@ -532,7 +534,7 @@ for key, handler in spawn_handlers.items():
532 534
 
533 535
 @pytest.fixture
534 536
 def running_ssh_agent(  # pragma: no cover [external]
535
-) -> Iterator[tests.RunningSSHAgentInfo]:
537
+) -> Iterator[tests.data.RunningSSHAgentInfo]:
536 538
     """Ensure a running SSH agent, if possible, as a pytest fixture.
537 539
 
538 540
     Check for a running SSH agent, or spawn a new one if possible.  We
... ...
@@ -555,10 +557,10 @@ def running_ssh_agent(  # pragma: no cover [external]
555 557
     """
556 558
 
557 559
     def prepare_environment(
558
-        agent_type: tests.KnownSSHAgent,
559
-    ) -> Iterator[tests.RunningSSHAgentInfo]:
560
+        agent_type: tests.data.KnownSSHAgent,
561
+    ) -> Iterator[tests.data.RunningSSHAgentInfo]:
560 562
         with pytest.MonkeyPatch.context() as monkeypatch:
561
-            if agent_type == tests.KnownSSHAgent.StubbedSSHAgent:
563
+            if agent_type == tests.data.KnownSSHAgent.StubbedSSHAgent:
562 564
                 monkeypatch.setattr(
563 565
                     ssh_agent.SSHAgentClient,
564 566
                     "SOCKET_PROVIDERS",
... ...
@@ -566,14 +568,14 @@ def running_ssh_agent(  # pragma: no cover [external]
566 568
                 )
567 569
                 monkeypatch.setenv(
568 570
                     "SSH_AUTH_SOCK",
569
-                    tests.StubbedSSHAgentSocketWithAddress.ADDRESS,
571
+                    tests.machinery.StubbedSSHAgentSocketWithAddress.ADDRESS,
570 572
                 )
571
-                yield tests.RunningSSHAgentInfo(
572
-                    tests.StubbedSSHAgentSocketWithAddress,
573
-                    tests.KnownSSHAgent.StubbedSSHAgent,
573
+                yield tests.data.RunningSSHAgentInfo(
574
+                    tests.machinery.StubbedSSHAgentSocketWithAddress,
575
+                    tests.data.KnownSSHAgent.StubbedSSHAgent,
574 576
                 )
575 577
             else:
576
-                yield tests.RunningSSHAgentInfo(
578
+                yield tests.data.RunningSSHAgentInfo(
577 579
                     os.environ["SSH_AUTH_SOCK"],
578 580
                     agent_type,
579 581
                 )
... ...
@@ -607,7 +609,7 @@ def running_ssh_agent(  # pragma: no cover [external]
607 609
 @pytest.fixture(params=spawn_handlers_params)
608 610
 def spawn_ssh_agent(
609 611
     request: pytest.FixtureRequest,
610
-) -> Iterator[tests.SpawnedSSHAgentInfo]:  # pragma: no cover [external]
612
+) -> Iterator[tests.data.SpawnedSSHAgentInfo]:  # pragma: no cover [external]
611 613
     """Spawn an isolated SSH agent, if possible, as a pytest fixture.
612 614
 
613 615
     Spawn a new SSH agent isolated from other SSH use by other
... ...
@@ -648,7 +650,7 @@ def spawn_ssh_agent(
648 650
 
649 651
 @pytest.fixture
650 652
 def ssh_agent_client_with_test_keys_loaded(  # noqa: C901
651
-    spawn_ssh_agent: tests.SpawnedSSHAgentInfo,
653
+    spawn_ssh_agent: tests.data.SpawnedSSHAgentInfo,
652 654
 ) -> Iterator[ssh_agent.SSHAgentClient]:
653 655
     """Provide an SSH agent with loaded test keys, as a pytest fixture.
654 656
 
... ...
@@ -711,7 +713,7 @@ def ssh_agent_client_with_test_keys_loaded(  # noqa: C901
711 713
         return (return_code, bytes(payload) + lifetime_constraint)
712 714
 
713 715
     try:
714
-        for key_type, key_struct in tests.ALL_KEYS.items():
716
+        for key_type, key_struct in tests.data.ALL_KEYS.items():
715 717
             private_key_data = key_struct.private_key_blob
716 718
             if private_key_data is None:  # pragma: no cover [failsafe]
717 719
                 continue
... ...
@@ -741,11 +743,11 @@ def ssh_agent_client_with_test_keys_loaded(  # noqa: C901
741 743
                     current_loaded_keys = frozenset({
742 744
                         pair.key for pair in client.list_keys()
743 745
                     })
744
-                    if agent_type == tests.KnownSSHAgent.Pageant and (
746
+                    if agent_type == tests.data.KnownSSHAgent.Pageant and (
745 747
                         key_struct.public_key_data in current_loaded_keys
746 748
                     ):
747 749
                         pass
748
-                    elif agent_type == tests.KnownSSHAgent.Pageant and (
750
+                    elif agent_type == tests.data.KnownSSHAgent.Pageant and (
749 751
                         not isolated
750 752
                     ):
751 753
                         request_code, payload = prepare_payload(
... ...
@@ -768,7 +770,7 @@ def ssh_agent_client_with_test_keys_loaded(  # noqa: C901
768 770
                 successfully_loaded_keys.add(key_type)
769 771
         yield client
770 772
     finally:
771
-        for key_type, key_struct in tests.ALL_KEYS.items():
773
+        for key_type, key_struct in tests.data.ALL_KEYS.items():
772 774
             if not isolated and (
773 775
                 key_type in successfully_loaded_keys
774 776
             ):  # pragma: no cover [external]
... ...
@@ -0,0 +1,1761 @@
1
+# SPDX-FileCopyrightText: 2025 Marco Ricci <software@the13thletter.info>
2
+#
3
+# SPDX-License-Identifier: Zlib
4
+
5
+"""Testing data for `derivepassphrase`.
6
+
7
+This is all the standalone, non-test-system-specific data set aside
8
+specifically to test `derivepassphrase`; this includes types, `vault`
9
+test configurations, test SSH keys and the like.
10
+
11
+This module only contains data, and some "mostly trivial" code such as
12
+accessors or suitability checkers; all code more complicated than that
13
+is in the `tests.data.callables` module (if independent of, and sensible
14
+to call outside, the testing environment) or the `tests.machinery`
15
+module (otherwise).  All data that specifically relates to the testing
16
+system itself is also in the `tests.machinery` module, not here.
17
+
18
+"""
19
+
20
+from __future__ import annotations
21
+
22
+import base64
23
+import enum
24
+from typing import TYPE_CHECKING
25
+
26
+from typing_extensions import NamedTuple
27
+
28
+from derivepassphrase import _types, ssh_agent, vault
29
+from derivepassphrase.ssh_agent import socketprovider
30
+
31
+__all__ = ()
32
+
33
+if TYPE_CHECKING:
34
+    from collections.abc import Mapping
35
+
36
+    from typing_extensions import Any
37
+
38
+
39
+# Types
40
+# =====
41
+
42
+# SSH test keys and agent info
43
+# ----------------------------
44
+
45
+
46
+class SSHTestKeyDeterministicSignatureClass(str, enum.Enum):
47
+    """The class of a deterministic signature from an SSH test key.
48
+
49
+    Attributes:
50
+        SPEC:
51
+            A deterministic signature directly implied by the
52
+            specification of the signature algorithm.
53
+        RFC_6979:
54
+            A deterministic signature as specified by RFC 6979.  Only
55
+            used with DSA and ECDSA keys (that aren't also EdDSA keys).
56
+        Pageant_068_080:
57
+            A deterministic signature as specified by Pageant 0.68.
58
+            Only used with DSA and ECDSA keys (that aren't also EdDSA
59
+            keys), and only used with Pageant from 0.68 up to and
60
+            including 0.80.
61
+
62
+            Usage of this signature class together with an ECDSA NIST
63
+            P-521 key [turned out to leak enough information per
64
+            signature to quickly compromise the entire private key
65
+            (CVE-2024-31497)][PUTTY_CVE_2024_31497], so newer Pageant
66
+            versions abandon this signature class in favor of RFC 6979.
67
+
68
+            [PUTTY_CVE_2024_31497]: https://www.chiark.greenend.org.uk/~sgtatham/putty/wishlist/vuln-p521-bias.html
69
+
70
+    """
71
+
72
+    SPEC = enum.auto()
73
+    """"""
74
+    RFC_6979 = enum.auto()
75
+    """"""
76
+    Pageant_068_080 = enum.auto()
77
+    """"""
78
+
79
+
80
+class SSHTestKeyDeterministicSignature(NamedTuple):
81
+    """An SSH test key deterministic signature.
82
+
83
+    Attributes:
84
+        signature:
85
+            The binary signature of the [vault UUID][vault.Vault.UUID]
86
+            under this signature class.
87
+        derived_passphrase:
88
+            The equivalent master passphrase derived from this
89
+            signature.
90
+        signature_class:
91
+            The [signature
92
+            class][SSHTestKeyDeterministicSignatureClass].
93
+
94
+    """
95
+
96
+    signature: bytes
97
+    """"""
98
+    derived_passphrase: bytes
99
+    """"""
100
+    signature_class: SSHTestKeyDeterministicSignatureClass = (
101
+        SSHTestKeyDeterministicSignatureClass.SPEC
102
+    )
103
+    """"""
104
+
105
+
106
+class SSHTestKey(NamedTuple):
107
+    """An SSH test key.
108
+
109
+    Attributes:
110
+        public_key:
111
+            The SSH public key string, as used e.g. by OpenSSH's
112
+            `authorized_keys` file.  Includes a comment.
113
+        public_key_data:
114
+            The SSH protocol wire format of the public key.
115
+        private_key:
116
+            A base64 encoded representation of the private key, in
117
+            OpenSSH's v1 private key format.
118
+        private_key_blob:
119
+            The SSH protocol wire format of the private key.
120
+        expected_signatures:
121
+            A mapping of deterministic signature classes to the
122
+            expected, deterministic signature (of that class) of the
123
+            vault UUID for this key, together with the respective
124
+            "equivalent master passphrase" derived from this signature.
125
+
126
+    """
127
+
128
+    public_key: bytes
129
+    """"""
130
+    public_key_data: bytes
131
+    """"""
132
+    private_key: bytes
133
+    """"""
134
+    private_key_blob: bytes
135
+    """"""
136
+    expected_signatures: Mapping[
137
+        SSHTestKeyDeterministicSignatureClass, SSHTestKeyDeterministicSignature
138
+    ]
139
+    """"""
140
+
141
+    def is_suitable(
142
+        self,
143
+        *,
144
+        client: ssh_agent.SSHAgentClient | None = None,
145
+    ) -> bool:
146
+        """Return if this key is suitable for use with vault.
147
+
148
+        Args:
149
+            client:
150
+                An optional SSH agent client to check for additional
151
+                deterministic key types. If not given, assume no such
152
+                types.
153
+
154
+        """
155
+        return vault.Vault.is_suitable_ssh_key(
156
+            self.public_key_data, client=client
157
+        )
158
+
159
+
160
+class KnownSSHAgent(str, enum.Enum):
161
+    """Known SSH agents.
162
+
163
+    Attributes:
164
+        UNKNOWN (str):
165
+            Not a known agent, or not known statically.
166
+        Pageant (str):
167
+            The agent from Simon Tatham's PuTTY suite.
168
+        OpenSSHAgent (str):
169
+            The agent from OpenBSD's OpenSSH suite.
170
+        StubbedSSHAgent (str):
171
+            The stubbed, fake agent pseudo-socket defined in this test
172
+            suite.
173
+
174
+    """
175
+
176
+    UNKNOWN = "(unknown)"
177
+    """"""
178
+    Pageant = "Pageant"
179
+    """"""
180
+    OpenSSHAgent = "OpenSSHAgent"
181
+    """"""
182
+    StubbedSSHAgent = "StubbedSSHAgent"
183
+    """"""
184
+
185
+
186
+class SpawnedSSHAgentInfo(NamedTuple):
187
+    """Info about a spawned SSH agent, as provided by some fixtures.
188
+
189
+    Differs from [`RunningSSHAgentInfo`][] in that this info object
190
+    already provides a functional client connected to the agent, but not
191
+    the address.
192
+
193
+    Attributes:
194
+        agent_type:
195
+            The agent's type.
196
+        client:
197
+            An SSH agent client connected to this agent.
198
+        isolated:
199
+            Whether this agent was spawned specifically for this test
200
+            suite, with attempts to isolate it from the user.  If false,
201
+            then the user may be interacting with the agent externally,
202
+            meaning e.g. keys other than the test keys may be visible in
203
+            this agent.
204
+
205
+    """
206
+
207
+    agent_type: KnownSSHAgent
208
+    """"""
209
+    client: ssh_agent.SSHAgentClient
210
+    """"""
211
+    isolated: bool
212
+    """"""
213
+
214
+
215
+class RunningSSHAgentInfo(NamedTuple):
216
+    """Info about a running SSH agent, as provided by some fixtures.
217
+
218
+    Differs from [`SpawnedSSHAgentInfo`][] in that this info object
219
+    provides only an address of the agent, not a functional client
220
+    already connected to it.  The running SSH agent may or may not be
221
+    isolated.
222
+
223
+    Attributes:
224
+        socket:
225
+            A socket address to connect to the agent.
226
+        agent_type:
227
+            The agent's type.
228
+
229
+    """
230
+
231
+    socket: str | type[_types.SSHAgentSocket]
232
+    """"""
233
+    agent_type: KnownSSHAgent
234
+    """"""
235
+
236
+    def require_external_address(self) -> str:  # pragma: no cover
237
+        if not isinstance(self.socket, str):
238
+            import pytest  # noqa: PLC0415
239
+
240
+            pytest.skip(
241
+                reason="This test requires a real, externally resolvable "
242
+                "address for the SSH agent socket."
243
+            )
244
+        return self.socket
245
+
246
+
247
+# Vault configurations
248
+# ====================
249
+
250
+
251
+class ValidationSettings(NamedTuple):
252
+    """Validation settings for [`VaultTestConfig`][]s.
253
+
254
+    Attributes:
255
+        allow_unknown_settings:
256
+            See [`_types.validate_vault_config`][].
257
+
258
+    """
259
+
260
+    allow_unknown_settings: bool
261
+    """"""
262
+
263
+
264
+class VaultTestConfig(NamedTuple):
265
+    """A (not necessarily valid) sample vault config, plus metadata.
266
+
267
+    Attributes:
268
+        config:
269
+            The actual configuration object.  Usually a [`dict`][].
270
+        comment:
271
+            An explanatory comment for what is wrong with this config,
272
+            or empty if the config is valid.  This is intended as
273
+            a debugging message to be shown to the user (e.g. when an
274
+            assertion fails), not as an error message to
275
+            programmatically match against.
276
+        validation_settings:
277
+            See [`_types.validate_vault_config`][].
278
+
279
+    """
280
+
281
+    config: Any
282
+    """"""
283
+    comment: str
284
+    """"""
285
+    validation_settings: ValidationSettings | None
286
+    """"""
287
+
288
+
289
+def is_valid_test_config(conf: VaultTestConfig, /) -> bool:
290
+    """Return true if the test config is valid.
291
+
292
+    Args:
293
+        conf: The test config to check.
294
+
295
+    """
296
+    return not conf.comment and conf.validation_settings in {
297
+        None,
298
+        (True,),
299
+    }
300
+
301
+
302
+def is_smudgable_vault_test_config(conf: VaultTestConfig) -> bool:
303
+    """Check whether this vault test config can be effectively smudged.
304
+
305
+    A "smudged" test config is one where falsy values (in the JavaScript
306
+    sense) can be replaced by other falsy values without changing the
307
+    meaning of the config.
308
+
309
+    Args:
310
+        conf: A test config to check.
311
+
312
+    Returns:
313
+        True if the test config can be smudged, False otherwise.
314
+
315
+    """
316
+    config = conf.config
317
+    return bool(
318
+        isinstance(config, dict)
319
+        and ("global" not in config or isinstance(config["global"], dict))
320
+        and ("services" in config and isinstance(config["services"], dict))
321
+        and all(isinstance(x, dict) for x in config["services"].values())
322
+        and (config["services"] or config.get("global"))
323
+    )
324
+
325
+
326
+def _test_config_ids(val: VaultTestConfig) -> Any:  # pragma: no cover
327
+    """pytest id function for VaultTestConfig objects."""
328
+    assert isinstance(val, VaultTestConfig)
329
+    return val[1] or (val[0], val[1], val[2])
330
+
331
+# Data objects
332
+# ============
333
+
334
+
335
+# SSH agent socket provider data
336
+# ------------------------------
337
+
338
+
339
+posix_entry = _types.SSHAgentSocketProviderEntry(
340
+    socketprovider.SocketProvider.resolve("posix"), "posix", ()
341
+)
342
+"""
343
+The standard [`_types.SSHAgentSocketProviderEntry`][] for the UNIX
344
+domain socket handler on POSIX systems.
345
+"""
346
+
347
+the_annoying_os_entry = _types.SSHAgentSocketProviderEntry(
348
+    socketprovider.SocketProvider.resolve("the_annoying_os"),
349
+    "the_annoying_os",
350
+    (),
351
+)
352
+"""
353
+The standard [`_types.SSHAgentSocketProviderEntry`][] for the named pipe
354
+handler on The Annoying Operating System.
355
+"""
356
+
357
+faulty_entry_callable = _types.SSHAgentSocketProviderEntry(
358
+    (),  # type: ignore[arg-type]
359
+    "tuple",
360
+    (),
361
+)
362
+"""
363
+A faulty [`_types.SSHAgentSocketProviderEntry`][]: the indicated handler
364
+is not a callable.
365
+"""
366
+
367
+faulty_entry_name_exists = _types.SSHAgentSocketProviderEntry(
368
+    socketprovider.SocketProvider.resolve("the_annoying_os"), "posix", ()
369
+)
370
+"""
371
+A faulty [`_types.SSHAgentSocketProviderEntry`][]: the indicated handler
372
+is already registered with a different callable.
373
+"""
374
+
375
+faulty_entry_alias_exists = _types.SSHAgentSocketProviderEntry(
376
+    socketprovider.SocketProvider.resolve("posix"),
377
+    "posix",
378
+    ("unix_domain", "the_annoying_os"),
379
+)
380
+"""
381
+A faulty [`_types.SSHAgentSocketProviderEntry`][]: the alias is already
382
+registered with a different callable.
383
+"""
384
+
385
+# SSH test keys
386
+# -------------
387
+
388
+
389
+ALL_KEYS: Mapping[str, SSHTestKey] = {
390
+    "ed25519": SSHTestKey(
391
+        private_key=rb"""-----BEGIN OPENSSH PRIVATE KEY-----
392
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
393
+QyNTUxOQAAACCBeIFoJtYCSF8P/zJIb+TBMIncHGpFBgnpCQ/7whJpdgAAAKDweO7H8Hju
394
+xwAAAAtzc2gtZWQyNTUxOQAAACCBeIFoJtYCSF8P/zJIb+TBMIncHGpFBgnpCQ/7whJpdg
395
+AAAEAbM/A869nkWZbe2tp3Dm/L6gitvmpH/aRZt8sBII3ExYF4gWgm1gJIXw//Mkhv5MEw
396
+idwcakUGCekJD/vCEml2AAAAG3Rlc3Qga2V5IHdpdGhvdXQgcGFzc3BocmFzZQEC
397
+-----END OPENSSH PRIVATE KEY-----
398
+""",
399
+        private_key_blob=bytes.fromhex("""
400
+            00 00 00 0b 73 73 68 2d 65 64 32 35 35 31 39
401
+            00 00 00 20
402
+            81 78 81 68 26 d6 02 48 5f 0f ff 32 48 6f e4 c1
403
+            30 89 dc 1c 6a 45 06 09 e9 09 0f fb c2 12 69 76
404
+            00 00 00 40
405
+            1b 33 f0 3c eb d9 e4 59 96 de da da 77 0e 6f cb
406
+            ea 08 ad be 6a 47 fd a4 59 b7 cb 01 20 8d c4 c5
407
+            81 78 81 68 26 d6 02 48 5f 0f ff 32 48 6f e4 c1
408
+            30 89 dc 1c 6a 45 06 09 e9 09 0f fb c2 12 69 76
409
+            00 00 00 1b 74 65 73 74 20 6b 65 79 20 77 69 74
410
+            68 6f 75 74 20 70 61 73 73 70 68 72 61 73 65
411
+"""),
412
+        public_key=rb"""ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIF4gWgm1gJIXw//Mkhv5MEwidwcakUGCekJD/vCEml2 test key without passphrase
413
+""",
414
+        public_key_data=bytes.fromhex("""
415
+            00 00 00 0b 73 73 68 2d 65 64 32 35 35 31 39
416
+            00 00 00 20
417
+            81 78 81 68 26 d6 02 48 5f 0f ff 32 48 6f e4 c1
418
+            30 89 dc 1c 6a 45 06 09 e9 09 0f fb c2 12 69 76
419
+"""),
420
+        expected_signatures={
421
+            SSHTestKeyDeterministicSignatureClass.SPEC: SSHTestKeyDeterministicSignature(
422
+                signature=bytes.fromhex("""
423
+                    00 00 00 0b 73 73 68 2d 65 64 32 35 35 31 39
424
+                    00 00 00 40
425
+                    f0 98 19 80 6c 1a 97 d5 26 03 6e cc e3 65 8f 86
426
+                    66 07 13 19 13 09 21 33 33 f9 e4 36 53 1d af fd
427
+                    0d 08 1f ec f8 73 9b 8c 5f 55 39 16 7c 53 54 2c
428
+                    1e 52 bb 30 ed 7f 89 e2 2f 69 51 55 d8 9e a6 02
429
+"""),
430
+                derived_passphrase=rb"""8JgZgGwal9UmA27M42WPhmYHExkTCSEzM/nkNlMdr/0NCB/s+HObjF9VORZ8U1QsHlK7MO1/ieIvaVFV2J6mAg==""",
431
+            ),
432
+        },
433
+    ),
434
+    # Currently only supported by PuTTY (which is deficient in other
435
+    # niceties of the SSH agent and the agent's client).
436
+    "ed448": SSHTestKey(
437
+        private_key=rb"""-----BEGIN OPENSSH PRIVATE KEY-----
438
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAASgAAAAlz
439
+c2gtZWQ0NDgAAAA54vZy009Wu8wExjvEb3hqtLz1GO/+d5vmGUbErWQ4AUO9mYLT
440
+zHJHc2m4s+yWzP29Cc3EcxizLG8AAAAA8BdhfCcXYXwnAAAACXNzaC1lZDQ0OAAA
441
+ADni9nLTT1a7zATGO8RveGq0vPUY7/53m+YZRsStZDgBQ72ZgtPMckdzabiz7JbM
442
+/b0JzcRzGLMsbwAAAAByM7GIMRvWJB3YD6SIpAF2uudX4ozZe0X917wPwiBrs373
443
+9TM1n94Nib6hrxGNmCk2iBQDe2KALPgA4vZy009Wu8wExjvEb3hqtLz1GO/+d5vm
444
+GUbErWQ4AUO9mYLTzHJHc2m4s+yWzP29Cc3EcxizLG8AAAAAG3Rlc3Qga2V5IHdp
445
+dGhvdXQgcGFzc3BocmFzZQECAwQFBgcICQ==
446
+-----END OPENSSH PRIVATE KEY-----
447
+""",
448
+        private_key_blob=bytes.fromhex("""
449
+            00 00 00 09 73 73 68 2d 65 64 34 34 38
450
+            00 00 00 39 e2 f6 72 d3 4f 56 bb cc 04
451
+            c6 3b c4 6f 78 6a b4 bc f5 18 ef fe 77 9b e6 19
452
+            46 c4 ad 64 38 01 43 bd 99 82 d3 cc 72 47 73 69
453
+            b8 b3 ec 96 cc fd bd 09 cd c4 73 18 b3 2c 6f 00
454
+            00 00 00 72 33 b1
455
+            88 31 1b d6 24 1d d8 0f a4 88 a4 01 76 ba e7 57
456
+            e2 8c d9 7b 45 fd d7 bc 0f c2 20 6b b3 7e f7 f5
457
+            33 35 9f de 0d 89 be a1 af 11 8d 98 29 36 88 14
458
+            03 7b 62 80 2c f8 00 e2 f6 72 d3 4f 56 bb cc 04
459
+            c6 3b c4 6f 78 6a b4 bc f5 18 ef fe 77 9b e6 19
460
+            46 c4 ad 64 38 01 43 bd 99 82 d3 cc 72 47 73 69
461
+            b8 b3 ec 96 cc fd bd 09 cd c4 73 18 b3 2c 6f 00
462
+            00 00 00 1b 74 65 73 74 20 6b 65 79 20 77 69
463
+            74 68 6f 75 74 20 70 61 73 73 70 68 72 61 73 65
464
+"""),
465
+        public_key=rb"""ssh-ed448 AAAACXNzaC1lZDQ0OAAAADni9nLTT1a7zATGO8RveGq0vPUY7/53m+YZRsStZDgBQ72ZgtPMckdzabiz7JbM/b0JzcRzGLMsbwA= test key without passphrase
466
+""",
467
+        public_key_data=bytes.fromhex("""
468
+            00 00 00 09 73 73 68 2d 65 64 34 34 38
469
+            00 00 00 39 e2 f6 72 d3 4f 56 bb cc 04
470
+            c6 3b c4 6f 78 6a b4 bc f5 18 ef fe 77 9b e6 19
471
+            46 c4 ad 64 38 01 43 bd 99 82 d3 cc 72 47 73 69
472
+            b8 b3 ec 96 cc fd bd 09 cd c4 73 18 b3 2c 6f 00
473
+        """),
474
+        expected_signatures={
475
+            SSHTestKeyDeterministicSignatureClass.SPEC: SSHTestKeyDeterministicSignature(
476
+                signature=bytes.fromhex("""
477
+                    00 00 00 09 73 73 68 2d 65 64 34 34 38
478
+                    00 00 00 72 06 86
479
+                    f4 64 a4 a6 ba d9 c3 22 c4 93 49 99 fc 11 de 67
480
+                    97 08 f2 d8 b7 3c 2c 13 e7 c5 1c 1e 92 a6 0e d8
481
+                    2f 6d 81 03 82 00 e3 72 e4 32 6d 72 d2 6d 32 84
482
+                    3f cc a9 1e 57 2c 00 9a b3 99 de 45 da ce 2e d1
483
+                    db e5 89 f3 35 be 24 58 90 c6 ca 04 f0 db 88 80
484
+                    db bd 77 7c 80 20 7f 3a 48 61 f6 1f ae a9 5e 53
485
+                    7b e0 9d 93 1e ea dc eb b5 cd 56 4c ea 8f 08 00
486
+"""),
487
+                derived_passphrase=rb"""Bob0ZKSmutnDIsSTSZn8Ed5nlwjy2Lc8LBPnxRwekqYO2C9tgQOCAONy5DJtctJtMoQ/zKkeVywAmrOZ3kXazi7R2+WJ8zW+JFiQxsoE8NuIgNu9d3yAIH86SGH2H66pXlN74J2THurc67XNVkzqjwgA""",
488
+            ),
489
+        },
490
+    ),
491
+    "rsa": SSHTestKey(
492
+        private_key=rb"""-----BEGIN OPENSSH PRIVATE KEY-----
493
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
494
+NhAAAAAwEAAQAAAYEAsaHu6Xs4cVsuDSNJlMCqoPVgmDgEviI8TfXmHKqX3JkIqI3LsvV7
495
+Ijf8WCdTveEq7CkuZhImtsR52AOEVAoU8mDXDNr+nJ5wUPzf1UIaRjDe0lcXW4SlF01hQs
496
+G4wYDuqxshwelraB/L3e0zhD7fjYHF8IbFsqGlFHWEwOtlfhhfbxJsTGguLm4A8/gdEJD5
497
+2rkqDcZpIXCHtJbCzW9aQpWcs/PDw5ylwl/3dB7jfxyfrGz4O3QrzsqhWEsip97mOmwl6q
498
+CHbq8V8x9zu89D/H+bG5ijqxhijbjcVUW3lZfw/97gy9J6rG31HNar5H8GycLTFwuCFepD
499
+mTEpNgQLKoe8ePIEPq4WHhFUovBdwlrOByUKKqxreyvWt5gkpTARz+9Lt8OjBO3rpqK8sZ
500
+VKH3sE3de2RJM3V9PJdmZSs2b8EFK3PsUGdlMPM9pn1uk4uIItKWBmooOynuD8Ll6aPwuW
501
+AFn3l8nLLyWdrmmEYzHWXiRjQJxy1Bi5AbHMOWiPAAAFkDPkuBkz5LgZAAAAB3NzaC1yc2
502
+EAAAGBALGh7ul7OHFbLg0jSZTAqqD1YJg4BL4iPE315hyql9yZCKiNy7L1eyI3/FgnU73h
503
+KuwpLmYSJrbEedgDhFQKFPJg1wza/pyecFD839VCGkYw3tJXF1uEpRdNYULBuMGA7qsbIc
504
+Hpa2gfy93tM4Q+342BxfCGxbKhpRR1hMDrZX4YX28SbExoLi5uAPP4HRCQ+dq5Kg3GaSFw
505
+h7SWws1vWkKVnLPzw8OcpcJf93Qe438cn6xs+Dt0K87KoVhLIqfe5jpsJeqgh26vFfMfc7
506
+vPQ/x/mxuYo6sYYo243FVFt5WX8P/e4MvSeqxt9RzWq+R/BsnC0xcLghXqQ5kxKTYECyqH
507
+vHjyBD6uFh4RVKLwXcJazgclCiqsa3sr1reYJKUwEc/vS7fDowTt66aivLGVSh97BN3Xtk
508
+STN1fTyXZmUrNm/BBStz7FBnZTDzPaZ9bpOLiCLSlgZqKDsp7g/C5emj8LlgBZ95fJyy8l
509
+na5phGMx1l4kY0CcctQYuQGxzDlojwAAAAMBAAEAAAF/cNVYT+Om4x9+SItcz5bOByGIOj
510
+yWUH8f9rRjnr5ILuwabIDgvFaVG+xM1O1hWADqzMnSEcknHRkTYEsqYPykAtxFvjOFEh70
511
+6qRUJ+fVZkqRGEaI3oWyWKTOhcCIYImtONvb0LOv/HQ2H2AXCoeqjST1qr/xSuljBtcB8u
512
+wxs3EqaO1yU7QoZpDcMX9plH7Rmc9nNfZcgrnktPk2deX2+Y/A5tzdVgG1IeqYp6CBMLNM
513
+uhL0OPdDehgBoDujx+rhkZ1gpo1wcULIM94NL7VSHBPX0Lgh9T+3j1HVP+YnMAvhfOvfct
514
+LlbJ06+TYGRAMuF2LPCAZM/m0FEyAurRgWxAjLXm+4kp2GAJXlw82deDkQ+P8cHNT6s9ZH
515
+R5YSy3lpZ35594ZMOLR8KqVvhgJGF6i9019BiF91SDxjE+sp6dNGfN8W+64tHdDv2a0Mso
516
++8Qjyx7sTpi++EjLU8Iy73/e4B8qbXMyheyA/UUfgMtNKShh6sLlrD9h2Sm9RFTuEAAADA
517
+Jh3u7WfnjhhKZYbAW4TsPNXDMrB0/t7xyAQgFmko7JfESyrJSLg1cO+QMOiDgD7zuQ9RSp
518
+NIKdPsnIna5peh979mVjb2HgnikjyJECmBpLdwZKhX7MnIvgKw5lnQXHboEtWCa1N58l7f
519
+srzwbi9pFUuUp9dShXNffmlUCjDRsVLbK5C6+iaIQyCWFYK8mc6dpNkIoPKf+Xg+EJCIFQ
520
+oITqeu30Gc1+M+fdZc2ghq0b6XLthh/uHEry8b68M5KglMAAAAwQDw1i+IdcvPV/3u/q9O
521
+/kzLpKO3tbT89sc1zhjZsDNjDAGluNr6n38iq/XYRZu7UTL9BG+EgFVfIUV7XsYT5e+BPf
522
+13VS94rzZ7maCsOlULX+VdMO2zBucHIoec9RUlRZrfB21B2W7YGMhbpoa5lN3lKJQ7afHo
523
+dXZUMp0cTFbOmbzJgSzO2/NE7BhVwmvcUzTDJGMMKuxBO6w99YKDKRKm0PNLFDz26rWm9L
524
+dNS2MVfVuPMTpzT26HQG4pFageq9cAAADBALzRBXdZF8kbSBa5MTUBVTTzgKQm1C772gJ8
525
+T01DJEXZsVtOv7mUC1/m/by6Hk4tPyvDBuGj9hHq4N7dPqGutHb1q5n0ADuoQjRW7BXw5Q
526
+vC2EAD91xexdorIA5BgXU+qltBqzzBVzVtF7+jOZOjfzOlaTX9I5I5veyeTaTxZj1XXUzi
527
+btBNdMEJJp7ifucYmoYAAwE7K+VlWagDEK2y8Mte9y9E+N0uO2j+h85sQt/UIb2iE/vhcg
528
+Bgp6142WnSCQAAABt0ZXN0IGtleSB3aXRob3V0IHBhc3NwaHJhc2UB
529
+-----END OPENSSH PRIVATE KEY-----
530
+""",
531
+        private_key_blob=bytes.fromhex("""
532
+            00 00 00 07 73 73 68 2d 72 73 61
533
+            00 00 01 81 00
534
+            b1 a1 ee e9 7b 38 71 5b 2e 0d 23 49 94 c0 aa a0
535
+            f5 60 98 38 04 be 22 3c 4d f5 e6 1c aa 97 dc 99
536
+            08 a8 8d cb b2 f5 7b 22 37 fc 58 27 53 bd e1 2a
537
+            ec 29 2e 66 12 26 b6 c4 79 d8 03 84 54 0a 14 f2
538
+            60 d7 0c da fe 9c 9e 70 50 fc df d5 42 1a 46 30
539
+            de d2 57 17 5b 84 a5 17 4d 61 42 c1 b8 c1 80 ee
540
+            ab 1b 21 c1 e9 6b 68 1f cb dd ed 33 84 3e df 8d
541
+            81 c5 f0 86 c5 b2 a1 a5 14 75 84 c0 eb 65 7e 18
542
+            5f 6f 12 6c 4c 68 2e 2e 6e 00 f3 f8 1d 10 90 f9
543
+            da b9 2a 0d c6 69 21 70 87 b4 96 c2 cd 6f 5a 42
544
+            95 9c b3 f3 c3 c3 9c a5 c2 5f f7 74 1e e3 7f 1c
545
+            9f ac 6c f8 3b 74 2b ce ca a1 58 4b 22 a7 de e6
546
+            3a 6c 25 ea a0 87 6e af 15 f3 1f 73 bb cf 43 fc
547
+            7f 9b 1b 98 a3 ab 18 62 8d b8 dc 55 45 b7 95 97
548
+            f0 ff de e0 cb d2 7a ac 6d f5 1c d6 ab e4 7f 06
549
+            c9 c2 d3 17 0b 82 15 ea 43 99 31 29 36 04 0b 2a
550
+            87 bc 78 f2 04 3e ae 16 1e 11 54 a2 f0 5d c2 5a
551
+            ce 07 25 0a 2a ac 6b 7b 2b d6 b7 98 24 a5 30 11
552
+            cf ef 4b b7 c3 a3 04 ed eb a6 a2 bc b1 95 4a 1f
553
+            7b 04 dd d7 b6 44 93 37 57 d3 c9 76 66 52 b3 66
554
+            fc 10 52 b7 3e c5 06 76 53 0f 33 da 67 d6 e9 38
555
+            b8 82 2d 29 60 66 a2 83 b2 9e e0 fc 2e 5e 9a 3f
556
+            0b 96 00 59 f7 97 c9 cb 2f 25 9d ae 69 84 63 31
557
+            d6 5e 24 63 40 9c 72 d4 18 b9 01 b1 cc 39 68 8f
558
+            00 00 00 03 01 00 01
559
+            00 00 01 7f
560
+            70 d5 58 4f e3 a6 e3 1f 7e 48 8b 5c cf 96 ce
561
+            07 21 88 3a 3c 96 50 7f 1f f6 b4 63 9e be 48 2e
562
+            ec 1a 6c 80 e0 bc 56 95 1b ec 4c d4 ed 61 58 00
563
+            ea cc c9 d2 11 c9 27 1d 19 13 60 4b 2a 60 fc a4
564
+            02 dc 45 be 33 85 12 1e f4 ea a4 54 27 e7 d5 66
565
+            4a 91 18 46 88 de 85 b2 58 a4 ce 85 c0 88 60 89
566
+            ad 38 db db d0 b3 af fc 74 36 1f 60 17 0a 87 aa
567
+            8d 24 f5 aa bf f1 4a e9 63 06 d7 01 f2 ec 31 b3
568
+            71 2a 68 ed 72 53 b4 28 66 90 dc 31 7f 69 94 7e
569
+            d1 99 cf 67 35 f6 5c 82 b9 e4 b4 f9 36 75 e5 f6
570
+            f9 8f c0 e6 dc dd 56 01 b5 21 ea 98 a7 a0 81 30
571
+            b3 4c ba 12 f4 38 f7 43 7a 18 01 a0 3b a3 c7 ea
572
+            e1 91 9d 60 a6 8d 70 71 42 c8 33 de 0d 2f b5 52
573
+            1c 13 d7 d0 b8 21 f5 3f b7 8f 51 d5 3f e6 27 30
574
+            0b e1 7c eb df 72 d2 e5 6c 9d 3a f9 36 06 44 03
575
+            2e 17 62 cf 08 06 4c fe 6d 05 13 20 2e ad 18 16
576
+            c4 08 cb 5e 6f b8 92 9d 86 00 95 e5 c3 cd 9d 78
577
+            39 10 f8 ff 1c 1c d4 fa b3 d6 47 47 96 12 cb 79
578
+            69 67 7e 79 f7 86 4c 38 b4 7c 2a a5 6f 86 02 46
579
+            17 a8 bd d3 5f 41 88 5f 75 48 3c 63 13 eb 29 e9
580
+            d3 46 7c df 16 fb ae 2d 1d d0 ef d9 ad 0c b2 8f
581
+            bc 42 3c b1 ee c4 e9 8b ef 84 8c b5 3c 23 2e f7
582
+            fd ee 01 f2 a6 d7 33 28 5e c8 0f d4 51 f8 0c b4
583
+            d2 92 86 1e ac 2e 5a c3 f6 1d 92 9b d4 45 4e e1
584
+            00 00 00 c0
585
+            26 1d ee ed 67 e7 8e 18 4a 65 86 c0 5b 84 ec 3c
586
+            d5 c3 32 b0 74 fe de f1 c8 04 20 16 69 28 ec 97
587
+            c4 4b 2a c9 48 b8 35 70 ef 90 30 e8 83 80 3e f3
588
+            b9 0f 51 4a 93 48 29 d3 ec 9c 89 da e6 97 a1 f7
589
+            bf 66 56 36 f6 1e 09 e2 92 3c 89 10 29 81 a4 b7
590
+            70 64 a8 57 ec c9 c8 be 02 b0 e6 59 d0 5c 76 e8
591
+            12 d5 82 6b 53 79 f2 5e df b2 bc f0 6e 2f 69 15
592
+            4b 94 a7 d7 52 85 73 5f 7e 69 54 0a 30 d1 b1 52
593
+            db 2b 90 ba fa 26 88 43 20 96 15 82 bc 99 ce 9d
594
+            a4 d9 08 a0 f2 9f f9 78 3e 10 90 88 15 0a 08 4e
595
+            a7 ae df 41 9c d7 e3 3e 7d d6 5c da 08 6a d1 be
596
+            97 2e d8 61 fe e1 c4 af 2f 1b eb c3 39 2a 09 4c
597
+            00 00 00 c1 00
598
+            f0 d6 2f 88 75 cb cf 57 fd ee fe af 4e fe 4c cb
599
+            a4 a3 b7 b5 b4 fc f6 c7 35 ce 18 d9 b0 33 63 0c
600
+            01 a5 b8 da fa 9f 7f 22 ab f5 d8 45 9b bb 51 32
601
+            fd 04 6f 84 80 55 5f 21 45 7b 5e c6 13 e5 ef 81
602
+            3d fd 77 55 2f 78 af 36 7b 99 a0 ac 3a 55 0b 5f
603
+            e5 5d 30 ed b3 06 e7 07 22 87 9c f5 15 25 45 9a
604
+            df 07 6d 41 d9 6e d8 18 c8 5b a6 86 b9 94 dd e5
605
+            28 94 3b 69 f1 e8 75 76 54 32 9d 1c 4c 56 ce 99
606
+            bc c9 81 2c ce db f3 44 ec 18 55 c2 6b dc 53 34
607
+            c3 24 63 0c 2a ec 41 3b ac 3d f5 82 83 29 12 a6
608
+            d0 f3 4b 14 3c f6 ea b5 a6 f4 b7 4d 4b 63 15 7d
609
+            5b 8f 31 3a 73 4f 6e 87 40 6e 29 15 a8 1e ab d7
610
+            00 00 00 c1 00
611
+            bc d1 05 77 59 17 c9 1b 48 16 b9 31 35 01 55 34
612
+            f3 80 a4 26 d4 2e fb da 02 7c 4f 4d 43 24 45 d9
613
+            b1 5b 4e bf b9 94 0b 5f e6 fd bc ba 1e 4e 2d 3f
614
+            2b c3 06 e1 a3 f6 11 ea e0 de dd 3e a1 ae b4 76
615
+            f5 ab 99 f4 00 3b a8 42 34 56 ec 15 f0 e5 0b c2
616
+            d8 40 03 f7 5c 5e c5 da 2b 20 0e 41 81 75 3e aa
617
+            5b 41 ab 3c c1 57 35 6d 17 bf a3 39 93 a3 7f 33
618
+            a5 69 35 fd 23 92 39 bd ec 9e 4d a4 f1 66 3d 57
619
+            5d 4c e2 6e d0 4d 74 c1 09 26 9e e2 7e e7 18 9a
620
+            86 00 03 01 3b 2b e5 65 59 a8 03 10 ad b2 f0 cb
621
+            5e f7 2f 44 f8 dd 2e 3b 68 fe 87 ce 6c 42 df d4
622
+            21 bd a2 13 fb e1 72 00 60 a7 ad 78 d9 69 d2 09
623
+            00 00 00 1b 74 65 73 74 20 6b 65 79 20 77 69
624
+            74 68 6f 75 74 20 70 61 73 73 70 68 72 61 73 65
625
+"""),
626
+        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
627
+""",
628
+        public_key_data=bytes.fromhex("""
629
+            00 00 00 07 73 73 68 2d 72 73 61
630
+            00 00 00 03 01 00 01
631
+            00 00 01 81 00
632
+            b1 a1 ee e9 7b 38 71 5b 2e 0d 23 49 94 c0 aa a0
633
+            f5 60 98 38 04 be 22 3c 4d f5 e6 1c aa 97 dc 99
634
+            08 a8 8d cb b2 f5 7b 22 37 fc 58 27 53 bd e1 2a
635
+            ec 29 2e 66 12 26 b6 c4 79 d8 03 84 54 0a 14 f2
636
+            60 d7 0c da fe 9c 9e 70 50 fc df d5 42 1a 46 30
637
+            de d2 57 17 5b 84 a5 17 4d 61 42 c1 b8 c1 80 ee
638
+            ab 1b 21 c1 e9 6b 68 1f cb dd ed 33 84 3e df 8d
639
+            81 c5 f0 86 c5 b2 a1 a5 14 75 84 c0 eb 65 7e 18
640
+            5f 6f 12 6c 4c 68 2e 2e 6e 00 f3 f8 1d 10 90 f9
641
+            da b9 2a 0d c6 69 21 70 87 b4 96 c2 cd 6f 5a 42
642
+            95 9c b3 f3 c3 c3 9c a5 c2 5f f7 74 1e e3 7f 1c
643
+            9f ac 6c f8 3b 74 2b ce ca a1 58 4b 22 a7 de e6
644
+            3a 6c 25 ea a0 87 6e af 15 f3 1f 73 bb cf 43 fc
645
+            7f 9b 1b 98 a3 ab 18 62 8d b8 dc 55 45 b7 95 97
646
+            f0 ff de e0 cb d2 7a ac 6d f5 1c d6 ab e4 7f 06
647
+            c9 c2 d3 17 0b 82 15 ea 43 99 31 29 36 04 0b 2a
648
+            87 bc 78 f2 04 3e ae 16 1e 11 54 a2 f0 5d c2 5a
649
+            ce 07 25 0a 2a ac 6b 7b 2b d6 b7 98 24 a5 30 11
650
+            cf ef 4b b7 c3 a3 04 ed eb a6 a2 bc b1 95 4a 1f
651
+            7b 04 dd d7 b6 44 93 37 57 d3 c9 76 66 52 b3 66
652
+            fc 10 52 b7 3e c5 06 76 53 0f 33 da 67 d6 e9 38
653
+            b8 82 2d 29 60 66 a2 83 b2 9e e0 fc 2e 5e 9a 3f
654
+            0b 96 00 59 f7 97 c9 cb 2f 25 9d ae 69 84 63 31
655
+            d6 5e 24 63 40 9c 72 d4 18 b9 01 b1 cc 39 68 8f
656
+"""),
657
+        expected_signatures={
658
+            SSHTestKeyDeterministicSignatureClass.SPEC: SSHTestKeyDeterministicSignature(
659
+                signature=bytes.fromhex("""
660
+                    00 00 00 07 73 73 68 2d 72 73 61
661
+                    00 00 01 80
662
+                    a2 10 7c 2e f6 bb 53 a8 74 2a a1 19 99 ad 81 be
663
+                    79 9c ed d6 9d 09 4e 6e c5 18 48 33 90 77 99 68
664
+                    f7 9e 03 5a cd 4e 18 eb 89 7d 85 a2 ee ae 4a 92
665
+                    f6 6f ce b9 fe 86 7f 2a 6b 31 da 6e 1a fe a2 a5
666
+                    88 b8 44 7f a1 76 73 b3 ec 75 b5 d0 a6 b9 15 97
667
+                    65 09 13 7d 94 21 d1 fb 5d 0f 8b 23 04 77 c2 c3
668
+                    55 22 b1 a0 09 8a f5 38 2a d6 7f 1b 87 29 a0 25
669
+                    d3 25 6f cb 64 61 07 98 dc 14 c5 84 f8 92 24 5e
670
+                    50 11 6b 49 e5 f0 cc 29 cb 29 a9 19 d8 a7 71 1f
671
+                    91 0b 05 b1 01 4b c2 5f 00 a5 b6 21 bf f8 2c 9d
672
+                    67 9b 47 3b 0a 49 6b 79 2d fc 1d ec 0c b0 e5 27
673
+                    22 d5 a9 f8 d3 c3 f9 df 48 68 e9 fb ef 3c dc 26
674
+                    bf cf ea 29 43 01 a6 e3 c5 51 95 f4 66 6d 8a 55
675
+                    e2 47 ec e8 30 45 4c ae 47 e7 c9 a4 21 8b 64 ba
676
+                    b6 88 f6 21 f8 73 b9 cb 11 a1 78 75 92 c6 5a e5
677
+                    64 fe ed 42 d9 95 99 e6 2b 6f 3c 16 3c 28 74 a4
678
+                    72 2f 0d 3f 2c 33 67 aa 35 19 8e e7 b5 11 2f b3
679
+                    f7 6a c5 02 e2 6f a3 42 e3 62 19 99 03 ea a5 20
680
+                    e7 a1 e3 bc c8 06 a3 b5 7c d6 76 5d df 6f 60 46
681
+                    83 2a 08 00 d6 d3 d9 a4 c1 41 8c f8 60 56 45 81
682
+                    da 3b a2 16 1f 9e 4e 75 83 17 da c3 53 c3 3e 19
683
+                    a4 1b bc d2 29 b8 78 61 2b 78 e6 b1 52 b0 d5 ec
684
+                    de 69 2c 48 62 d9 fd d1 9b 6b b0 49 db d3 ff 38
685
+                    e7 10 d9 2d ce 9f 0d 5e 09 7b 37 d2 7b c3 bf ce
686
+"""),
687
+                derived_passphrase=rb"""ohB8Lva7U6h0KqEZma2Bvnmc7dadCU5uxRhIM5B3mWj3ngNazU4Y64l9haLurkqS9m/Ouf6GfyprMdpuGv6ipYi4RH+hdnOz7HW10Ka5FZdlCRN9lCHR+10PiyMEd8LDVSKxoAmK9Tgq1n8bhymgJdMlb8tkYQeY3BTFhPiSJF5QEWtJ5fDMKcspqRnYp3EfkQsFsQFLwl8ApbYhv/gsnWebRzsKSWt5Lfwd7Ayw5Sci1an408P530ho6fvvPNwmv8/qKUMBpuPFUZX0Zm2KVeJH7OgwRUyuR+fJpCGLZLq2iPYh+HO5yxGheHWSxlrlZP7tQtmVmeYrbzwWPCh0pHIvDT8sM2eqNRmO57URL7P3asUC4m+jQuNiGZkD6qUg56HjvMgGo7V81nZd329gRoMqCADW09mkwUGM+GBWRYHaO6IWH55OdYMX2sNTwz4ZpBu80im4eGEreOaxUrDV7N5pLEhi2f3Rm2uwSdvT/zjnENktzp8NXgl7N9J7w7/O""",
688
+            ),
689
+        },
690
+    ),
691
+    "dsa1024": SSHTestKey(
692
+        private_key=rb"""-----BEGIN OPENSSH PRIVATE KEY-----
693
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABsQAAAAdzc2gtZH
694
+NzAAAAgQC7KAZXqBGNVLBQPrcMYAoNW54BhD8aIhe7BDWYzJcsaMt72VKSkguZ8+XR7nRa
695
+0C/ZsBi+uJp0dpxy9ZMTOWX4u5YPMeQcXEdGExZIfimGqSOAsy6fCld2IfJZJZExcCmhe9
696
+Ssjsd3YSAPJRluOXFQc95MZoR5hMwlIDD8QzrE7QAAABUA99nOZOgd7aHMVGoXpUEBcn7H
697
+ossAAACALr2Ag3hxM3rKdxzVUw8fX0VVPXO+3+Kr8hGe0Kc/7NwVaBVL1GQ8fenBuWynpA
698
+UbH0wo3h1wkB/8hX6p+S8cnu5rIBlUuVNwLw/bIYohK98LfqTYK/V+g6KD+8m34wvEiXZm
699
+qywY54n2bksch1Nqvj/tNpLzExSx/XS0kSM1aigAAACAbQNRPcVEuGDrEcf+xg5tgAejPX
700
+BPXr/Jss+Chk64km3mirMYjAWyWYtVcgT+7hOYxtYRin8LyMLqKRmqa0Q5UrvDfChgLhvs
701
+G9YSb/Mpw5qm8PiHSafwhkaz/te3+8hKogqoe7sd+tCF06IpJr5k70ACiNtRGqssNF8Elr
702
+l1efYAAAH4swlfVrMJX1YAAAAHc3NoLWRzcwAAAIEAuygGV6gRjVSwUD63DGAKDVueAYQ/
703
+GiIXuwQ1mMyXLGjLe9lSkpILmfPl0e50WtAv2bAYvriadHaccvWTEzll+LuWDzHkHFxHRh
704
+MWSH4phqkjgLMunwpXdiHyWSWRMXApoXvUrI7Hd2EgDyUZbjlxUHPeTGaEeYTMJSAw/EM6
705
+xO0AAAAVAPfZzmToHe2hzFRqF6VBAXJ+x6LLAAAAgC69gIN4cTN6yncc1VMPH19FVT1zvt
706
+/iq/IRntCnP+zcFWgVS9RkPH3pwblsp6QFGx9MKN4dcJAf/IV+qfkvHJ7uayAZVLlTcC8P
707
+2yGKISvfC36k2Cv1foOig/vJt+MLxIl2ZqssGOeJ9m5LHIdTar4/7TaS8xMUsf10tJEjNW
708
+ooAAAAgG0DUT3FRLhg6xHH/sYObYAHoz1wT16/ybLPgoZOuJJt5oqzGIwFslmLVXIE/u4T
709
+mMbWEYp/C8jC6ikZqmtEOVK7w3woYC4b7BvWEm/zKcOapvD4h0mn8IZGs/7Xt/vISqIKqH
710
+u7HfrQhdOiKSa+ZO9AAojbURqrLDRfBJa5dXn2AAAAFQDJHfenj4EJ9WkehpdJatPBlqCW
711
+0gAAABt0ZXN0IGtleSB3aXRob3V0IHBhc3NwaHJhc2UBAgMEBQYH
712
+-----END OPENSSH PRIVATE KEY-----
713
+""",
714
+        private_key_blob=bytes.fromhex("""
715
+            00 00 00 07 73 73 68 2d 64 73 73
716
+            00 00 00 81 00
717
+            bb 28 06 57 a8 11 8d 54 b0 50 3e b7 0c 60 0a 0d
718
+            5b 9e 01 84 3f 1a 22 17 bb 04 35 98 cc 97 2c 68
719
+            cb 7b d9 52 92 92 0b 99 f3 e5 d1 ee 74 5a d0 2f
720
+            d9 b0 18 be b8 9a 74 76 9c 72 f5 93 13 39 65 f8
721
+            bb 96 0f 31 e4 1c 5c 47 46 13 16 48 7e 29 86 a9
722
+            23 80 b3 2e 9f 0a 57 76 21 f2 59 25 91 31 70 29
723
+            a1 7b d4 ac 8e c7 77 61 20 0f 25 19 6e 39 71 50
724
+            73 de 4c 66 84 79 84 cc 25 20 30 fc 43 3a c4 ed
725
+            00 00 00 15 00 f7 d9 ce 64
726
+            e8 1d ed a1 cc 54 6a 17 a5 41 01 72 7e c7 a2 cb
727
+            00 00 00 80
728
+            2e bd 80 83 78 71 33 7a ca 77 1c d5 53 0f 1f 5f
729
+            45 55 3d 73 be df e2 ab f2 11 9e d0 a7 3f ec dc
730
+            15 68 15 4b d4 64 3c 7d e9 c1 b9 6c a7 a4 05 1b
731
+            1f 4c 28 de 1d 70 90 1f fc 85 7e a9 f9 2f 1c 9e
732
+            ee 6b 20 19 54 b9 53 70 2f 0f db 21 8a 21 2b df
733
+            0b 7e a4 d8 2b f5 7e 83 a2 83 fb c9 b7 e3 0b c4
734
+            89 76 66 ab 2c 18 e7 89 f6 6e 4b 1c 87 53 6a be
735
+            3f ed 36 92 f3 13 14 b1 fd 74 b4 91 23 35 6a 28
736
+            00 00 00 80
737
+            6d 03 51 3d c5 44 b8 60 eb 11 c7 fe c6 0e 6d 80
738
+            07 a3 3d 70 4f 5e bf c9 b2 cf 82 86 4e b8 92 6d
739
+            e6 8a b3 18 8c 05 b2 59 8b 55 72 04 fe ee 13 98
740
+            c6 d6 11 8a 7f 0b c8 c2 ea 29 19 aa 6b 44 39 52
741
+            bb c3 7c 28 60 2e 1b ec 1b d6 12 6f f3 29 c3 9a
742
+            a6 f0 f8 87 49 a7 f0 86 46 b3 fe d7 b7 fb c8 4a
743
+            a2 0a a8 7b bb 1d fa d0 85 d3 a2 29 26 be 64 ef
744
+            40 02 88 db 51 1a ab 2c 34 5f 04 96 b9 75 79 f6
745
+            00 00 00 15 00 c9 1d f7 a7
746
+            8f 81 09 f5 69 1e 86 97 49 6a d3 c1 96 a0 96 d2
747
+            00 00 00 1b 74 65 73 74 20 6b 65 79 20 77 69
748
+            74 68 6f 75 74 20 70 61 73 73 70 68 72 61 73 65
749
+"""),
750
+        public_key=rb"""ssh-dss AAAAB3NzaC1kc3MAAACBALsoBleoEY1UsFA+twxgCg1bngGEPxoiF7sENZjMlyxoy3vZUpKSC5nz5dHudFrQL9mwGL64mnR2nHL1kxM5Zfi7lg8x5BxcR0YTFkh+KYapI4CzLp8KV3Yh8lklkTFwKaF71KyOx3dhIA8lGW45cVBz3kxmhHmEzCUgMPxDOsTtAAAAFQD32c5k6B3tocxUahelQQFyfseiywAAAIAuvYCDeHEzesp3HNVTDx9fRVU9c77f4qvyEZ7Qpz/s3BVoFUvUZDx96cG5bKekBRsfTCjeHXCQH/yFfqn5Lxye7msgGVS5U3AvD9shiiEr3wt+pNgr9X6DooP7ybfjC8SJdmarLBjnifZuSxyHU2q+P+02kvMTFLH9dLSRIzVqKAAAAIBtA1E9xUS4YOsRx/7GDm2AB6M9cE9ev8myz4KGTriSbeaKsxiMBbJZi1VyBP7uE5jG1hGKfwvIwuopGaprRDlSu8N8KGAuG+wb1hJv8ynDmqbw+IdJp/CGRrP+17f7yEqiCqh7ux360IXToikmvmTvQAKI21Eaqyw0XwSWuXV59g== test key without passphrase
751
+""",
752
+        public_key_data=bytes.fromhex("""
753
+            00 00 00 07 73 73 68 2d 64 73 73
754
+            00 00 00 81 00
755
+            bb 28 06 57 a8 11 8d 54 b0 50 3e b7 0c 60 0a 0d
756
+            5b 9e 01 84 3f 1a 22 17 bb 04 35 98 cc 97 2c 68
757
+            cb 7b d9 52 92 92 0b 99 f3 e5 d1 ee 74 5a d0 2f
758
+            d9 b0 18 be b8 9a 74 76 9c 72 f5 93 13 39 65 f8
759
+            bb 96 0f 31 e4 1c 5c 47 46 13 16 48 7e 29 86 a9
760
+            23 80 b3 2e 9f 0a 57 76 21 f2 59 25 91 31 70 29
761
+            a1 7b d4 ac 8e c7 77 61 20 0f 25 19 6e 39 71 50
762
+            73 de 4c 66 84 79 84 cc 25 20 30 fc 43 3a c4 ed
763
+            00 00 00 15 00 f7 d9 ce 64
764
+            e8 1d ed a1 cc 54 6a 17 a5 41 01 72 7e c7 a2 cb
765
+            00 00 00 80
766
+            2e bd 80 83 78 71 33 7a ca 77 1c d5 53 0f 1f 5f
767
+            45 55 3d 73 be df e2 ab f2 11 9e d0 a7 3f ec dc
768
+            15 68 15 4b d4 64 3c 7d e9 c1 b9 6c a7 a4 05 1b
769
+            1f 4c 28 de 1d 70 90 1f fc 85 7e a9 f9 2f 1c 9e
770
+            ee 6b 20 19 54 b9 53 70 2f 0f db 21 8a 21 2b df
771
+            0b 7e a4 d8 2b f5 7e 83 a2 83 fb c9 b7 e3 0b c4
772
+            89 76 66 ab 2c 18 e7 89 f6 6e 4b 1c 87 53 6a be
773
+            3f ed 36 92 f3 13 14 b1 fd 74 b4 91 23 35 6a 28
774
+            00 00 00 80
775
+            6d 03 51 3d c5 44 b8 60 eb 11 c7 fe c6 0e 6d 80
776
+            07 a3 3d 70 4f 5e bf c9 b2 cf 82 86 4e b8 92 6d
777
+            e6 8a b3 18 8c 05 b2 59 8b 55 72 04 fe ee 13 98
778
+            c6 d6 11 8a 7f 0b c8 c2 ea 29 19 aa 6b 44 39 52
779
+            bb c3 7c 28 60 2e 1b ec 1b d6 12 6f f3 29 c3 9a
780
+            a6 f0 f8 87 49 a7 f0 86 46 b3 fe d7 b7 fb c8 4a
781
+            a2 0a a8 7b bb 1d fa d0 85 d3 a2 29 26 be 64 ef
782
+            40 02 88 db 51 1a ab 2c 34 5f 04 96 b9 75 79 f6
783
+"""),
784
+        expected_signatures={
785
+            SSHTestKeyDeterministicSignatureClass.RFC_6979: SSHTestKeyDeterministicSignature(
786
+                signature=bytes.fromhex("""
787
+                    00 00 00 07 73 73 68 2d 64 73 73
788
+                    00 00 00 28 11 5f 4d 13 c2 ee 61 97
789
+                    1e f6 23 14 3b 2b dd cf 06 c0 71 13 cc ac 34 19
790
+                    ad 36 8d 79 aa 25 fb 5e 4f ea fe 6b 5b fa 57 42
791
+"""),
792
+                derived_passphrase=rb"""EV9NE8LuYZce9iMUOyvdzwbAcRPMrDQZrTaNeaol+15P6v5rW/pXQg==""",
793
+                signature_class=SSHTestKeyDeterministicSignatureClass.RFC_6979,
794
+            ),
795
+            SSHTestKeyDeterministicSignatureClass.Pageant_068_080: SSHTestKeyDeterministicSignature(
796
+                signature=bytes.fromhex("""
797
+                    00 00 00 07 73 73 68 2d 64 73 73
798
+                    00 00 00 28 0b f7 a8 ab 89 f5 b6 c4
799
+                    1c 9b 78 2c 46 35 69 e2 88 b7 eb 55 37 48 7f 6d
800
+                    49 a1 e6 de 58 1a 04 eb e6 28 99 0e 3c fd 3b 48
801
+"""),
802
+                derived_passphrase=rb"""C/eoq4n1tsQcm3gsRjVp4oi361U3SH9tSaHm3lgaBOvmKJkOPP07SA==""",
803
+                signature_class=SSHTestKeyDeterministicSignatureClass.Pageant_068_080,
804
+            ),
805
+        },
806
+    ),
807
+    "ecdsa256": SSHTestKey(
808
+        private_key=rb"""-----BEGIN OPENSSH PRIVATE KEY-----
809
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS
810
+1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQTLbU0zDwsk2Dvp+VYIrsNVf5gWwz2S
811
+3SZ8TbxiQRkpnGSVqyIoHJOJc+NQItAa7xlJ/8Z6gfz57Z3apUkaMJm6AAAAuKeY+YinmP
812
+mIAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBMttTTMPCyTYO+n5
813
+Vgiuw1V/mBbDPZLdJnxNvGJBGSmcZJWrIigck4lz41Ai0BrvGUn/xnqB/PntndqlSRowmb
814
+oAAAAhAKIl/3n0pKVIxpZkXTGtii782Qr4yIcvHdpxjO/QsIqKAAAAG3Rlc3Qga2V5IHdp
815
+dGhvdXQgcGFzc3BocmFzZQECAwQ=
816
+-----END OPENSSH PRIVATE KEY-----
817
+""",
818
+        private_key_blob=bytes.fromhex("""
819
+            00 00 00 13 65 63 64
820
+            73 61 2d 73 68 61 32 2d 6e 69 73 74 70 32 35 36
821
+            00 00 00 08 6e 69 73 74 70 32 35 36
822
+            00 00 00 41 04
823
+            cb 6d 4d 33 0f 0b 24 d8 3b e9 f9 56 08 ae c3 55
824
+            7f 98 16 c3 3d 92 dd 26 7c 4d bc 62 41 19 29 9c
825
+            64 95 ab 22 28 1c 93 89 73 e3 50 22 d0 1a ef 19
826
+            49 ff c6 7a 81 fc f9 ed 9d da a5 49 1a 30 99 ba
827
+            00 00 00 21 00
828
+            a2 25 ff 79 f4 a4 a5 48 c6 96 64 5d 31 ad 8a 2e
829
+            fc d9 0a f8 c8 87 2f 1d da 71 8c ef d0 b0 8a 8a
830
+            00 00 00 1b 74 65 73 74 20 6b 65 79 20 77 69
831
+            74 68 6f 75 74 20 70 61 73 73 70 68 72 61 73 65
832
+"""),
833
+        public_key=rb"""ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBMttTTMPCyTYO+n5Vgiuw1V/mBbDPZLdJnxNvGJBGSmcZJWrIigck4lz41Ai0BrvGUn/xnqB/PntndqlSRowmbo= test key without passphrase
834
+""",
835
+        public_key_data=bytes.fromhex("""
836
+            00 00 00 13 65 63 64
837
+            73 61 2d 73 68 61 32 2d 6e 69 73 74 70 32 35 36
838
+            00 00 00 08 6e 69 73 74 70 32 35 36
839
+            00 00 00 41 04
840
+            cb 6d 4d 33 0f 0b 24 d8 3b e9 f9 56 08 ae c3 55
841
+            7f 98 16 c3 3d 92 dd 26 7c 4d bc 62 41 19 29 9c
842
+            64 95 ab 22 28 1c 93 89 73 e3 50 22 d0 1a ef 19
843
+            49 ff c6 7a 81 fc f9 ed 9d da a5 49 1a 30 99 ba
844
+"""),
845
+        expected_signatures={
846
+            SSHTestKeyDeterministicSignatureClass.RFC_6979: SSHTestKeyDeterministicSignature(
847
+                signature=bytes.fromhex("""
848
+                    00 00 00 13 65 63 64
849
+                    73 61 2d 73 68 61 32 2d 6e 69 73 74 70 32 35 36
850
+                    00 00 00 49
851
+                    00 00 00 20
852
+                    22 ad 23 8a 9c 5d ca 4e ea 73 e7 29 77 ab a8 b2
853
+                    2e 01 d8 de 11 ae c9 b3 57 ce d5 84 9c 85 73 eb
854
+                    00 00 00 21 00
855
+                    9b 1a cb dd 45 89 f0 37 95 9c a2 d8 ac c3 f7 71
856
+                    55 33 50 86 9e cb 3a 95 e4 68 80 1a 9d d6 d5 bc
857
+"""),
858
+                derived_passphrase=rb"""AAAAICKtI4qcXcpO6nPnKXerqLIuAdjeEa7Js1fO1YSchXPrAAAAIQCbGsvdRYnwN5Wcotisw/dxVTNQhp7LOpXkaIAandbVvA==""",
859
+                signature_class=SSHTestKeyDeterministicSignatureClass.RFC_6979,
860
+            ),
861
+            SSHTestKeyDeterministicSignatureClass.Pageant_068_080: SSHTestKeyDeterministicSignature(
862
+                signature=bytes.fromhex("""
863
+                    00 00 00 13 65 63 64
864
+                    73 61 2d 73 68 61 32 2d 6e 69 73 74 70 32 35 36
865
+                    00 00 00 49
866
+                    00 00 00 21 00
867
+                    b7 9e 4f ec ec 9b 77 dd 12 d9 43 a2 f5 bf b5 34
868
+                    91 e0 89 44 e6 20 48 36 fa 75 22 77 86 38 de 21
869
+                    00 00 00 20
870
+                    3f d8 04 0f fa f5 bc d2 26 e0 4c 0c 77 5d 0e 08
871
+                    ec 30 04 8e 42 58 41 96 f6 7e 4f d2 14 39 f4 87
872
+"""),
873
+                derived_passphrase=rb"""AAAAIQC3nk/s7Jt33RLZQ6L1v7U0keCJROYgSDb6dSJ3hjjeIQAAACA/2AQP+vW80ibgTAx3XQ4I7DAEjkJYQZb2fk/SFDn0hw==""",
874
+                signature_class=SSHTestKeyDeterministicSignatureClass.Pageant_068_080,
875
+            ),
876
+        },
877
+    ),
878
+    "ecdsa384": SSHTestKey(
879
+        private_key=rb"""-----BEGIN OPENSSH PRIVATE KEY-----
880
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAiAAAABNlY2RzYS
881
+1zaGEyLW5pc3RwMzg0AAAACG5pc3RwMzg0AAAAYQSgkOjkAvq7v5vHuj3KBL4/EAWcn5hZ
882
+DyKcbyV0eBMGFq7hKXQlZqIahLVqeMR0QqmkxNJ2rly2VHcXneq3vZ+9fIsWCOdYk5WP3N
883
+ZPzv911Xn7wbEkC7QndD5zKlm4pBUAAADomhj+IZoY/iEAAAATZWNkc2Etc2hhMi1uaXN0
884
+cDM4NAAAAAhuaXN0cDM4NAAAAGEEoJDo5AL6u7+bx7o9ygS+PxAFnJ+YWQ8inG8ldHgTBh
885
+au4Sl0JWaiGoS1anjEdEKppMTSdq5ctlR3F53qt72fvXyLFgjnWJOVj9zWT87/ddV5+8Gx
886
+JAu0J3Q+cypZuKQVAAAAMQD5sTy8p+B1cn/DhOmXquui1BcxvASqzzevkBlbQoBa73y04B
887
+2OdqVOVRkwZWRROz0AAAAbdGVzdCBrZXkgd2l0aG91dCBwYXNzcGhyYXNlAQIDBA==
888
+-----END OPENSSH PRIVATE KEY-----
889
+""",
890
+        private_key_blob=bytes.fromhex("""
891
+            00 00 00 13 65 63 64
892
+            73 61 2d 73 68 61 32 2d 6e 69 73 74 70 33 38 34
893
+            00 00 00 08 6e 69 73 74 70 33 38 34
894
+            00 00 00 61 04
895
+            a0 90 e8 e4 02 fa bb bf 9b c7 ba 3d ca 04 be 3f
896
+            10 05 9c 9f 98 59 0f 22 9c 6f 25 74 78 13 06 16
897
+            ae e1 29 74 25 66 a2 1a 84 b5 6a 78 c4 74 42 a9
898
+            a4 c4 d2 76 ae 5c b6 54 77 17 9d ea b7 bd 9f bd
899
+            7c 8b 16 08 e7 58 93 95 8f dc d6 4f ce ff 75 d5
900
+            79 fb c1 b1 24 0b b4 27 74 3e 73 2a 59 b8 a4 15
901
+            00 00 00 31 00
902
+            f9 b1 3c bc a7 e0 75 72 7f c3 84 e9 97 aa eb a2
903
+            d4 17 31 bc 04 aa cf 37 af 90 19 5b 42 80 5a ef
904
+            7c b4 e0 1d 8e 76 a5 4e 55 19 30 65 64 51 3b 3d
905
+            00 00 00 1b 74 65 73 74 20 6b 65 79 20 77 69
906
+            74 68 6f 75 74 20 70 61 73 73 70 68 72 61 73 65
907
+"""),
908
+        public_key=rb"""ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBKCQ6OQC+ru/m8e6PcoEvj8QBZyfmFkPIpxvJXR4EwYWruEpdCVmohqEtWp4xHRCqaTE0nauXLZUdxed6re9n718ixYI51iTlY/c1k/O/3XVefvBsSQLtCd0PnMqWbikFQ== test key without passphrase
909
+""",
910
+        public_key_data=bytes.fromhex("""
911
+            00 00 00 13 65 63 64
912
+            73 61 2d 73 68 61 32 2d 6e 69 73 74 70 33 38 34
913
+            00 00 00 08 6e 69 73 74 70 33 38 34
914
+            00 00 00 61 04
915
+            a0 90 e8 e4 02 fa bb bf 9b c7 ba 3d ca 04 be 3f
916
+            10 05 9c 9f 98 59 0f 22 9c 6f 25 74 78 13 06 16
917
+            ae e1 29 74 25 66 a2 1a 84 b5 6a 78 c4 74 42 a9
918
+            a4 c4 d2 76 ae 5c b6 54 77 17 9d ea b7 bd 9f bd
919
+            7c 8b 16 08 e7 58 93 95 8f dc d6 4f ce ff 75 d5
920
+            79 fb c1 b1 24 0b b4 27 74 3e 73 2a 59 b8 a4 15
921
+"""),
922
+        expected_signatures={
923
+            SSHTestKeyDeterministicSignatureClass.RFC_6979: SSHTestKeyDeterministicSignature(
924
+                signature=bytes.fromhex("""
925
+                    00 00 00 13 65 63 64
926
+                    73 61 2d 73 68 61 32 2d 6e 69 73 74 70 33 38 34
927
+                    00 00 00 68
928
+                    00 00 00 30
929
+                    78 e1 a8 f5 8c d2 7a 21 e5 a2 ca e6 d0 1a 19 f8
930
+                    3a 1c 39 7e 71 a0 e6 7e 93 83 49 95 05 01 d0 3e
931
+                    23 22 cd 09 63 7f 7c 6c b0 97 44 6d 7e 48 39 87
932
+                    00 00 00 30
933
+                    10 ee 85 51 77 2b 91 2c e9 42 79 66 59 8a a2 c0
934
+                    d2 c8 8a 8f 2f 8f 33 87 9e 12 54 e4 da 02 f9 e7
935
+                    95 f5 82 6f 82 2b 38 6d 6e 5d 17 15 ac 12 e7 62
936
+"""),
937
+                derived_passphrase=rb"""AAAAMHjhqPWM0noh5aLK5tAaGfg6HDl+caDmfpODSZUFAdA+IyLNCWN/fGywl0Rtfkg5hwAAADAQ7oVRdyuRLOlCeWZZiqLA0siKjy+PM4eeElTk2gL555X1gm+CKzhtbl0XFawS52I=""",
938
+                signature_class=SSHTestKeyDeterministicSignatureClass.RFC_6979,
939
+            ),
940
+            SSHTestKeyDeterministicSignatureClass.Pageant_068_080: SSHTestKeyDeterministicSignature(
941
+                signature=bytes.fromhex("""
942
+                    00 00 00 13 65 63 64
943
+                    73 61 2d 73 68 61 32 2d 6e 69 73 74 70 33 38 34
944
+                    00 00 00 69
945
+                    00 00 00 30
946
+                    4b 3e b7 22 c2 87 77 6d e0 3e f5 05 75 36 b6 0f
947
+                    cd 9f a4 49 c7 48 ef 76 fd ea 4b 49 e3 b1 f2 22
948
+                    d5 41 22 d7 96 b2 29 70 ff bb 81 97 27 e2 35 60
949
+                    00 00 00 31 00
950
+                    c8 a4 d8 62 fe f2 a6 63 97 98 08 c7 39 24 b2 55
951
+                    0a b8 e7 79 ab a6 62 96 3e cc ea 73 e2 fb dc 46
952
+                    d6 25 b9 c8 0c e8 3e 33 91 51 78 25 a8 c5 46 85
953
+"""),
954
+                derived_passphrase=rb"""AAAAMEs+tyLCh3dt4D71BXU2tg/Nn6RJx0jvdv3qS0njsfIi1UEi15ayKXD/u4GXJ+I1YAAAADEAyKTYYv7ypmOXmAjHOSSyVQq453mrpmKWPszqc+L73EbWJbnIDOg+M5FReCWoxUaF""",
955
+                signature_class=SSHTestKeyDeterministicSignatureClass.Pageant_068_080,
956
+            ),
957
+        },
958
+    ),
959
+    "ecdsa521": SSHTestKey(
960
+        private_key=rb"""-----BEGIN OPENSSH PRIVATE KEY-----
961
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAArAAAABNlY2RzYS
962
+1zaGEyLW5pc3RwNTIxAAAACG5pc3RwNTIxAAAAhQQASVOdwDznmlcGqiLvFtYeVtrAEiVz
963
+iIfsL7jEM8Utu/m8WSkPFQtjwqdFw+WfZ0mi6qMbEFgi/ELzZSKVteCSbcMAhqAkOMFKiD
964
+u4bxvsM6bT02Ru7q2yT41ySyGhUD0QySBnI6Ckt/wnQ1TEpj8zDKiRErxs9e6QLGElNRkz
965
+LPMs+mMAAAEY2FXeh9hV3ocAAAATZWNkc2Etc2hhMi1uaXN0cDUyMQAAAAhuaXN0cDUyMQ
966
+AAAIUEAElTncA855pXBqoi7xbWHlbawBIlc4iH7C+4xDPFLbv5vFkpDxULY8KnRcPln2dJ
967
+ouqjGxBYIvxC82UilbXgkm3DAIagJDjBSog7uG8b7DOm09Nkbu6tsk+NckshoVA9EMkgZy
968
+OgpLf8J0NUxKY/MwyokRK8bPXukCxhJTUZMyzzLPpjAAAAQSFqUmKK7lGQzxT6GKZSLDju
969
+U3otwLYnuj+/5AdzuB/zotu95UdFv9I2DNXzd9E4WAyz6IqBBNcsMkxrzHAdqsYDAAAAG3
970
+Rlc3Qga2V5IHdpdGhvdXQgcGFzc3BocmFzZQ==
971
+-----END OPENSSH PRIVATE KEY-----
972
+""",
973
+        private_key_blob=bytes.fromhex("""
974
+            00 00 00 13 65 63 64
975
+            73 61 2d 73 68 61 32 2d 6e 69 73 74 70 35 32 31
976
+            00 00 00 08 6e 69 73 74 70 35 32 31
977
+            00 00 00 85 04 00 49 53 9d
978
+            c0 3c e7 9a 57 06 aa 22 ef 16 d6 1e 56 da c0 12
979
+            25 73 88 87 ec 2f b8 c4 33 c5 2d bb f9 bc 59 29
980
+            0f 15 0b 63 c2 a7 45 c3 e5 9f 67 49 a2 ea a3 1b
981
+            10 58 22 fc 42 f3 65 22 95 b5 e0 92 6d c3 00 86
982
+            a0 24 38 c1 4a 88 3b b8 6f 1b ec 33 a6 d3 d3 64
983
+            6e ee ad b2 4f 8d 72 4b 21 a1 50 3d 10 c9 20 67
984
+            23 a0 a4 b7 fc 27 43 54 c4 a6 3f 33 0c a8 91 12
985
+            bc 6c f5 ee 90 2c 61 25 35 19 33 2c f3 2c fa 63
986
+            00 00 00 41 21
987
+            6a 52 62 8a ee 51 90 cf 14 fa 18 a6 52 2c 38 ee
988
+            53 7a 2d c0 b6 27 ba 3f bf e4 07 73 b8 1f f3 a2
989
+            db bd e5 47 45 bf d2 36 0c d5 f3 77 d1 38 58 0c
990
+            b3 e8 8a 81 04 d7 2c 32 4c 6b cc 70 1d aa c6 03
991
+            00 00 00 1b 74 65 73 74 20 6b 65 79 20 77 69
992
+            74 68 6f 75 74 20 70 61 73 73 70 68 72 61 73 65
993
+"""),
994
+        public_key=rb"""ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBABJU53APOeaVwaqIu8W1h5W2sASJXOIh+wvuMQzxS27+bxZKQ8VC2PCp0XD5Z9nSaLqoxsQWCL8QvNlIpW14JJtwwCGoCQ4wUqIO7hvG+wzptPTZG7urbJPjXJLIaFQPRDJIGcjoKS3/CdDVMSmPzMMqJESvGz17pAsYSU1GTMs8yz6Yw== test key without passphrase
995
+""",
996
+        public_key_data=bytes.fromhex("""
997
+            00 00 00 13 65 63 64
998
+            73 61 2d 73 68 61 32 2d 6e 69 73 74 70 35 32 31
999
+            00 00 00 08 6e 69 73 74 70 35 32 31
1000
+            00 00 00 85 04 00 49 53 9d
1001
+            c0 3c e7 9a 57 06 aa 22 ef 16 d6 1e 56 da c0 12
1002
+            25 73 88 87 ec 2f b8 c4 33 c5 2d bb f9 bc 59 29
1003
+            0f 15 0b 63 c2 a7 45 c3 e5 9f 67 49 a2 ea a3 1b
1004
+            10 58 22 fc 42 f3 65 22 95 b5 e0 92 6d c3 00 86
1005
+            a0 24 38 c1 4a 88 3b b8 6f 1b ec 33 a6 d3 d3 64
1006
+            6e ee ad b2 4f 8d 72 4b 21 a1 50 3d 10 c9 20 67
1007
+            23 a0 a4 b7 fc 27 43 54 c4 a6 3f 33 0c a8 91 12
1008
+            bc 6c f5 ee 90 2c 61 25 35 19 33 2c f3 2c fa 63
1009
+"""),
1010
+        expected_signatures={
1011
+            SSHTestKeyDeterministicSignatureClass.RFC_6979: SSHTestKeyDeterministicSignature(
1012
+                signature=bytes.fromhex("""
1013
+                    00 00 00 13 65 63 64
1014
+                    73 61 2d 73 68 61 32 2d 6e 69 73 74 70 35 32 31
1015
+                    00 00 00 8b
1016
+                    00 00 00 42 01 d8
1017
+                    ea c2 1e 55 c6 9e dd 4b 00 ed 1b 93 19 cc 9b 74
1018
+                    27 44 c0 c0 e3 5b 3d 81 15 00 12 cc 07 89 54 97
1019
+                    ec 60 42 ad e6 40 c1 c6 5f c0 1b c3 0a 8e 58 6e
1020
+                    da 3f a9 57 90 04 79 46 1d 48 bb 19 67 e9 65 19
1021
+                    00 00 00 41 7d
1022
+                    58 e0 2e d7 86 2e 36 8c 1a 44 23 af 19 e7 51 97
1023
+                    bb fb 32 90 a1 35 bb 88 d7 b5 22 37 b3 99 ba e4
1024
+                    a7 9d 2d 56 14 0a f5 68 f5 cc 38 84 e9 b6 c6 71
1025
+                    7a 3b 87 e7 7a b1 37 e7 1d e6 80 96 d1 a6 1e bc
1026
+"""),
1027
+                derived_passphrase=rb"""AAAAQgHY6sIeVcae3UsA7RuTGcybdCdEwMDjWz2BFQASzAeJVJfsYEKt5kDBxl/AG8MKjlhu2j+pV5AEeUYdSLsZZ+llGQAAAEF9WOAu14YuNowaRCOvGedRl7v7MpChNbuI17UiN7OZuuSnnS1WFAr1aPXMOITptsZxejuH53qxN+cd5oCW0aYevA==""",
1028
+                signature_class=SSHTestKeyDeterministicSignatureClass.RFC_6979,
1029
+            ),
1030
+            SSHTestKeyDeterministicSignatureClass.Pageant_068_080: SSHTestKeyDeterministicSignature(
1031
+                signature=bytes.fromhex("""
1032
+                    00 00 00 13 65 63 64
1033
+                    73 61 2d 73 68 61 32 2d 6e 69 73 74 70 35 32 31
1034
+                    00 00 00 8c
1035
+                    00 00 00 42 01 ce
1036
+                    fe 9d 66 b6 01 76 2e 86 c2 ab 68 62 73 44 05 23
1037
+                    fd d1 79 07 fc 45 f5 c0 83 36 88 61 d4 04 79 90
1038
+                    b0 ef 8b 3c b5 55 0e cc 26 6b a0 3e 6a 04 48 ca
1039
+                    e4 6a a5 a0 cf 91 5f 71 6f 37 9a 0f 6b a9 fb 9b
1040
+                    00 00 00 42 01 6d
1041
+                    21 77 c6 13 fa ea ac de 90 19 24 5a d2 61 39 d9
1042
+                    66 9b 86 1a 41 04 58 a2 9b b8 93 b6 6f 82 23 f2
1043
+                    01 23 c7 ff 5a d3 86 95 0f da 28 f9 3b e3 9c 27
1044
+                    e7 b2 d7 66 4e 5f 38 36 4c 8c be 76 4e fa 0a 2d
1045
+"""),
1046
+                derived_passphrase=rb"""AAAAQgHO/p1mtgF2LobCq2hic0QFI/3ReQf8RfXAgzaIYdQEeZCw74s8tVUOzCZroD5qBEjK5GqloM+RX3FvN5oPa6n7mwAAAEIBbSF3xhP66qzekBkkWtJhOdlmm4YaQQRYopu4k7ZvgiPyASPH/1rThpUP2ij5O+OcJ+ey12ZOXzg2TIy+dk76Ci0=""",
1047
+                signature_class=SSHTestKeyDeterministicSignatureClass.Pageant_068_080,
1048
+            ),
1049
+        },
1050
+    ),
1051
+}
1052
+"""The master list of SSH test keys."""
1053
+SUPPORTED_KEYS: Mapping[str, SSHTestKey] = {
1054
+    k: v for k, v in ALL_KEYS.items() if v.is_suitable()
1055
+}
1056
+"""The subset of SSH test keys suitable for use with vault."""
1057
+UNSUITABLE_KEYS: Mapping[str, SSHTestKey] = {
1058
+    k: v for k, v in ALL_KEYS.items() if not v.is_suitable()
1059
+}
1060
+"""The subset of SSH test keys not suitable for use with vault."""
1061
+
1062
+
1063
+# Vault test configurations
1064
+# -------------------------
1065
+
1066
+
1067
+TEST_CONFIGS: list[VaultTestConfig] = [
1068
+    VaultTestConfig(None, "not a dict", None),
1069
+    VaultTestConfig({}, "missing required keys", None),
1070
+    VaultTestConfig(
1071
+        {"global": None, "services": {}}, "bad config value: global", None
1072
+    ),
1073
+    VaultTestConfig(
1074
+        {"global": {"key": 123}, "services": {}},
1075
+        "bad config value: global.key",
1076
+        None,
1077
+    ),
1078
+    VaultTestConfig(
1079
+        {"global": {"phrase": "abc", "key": "..."}, "services": {}},
1080
+        "",
1081
+        None,
1082
+    ),
1083
+    VaultTestConfig({"services": None}, "bad config value: services", None),
1084
+    VaultTestConfig(
1085
+        {"services": {"1": {}, 2: {}}}, 'bad config value: services."2"', None
1086
+    ),
1087
+    VaultTestConfig(
1088
+        {"services": {"1": {}, "2": 2}}, 'bad config value: services."2"', None
1089
+    ),
1090
+    VaultTestConfig(
1091
+        {"services": {"sv": {"notes": ["sentinel", "list"]}}},
1092
+        "bad config value: services.sv.notes",
1093
+        None,
1094
+    ),
1095
+    VaultTestConfig(
1096
+        {"services": {"sv": {"notes": "blah blah blah"}}}, "", None
1097
+    ),
1098
+    VaultTestConfig(
1099
+        {"services": {"sv": {"length": "200"}}},
1100
+        "bad config value: services.sv.length",
1101
+        None,
1102
+    ),
1103
+    VaultTestConfig(
1104
+        {"services": {"sv": {"length": 0.5}}},
1105
+        "bad config value: services.sv.length",
1106
+        None,
1107
+    ),
1108
+    VaultTestConfig(
1109
+        {"services": {"sv": {"length": ["sentinel", "list"]}}},
1110
+        "bad config value: services.sv.length",
1111
+        None,
1112
+    ),
1113
+    VaultTestConfig(
1114
+        {"services": {"sv": {"length": -10}}},
1115
+        "bad config value: services.sv.length",
1116
+        None,
1117
+    ),
1118
+    VaultTestConfig(
1119
+        {"services": {"sv": {"lower": "10"}}},
1120
+        "bad config value: services.sv.lower",
1121
+        None,
1122
+    ),
1123
+    VaultTestConfig(
1124
+        {"services": {"sv": {"upper": -10}}},
1125
+        "bad config value: services.sv.upper",
1126
+        None,
1127
+    ),
1128
+    VaultTestConfig(
1129
+        {"services": {"sv": {"number": ["sentinel", "list"]}}},
1130
+        "bad config value: services.sv.number",
1131
+        None,
1132
+    ),
1133
+    VaultTestConfig(
1134
+        {
1135
+            "global": {"phrase": "my secret phrase"},
1136
+            "services": {"sv": {"length": 10}},
1137
+        },
1138
+        "",
1139
+        None,
1140
+    ),
1141
+    VaultTestConfig(
1142
+        {"services": {"sv": {"length": 10, "phrase": "..."}}}, "", None
1143
+    ),
1144
+    VaultTestConfig(
1145
+        {"services": {"sv": {"length": 10, "key": "..."}}}, "", None
1146
+    ),
1147
+    VaultTestConfig(
1148
+        {"services": {"sv": {"upper": 10, "key": "..."}}}, "", None
1149
+    ),
1150
+    VaultTestConfig(
1151
+        {"services": {"sv": {"phrase": "abc", "key": "..."}}}, "", None
1152
+    ),
1153
+    VaultTestConfig(
1154
+        {
1155
+            "global": {"phrase": "abc"},
1156
+            "services": {"sv": {"phrase": "abc", "length": 10}},
1157
+        },
1158
+        "",
1159
+        None,
1160
+    ),
1161
+    VaultTestConfig(
1162
+        {
1163
+            "global": {"key": "..."},
1164
+            "services": {"sv": {"phrase": "abc", "length": 10}},
1165
+        },
1166
+        "",
1167
+        None,
1168
+    ),
1169
+    VaultTestConfig(
1170
+        {
1171
+            "global": {"key": "..."},
1172
+            "services": {"sv": {"phrase": "abc", "key": "...", "length": 10}},
1173
+        },
1174
+        "",
1175
+        None,
1176
+    ),
1177
+    VaultTestConfig(
1178
+        {
1179
+            "global": {"key": "..."},
1180
+            "services": {
1181
+                "sv1": {"phrase": "abc", "length": 10, "upper": 1},
1182
+                "sv2": {"length": 10, "repeat": 1, "lower": 1},
1183
+            },
1184
+        },
1185
+        "",
1186
+        None,
1187
+    ),
1188
+    VaultTestConfig(
1189
+        {
1190
+            "global": {"key": "...", "unicode_normalization_form": "NFC"},
1191
+            "services": {
1192
+                "sv1": {"phrase": "abc", "length": 10, "upper": 1},
1193
+                "sv2": {"length": 10, "repeat": 1, "lower": 1},
1194
+            },
1195
+        },
1196
+        "",
1197
+        None,
1198
+    ),
1199
+    VaultTestConfig(
1200
+        {
1201
+            "global": {"key": "...", "unicode_normalization_form": True},
1202
+            "services": {},
1203
+        },
1204
+        "bad config value: global.unicode_normalization_form",
1205
+        None,
1206
+    ),
1207
+    VaultTestConfig(
1208
+        {
1209
+            "global": {"key": "...", "unicode_normalization_form": "NFC"},
1210
+            "services": {
1211
+                "sv1": {"phrase": "abc", "length": 10, "upper": 1},
1212
+                "sv2": {"length": 10, "repeat": 1, "lower": 1},
1213
+            },
1214
+        },
1215
+        "",
1216
+        ValidationSettings(True),
1217
+    ),
1218
+    VaultTestConfig(
1219
+        {
1220
+            "global": {"key": "...", "unicode_normalization_form": "NFC"},
1221
+            "services": {
1222
+                "sv1": {"phrase": "abc", "length": 10, "upper": 1},
1223
+                "sv2": {"length": 10, "repeat": 1, "lower": 1},
1224
+            },
1225
+        },
1226
+        "extension/unknown key: .global.unicode_normalization_form",
1227
+        ValidationSettings(False),
1228
+    ),
1229
+    VaultTestConfig(
1230
+        {
1231
+            "global": {"key": "...", "unknown_key": True},
1232
+            "services": {
1233
+                "sv1": {"phrase": "abc", "length": 10, "upper": 1},
1234
+                "sv2": {"length": 10, "repeat": 1, "lower": 1},
1235
+            },
1236
+        },
1237
+        "",
1238
+        ValidationSettings(True),
1239
+    ),
1240
+    VaultTestConfig(
1241
+        {
1242
+            "global": {"key": "...", "unknown_key": True},
1243
+            "services": {
1244
+                "sv1": {"phrase": "abc", "length": 10, "upper": 1},
1245
+                "sv2": {"length": 10, "repeat": 1, "lower": 1},
1246
+            },
1247
+        },
1248
+        "unknown key: .global.unknown_key",
1249
+        ValidationSettings(False),
1250
+    ),
1251
+    VaultTestConfig(
1252
+        {
1253
+            "global": {"key": "..."},
1254
+            "services": {
1255
+                "sv1": {"phrase": "abc", "length": 10, "upper": 1},
1256
+                "sv2": {
1257
+                    "length": 10,
1258
+                    "repeat": 1,
1259
+                    "lower": 1,
1260
+                    "unknown_key": True,
1261
+                },
1262
+            },
1263
+        },
1264
+        "unknown key: .services.sv2.unknown_key",
1265
+        ValidationSettings(False),
1266
+    ),
1267
+    VaultTestConfig(
1268
+        {
1269
+            "global": {"key": "...", "unicode_normalization_form": "NFC"},
1270
+            "services": {
1271
+                "sv1": {"phrase": "abc", "length": 10, "upper": 1},
1272
+                "sv2": {
1273
+                    "length": 10,
1274
+                    "repeat": 1,
1275
+                    "lower": 1,
1276
+                    "unknown_key": True,
1277
+                },
1278
+            },
1279
+        },
1280
+        "",
1281
+        ValidationSettings(True),
1282
+    ),
1283
+    VaultTestConfig(
1284
+        {
1285
+            "global": {"key": "...", "unicode_normalization_form": "NFC"},
1286
+            "services": {
1287
+                "sv1": {"phrase": "abc", "length": 10, "upper": 1},
1288
+                "sv2": {
1289
+                    "length": 10,
1290
+                    "repeat": 1,
1291
+                    "lower": 1,
1292
+                    "unknown_key": True,
1293
+                },
1294
+            },
1295
+        },
1296
+        "",
1297
+        ValidationSettings(True),
1298
+    ),
1299
+    VaultTestConfig(
1300
+        {
1301
+            "global": {"key": "...", "unicode_normalization_form": "NFC"},
1302
+            "services": {
1303
+                "sv1": {"phrase": "abc", "length": 10, "upper": 1},
1304
+                "sv2": {
1305
+                    "length": 10,
1306
+                    "repeat": 1,
1307
+                    "lower": 1,
1308
+                    "unknown_key": True,
1309
+                },
1310
+            },
1311
+        },
1312
+        "",
1313
+        ValidationSettings(True),
1314
+    ),
1315
+]
1316
+"""The master list of test configurations for vault."""
1317
+
1318
+
1319
+# Common vault sample settings, passphrases, results, etc.
1320
+# --------------------------------------------------------
1321
+
1322
+
1323
+DUMMY_SERVICE = "service1"
1324
+"""A standard/sample service name."""
1325
+DUMMY_PASSPHRASE = "my secret passphrase"
1326
+"""A standard/sample passphrase."""
1327
+DUMMY_KEY1 = SUPPORTED_KEYS["ed25519"].public_key_data
1328
+"""A sample universally supported SSH test key (in wire format)."""
1329
+DUMMY_KEY1_B64 = base64.standard_b64encode(DUMMY_KEY1).decode("ASCII")
1330
+"""
1331
+A sample universally supported SSH test key (in `authorized_keys` format).
1332
+"""
1333
+DUMMY_KEY2 = SUPPORTED_KEYS["rsa"].public_key_data
1334
+"""A second supported SSH test key (in wire format)."""
1335
+DUMMY_KEY2_B64 = base64.standard_b64encode(DUMMY_KEY2).decode("ASCII")
1336
+"""A second supported SSH test key (in `authorized_keys` format)."""
1337
+DUMMY_KEY3 = SUPPORTED_KEYS["ed448"].public_key_data
1338
+"""A third supported SSH test key (in wire format)."""
1339
+DUMMY_KEY3_B64 = base64.standard_b64encode(DUMMY_KEY3).decode("ASCII")
1340
+"""A third supported SSH test key (in `authorized_keys` format)."""
1341
+DUMMY_CONFIG_SETTINGS = {
1342
+    "length": 10,
1343
+    "upper": 1,
1344
+    "lower": 1,
1345
+    "repeat": 5,
1346
+    "number": 1,
1347
+    "space": 1,
1348
+    "dash": 1,
1349
+    "symbol": 1,
1350
+}
1351
+"""Sample vault settings."""
1352
+DUMMY_RESULT_PASSPHRASE = b".2V_QJkd o"
1353
+"""
1354
+The passphrase derived from [`DUMMY_SERVICE`][] using [`DUMMY_PASSPHRASE`][].
1355
+"""
1356
+DUMMY_RESULT_KEY1 = b"E<b<{ -7iG"
1357
+"""
1358
+The passphrase derived from [`DUMMY_SERVICE`][] using [`DUMMY_KEY1`][].
1359
+"""
1360
+DUMMY_PHRASE_FROM_KEY1_RAW = (
1361
+    b"\x00\x00\x00\x0bssh-ed25519"
1362
+    b"\x00\x00\x00@\xf0\x98\x19\x80l\x1a\x97\xd5&\x03n"
1363
+    b"\xcc\xe3e\x8f\x86f\x07\x13\x19\x13\t!33\xf9\xe46S"
1364
+    b"\x1d\xaf\xfd\r\x08\x1f\xec\xf8s\x9b\x8c_U9\x16|ST,"
1365
+    b"\x1eR\xbb0\xed\x7f\x89\xe2/iQU\xd8\x9e\xa6\x02"
1366
+)
1367
+"""
1368
+The "equivalent master passphrase" derived from [`DUMMY_KEY1`][] (raw format).
1369
+"""
1370
+DUMMY_PHRASE_FROM_KEY1 = b"8JgZgGwal9UmA27M42WPhmYHExkTCSEzM/nkNlMdr/0NCB/s+HObjF9VORZ8U1QsHlK7MO1/ieIvaVFV2J6mAg=="
1371
+"""
1372
+The "equivalent master passphrase" derived from [`DUMMY_KEY1`][] (in base64).
1373
+"""
1374
+
1375
+
1376
+# Vault native configuration storage data
1377
+# ---------------------------------------
1378
+
1379
+
1380
+VAULT_MASTER_KEY = "vault key"
1381
+"""
1382
+The storage passphrase used to encrypt all sample vault native configurations.
1383
+"""
1384
+VAULT_V02_CONFIG = "P7xeh5y4jmjpJ2pFq4KUcTVoaE9ZOEkwWmpVTURSSWQxbGt6emN4aFE4eFM3anVPbDRNTGpOLzY3eDF5aE1YTm5LNWh5Q1BwWTMwM3M5S083MWRWRFlmOXNqSFJNcStGMWFOS3c2emhiOUNNenZYTmNNMnZxaUErdlRoOGF2ZHdGT1ZLNTNLOVJQcU9jWmJrR3g5N09VcVBRZ0ZnSFNUQy9HdFVWWnFteVhRVkY3MHNBdnF2ZWFEbFBseWRGelE1c3BFTnVUckRQdWJSL29wNjFxd2Y2ZVpob3VyVzRod3FKTElTenJ1WTZacTJFOFBtK3BnVzh0QWVxcWtyWFdXOXYyenNQeFNZbWt1MDU2Vm1kVGtISWIxWTBpcWRFbyswUVJudVVhZkVlNVpGWDA4WUQ2Q2JTWW81SnlhQ2Zxa3cxNmZoQjJES0Uyd29rNXpSck5iWVBrVmEwOXFya1NpMi9saU5LL3F0M3N3MjZKekNCem9ER2svWkZ0SUJLdmlHRno0VlQzQ3pqZTBWcTM3YmRiNmJjTkhqUHZoQ0NxMW1ldW1XOFVVK3pQMEtUMkRMVGNvNHFlOG40ck5KcGhsYXg1b1VzZ1NYU1B2T3RXdEkwYzg4NWE3YWUzOWI1MDI0MThhMWZjODQ3MDA2OTJmNDQ0MDkxNGFiNmRlMGQ2YjZiNjI5NGMwN2IwMmI4MGZi"
1385
+"""
1386
+A sample vault native configuration, in v0.2 format, encoded in base64
1387
+and encrypted with [`VAULT_MASTER_KEY`][].
1388
+"""
1389
+VAULT_V02_CONFIG_DATA = {
1390
+    "global": {
1391
+        "phrase": DUMMY_PASSPHRASE.rstrip("\n"),
1392
+    },
1393
+    "services": {
1394
+        "(meta)": {
1395
+            "notes": "This config was originally in v0.2 format.",
1396
+        },
1397
+        DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy(),
1398
+    },
1399
+}
1400
+"""
1401
+The plaintext contents (a vault native configuration) stored in
1402
+[`VAULT_V02_CONFIG`][].
1403
+"""
1404
+VAULT_V03_CONFIG = "sBPBrr8BFHPxSJkV/A53zk9zwDQHFxLe6UIusCVvzFQre103pcj5xxmE11lMTA0U2QTYjkhRXKkH5WegSmYpAnzReuRsYZlWWp6N4kkubf+twZ9C3EeggPm7as2Af4TICHVbX4uXpIHeQJf9y1OtqrO+SRBrgPBzgItoxsIxebxVKgyvh1CZQOSkn7BIzt9xKhDng3ubS4hQ91fB0QCumlldTbUl8tj4Xs5JbvsSlUMxRlVzZ0OgAOrSsoWELXmsp6zXFa9K6wIuZa4wQuMLQFHiA64JO1CR3I+rviWCeMlbTOuJNx6vMB5zotKJqA2hIUpN467TQ9vI4g/QTo40m5LT2EQKbIdTvBQAzcV4lOcpr5Lqt4LHED5mKvm/4YfpuuT3I3XCdWfdG5SB7ciiB4Go+xQdddy3zZMiwm1fEwIB8XjFf2cxoJdccLQ2yxf+9diedBP04EsMHrvxKDhQ7/vHl7xF2MMFTDKl3WFd23vvcjpR1JgNAKYprG/e1p/7"
1405
+"""
1406
+A sample vault native configuration, in v0.3 format, encoded in base64
1407
+and encrypted with [`VAULT_MASTER_KEY`][].
1408
+"""
1409
+VAULT_V03_CONFIG_DATA = {
1410
+    "global": {
1411
+        "phrase": DUMMY_PASSPHRASE.rstrip("\n"),
1412
+    },
1413
+    "services": {
1414
+        "(meta)": {
1415
+            "notes": "This config was originally in v0.3 format.",
1416
+        },
1417
+        DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy(),
1418
+    },
1419
+}
1420
+"""
1421
+The plaintext contents (a vault native configuration) stored in
1422
+[`VAULT_V03_CONFIG`][].
1423
+"""
1424
+VAULT_STOREROOM_CONFIG_ZIPPED = b"""
1425
+UEsDBBQAAAAIAJ1WGVnTVFGT0gAAAOYAAAAFAAAALmtleXMFwclSgzAAANC7n9GrBzBldcYDE5Al
1426
+EKbFAvGWklBAtqYsBcd/973fw8LFox76w/vb34tzhD5OATeEAk6tJ6Fbp3WrvkJO7l0KIjtxCLfY
1427
+ORm8ScEDPbNkyVwGLmZNTuQzXPMl/GnLO0I2PmUhRcxSj2Iy6PUy57up4thL6zndYwtyORpyCTGy
1428
+ibbjIeq/K/9atsHkl680nwsKFVk1i97gbGhG4gC5CMS8aUx8uebuToRCDsAT61UQVp0yEjw1bhm1
1429
+6UPWzM2wyfMGMyY1ox5HH/9QSwMEFAAAAAgAnVYZWd1pX+EFAwAA1AMAAAIAAAAwMA3ON7abQAAA
1430
+wP4fwy0FQUR3ZASLYEkCOnKOEtHPd7e7KefPr71YP800/vqN//3hAywvUaCcTYb6TbKS/kYcVnvG
1431
+wGA5N8ksjpFNCu5BZGu953GdoVnOfN6PNXoluWOS2JzO23ELNJ2m9nDn0uDhwC39VHJT1pQdejIw
1432
+CovQTEWmBH53FJufhNSZKQG5s1fMcw9hqn3NbON6wRDquOjLe/tqWkG1yiQDSF5Ail8Wd2UaA7vo
1433
+40QorG1uOBU7nPlDx/cCTDpSqwTZDkkAt6Zy9RT61NUZqHSMIgKMerj3njXOK+1q5sA/upSGvMrN
1434
+7/JpSEhcmu7GDvQJ8TyLos6vPCSmxO6RRG3X4BLpqHkTgeqHz+YDZwTV+6y5dvSmTSsCP5uPCmi+
1435
+7r9irZ1m777iL2R8NFH0QDIo1GFsy1NrUvWq4TGuvVIbkHrML5mFdR6ajNhRjL/6//1crYAMLHxo
1436
+qkjGz2Wck2dmRd96mFFAfdQ1/BqDgi6X/KRwHL9VmhpdjcKJhuE04xLYgTCyKLv8TkFfseNAbN3N
1437
+7KvVW7QVF97W50pzXzy3Ea3CatNQkJ1DnkR0vc0dsHd1Zr0o1acUaAa65B2yjYXCk3TFlMo9TNce
1438
+OWBXzJrpaZ4N7bscdwCF9XYesSMpxBDpwyCIVyJ8tHZVf/iS4pE6u+XgvD42yef+ujhM/AyboqPk
1439
+sFNV/XoNpmWIySdkTMmwu72q1GfPqr01ze/TzCVrCe0KkFcZhe77jrLPOnRCIarF2c9MMHNfmguU
1440
+A0tJ8HodQb/zehL6C9KSiNWfG+NlK1Dro1sGKhiJETLMFru272CNlwQJmzTHuKAXuUvJmQCfmLfL
1441
+EPrxoE08fu+v6DKnSopnG8GTkbscPZ+K5q2kC6m7pCizKO1sLKG7fMBRnJxnel/vmpY2lFCB4ADy
1442
+no+dvqBl6z3X/ji9AFXC9X8HRd+8u57OS1zV4OhiVd7hMy1U8F5qbIBms+FS6QbL9NhIb2lFN4VO
1443
+3+ITZz1sPJBl68ZgJWOV6O4F5cAHGKl/UEsDBBQAAAAIAJ1WGVn9pqLBygEAACsCAAACAAAAMDMN
1444
+z8mWa0AAANB9f0ZvLZQhyDsnC0IMJShDBTuzJMZoktLn/ft79w/u7/dWvZb7OHz/Yf5+yYUBMTNK
1445
+RrCI1xIQs67d6yI6bM75waX0gRLdKMGyC5O2SzBLs57V4+bqxo5xI2DraLTVeniUXLxkLyjRnC4u
1446
+24Vp+7p+ppt9DlVNNZp7rskQDOe47mbgViNeE5oXpg/oDgTcfQYNvt8V0OoyKbIiNymOW/mB3hze
1447
+D1EHqTWQvFZB5ANGpLMM0U10xWYAClzuVJXKm/n/8JgVaobY38IjzxXyk4iPkQUuYtws73Kan871
1448
+R3mZa7/j0pO6Wu0LuoV+czp9yZEH/SU42lCgjEsZ9Mny3tHaF09QWU4oB7HI+LBhKnFJ9c0bHEky
1449
+OooHgzgTIa0y8fbpst30PEUwfUAS+lYzPXG3y+QUiy5nrJFPb0IwESd9gIIOVSfZK63wvD5ueoxj
1450
+O9bn2gutSFT6GO17ibguhXtItAjPbZWfyyQqHRyeBcpT7qbzQ6H1Of5clEqVdNcetAg8ZMKoWTbq
1451
+/vSSQ2lpkEqT0tEQo7zwKBzeB37AysB5hhDCPn1gUTER6d+1S4dzwO7HhDf9kG+3botig2Xm1Dz9
1452
+A1BLAwQUAAAACACdVhlZs14oCcgBAAArAgAAAgAAADA5BcHJkqIwAADQe39GXz2wE5gqDxAGQRZF
1453
+QZZbDIFG2YwIga7593nv93sm9N0M/fcf4d+XcUlVE+kvustz3BU7FjHOaW+u6TRsfNKzLh74mO1w
1454
+IXUlM/2sGKKuY5sYrW5N+oGqit2zLBYv57mFvH/S8pWGYDGzUnU1CdTL3B4Yix+Hk8E/+m0cSi2E
1455
+dnAibw1brWVXM++8iYcUg84TMbJXntFYCyrNw1NF+008I02PeH4C8oDID6fIoKvsw3p7WJJ/I9Yp
1456
+a6oJzlJiP5JGxRxZPj50N6EMtzNB+tZoIGxgtOFVpiJ05yMQFztY6I6LKIgvXW/s919GIjGshqdM
1457
+XVPFxaKG4p9Iux/xazf48FY8O7SMmbQC1VsXIYo+7eSpIY67VzrCoh41wXPklOWS6CV8RR/JBSqq
1458
+8lHkcz8L21lMCOrVR1Cs0ls4HLIhUkqr9YegTJ67VM7xevUsgOI7BkPDldiulRgX+sdPheCyCacu
1459
+e7/b/nk0SXWF7ZBxsR1awYqwkFKz41/1bZDsETsmd8n1DHycGIvRULv3yYhKcvWQ4asAMhP1ks5k
1460
+AgOcrM+JFvpYA86Ja8HCqCg8LihEI1e7+m8F71Lpavv/UEsDBBQAAAAIAJ1WGVnKO2Ji+AEAAGsC
1461
+AAACAAAAMWENx7dyo0AAANDen+GWAonMzbggLsJakgGBOhBLlGBZsjz373eve7+fKyJTM/Sff85/
1462
+P5QMwMFfAWipfXwvFPWU582cd3t7JVV5pBV0Y1clL4eKUd0w1m1M5JrkgW5PlfpOVedgABSe4zPY
1463
+LnSIZVuen5Eua9QY8lQ7rxW7YIqeajhgLfL54BIcY90fd8ANixlcM8V23Z03U35Txba0BbSguc0f
1464
+NRF83cWp+7rOYgNO9wWLs915oQmWAqAtqRYCiWlgAtxYFg0MnNS4/G80FvFmQTh0cjwcF1xEVPeW
1465
+l72ky84PEA0QMgRtQW+HXWtE0/vQTtNKzvNqPfrGZCldL5nk9PWhhPEQ/azyW11bz2eB+aM0g0r7
1466
+0/5YkO9er10YonsBT1rEn0lfBXDHwtwbxG2bdqELTuEtX2+OEih7K43rN2EvpXX47azaNpe/drIz
1467
+wgAdhpfZ/mZwaGFX0c7r5HCTnroNRi5Bx/vu7m1A7Nt1dix4Gl/aPLCWQzpwmdIMJDiqD1RGpc5v
1468
++pDLrpfhZOVhLjAPSQ0V7mm/XNSca8oIsDjwdvR438RQCU56mrlypklS4/tJAe0JZNZIgBmJszjG
1469
+AFbsmNYTJ9GmULB9lXmTWmrME592S285iWU5SsJcE1s+3oQw9QrvWB+e3bGAd9e+VFmFqr6+/gFQ
1470
+SwECHgMUAAAACACdVhlZ01RRk9IAAADmAAAABQAAAAAAAAABAAAApIEAAAAALmtleXNQSwECHgMU
1471
+AAAACACdVhlZ3Wlf4QUDAADUAwAAAgAAAAAAAAABAAAApIH1AAAAMDBQSwECHgMUAAAACACdVhlZ
1472
+/aaiwcoBAAArAgAAAgAAAAAAAAABAAAApIEaBAAAMDNQSwECHgMUAAAACACdVhlZs14oCcgBAAAr
1473
+AgAAAgAAAAAAAAABAAAApIEEBgAAMDlQSwECHgMUAAAACACdVhlZyjtiYvgBAABrAgAAAgAAAAAA
1474
+AAABAAAApIHsBwAAMWFQSwUGAAAAAAUABQDzAAAABAoAAAAA
1475
+"""
1476
+"""
1477
+A sample vault native configuration, in storeroom format, encrypted with
1478
+[`VAULT_MASTER_KEY`][].  The configuration is compressed (zip archive)
1479
+and then encoded in base64.
1480
+"""
1481
+VAULT_STOREROOM_CONFIG_DATA = {
1482
+    "global": {
1483
+        "phrase": DUMMY_PASSPHRASE.rstrip("\n"),
1484
+    },
1485
+    "services": {
1486
+        "(meta)": {
1487
+            "notes": "This config was originally in storeroom format.",
1488
+        },
1489
+        DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy(),
1490
+    },
1491
+}
1492
+"""
1493
+The parsed vault configuration stored in
1494
+[`VAULT_STOREROOM_CONFIG_ZIPPED`][].
1495
+"""
1496
+
1497
+VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED_JAVASCRIPT_SOURCE = """
1498
+// Executed in the top-level directory of the vault project code, in Node.js.
1499
+const storeroom = require('storeroom')
1500
+const Store = require('./lib/store.js')
1501
+let store = new Store(storeroom.createFileAdapter('./broken-dir'), 'vault key')
1502
+await store._storeroom.put('/services/array/', ['entry1','entry2'])
1503
+// The resulting "broken-dir" was then zipped manually.
1504
+"""
1505
+"""
1506
+The JavaScript source for the script that generated the storeroom
1507
+archive in [`VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED`][].
1508
+"""
1509
+VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED = b"""
1510
+UEsDBBQAAgAIAHijH1kjc0ql0gAAAOYAAAAFAAAALmtleXMFwclygjAAANB7P8Mrh7LIYmd6oGxC
1511
+HKwTJJgbNpBKCpGAhNTpv/e952ZpxHTjw+bN+HuJJABEikvHecD0pLgpgYKWjue0CZGk19mKF+4f
1512
+0AoLrXKh+ckk13nmxVk/KFE28eEHkBgJTISvRUVMQ0N5aRapLgWs/M7NSXV7qs0s2aIEstUG5FHv
1513
+fo/HKjpdUJMGK86vs2rOJFGyrx9ZK4iWW+LefwSTYxhYOlWpb0PpgXsV4dHNTz5skcJqpPUudZf9
1514
+jCFD0vxChL6ajm0P0prY+z9QSwMEFAACAAgAeKMfWX4L7vDYAQAAPwIAAAIAAAAwNQXByZKiMAAA
1515
+0Ht/Rl85sIR1qvqAouxbJAG8kWYxgCKICEzNv897f7+XanrR4fH9h//3pVdF8qmVeWjW+STwSbak
1516
+4e3CS00h2AcrQIcghm0lOcrLdJfuaOFqg5zEsW9lTbJMtIId5ezNGM9jPKaxeriXXm45pGuHCwFP
1517
+/gmcXKWGeU3sHfj93iIf6p0xrfQIGGJOvayKjzypUqb99Bllo9IwNP2FZjxmBWDw0NRzJrxr/4Qj
1518
+qp4ted4f91ZaR8+64C0BJBzDngElJEFLdA2WBcip2R/VZIG219WT3JlkbFrYSjhHWeb47igytTpo
1519
+USPjEJWVol0cVpD6iX1/mGM2BpHAFa+fLx3trXgbXaVmjyZVzUKDh/XqnovnLs529UGYCAdj8Xnx
1520
+vWwfWclm5uIB8cHbElx6G82Zs8RQnkDsyGVDbNaMOO7lMQF7o1Uy7Q9GuSWcFMK4KBAbcwm4l8RY
1521
++2ema46H3/S31IW1LOFpoZxjwyBS69dWS7/ulVxJfbuydMvZMeWpmerjUHnKaQdumibSeSOXh+zg
1522
+XU6w6SsKAjHWXCTjRehWmyNnI7z3+epr1RzUlnDcUMiYQ/seaNefgNx4jIbOw92FC2hxnZOJupK9
1523
+M1WVdH3+8x9QSwMEFAACAAgAeKMfWUXRU2i7AQAAFwIAAAIAAAAxYQ3QyZZjUAAA0H19Rm2zCGLs
1524
+c2rxzDMxBTtTEA8hnqlO/3v3/YT7+71W86cdh+8/+N8vUMGNNAjWlNHgsyBlwCpgBd/a2rrW0qwg
1525
+p/CmvT4PTpwjHztJ2T10Jc2Fc8O7eHQb9MawAbxSKscxFAjz5wnJviaOMT5kEIZS+ibU6GgqU61P
1526
+lbeYRIiNCfK1VeHMFCpUhZ1ipnh50kux5N2jph5aMvc+HOR3lQgx9MJpMzQ2oNxSfEm7wZ5s0GYb
1527
+Bgy2xwaEMXNRnbzlbijZJi0M7yXNKS7nS1uFMtsapEc204YOBbOY4VK6L/9jS2ez56ybGkQPfn6+
1528
+QCwTqvkR5ieuRhF0zcoPLld+OUlI0RfEPnYHKEG7gtSya/Z1Hh77Xq4ytJHdr7WmXt7BUFA8Sffm
1529
+obXI31UOyVNLW0y4WMKDWq+atKGbU5BDUayoITMqvCteAZfJvnR4kZftMaFEG5ln7ptpdzpl10m3
1530
+G2rgUwTjPBJKomnOtJpdwm1tXm6IMPQ6IPy7oMDC5JjrmxAPXwdPnY/i07Go6EKSYjbkj8vdj/BR
1531
+rAMe2wnzdJaRhKv8kPVG1VqNdzm6xLb/Cf8AUEsDBBQAAgAIAHijH1kaCPeauQEAABcCAAACAAAA
1532
+MWUFwTmyokAAAND8H+OnBAKyTpVBs8iOIG2zZM0OigJCg07N3ee9v7+kmt/d6/n7h/n3AyJEvoaD
1533
+gtd8f4RxATnaHVeGNjyuolVVL+mY8Tms5ldfgYseNYMzRYJj3+i3iUgqlT5D1r7j1Bh5qVzi14X0
1534
+jpuH7DBKeeot2jWI5mPubptvV567pX2U3OC6ccxWmyo2Dd3ehUkbPP4uiDgWDZzFg/fFETIawMng
1535
+ahWHB2cfc2bM2kugNhWLS4peUBp36UWqMpF6+sLeUxAVZ24u08MDNMpNk81VDgiftnfBTBBhBGm0
1536
+RNpzxMMOPnCx3RRFgttiJTydfkB9MeZ9pvxP9jUm/fndQfJI83CsBxcEWhbjzlEparc3VS2s4LjR
1537
+3Xafw3HLSlPqylHOWK2vc2ZJoObwqrCaFRg7kz1+z08SGu8pe0EHaII6FSxL7VM+rfVgpc1045Ut
1538
+6ayCQ0TwRL5m4oMYkZbFnivCBTY3Cdji2SQ+gh8m3A6YkFxXUH0Vz9Is8JZaLFyi24GjyZZ9rGuk
1539
+Y6w53oLyTF/fSzG24ghCDZ6pOgB5qyfk4z2mUmH7pwxNCoHZ1oaxeTSn039QSwECHgMUAAIACAB4
1540
+ox9ZI3NKpdIAAADmAAAABQAAAAAAAAABAAAApIEAAAAALmtleXNQSwECHgMUAAIACAB4ox9Zfgvu
1541
+8NgBAAA/AgAAAgAAAAAAAAABAAAApIH1AAAAMDVQSwECHgMUAAIACAB4ox9ZRdFTaLsBAAAXAgAA
1542
+AgAAAAAAAAABAAAApIHtAgAAMWFQSwECHgMUAAIACAB4ox9ZGgj3mrkBAAAXAgAAAgAAAAAAAAAB
1543
+AAAApIHIBAAAMWVQSwUGAAAAAAQABADDAAAAoQYAAAAA
1544
+"""
1545
+"""
1546
+A sample corrupted storeroom archive, encrypted with
1547
+[`VAULT_MASTER_KEY`][].  The configuration is compressed (zip archive)
1548
+and then encoded in base64.
1549
+
1550
+The archive contains a directory `/services/array/` that claims to have
1551
+two child items 'entry1' and 'entry2', but no such child items are
1552
+present in the archive.  See
1553
+[`VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED_JAVASCRIPT_SOURCE`][] for
1554
+the exact script that created this archive.
1555
+"""
1556
+
1557
+VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED2_JAVASCRIPT_SOURCE = """
1558
+// Executed in the top-level directory of the vault project code, in Node.js.
1559
+const storeroom = require('storeroom')
1560
+const Store = require('./lib/store.js')
1561
+let store = new Store(storeroom.createFileAdapter('./broken-dir'), 'vault key')
1562
+await store._storeroom.put('/services/array/', 'not a directory index')
1563
+// The resulting "broken-dir" was then zipped manually.
1564
+"""
1565
+"""
1566
+The JavaScript source for the script that generated the storeroom
1567
+archive in [`VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED2`][].
1568
+"""
1569
+VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED2 = b"""
1570
+UEsDBAoAAAAAAM6NSVmrcHdV5gAAAOYAAAAFAAAALmtleXN7InZlcnNpb24iOjF9CkV3ZS9LZkJp
1571
+L0V0OUcrZmxYM3gxaFU4ZjE4YlE3S253bHoxN0IxSDE3cUhVOGdWK2RpWWY5MTdFZ0YrSStidEpZ
1572
+VXBzWVZVck45OC9uLzdsZnl2NUdGVEg2NWZxVy93YjlOc2MxeEZ4ck43Q3p4eTZ5MVAxZzFPb2VK
1573
+b0RZU3J6YXlwT0E2M3pidmk0ZTRiREMyNXhPTXl5NHBoMDFGeGdnQmpSNnpUcmR2UDk2UlZQd0I5
1574
+WitOZkZWZUlXT1NQN254ZFNYMGdFbkZ4SDBmWDkzNTFaTTZnPVBLAwQKAAAAAADOjUlZJg3/BhcC
1575
+AAAXAgAAAgAAADBieyJ2ZXJzaW9uIjoxfQpBVXJJMjNDQ2VpcW14cUZRMlV4SUpBaUoxNEtyUzh2
1576
+SXpIa2xROURBaFRlVHNFMmxPVUg4WUhTcUk1cXRGSHBqY3c1WkRkZmRtUlEwQXVGRjllY3lkam14
1577
+dDdUemRYLzNmNFUvTGlVV2dLRmQ1K1FEN3BlVlE1bWpqeHNlUEpHTDlhTWlKaGxSUVB4SmtUbjBx
1578
+U2poM1RUT0ZZbVAzV0JkdlUyWnF2RzhaSDk2cU1WcnZsQ0dMRmZTc2svVXlvcHZKdENONUVXcTRZ
1579
+SDUwNFNiejFIUVhWd2RjejlrS1BuR3J6SVA4ZmZtZnhXQ0U0TmtLb0ZPQXZuNkZvS3FZdGlGbFE9
1580
+PQpBVXBMUVMrMG9VeEZTeCtxbTB3SUtyM1MvTVJxYWJJTFlEUnY0aHlBMVE2TGR2Nlk0UmJ0enVz
1581
+NzRBc0cxbVhhenlRU2hlZVowdk0xM2ZyTFA4YlV0VHBaRyszNXF1eUhLM2NaWVJRZUxKM0JzejZz
1582
+b0xaQjNZTkpNenFxTTQrdzM1U0FZZ2lMU1NkN05NeWVrTHNhRUIzRDFOajlTRk85K3NGNEpFMWVL
1583
+UXpNMkltNk9qOUNVQjZUSTV3UitibksxN1BnY2RaeTZUMVRMWElVREVxcDg4dWdsWmRFTVcrNU9k
1584
+aE5ZbXEzZERWVWV4UnJpM1AwUmVBSi9KMGdJNkNoUUE9PVBLAwQKAAAAAADOjUlZTNfdphcCAAAX
1585
+AgAAAgAAADBmeyJ2ZXJzaW9uIjoxfQpBWVJqOVpIUktGUEVKOHM2YVY2TkRoTk5jQlZ5cGVYUmdz
1586
+cnBldFQ0cGhJRGROWFdGYzRia0daYkJxMngwRDFkcVNjYWk5UzEveDZ2K28zRE0rVEF2OVE3ZFVR
1587
+QWVKR3RmRkhJZDZxWW0ybEdNSnF5WTRNWm14aE9YdXliend0V3Q4Mnhvb041QTZNcWpINmxKQllD
1588
+UUN3ZEJjb3RER0EwRnlnVTEzeHV2WnIzT1puZnFFRGRqbzMxNkw5aExDN1RxMTYwUHpBOXJOSDMz
1589
+ZkNBcUhIVXZiYlFQQWErekw1d3dEN3FlWkY2MHdJaEwvRmk5L3JhNGJDcHZRNC9ORWpRd3c9PQpB
1590
+WWNGUDB1Y2xMMHh3ZDM2UXZXbm4wWXFsOU5WV0s3c05CMTdjdmM3N3VDZ0J2OE9XYkR5UHk5d05h
1591
+R2NQQzdzcVdZdHpZRlBHR0taVjhVUzA1YTVsV1BabDNGVFNuQXNtekxPelBlcFZxaitleDU3aEsx
1592
+QnV1bHkrUCtYQkE0YUtsaDM3c0RJL3I0UE1BVlJuMDNoSDJ5dEhDMW9PbjF0V1M5Q1NLV1pSMThh
1593
+djdTT0RBMVBNRnFYTmZKZVNTaVJiQ2htbDdOcFVLbjlXSGJZandybDlqN0JSdy9kWjhNQldCb3Ns
1594
+Nlc1dGZtdnJMVHhGRFBXYUgzSUp0T0czMEI1M3c9PVBLAwQKAAAAAADOjUlZn9rNID8CAAA/AgAA
1595
+AgAAADFkeyJ2ZXJzaW9uIjoxfQpBYWFBb3lqaGljVDZ4eXh1c0U0RVlDZCtxbE81Z0dEYTBNSFVS
1596
+MmgrSW9QMHV4UkY3b1BRS2czOHlQUEN3Ny9MYVJLQ0dQZ0RyZ2RpTWJTeUwzZ3ZNMFhseVpVMVBW
1597
+QVJvNEFETU9lbXgrOWhtS0hjQWNKMG5EeW5oSkhGYTYyb2xyQUNxekZzblhKNVBSeEVTVzVEbUh0
1598
+Ui9nRm5Wa1FvalhyVW4ybmpYMjVVanZQaXhlMU96Y0daMmQ0MjdVTGdnY1hqMkhSdjJiZldDNDUw
1599
+SGFXS3FDckZlYWlrQ2xkUUM2WGV3SkxZUjdvQUY3UjVha2ttK3M2MXNCRTVCaTg0QmJLWHluc1NG
1600
+ejE0TXFrd2JMK1VMYVk9CkFUT3dqTUFpa3Q4My9NTW5KRXQ2b3EyNFN4KzJKNDc2K2gyTmEzbHUr
1601
+MDg0cjlBT25aaUk0TmlYV0N1Q0lzakEzcTBwUHFJS1VXZHlPQW9uM2VHY0huZUppWUtVYllBaUJI
1602
+MVNmbnhQQkMzZkFMRklybkQ4Y0VqeGpPcUFUaTQ5dE1mRmtib0dNQ3dEdFY0V3NJL0tLUlRCOFd1
1603
+MnNXK2J0V3QzVWlvZG9ZeUVLTDk3ekNNemZqdGptejF4SDhHTXY5WDVnaG9NSW5RQVNvYlRreVZ4
1604
+dWo5YnlDazdNbU0vK21ZL3AwZE9oYVY0Nncwcm04UGlvWEtzdzR4bXB3ditDWC9PRXV3Uy9meDJT
1605
+Y0lOQnNuYVRiWT1QSwECHgMKAAAAAADOjUlZq3B3VeYAAADmAAAABQAAAAAAAAAAAAAApIEAAAAA
1606
+LmtleXNQSwECHgMKAAAAAADOjUlZJg3/BhcCAAAXAgAAAgAAAAAAAAAAAAAApIEJAQAAMGJQSwEC
1607
+HgMKAAAAAADOjUlZTNfdphcCAAAXAgAAAgAAAAAAAAAAAAAApIFAAwAAMGZQSwECHgMKAAAAAADO
1608
+jUlZn9rNID8CAAA/AgAAAgAAAAAAAAAAAAAApIF3BQAAMWRQSwUGAAAAAAQABADDAAAA1gcAAAAA
1609
+"""
1610
+"""
1611
+A sample corrupted storeroom archive, encrypted with
1612
+[`VAULT_MASTER_KEY`][].  The configuration is compressed (zip archive)
1613
+and then encoded in base64.
1614
+
1615
+The archive contains a directory `/services/array/` whose list of child
1616
+items does not adhere to the serialization format.  See
1617
+[`VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED2_JAVASCRIPT_SOURCE`][] for
1618
+the exact script that created this archive.
1619
+"""
1620
+
1621
+VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED3_JAVASCRIPT_SOURCE = """
1622
+// Executed in the top-level directory of the vault project code, in Node.js.
1623
+const storeroom = require('storeroom')
1624
+const Store = require('./lib/store.js')
1625
+let store = new Store(storeroom.createFileAdapter('./broken-dir'), 'vault key')
1626
+await store._storeroom.put('/services/array/', [null, 1, true, [], {}])
1627
+// The resulting "broken-dir" was then zipped manually.
1628
+"""
1629
+"""
1630
+The JavaScript source for the script that generated the storeroom
1631
+archive in [`VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED3`][].
1632
+"""
1633
+VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED3 = b"""
1634
+UEsDBAoAAAAAAEOPSVnVlcff5gAAAOYAAAAFAAAALmtleXN7InZlcnNpb24iOjF9CkV4dVBHUDBi
1635
+YkxrUVdvWnV5ZUJQRy8xdmM2MCt6MThOa3BsS09ydFAvUTVnQmxkYVpIOG10dTE5VWZFNGdGRGRj
1636
+eHJtWUd4eXZDZFNqcVlOaDh4cTlzM3VydkdRTWFwcnhtdlZGZUxoSW4zZnVlTDAweEk0ZmlLenZN
1637
+MmthUlRsNWNORGh3eUNlWVk4dzhBcXNhYjNyVWVsOEE0eVQ0cHU2d2tmQ3dTWUdqeG5HR29EcWJK
1638
+VnVJVWNpZVBEcU9PTzU2b0MyMG9lT01adFVkTUtxV28zYnFZPVBLAwQKAAAAAABDj0lZ77OVHxcC
1639
+AAAXAgAAAgAAADBjeyJ2ZXJzaW9uIjoxfQpBZllFQVVobEkyU2lZeGlrdWh0RzRNbUN3L1V2THBN
1640
+VVhwVlB0NlRwdzRyNGdocVJhbGZWZ0hxUHFtbTczSnltdFFrNnZnR2JRdUpiQmVlYjYwOHNrMGk4
1641
+ZFJVZjNwdlc2SnUyejljQkdwOG5mTFpTdlNad1lLN09UK2gzSDNDcmoxbXNicEZUcHVldW81NXc1
1642
+dGdYMnBuWXNWTVcrczdjaHEyMUIya2lIVEZrdGt1MXlaRzhPYkVUQjNCOFNGODVVbi9CUjFEMHJ1
1643
+ME9zOWl4ZWM2VmNTMitTZndtNnNtSlk2ZW9ZNTJzOGJNRGdYMndjQ0srREdkOEo2VWp0NG5OQVE9
1644
+PQpBUWlPRnRZcmJybWUycEwxRFpGT1BjU0RHOUN2cVkvbHhTWGIwaVJUdmtIWFc2bEtHL0p4RUtU
1645
+d3RTc0RTeDhsMTUvaHRmbWpOQ2tuTzhLVEFoKzhRQm5FbjZ0a2x5Y3BmeEIrTUxLRjFCM1Q1bjcv
1646
+T0VUMExMdmgxU2k1bnRRNXhTUHZZNWtXeUMyZjhXUXFZb3FSNU5JVENMeDV6dWNsQ3dGb2kvVXc4
1647
+OWNNWjM1MHBSbThzUktJbjJFeDUrQ1JwS3ZHdnBHbFJaTmk5VHZmVkNic1FCalR3MC9aeklTdzVQ
1648
+NW9BVWE2U1ExUVFnNHg4VUNkY0s2QUNLaFluY0d4TVE9PVBLAwQKAAAAAABDj0lZGk9LVj8CAAA/
1649
+AgAAAgAAADE0eyJ2ZXJzaW9uIjoxfQpBY1g2NVpMUWk4ck9pUlIyWGEwQlFHQVhQVWF2aHNJVGVY
1650
+c2dzRk9OUmFTRzJCQlg0SGxJRHpwRUd5aDUrZ2czZVRwWDFNOERua3pMeTVzcWRkMFpmK3padTgz
1651
+Qm52Y1JPREVIVDllUW91YUtPTWltdlRYanNuSXAxUHo5VGY1TlRkRjNJVTd2V1lhUDg4WTI5NG1i
1652
+c1VVL2RKVTZqZ3ZDbUw2cE1VZ28xUU12bGJnaVp3cDV1RDFQZXlrSXdKVWdJSEgxTEpnYi9xU2tW
1653
+c25leW1XY1RXR0NobzRvZGx3S2hJWmFCelhvNFhlN2U1V2I2VHA3Rkk5VUpVcmZIRTAvcVdrZUZE
1654
+VmxlazY3cUx3ZFZXcU9DdFk9CkFhSGR0QjhydmQ0U3N4ZmJ5eU1OOHIzZEoxeHA5NmFIRTQvalNi
1655
+Z05hZWttaDkyb2ROM1F4MUlqYXZsYVkxeEt1eFF3KzlwTHFIcTF5a1JSRjQzL2RVWGFIRk5UU0NX
1656
+OVFsdmd3KzMwa1ZhSEdXRllvbFRnRWE4djQ3b3VrbGlmc01PZGM0YVNKb2R4ZUFJcVc3Q1cwdDVR
1657
+b2RUbWREUXpqc3phZkQ4R2VOd2NFQjdGMHI2RzNoZEJlQndxd3Z6eENVYnpSUmU5bEQ3NjQ3RFp1
1658
+bEo1U3c4amlvV0paTW40NlZhV3BYUXk4UnNva3hHaW00WUpybUZIQ2JkVU9qSWJsUmQ1Z3VhUDNU
1659
+M0NxeHRPdC94b1BhOD1QSwMECgAAAAAAQ49JWVJM8QYXAgAAFwIAAAIAAAAxNnsidmVyc2lvbiI6
1660
+MX0KQVlCWDF6M21qUlQrand4M2FyNkFpemxnalJZbUM0ZHg5NkxVQVBTVHNMWXJKVHFtWnd5N0Jy
1661
+OFlCcElVamorMHdlT3lNaUtLVnFwaER3RXExNWFqUmlSZUVEQURTVHZwWmlLZUlnZjR5elUzZXNP
1662
+eDJ2U2J1bXhTK0swUGZVa2tsSy9TRmRiU3EvUHFMRjBDRTVCMXNyKzJLYTB2WlJmak94R3VFeFRD
1663
+RXozN0ZlWDNNR3NCNkhZVHEzaUJWcUR6NVB6eHpCWWM5Kyt6RitLS1RnMVp2NGRtRmVQTC9JSEY5
1664
+WnV6TWlqRXdCRkE3WnJ0dkRqd3ZYcWtsMVpsR0c4eUV3PT0KQVhUWkRLVnNleldpR1RMUVZqa2hX
1665
+bXBnK05MYlM0M2MxZEpvK2xGcC9yWUJYZkw3Wll5cGdjWE5IWXNzd01nc2VSSTAzNmt6bGZkdGNa
1666
+bTdiUUN6M2JuQmZ6ZlorZFFuT2Y5STVSU2l0QzB2UmsydkQrOFdwbmRPSzNucGY5S0VpWklOSzVq
1667
+TEZGTTJDTkNmQzBabXNRUlF3T0k2N3l5ZHhjVnFDMXBnWHV6QXRXamlsSUpnN0p6eUtsY3BJUGJu
1668
+SUc0UzRSUlhIdW1wZnpoeWFZWkd6T0FDamRSYTZIMWJxYkJkZXFaSHMvQXJvM25mVjdlbjhxSUE5
1669
+aVUrbnNweXFnPT1QSwECHgMKAAAAAABDj0lZ1ZXH3+YAAADmAAAABQAAAAAAAAAAAAAApIEAAAAA
1670
+LmtleXNQSwECHgMKAAAAAABDj0lZ77OVHxcCAAAXAgAAAgAAAAAAAAAAAAAApIEJAQAAMGNQSwEC
1671
+HgMKAAAAAABDj0lZGk9LVj8CAAA/AgAAAgAAAAAAAAAAAAAApIFAAwAAMTRQSwECHgMKAAAAAABD
1672
+j0lZUkzxBhcCAAAXAgAAAgAAAAAAAAAAAAAApIGfBQAAMTZQSwUGAAAAAAQABADDAAAA1gcAAAAA
1673
+"""
1674
+"""
1675
+A sample corrupted storeroom archive, encrypted with
1676
+[`VAULT_MASTER_KEY`][].  The configuration is compressed (zip archive)
1677
+and then encoded in base64.
1678
+
1679
+The archive contains a directory `/services/array/` whose list of child
1680
+items are not all valid item names.  See
1681
+[`VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED3_JAVASCRIPT_SOURCE`][] for
1682
+the exact script that created this archive.
1683
+"""
1684
+
1685
+VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED4_JAVASCRIPT_SOURCE = """
1686
+// Executed in the top-level directory of the vault project code, in Node.js.
1687
+const storeroom = require('storeroom')
1688
+const Store = require('./lib/store.js')
1689
+let store = new Store(storeroom.createFileAdapter('./broken-dir'), 'vault key')
1690
+await store._storeroom.put('/dir/subdir/', [])
1691
+await store._storeroom.put('/dir/', [])
1692
+// The resulting "broken-dir" was then zipped manually.
1693
+"""
1694
+"""
1695
+The JavaScript source for the script that generated the storeroom
1696
+archive in [`VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED4`][].
1697
+"""
1698
+VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED4 = b"""
1699
+UEsDBAoAAAAAAE+5SVloORS+5gAAAOYAAAAFAAAALmtleXN7InZlcnNpb24iOjF9CkV6dWRoNkRQ
1700
+YTlNSWFabHZ5TytVYTFuamhjV2hIaTFBU0lKYW5zcXBxVlA0blN2V0twUzdZOUc2bjFSbi8vUnVM
1701
+VitwcHp5SC9RQk83R0hFenNVMzdCUzFwUmVVeGhxUVlVTE56OXZvQ0crM1ZaL3VncU44dDJiU05m
1702
+Nyt5K3hiNng2aVlFUmNZYTJ0UkhzZVdIc0laTE9ha2lDb0lRVGV3cndwYjVMM2pnd0E3SXBzaDkz
1703
+QkxHSzM5dXNYNmo0R0I2WkRUeW5JcGk4V3JkbDhnWVZCN0tVPVBLAwQKAAAAAABPuUlZ663uUhcC
1704
+AAAXAgAAAgAAADAzeyJ2ZXJzaW9uIjoxfQpBV2wzS2gzd21ZSFVZZU1RR3BLSVowdVd1VXFna09h
1705
+YmRjNzNYYXVsZTNtVS9sN2Zvd1AyS21jbFp3ZDM5V3lYVzRTcEw4R0l4YStDZW51S3V0Wm5nb0FR
1706
+bWlnaUJUbkFaais5TENCcGNIWlZNY2RBVkgxKzBFNGpsanZ1UkVwZ0tPS05LZjRsTUl1QnZ4VmFB
1707
+ZkdwNHJYNEZ4MmpPSlk1Y3NQZzBBRFBoZVAwN29GWVQ3alorSUNEK1AxNGZPdWpwMGRUeDRrTDIy
1708
+LzlqalRDNXBCNVF5NW5iOUx3Zk5DUWViSUVpaTZpbU0vRmFrK1dtV05tMndqMERSTEc4RHY3ZkE9
1709
+PQpBU0c3NTNGTVVwWmxjK3E1YXRzcC93OUNqN2JPOFlpY24wZHg2UGloTmwzUS9WSjVVeGJmU3l0
1710
+ZDFDNDBRU2xXeTJqOTJDWUd3VER6eEdBMXVnb0FCYi9kTllTelVwbHJFb3BuUVphYXdsdTVwV2x0
1711
+Y1E5WTcveWN4S2E4b0JaaGY3RkFYcGo2c01wUW9zNzI5VFVabFd4UmI4VFRtN2FrVnR1OXcvYXlK
1712
+RS9reDh4ZUYxSGJlc3Q4N1IxTGg2ODd3dS9XVUN2ZjNXYXo1VjNnZWY0RnpUTXg0bkpqSlZOd0U0
1713
+SzAxUTlaVzQ0bmVvbExPUVI1MkZDeDZvbml3RW9tenc9PVBLAwQKAAAAAABPuUlZRXky4CsCAAAr
1714
+AgAAAgAAADEweyJ2ZXJzaW9uIjoxfQpBWmlYWVlvNUdCY2d5dkFRaGtyK2ZjUkdVSkdabDd2dE5w
1715
+T2Mrd1VzbXJhQWhRN3dKdlYraGhKcTlrcWNKQnBWU0gyUTBTTVVhb29iNjBJM1NYNUNtTkJRU2FH
1716
+M3prd0Y0T2F4TnpCZUh0NFlpaDd4Y3p2ak4xR0hISDJQYW0xam05K09ja3JLVmNMVURtNXRKb2ZC
1717
+Z1E4Q2NwMGZMVkdEaURjNWF0MjVMc2piQVcvNkZFSnJ5VVBHWis4UVdYRmlWMGdtVVZybVc3VUFy
1718
+dGhJQitWNTdZS1BORi95Nng2OU43UTFQbmp1cUczdlpybzljMEJ3d012NWoyc3BMMTJHcTdzTDZE
1719
+alB1d0dHbnB2MkVZQTFLbmc9CkFTdjQwUkgzRmxzbGVlU1NjRlZNRmh3dEx6eEYxK2xpcmxEL29X
1720
+alJLQ05qVWZhUVpJTWpqMWRoVkhOakNUTWhWZ1ZONkl3b04xTnFOMEV6cmdhaTFBWnNiMm9UczYw
1721
+QkI1UGh0U0hhQ2U2WllUeE1JemFPS2FIK0w2eHhtaXIrTlQxNTRXS0x5amJMams3MU1na3Nwa0Yy
1722
+WDBJMnlaWW5IUUM0bmdEL24yZzRtSVI2Q1hWL0JOUXNzeTBEeXdGLzN6eGRRYWw5cFBtVk1qYnFu
1723
+cHY5SFNqRTg4S25naVpBWFhJWU1OVGF2L3Q3Y3dEWGdNekhKTlU0Y2xnVUtIQVZ3QT09UEsDBAoA
1724
+AAAAAE+5SVkPfKx9FwIAABcCAAACAAAAMWR7InZlcnNpb24iOjF9CkFYbHNLRzQwZG5ibTJvcXdY
1725
+U2ZrSWp3Mmxpa0lDS3hVOXU3TU52VkZ1NEJ2R1FVVitSVVdsS3MxL25TSlBtM2U2OTRvVHdoeDFo
1726
+RFF3U0M5U0QvbXd5bnpjSTloUnRCUWVXMkVMOVU5L1ZGcHFsVWY3Z1ZOMHZ0ZWpXYnV4QnhsZlRD
1727
+Tys4SFBwU2Zaa2VOUld5R2JNdzBFSU9LTmxRYjk3OUF0c1g3THR0NytaTkJnakZHYkZxaHdwa3kx
1728
+WUNDVng1UmNZZ2tma2ZjWnVncGpzc1RzNVFvK1p3QXBEcDZ4V3JjSHMxUDhvNktBRzAwcjZZbkNM
1729
+N2ErU1dwZmVNTUJhZz09CkFadVF0cFZMWmVvb292NkdyQlpnb3B6VmRGUXBlK1h6QXZuZ2dPVnZM
1730
+VWtCYVF2akl5K1VLdXVUVlFoQ1JiMVp6dGZQL2dsNnoxOEsyZW5sQlo2bGJTZnoxTlBWeUVzYXB3
1731
+dDVpUVh4azd5UkJlZks1cFlsNTduUXlmcFZQbzlreFpnOVdHTkV3NVJ5MkExemhnNGl6TWxLRmJh
1732
+UjZFZ0FjQ3NFOXAveGRLa29ZNjhOUlZmNXJDM3lMQjc3ZWgyS1hCUld2WDNZcE9XdW00OGtsbmtI
1733
+akJjMFpiQmUrT3NZb3d5cXpoRFA2ZGQxRlFnMlFjK09vc3B4V0sycld4M01HZz09UEsBAh4DCgAA
1734
+AAAAT7lJWWg5FL7mAAAA5gAAAAUAAAAAAAAAAAAAAKSBAAAAAC5rZXlzUEsBAh4DCgAAAAAAT7lJ
1735
+Weut7lIXAgAAFwIAAAIAAAAAAAAAAAAAAKSBCQEAADAzUEsBAh4DCgAAAAAAT7lJWUV5MuArAgAA
1736
+KwIAAAIAAAAAAAAAAAAAAKSBQAMAADEwUEsBAh4DCgAAAAAAT7lJWQ98rH0XAgAAFwIAAAIAAAAA
1737
+AAAAAAAAAKSBiwUAADFkUEsFBgAAAAAEAAQAwwAAAMIHAAAAAA==
1738
+"""
1739
+"""
1740
+A sample corrupted storeroom archive, encrypted with
1741
+[`VAULT_MASTER_KEY`][].  The configuration is compressed (zip archive)
1742
+and then encoded in base64.
1743
+
1744
+The archive contains two directories `/dir/` and `/dir/subdir/`, where
1745
+`/dir/subdir/` is a correctly serialized directory, but `/dir/` does not
1746
+contain `/dir/subdir/` in its list of child items.  See
1747
+[`VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED4_JAVASCRIPT_SOURCE`][] for
1748
+the exact script that created this archive.
1749
+"""
1750
+
1751
+# Error messages
1752
+# ==============
1753
+
1754
+CANNOT_LOAD_CRYPTOGRAPHY = (
1755
+    "Cannot load the required Python module 'cryptography'."
1756
+)
1757
+"""
1758
+The expected `derivepassphrase` error message when the `cryptography`
1759
+module cannot be loaded, which is needed e.g. by the `export vault`
1760
+subcommands.
1761
+"""
... ...
@@ -0,0 +1,272 @@
1
+# SPDX-FileCopyrightText: 2025 Marco Ricci <software@the13thletter.info>
2
+#
3
+# SPDX-License-Identifier: Zlib
4
+
5
+from __future__ import annotations
6
+
7
+import os
8
+import shlex
9
+import stat
10
+from typing import TYPE_CHECKING
11
+
12
+import tests.data
13
+from derivepassphrase import _types, ssh_agent, vault
14
+
15
+__all__ = ()
16
+
17
+if TYPE_CHECKING:
18
+    import socket
19
+    from collections.abc import Iterator
20
+
21
+    from typing_extensions import Any
22
+
23
+
24
+# Stubbed actions
25
+# ===============
26
+
27
+
28
+# SSH agent client
29
+# ----------------
30
+
31
+
32
+def list_keys(self: Any = None) -> list[_types.SSHKeyCommentPair]:
33
+    """Return a list of all SSH test keys, as key/comment pairs.
34
+
35
+    Intended as a monkeypatching replacement for
36
+    [`ssh_agent.SSHAgentClient.list_keys`][].
37
+
38
+    """
39
+    del self  # Unused.
40
+    Pair = _types.SSHKeyCommentPair  # noqa: N806
41
+    return [
42
+        Pair(value.public_key_data, f"{key} test key".encode("ASCII"))
43
+        for key, value in tests.data.ALL_KEYS.items()
44
+    ]
45
+
46
+
47
+def sign(
48
+    self: Any, key: bytes | bytearray, message: bytes | bytearray
49
+) -> bytes:
50
+    """Return the signature of `message` under `key`.
51
+
52
+    Can only handle keys in [`SUPPORTED_KEYS`][], and only the vault
53
+    UUID as the message.
54
+
55
+    Intended as a monkeypatching replacement for
56
+    [`ssh_agent.SSHAgentClient.sign`][].
57
+
58
+    """
59
+    del self  # Unused.
60
+    assert message == vault.Vault.UUID
61
+    for value in tests.data.SUPPORTED_KEYS.values():
62
+        if value.public_key_data == key:  # pragma: no branch
63
+            return value.expected_signatures[
64
+                tests.data.SSHTestKeyDeterministicSignatureClass.SPEC
65
+            ].signature
66
+    raise AssertionError
67
+
68
+
69
+def list_keys_singleton(self: Any = None) -> list[_types.SSHKeyCommentPair]:
70
+    """Return a singleton list of the first supported SSH test key.
71
+
72
+    The key is returned as a key/comment pair.
73
+
74
+    Intended as a monkeypatching replacement for
75
+    [`ssh_agent.SSHAgentClient.list_keys`][].
76
+
77
+    """
78
+    del self  # Unused.
79
+    Pair = _types.SSHKeyCommentPair  # noqa: N806
80
+    list1 = [
81
+        Pair(value.public_key_data, f"{key} test key".encode("ASCII"))
82
+        for key, value in tests.data.SUPPORTED_KEYS.items()
83
+    ]
84
+    return list1[:1]
85
+
86
+
87
+# CLI machinery
88
+# -------------
89
+
90
+
91
+def suitable_ssh_keys(conn: Any) -> Iterator[_types.SSHKeyCommentPair]:
92
+    """Return a two-item list of SSH test keys (key/comment pairs).
93
+
94
+    Intended as a monkeypatching replacement for
95
+    `cli_machinery.get_suitable_ssh_keys` to better script and test the
96
+    interactive key selection.  When used this way, `derivepassphrase`
97
+    believes that only those two keys are loaded and suitable.
98
+
99
+    """
100
+    del conn  # Unused.
101
+    Pair = _types.SSHKeyCommentPair  # noqa: N806
102
+    yield from [
103
+        Pair(tests.data.DUMMY_KEY1, b"no comment"),
104
+        Pair(tests.data.DUMMY_KEY2, b"a comment"),
105
+    ]
106
+
107
+
108
+def auto_prompt(*args: Any, **kwargs: Any) -> str:
109
+    """Return [`DUMMY_PASSPHRASE`][].
110
+
111
+    Intended as a monkeypatching replacement for
112
+    `cli.prompt_for_passphrase` to better script and test the
113
+    interactive passphrase queries.
114
+
115
+    """
116
+    del args, kwargs  # Unused.
117
+    return tests.data.DUMMY_PASSPHRASE
118
+
119
+
120
+# `vault` module
121
+# --------------
122
+
123
+
124
+def phrase_from_key(
125
+    key: bytes,
126
+    /,
127
+    *,
128
+    conn: ssh_agent.SSHAgentClient | socket.socket | None = None,
129
+) -> bytes:
130
+    """Return the "equivalent master passphrase" for key.
131
+
132
+    Only works for key [`DUMMY_KEY1`][].
133
+
134
+    Intended as a monkeypatching replacement for
135
+    [`vault.Vault.phrase_from_key`][], bypassing communication with an
136
+    actual SSH agent.
137
+
138
+    """
139
+    del conn
140
+    if key == tests.data.DUMMY_KEY1:  # pragma: no branch
141
+        return tests.data.DUMMY_PHRASE_FROM_KEY1
142
+    raise KeyError(key)  # pragma: no cover
143
+
144
+
145
+# SSH agent socket provider data (with callables)
146
+# ===============================================
147
+
148
+
149
+def provider_entry_provider() -> _types.SSHAgentSocket:  # pragma: no cover
150
+    """A pseudo provider for a [`_types.SSHAgentSocketProviderEntry`][]."""
151
+    msg = "We are not supposed to be called!"
152
+    raise AssertionError(msg)
153
+
154
+
155
+provider_entry1 = _types.SSHAgentSocketProviderEntry(
156
+    provider_entry_provider, "entry1", ("entry1a", "entry1b", "entry1c")
157
+)
158
+"""A sample [`_types.SSHAgentSocketProviderEntry`][]."""
159
+
160
+provider_entry2 = _types.SSHAgentSocketProviderEntry(
161
+    provider_entry_provider, "entry2", ("entry2d", "entry2e")
162
+)
163
+
164
+
165
+# SSH agent output parsing
166
+# ========================
167
+
168
+
169
+def parse_sh_export_line(line: str, *, env_name: str) -> str:
170
+    """Parse the output of typical SSH agents' SSH_AUTH_SOCK lines.
171
+
172
+    Intentionally parses only a small subset of sh(1) syntax which works
173
+    with current OpenSSH and PuTTY output.  We require exactly one
174
+    variable setting, and one export instruction, both on the same line,
175
+    and perhaps combined into one statement.  Terminating semicolons
176
+    after each command are ignored.
177
+
178
+    Args:
179
+        line:
180
+            A line of sh(1) script to parse.
181
+        env_name:
182
+            The name of the environment variable to expect.
183
+
184
+    Returns:
185
+        The parsed environment variable value.
186
+
187
+    Raises:
188
+        ValueError:
189
+            Cannot parse the sh script.  Perhaps it is too complex,
190
+            perhaps it is malformed.
191
+
192
+    """
193
+    line = line.rstrip("\r\n")
194
+    shlex_parser = shlex.shlex(
195
+        instream=line, posix=True, punctuation_chars=True
196
+    )
197
+    shlex_parser.whitespace = " \t"
198
+    tokens = list(shlex_parser)
199
+    orig_tokens = tokens.copy()
200
+    if tokens[-1] == ";":
201
+        tokens.pop()
202
+    if tokens[-3:] == [";", "export", env_name]:
203
+        tokens[-3:] = []
204
+        tokens[:0] = ["export"]
205
+    if not (
206
+        len(tokens) == 2
207
+        and tokens[0] == "export"
208
+        and tokens[1].startswith(f"{env_name}=")
209
+    ):
210
+        msg = f"Cannot parse sh line: {orig_tokens!r} -> {tokens!r}"
211
+        raise ValueError(msg)
212
+    return tokens[1].split("=", 1)[1]
213
+
214
+
215
+# General file system actions
216
+# ===========================
217
+
218
+
219
+def make_file_readonly(
220
+    pathname: str | bytes | os.PathLike[str],
221
+    /,
222
+    *,
223
+    try_race_free_implementation: bool = True,
224
+) -> None:
225
+    """Mark a file as read-only.
226
+
227
+    On POSIX, this entails removing the write permission bits for user,
228
+    group and other, and ensuring the read permission bit for user is
229
+    set.
230
+
231
+    Unfortunately, The Annoying OS (a.k.a. Microsoft Windows) has its
232
+    own rules: Set exactly(?) the read permission bit for user to make
233
+    the file read-only, and set exactly(?) the write permission bit for
234
+    user to make the file read/write; all other permission bit settings
235
+    are ignored.
236
+
237
+    The cross-platform procedure therefore is:
238
+
239
+    1. Call `os.stat` on the file, noting the permission bits.
240
+    2. Calculate the new permission bits POSIX-style.
241
+    3. Call `os.chmod` with permission bit `stat.S_IREAD`.
242
+    4. Call `os.chmod` with the correct POSIX-style permissions.
243
+
244
+    If the platform supports it, we use a file descriptor instead of
245
+    a path name.  Otherwise, we use the same path name multiple times,
246
+    and are susceptible to race conditions.
247
+
248
+    """
249
+    fname: int | str | bytes | os.PathLike
250
+    if try_race_free_implementation and {os.stat, os.chmod} <= os.supports_fd:
251
+        # The Annoying OS (v11 at least) supports fstat and fchmod, but
252
+        # does not support changing the file mode on file descriptors
253
+        # for read-only files.
254
+        fname = os.open(
255
+            pathname,
256
+            os.O_RDWR
257
+            | getattr(os, "O_CLOEXEC", 0)
258
+            | getattr(os, "O_NOCTTY", 0),
259
+        )
260
+    else:
261
+        fname = pathname
262
+    try:
263
+        orig_mode = os.stat(fname).st_mode  # noqa: PTH116
264
+        new_mode = (
265
+            orig_mode & ~stat.S_IWUSR & ~stat.S_IWGRP & ~stat.S_IWOTH
266
+            | stat.S_IREAD
267
+        )
268
+        os.chmod(fname, stat.S_IREAD)  # noqa: PTH101
269
+        os.chmod(fname, new_mode)  # noqa: PTH101
270
+    finally:
271
+        if isinstance(fname, int):
272
+            os.close(fname)
... ...
@@ -0,0 +1,644 @@
1
+# SPDX-FileCopyrightText: 2025 Marco Ricci <software@the13thletter.info>
2
+#
3
+# SPDX-License-Identifier: Zlib
4
+
5
+from __future__ import annotations
6
+
7
+import contextlib
8
+import errno
9
+import logging
10
+import os
11
+import re
12
+import sys
13
+from typing import TYPE_CHECKING, TypedDict
14
+
15
+import click.testing
16
+from typing_extensions import NamedTuple
17
+
18
+import tests.data
19
+from derivepassphrase import _types, cli, ssh_agent, vault
20
+from derivepassphrase.ssh_agent import socketprovider
21
+
22
+__all__ = ()
23
+
24
+if TYPE_CHECKING:
25
+    from collections.abc import Callable, Iterator, Mapping, Sequence
26
+    from contextlib import AbstractContextManager
27
+    from typing import IO, NotRequired
28
+
29
+    from typing_extensions import Any, Buffer, Self
30
+
31
+
32
+# Test suite settings
33
+# ===================
34
+
35
+MIN_CONCURRENCY = 4
36
+"""
37
+The minimum amount of concurrent threads used for testing.
38
+"""
39
+
40
+
41
+def get_concurrency_limit() -> int:
42
+    """Return the imposed limit on the number of concurrent threads.
43
+
44
+    We use [`os.process_cpu_count`][] as the limit on Python 3.13 and
45
+    higher, and [`os.cpu_count`][] on Python 3.12 and below.  On
46
+    Python 3.12 and below, we explicitly support the `PYTHON_CPU_COUNT`
47
+    environment variable.  We guarantee at least [`MIN_CONCURRENCY`][]
48
+    many threads in any case.
49
+
50
+    """  # noqa: RUF002
51
+    result: int | None = None
52
+    if sys.version_info >= (3, 13):
53
+        result = os.process_cpu_count()
54
+    else:
55
+        with contextlib.suppress(KeyError, ValueError):
56
+            result = result or int(os.environ["PYTHON_CPU_COUNT"], 10)
57
+        with contextlib.suppress(AttributeError):
58
+            result = result or len(os.sched_getaffinity(os.getpid()))
59
+    return max(result if result is not None else 0, MIN_CONCURRENCY)
60
+
61
+
62
+# Log/Error message searching
63
+# ===========================
64
+
65
+
66
+def message_emitted_factory(
67
+    level: int,
68
+    *,
69
+    logger_name: str = cli.PROG_NAME,
70
+) -> Callable[[str | re.Pattern[str], Sequence[tuple[str, int, str]]], bool]:
71
+    """Return a function to test if a matching message was emitted.
72
+
73
+    Args:
74
+        level: The level to match messages at.
75
+        logger_name: The name of the logger to match against.
76
+
77
+    """
78
+
79
+    def message_emitted(
80
+        text: str | re.Pattern[str],
81
+        record_tuples: Sequence[tuple[str, int, str]],
82
+    ) -> bool:
83
+        """Return true if a matching message was emitted.
84
+
85
+        Args:
86
+            text: Substring or pattern to match against.
87
+            record_tuples: Items to match.
88
+
89
+        """
90
+
91
+        def check_record(record: tuple[str, int, str]) -> bool:
92
+            if record[:2] != (logger_name, level):
93
+                return False
94
+            if isinstance(text, str):
95
+                return text in record[2]
96
+            return text.match(record[2]) is not None  # pragma: no cover
97
+
98
+        return any(map(check_record, record_tuples))
99
+
100
+    return message_emitted
101
+
102
+
103
+# No need to assert debug messages as of yet.
104
+info_emitted = message_emitted_factory(logging.INFO)
105
+warning_emitted = message_emitted_factory(logging.WARNING)
106
+deprecation_warning_emitted = message_emitted_factory(
107
+    logging.WARNING, logger_name=f"{cli.PROG_NAME}.deprecation"
108
+)
109
+deprecation_info_emitted = message_emitted_factory(
110
+    logging.INFO, logger_name=f"{cli.PROG_NAME}.deprecation"
111
+)
112
+error_emitted = message_emitted_factory(logging.ERROR)
113
+
114
+
115
+# click.testing.CliRunner handling
116
+# ================================
117
+
118
+
119
+class ReadableResult(NamedTuple):
120
+    """Helper class for formatting and testing click.testing.Result objects."""
121
+
122
+    exception: BaseException | None
123
+    exit_code: int
124
+    stdout: str
125
+    stderr: str
126
+
127
+    def clean_exit(
128
+        self, *, output: str = "", empty_stderr: bool = False
129
+    ) -> bool:
130
+        """Return whether the invocation exited cleanly.
131
+
132
+        Args:
133
+            output:
134
+                An expected output string.
135
+
136
+        """
137
+        return (
138
+            (
139
+                not self.exception
140
+                or (
141
+                    isinstance(self.exception, SystemExit)
142
+                    and self.exit_code == 0
143
+                )
144
+            )
145
+            and (not output or output in self.stdout)
146
+            and (not empty_stderr or not self.stderr)
147
+        )
148
+
149
+    def error_exit(
150
+        self,
151
+        *,
152
+        error: str | re.Pattern[str] | type[BaseException] = BaseException,
153
+        record_tuples: Sequence[tuple[str, int, str]] = (),
154
+    ) -> bool:
155
+        """Return whether the invocation exited uncleanly.
156
+
157
+        Args:
158
+            error:
159
+                An expected error message, or an expected numeric error
160
+                code, or an expected exception type.
161
+
162
+        """
163
+
164
+        def error_match(error: str | re.Pattern[str], line: str) -> bool:
165
+            return (
166
+                error in line
167
+                if isinstance(error, str)
168
+                else error.match(line) is not None
169
+            )
170
+
171
+        # TODO(the-13th-letter): Rewrite using structural pattern matching.
172
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
173
+        if isinstance(error, type):
174
+            return isinstance(self.exception, error)
175
+        else:  # noqa: RET505
176
+            assert isinstance(error, (str, re.Pattern))
177
+            return (
178
+                isinstance(self.exception, SystemExit)
179
+                and self.exit_code > 0
180
+                and (
181
+                    not error
182
+                    or any(
183
+                        error_match(error, line)
184
+                        for line in self.stderr.splitlines(True)
185
+                    )
186
+                    or tests.machinery.error_emitted(error, record_tuples)
187
+                )
188
+            )
189
+
190
+
191
+class CliRunner:
192
+    """An abstracted CLI runner class.
193
+
194
+    Intended to provide similar functionality and scope as the
195
+    [`click.testing.CliRunner`][] class, though not necessarily
196
+    `click`-specific.  Also allows for seamless migration away from
197
+    `click`, if/when we decide this.
198
+
199
+    """
200
+
201
+    _SUPPORTS_MIX_STDERR_ATTRIBUTE = not hasattr(click.testing, "StreamMixer")
202
+    """
203
+    True if and only if [`click.testing.CliRunner`][] supports the
204
+    `mix_stderr` attribute.  It was removed in 8.2.0 in favor of the
205
+    `click.testing.StreamMixer` class.
206
+
207
+    See also
208
+    [`pallets/click#2523`](https://github.com/pallets/click/pull/2523).
209
+    """
210
+
211
+    def __init__(
212
+        self,
213
+        *,
214
+        mix_stderr: bool = False,
215
+        color: bool | None = None,
216
+    ) -> None:
217
+        self.color = color
218
+        self.mix_stderr = mix_stderr
219
+
220
+        class MixStderrAttribute(TypedDict):
221
+            mix_stderr: NotRequired[bool]
222
+
223
+        mix_stderr_args: MixStderrAttribute = (
224
+            {"mix_stderr": mix_stderr}
225
+            if self._SUPPORTS_MIX_STDERR_ATTRIBUTE
226
+            else {}
227
+        )
228
+        self.click_testing_clirunner = click.testing.CliRunner(
229
+            **mix_stderr_args
230
+        )
231
+
232
+    def invoke(
233
+        self,
234
+        cli: click.BaseCommand,
235
+        args: Sequence[str] | str | None = None,
236
+        input: str | bytes | IO[Any] | None = None,
237
+        env: Mapping[str, str | None] | None = None,
238
+        catch_exceptions: bool = True,
239
+        color: bool | None = None,
240
+        **extra: Any,
241
+    ) -> ReadableResult:
242
+        if color is None:  # pragma: no cover
243
+            color = self.color if self.color is not None else False
244
+        raw_result = self.click_testing_clirunner.invoke(
245
+            cli,
246
+            args=args,
247
+            input=input,
248
+            env=env,
249
+            catch_exceptions=catch_exceptions,
250
+            color=color,
251
+            **extra,
252
+        )
253
+        # In 8.2.0, r.stdout is no longer a property aliasing the
254
+        # `output` attribute, but rather the raw stdout value.
255
+        try:
256
+            stderr = raw_result.stderr
257
+        except ValueError:
258
+            stderr = raw_result.stdout
259
+        return ReadableResult(
260
+            raw_result.exception,
261
+            raw_result.exit_code,
262
+            (raw_result.stdout if not self.mix_stderr else raw_result.output)
263
+            or "",
264
+            stderr or "",
265
+        )
266
+        return ReadableResult.parse(raw_result)
267
+
268
+    def isolated_filesystem(
269
+        self,
270
+        temp_dir: str | os.PathLike[str] | None = None,
271
+    ) -> AbstractContextManager[str]:
272
+        return self.click_testing_clirunner.isolated_filesystem(
273
+            temp_dir=temp_dir
274
+        )
275
+
276
+
277
+# Stubbed SSH agent socket
278
+# ========================
279
+
280
+# Base variant
281
+# ------------
282
+
283
+
284
+@socketprovider.SocketProvider.register("stub_agent")
285
+class StubbedSSHAgentSocket:
286
+    """A stubbed SSH agent presenting an [`_types.SSHAgentSocket`][]."""
287
+
288
+    _SOCKET_IS_CLOSED = "Socket is closed."
289
+    _NO_FLAG_SUPPORT = "This stubbed SSH agent socket does not support flags."
290
+    _PROTOCOL_VIOLATION = "SSH agent protocol violation."
291
+    _INVALID_REQUEST = "Invalid request."
292
+    _UNSUPPORTED_REQUEST = "Unsupported request."
293
+
294
+    HEADER_SIZE = 4
295
+    CODE_SIZE = 1
296
+
297
+    KNOWN_EXTENSIONS = frozenset({
298
+        "query",
299
+        "list-extended@putty.projects.tartarus.org",
300
+    })
301
+    """Known and implemented protocol extensions."""
302
+
303
+    def __init__(self, *extensions: str) -> None:
304
+        """Initialize the agent."""
305
+        self.send_to_client = bytearray()
306
+        """
307
+        The buffered response to the client, read piecemeal by [`recv`][].
308
+        """
309
+        self.receive_from_client = bytearray()
310
+        """The last request issued by the client."""
311
+        self.closed = False
312
+        """True if the connection is closed, false otherwise."""
313
+        self.enabled_extensions = frozenset(extensions) & self.KNOWN_EXTENSIONS
314
+        """
315
+        Extensions actually enabled in this particular stubbed SSH agent.
316
+        """
317
+        self.try_rfc6979 = False
318
+        """
319
+        Attempt to issue DSA and ECDSA signatures according to RFC 6979?
320
+        """
321
+        self.try_pageant_068_080 = False
322
+        """
323
+        Attempt to issue DSA and ECDSA signatures as per Pageant 0.68–0.80?
324
+        """  # noqa: RUF001
325
+
326
+    def __enter__(self) -> Self:
327
+        """Return self."""
328
+        return self
329
+
330
+    def __exit__(self, *args: object) -> None:
331
+        """Mark the agent's socket as closed."""
332
+        self.closed = True
333
+
334
+    def sendall(self, data: Buffer, flags: int = 0, /) -> None:
335
+        """Send data to the SSH agent.
336
+
337
+        The signature, and behavior, is identical to
338
+        [`socket.socket.sendall`][].  Upon successful sending, this
339
+        agent will parse the request, call the appropriate handler, and
340
+        buffer the result such that it can be read via [`recv`][], in
341
+        accordance with the SSH agent protocol.
342
+
343
+        Args:
344
+            data: Binary data to send to the agent.
345
+            flags: Reserved.  Must be 0.
346
+
347
+        Returns:
348
+            Nothing.  The result should be requested via [`recv`][], and
349
+            interpreted in accordance with the SSH agent protocol.
350
+
351
+        Raises:
352
+            AssertionError:
353
+                The flags argument, if specified, must be 0.
354
+            ValueError:
355
+                The agent's socket is already closed.  No further
356
+                requests can be sent.
357
+
358
+        """
359
+        assert not flags, self._NO_FLAG_SUPPORT
360
+        if self.closed:
361
+            raise ValueError(self._SOCKET_IS_CLOSED)
362
+        self.receive_from_client.extend(memoryview(data))
363
+        try:
364
+            self.parse_client_request_and_dispatch()
365
+        except ValueError:
366
+            payload = int.to_bytes(_types.SSH_AGENT.FAILURE.value, 1, "big")
367
+            self.send_to_client.extend(int.to_bytes(len(payload), 4, "big"))
368
+            self.send_to_client.extend(payload)
369
+        finally:
370
+            self.receive_from_client.clear()
371
+
372
+    def recv(self, count: int, flags: int = 0, /) -> bytes:
373
+        """Read data from the SSH agent.
374
+
375
+        As per the SSH agent protocol, data is only available to be read
376
+        immediately after a request via [`sendall`][].  Calls to
377
+        [`recv`][] at other points in time that attempt to read data
378
+        violate the protocol, and will fail.  Notwithstanding the last
379
+        sentence, at any point in time, though pointless, it is
380
+        additionally permissible to read 0 bytes from the agent, or any
381
+        number of bytes from a closed socket.
382
+
383
+        Args:
384
+            count:
385
+                Number of bytes to read from the agent.
386
+            flags:
387
+                Reserved.  Must be 0.
388
+
389
+        Returns:
390
+            (A chunk of) the SSH agent's response to the most recent
391
+            request.  If reading 0 bytes, or if reading from a closed
392
+            socket, the returned chunk is always an empty byte string.
393
+
394
+        Raises:
395
+            AssertionError:
396
+                The flags argument, if specified, must be 0.
397
+
398
+                Alternatively, `recv` was called when there was no
399
+                response to be obtained, in violation of the SSH agent
400
+                protocol.
401
+
402
+        """
403
+        assert not flags, self._NO_FLAG_SUPPORT
404
+        assert not count or self.closed or self.send_to_client, (
405
+            self._PROTOCOL_VIOLATION
406
+        )
407
+        ret = bytes(self.send_to_client[:count])
408
+        del self.send_to_client[:count]
409
+        return ret
410
+
411
+    def parse_client_request_and_dispatch(self) -> None:
412
+        """Parse the client request and call the matching handler.
413
+
414
+        This agent supports the
415
+        [`SSH_AGENTC_REQUEST_IDENTITIES`][_types.SSH_AGENTC.REQUEST_IDENTITIES],
416
+        [`SSH_AGENTC_SIGN_REQUEST`][_types.SSH_AGENTC.SIGN_REQUEST] and
417
+        the [`SSH_AGENTC_EXTENSION`][_types.SSH_AGENTC.EXTENSION]
418
+        request types.
419
+
420
+        """
421
+
422
+        if len(self.receive_from_client) < self.HEADER_SIZE + self.CODE_SIZE:
423
+            raise ValueError(self._INVALID_REQUEST)
424
+        target_header = ssh_agent.SSHAgentClient.uint32(
425
+            len(self.receive_from_client) - self.HEADER_SIZE
426
+        )
427
+        if target_header != self.receive_from_client[: self.HEADER_SIZE]:
428
+            raise ValueError(self._INVALID_REQUEST)
429
+        code = _types.SSH_AGENTC(
430
+            int.from_bytes(
431
+                self.receive_from_client[
432
+                    self.HEADER_SIZE : self.HEADER_SIZE + self.CODE_SIZE
433
+                ],
434
+                "big",
435
+            )
436
+        )
437
+
438
+        def is_enabled_extension(extension: str) -> bool:
439
+            if (
440
+                extension not in self.enabled_extensions
441
+                or code != _types.SSH_AGENTC.EXTENSION
442
+            ):
443
+                return False
444
+            string = ssh_agent.SSHAgentClient.string
445
+            extension_marker = b"\x1b" + string(extension.encode("ascii"))
446
+            return self.receive_from_client.startswith(extension_marker, 4)
447
+
448
+        result: Buffer | Iterator[int]
449
+        if code == _types.SSH_AGENTC.REQUEST_IDENTITIES:
450
+            result = self.request_identities(list_extended=False)
451
+        elif code == _types.SSH_AGENTC.SIGN_REQUEST:
452
+            result = self.sign()
453
+        elif is_enabled_extension("query"):
454
+            result = self.query_extensions()
455
+        elif is_enabled_extension("list-extended@putty.projects.tartarus.org"):
456
+            result = self.request_identities(list_extended=True)
457
+        else:
458
+            raise ValueError(self._UNSUPPORTED_REQUEST)
459
+        self.send_to_client.extend(
460
+            ssh_agent.SSHAgentClient.string(bytes(result))
461
+        )
462
+
463
+    def query_extensions(self) -> Iterator[int]:
464
+        """Answer an `SSH_AGENTC_EXTENSION` request.
465
+
466
+        Yields:
467
+            The bytes payload of the response, without the protocol
468
+            framing.  The payload is yielded byte by byte, as an
469
+            iterable of 8-bit integers.
470
+
471
+        """
472
+        yield _types.SSH_AGENT.EXTENSION_RESPONSE.value
473
+        yield from ssh_agent.SSHAgentClient.string(b"query")
474
+        extension_answers = [
475
+            b"query",
476
+            b"list-extended@putty.projects.tartarus.org",
477
+        ]
478
+        for a in extension_answers:
479
+            yield from ssh_agent.SSHAgentClient.string(a)
480
+
481
+    def request_identities(
482
+        self, *, list_extended: bool = False
483
+    ) -> Iterator[int]:
484
+        """Answer an `SSH_AGENTC_REQUEST_IDENTITIES` request.
485
+
486
+        Args:
487
+            list_extended:
488
+                If true, answer an `SSH_AGENTC_EXTENSION` request for
489
+                the `list-extended@putty.projects.tartarus.org`
490
+                extension. Otherwise, answer an
491
+                `SSH_AGENTC_REQUEST_IDENTITIES` request.
492
+
493
+        Yields:
494
+            The bytes payload of the response, without the protocol
495
+            framing.  The payload is yielded byte by byte, as an
496
+            iterable of 8-bit integers.
497
+
498
+        """
499
+        if list_extended:
500
+            yield _types.SSH_AGENT.SUCCESS.value
501
+        else:
502
+            yield _types.SSH_AGENT.IDENTITIES_ANSWER.value
503
+        signature_classes = [
504
+            tests.data.SSHTestKeyDeterministicSignatureClass.SPEC,
505
+        ]
506
+        if (
507
+            "list-extended@putty.projects.tartarus.org"
508
+            in self.enabled_extensions
509
+        ):
510
+            signature_classes.append(
511
+                tests.data.SSHTestKeyDeterministicSignatureClass.RFC_6979
512
+            )
513
+        keys = [
514
+            v
515
+            for v in tests.data.ALL_KEYS.values()
516
+            if any(cls in v.expected_signatures for cls in signature_classes)
517
+        ]
518
+        yield from ssh_agent.SSHAgentClient.uint32(len(keys))
519
+        for key in keys:
520
+            yield from ssh_agent.SSHAgentClient.string(key.public_key_data)
521
+            yield from ssh_agent.SSHAgentClient.string(
522
+                b"test key without passphrase"
523
+            )
524
+            if list_extended:
525
+                yield from ssh_agent.SSHAgentClient.string(
526
+                    ssh_agent.SSHAgentClient.uint32(0)
527
+                )
528
+
529
+    def sign(self) -> bytes:
530
+        """Answer an `SSH_AGENTC_SIGN_REQUEST` request.
531
+
532
+        Returns:
533
+            The bytes payload of the response, without the protocol
534
+            framing.
535
+
536
+        """
537
+        try_rfc6979 = (
538
+            "list-extended@putty.projects.tartarus.org"
539
+            in self.enabled_extensions
540
+        )
541
+        spec = tests.data.SSHTestKeyDeterministicSignatureClass.SPEC
542
+        rfc6979 = tests.data.SSHTestKeyDeterministicSignatureClass.RFC_6979
543
+        key_blob, rest = ssh_agent.SSHAgentClient.unstring_prefix(
544
+            self.receive_from_client[self.HEADER_SIZE + self.CODE_SIZE :]
545
+        )
546
+        sign_data, rest = ssh_agent.SSHAgentClient.unstring_prefix(rest)
547
+        if len(rest) != 4:
548
+            raise ValueError(self._INVALID_REQUEST)
549
+        flags = int.from_bytes(rest, "big")
550
+        if flags:
551
+            raise ValueError(self._UNSUPPORTED_REQUEST)
552
+        if sign_data != vault.Vault.UUID:
553
+            raise ValueError(self._UNSUPPORTED_REQUEST)
554
+        for key in tests.data.ALL_KEYS.values():
555
+            if key.public_key_data == key_blob:
556
+                if spec in key.expected_signatures:
557
+                    return int.to_bytes(
558
+                        _types.SSH_AGENT.SIGN_RESPONSE.value, 1, "big"
559
+                    ) + ssh_agent.SSHAgentClient.string(
560
+                        key.expected_signatures[spec].signature
561
+                    )
562
+                if try_rfc6979 and rfc6979 in key.expected_signatures:
563
+                    return int.to_bytes(
564
+                        _types.SSH_AGENT.SIGN_RESPONSE.value, 1, "big"
565
+                    ) + ssh_agent.SSHAgentClient.string(
566
+                        key.expected_signatures[rfc6979].signature
567
+                    )
568
+                raise ValueError(self._UNSUPPORTED_REQUEST)
569
+        raise ValueError(self._UNSUPPORTED_REQUEST)
570
+
571
+
572
+# Standard variant
573
+# ----------------
574
+
575
+
576
+@socketprovider.SocketProvider.register("stub_with_address")
577
+class StubbedSSHAgentSocketWithAddress(StubbedSSHAgentSocket):
578
+    """A [`StubbedSSHAgentSocket`][] requiring a specific address."""
579
+
580
+    ADDRESS = "stub-ssh-agent:"
581
+    """The correct address for connecting to this stubbed agent."""
582
+
583
+    def __init__(self, *extensions: str) -> None:
584
+        """Initialize the agent, based on `SSH_AUTH_SOCK`.
585
+
586
+        Socket addresses of the form `stub-ssh-agent:<errno_value>` will
587
+        raise an [`OSError`][] (or the respective subclass) with the
588
+        specified [`errno`][] value.  For example,
589
+        `stub-ssh-agent:EPERM` will raise a [`PermissionError`][].
590
+
591
+        Raises:
592
+            KeyError:
593
+                The `SSH_AUTH_SOCK` environment variable is not set.
594
+            OSError:
595
+                The address in `SSH_AUTH_SOCK` is unsuited.
596
+
597
+        """
598
+        super().__init__(*extensions)
599
+        try:
600
+            orig_address = os.environ["SSH_AUTH_SOCK"]
601
+        except KeyError as exc:
602
+            msg = "SSH_AUTH_SOCK environment variable"
603
+            raise KeyError(msg) from exc
604
+        address = orig_address
605
+        if not address.startswith(self.ADDRESS):
606
+            address = self.ADDRESS + "ENOENT"
607
+        errcode = address.removeprefix(self.ADDRESS)
608
+        if errcode and not (
609
+            errcode.startswith("E") and hasattr(errno, errcode)
610
+        ):
611
+            errcode = "EINVAL"
612
+        if errcode:
613
+            errno_val = getattr(errno, errcode)
614
+            raise OSError(errno_val, os.strerror(errno_val), orig_address)
615
+
616
+
617
+# Deterministic variant
618
+# ---------------------
619
+
620
+
621
+@socketprovider.SocketProvider.register(
622
+    "stub_with_address_and_deterministic_dsa"
623
+)
624
+class StubbedSSHAgentSocketWithAddressAndDeterministicDSA(
625
+    StubbedSSHAgentSocketWithAddress
626
+):
627
+    """A [`StubbedSSHAgentSocketWithAddress`][] supporting deterministic DSA."""
628
+
629
+    def __init__(self) -> None:
630
+        """Initialize the agent.
631
+
632
+        Set the supported extensions, and try issuing RFC 6979 and
633
+        Pageant 0.68–0.80 DSA/ECDSA signatures, if possible.  See the
634
+        [superclass constructor][StubbedSSHAgentSocketWithAddress] for
635
+        other details.
636
+
637
+        Raises:
638
+            KeyError: See superclass.
639
+            OSError: See superclass.
640
+
641
+        """  # noqa: RUF002
642
+        super().__init__("query", "list-extended@putty.projects.tartarus.org")
643
+        self.try_rfc6979 = True
644
+        self.try_pageant_068_080 = True
... ...
@@ -0,0 +1,168 @@
1
+# SPDX-FileCopyrightText: 2025 Marco Ricci <software@the13thletter.info>
2
+#
3
+# SPDX-License-Identifier: Zlib
4
+
5
+"""`hypothesis` testing machinery for the `derivepassphrase` test suite.
6
+
7
+This is all the `hypothesis`-specific data and functionality used in the
8
+`derivepassphrase` test suite; this includes custom `hypothesis`
9
+strategies, or state machines, or state machine helper functions, or
10
+functions interacting with the `hypothesis` settings.
11
+
12
+All similar-minded code requiring only plain `pytest` lives in [the
13
+`pytest` sibling module][tests.machinery.pytest].
14
+
15
+"""
16
+
17
+from __future__ import annotations
18
+
19
+import copy
20
+from typing import TYPE_CHECKING
21
+
22
+import hypothesis
23
+from hypothesis import strategies
24
+
25
+import tests.data
26
+import tests.machinery
27
+from derivepassphrase import _types
28
+
29
+__all__ = ()
30
+
31
+if TYPE_CHECKING:
32
+    from typing_extensions import Any
33
+
34
+
35
+# Hypothesis settings management
36
+# ==============================
37
+
38
+
39
+def get_concurrency_step_count(
40
+    settings: hypothesis.settings | None = None,
41
+) -> int:
42
+    """Return the desired step count for concurrency-related tests.
43
+
44
+    This is the smaller of the [general concurrency
45
+    limit][tests.machinery.get_concurrency_limit] and the step count
46
+    from the current hypothesis settings.
47
+
48
+    Args:
49
+        settings:
50
+            The hypothesis settings for a specific tests.  If not given,
51
+            then the current profile will be queried directly.
52
+
53
+    """
54
+    if settings is None:  # pragma: no cover
55
+        settings = hypothesis.settings()
56
+    return min(tests.machinery.get_concurrency_limit(), settings.stateful_step_count)
57
+
58
+
59
+# Hypothesis strategies
60
+# =====================
61
+
62
+
63
+@strategies.composite
64
+def vault_full_service_config(draw: strategies.DrawFn) -> dict[str, int]:
65
+    """Hypothesis strategy for full vault service configurations.
66
+
67
+    Returns a sample configuration with restrictions on length, repeat
68
+    count, and all character classes, while ensuring the settings are
69
+    not obviously unsatisfiable.
70
+
71
+    Args:
72
+        draw:
73
+            The `draw` function, as provided for by hypothesis.
74
+
75
+    """
76
+    repeat = draw(strategies.integers(min_value=0, max_value=10))
77
+    lower = draw(strategies.integers(min_value=0, max_value=10))
78
+    upper = draw(strategies.integers(min_value=0, max_value=10))
79
+    number = draw(strategies.integers(min_value=0, max_value=10))
80
+    space = draw(strategies.integers(min_value=0, max_value=repeat))
81
+    dash = draw(strategies.integers(min_value=0, max_value=10))
82
+    symbol = draw(strategies.integers(min_value=0, max_value=10))
83
+    length = draw(
84
+        strategies.integers(
85
+            min_value=max(1, lower + upper + number + space + dash + symbol),
86
+            max_value=70,
87
+        )
88
+    )
89
+    hypothesis.assume(lower + upper + number + dash + symbol > 0)
90
+    hypothesis.assume(lower + upper + number + space + symbol > 0)
91
+    hypothesis.assume(repeat >= space)
92
+    return {
93
+        "lower": lower,
94
+        "upper": upper,
95
+        "number": number,
96
+        "space": space,
97
+        "dash": dash,
98
+        "symbol": symbol,
99
+        "repeat": repeat,
100
+        "length": length,
101
+    }
102
+
103
+
104
+@strategies.composite
105
+def smudged_vault_test_config(
106
+    draw: strategies.DrawFn,
107
+    config: Any = strategies.sampled_from(tests.data.TEST_CONFIGS).filter(  # noqa: B008
108
+        tests.data.is_smudgable_vault_test_config
109
+    ),
110
+) -> Any:
111
+    """Hypothesis strategy to replace falsy values with other falsy values.
112
+
113
+    Uses [`_types.js_truthiness`][] internally, which is tested
114
+    separately by
115
+    [`tests.test_derivepassphrase_types.test_100_js_truthiness`][].
116
+
117
+    Args:
118
+        draw:
119
+            The `draw` function, as provided for by hypothesis.
120
+        config:
121
+            A strategy which generates [`VaultTestConfig`][] objects.
122
+
123
+    Returns:
124
+        A new [`VaultTestConfig`][] where some falsy values have been
125
+        replaced or added.
126
+
127
+    """
128
+
129
+    falsy = (None, False, 0, 0.0, "", float("nan"))
130
+    falsy_no_str = (None, False, 0, 0.0, float("nan"))
131
+    falsy_no_zero = (None, False, "", float("nan"))
132
+    conf = draw(config)
133
+    hypothesis.assume(tests.data.is_smudgable_vault_test_config(conf))
134
+    obj = copy.deepcopy(conf.config)
135
+    services: list[dict[str, Any]] = list(obj["services"].values())
136
+    if "global" in obj:
137
+        services.append(obj["global"])
138
+    assert all(isinstance(x, dict) for x in services), (
139
+        "is_smudgable_vault_test_config guard failed to "
140
+        "ensure each settings dict is a dict"
141
+    )
142
+    for service in services:
143
+        for key in ("phrase",):
144
+            value = service.get(key)
145
+            if not _types.js_truthiness(value) and value != "":
146
+                service[key] = draw(strategies.sampled_from(falsy_no_str))
147
+        for key in (
148
+            "notes",
149
+            "key",
150
+            "length",
151
+            "repeat",
152
+        ):
153
+            value = service.get(key)
154
+            if not _types.js_truthiness(value):
155
+                service[key] = draw(strategies.sampled_from(falsy))
156
+        for key in (
157
+            "lower",
158
+            "upper",
159
+            "number",
160
+            "space",
161
+            "dash",
162
+            "symbol",
163
+        ):
164
+            value = service.get(key)
165
+            if not _types.js_truthiness(value) and value != 0:
166
+                service[key] = draw(strategies.sampled_from(falsy_no_zero))
167
+    hypothesis.assume(obj != conf.config)
168
+    return tests.data.VaultTestConfig(obj, conf.comment, conf.validation_settings)
... ...
@@ -0,0 +1,514 @@
1
+# SPDX-FileCopyrightText: 2025 Marco Ricci <software@the13thletter.info>
2
+#
3
+# SPDX-License-Identifier: Zlib
4
+
5
+"""`pytest` testing machinery for the `derivepassphrase` test suite.
6
+
7
+This is all the `pytest`-specific data and functionality used in the
8
+`derivepassphrase` test suite; this includes `pytest` marks and
9
+monkeypatched functions (or functions relying heavily internally on
10
+monkeypatching).
11
+
12
+All code requiring *more* than plain `pytest` lives in its own sibling
13
+module, e.g., the `hypothesis`-related stuff lives in the [`hypothesis`
14
+sibling module][tests.machinery.hypothesis].
15
+
16
+"""
17
+
18
+from __future__ import annotations
19
+
20
+import base64
21
+import contextlib
22
+import importlib.util
23
+import json
24
+import os
25
+import pathlib
26
+import sys
27
+import tempfile
28
+import types
29
+import zipfile
30
+from typing import TYPE_CHECKING
31
+
32
+import pytest
33
+from typing_extensions import assert_never, overload
34
+
35
+import tests.data
36
+import tests.machinery
37
+from derivepassphrase._internals import cli_helpers, cli_machinery
38
+from derivepassphrase.ssh_agent import socketprovider
39
+
40
+__all__ = ()
41
+
42
+if TYPE_CHECKING:
43
+    from collections.abc import Callable, Iterator, Sequence
44
+    from contextlib import AbstractContextManager
45
+
46
+    from typing_extensions import Any
47
+
48
+
49
+# Marks
50
+# =====
51
+
52
+
53
+skip_if_cryptography_support = pytest.mark.skipif(
54
+    importlib.util.find_spec("cryptography") is not None,
55
+    reason='cryptography support available; cannot test "no support" scenario',
56
+)
57
+"""
58
+A cached pytest mark to skip this test if cryptography support is
59
+available.  Usually this means that the test targets
60
+`derivepassphrase`'s fallback functionality, which is not available
61
+whenever the primary functionality is.
62
+"""
63
+skip_if_no_cryptography_support = pytest.mark.skipif(
64
+    importlib.util.find_spec("cryptography") is None,
65
+    reason='no "cryptography" support',
66
+)
67
+"""
68
+A cached pytest mark to skip this test if cryptography support is not
69
+available.  Usually this means that the test targets the
70
+`derivepassphrase export vault` subcommand, whose functionality depends
71
+on cryptography support being available.
72
+"""
73
+skip_if_on_the_annoying_os = pytest.mark.skipif(
74
+    sys.platform == "win32",
75
+    reason="The Annoying OS behaves differently.",
76
+)
77
+"""
78
+A cached pytest mark to skip this test if running on The Annoying
79
+Operating System, a.k.a. Microsoft Windows.  Usually this is due to
80
+unnecessary and stupid differences in the OS internals, and these
81
+differences are deemed irreconcilable in the context of the decorated
82
+test, so the test is to be skipped.
83
+
84
+See also:
85
+    [`xfail_on_the_annoying_os`][]
86
+
87
+"""
88
+skip_if_no_multiprocessing_support = pytest.mark.skipif(
89
+    importlib.util.find_spec("multiprocessing") is None,
90
+    reason='no "multiprocessing" support',
91
+)
92
+"""
93
+A cached pytest mark to skip this test if multiprocessing support is not
94
+available.  Usually this means that the test targets the concurrency
95
+features of `derivepassphrase`, which is generally only possible to test
96
+in separate processes because the testing machinery operates on
97
+process-global state.
98
+"""
99
+
100
+
101
+def xfail_on_the_annoying_os(
102
+    f: Callable | None = None,
103
+    /,
104
+    *,
105
+    reason: str = "",
106
+) -> pytest.MarkDecorator | Any:  # pragma: no cover
107
+    """Annotate a test which fails on The Annoying OS.
108
+
109
+    Annotate a test to indicate that it fails on The Annoying Operating
110
+    System, a.k.a. Microsoft Windows.  Usually this is due to
111
+    differences in the design of OS internals, and usually, these
112
+    differences are both unnecessary and stupid.
113
+
114
+    Args:
115
+        f:
116
+            A callable to decorate.  If not given, return the pytest
117
+            mark directly.
118
+        reason:
119
+            An optional, more detailed reason stating why this test
120
+            fails on The Annoying OS.
121
+
122
+    Returns:
123
+        The callable, marked as an expected failure on the Annoying OS,
124
+        or alternatively a suitable pytest mark if no callable was
125
+        passed.  The reason will begin with the phrase "The Annoying OS
126
+        behaves differently.", and the optional detailed reason, if not
127
+        empty, will follow.
128
+
129
+    """
130
+    import hypothesis  # noqa: PLC0415
131
+
132
+    base_reason = "The Annoying OS behaves differently."
133
+    full_reason = base_reason if not reason else f"{base_reason}  {reason}"
134
+    mark = pytest.mark.xfail(
135
+        sys.platform == "win32",
136
+        reason=full_reason,
137
+        raises=(AssertionError, hypothesis.errors.FailedHealthCheck),
138
+        strict=True,
139
+    )
140
+    return mark if f is None else mark(f)
141
+
142
+
143
+# Parameter sets
144
+# ==============
145
+
146
+
147
+class Parametrize(types.SimpleNamespace):
148
+    VAULT_CONFIG_FORMATS_DATA = pytest.mark.parametrize(
149
+        ["config", "format", "config_data"],
150
+        [
151
+            pytest.param(
152
+                tests.data.VAULT_V02_CONFIG,
153
+                "v0.2",
154
+                tests.data.VAULT_V02_CONFIG_DATA,
155
+                id="0.2",
156
+            ),
157
+            pytest.param(
158
+                tests.data.VAULT_V03_CONFIG,
159
+                "v0.3",
160
+                tests.data.VAULT_V03_CONFIG_DATA,
161
+                id="0.3",
162
+            ),
163
+            pytest.param(
164
+                tests.data.VAULT_STOREROOM_CONFIG_ZIPPED,
165
+                "storeroom",
166
+                tests.data.VAULT_STOREROOM_CONFIG_DATA,
167
+                id="storeroom",
168
+            ),
169
+        ],
170
+    )
171
+
172
+
173
+# Monkeypatchings
174
+# ===============
175
+
176
+
177
+@contextlib.contextmanager
178
+def faked_entry_point_list(  # noqa: C901
179
+    additional_entry_points: Sequence[importlib.metadata.EntryPoint],
180
+    remove_conflicting_entries: bool = False,
181
+) -> Iterator[Sequence[str]]:
182
+    """Yield a context where additional entry points are visible.
183
+
184
+    Args:
185
+        additional_entry_points:
186
+            A sequence of entry point objects that should additionally
187
+            be visible.
188
+        remove_conflicting_entries:
189
+            If true, remove all names provided by the additional entry
190
+            points, otherwise leave them untouched.
191
+
192
+    Yields:
193
+        A sequence of registry names that are newly available within the
194
+        context.
195
+
196
+    """
197
+    true_entry_points = importlib.metadata.entry_points()
198
+    additional_entry_points = list(additional_entry_points)
199
+
200
+    if sys.version_info >= (3, 12):
201
+        new_entry_points = importlib.metadata.EntryPoints(
202
+            list(true_entry_points) + additional_entry_points
203
+        )
204
+
205
+        @overload
206
+        def mangled_entry_points(
207
+            *, group: None = None
208
+        ) -> importlib.metadata.EntryPoints: ...
209
+
210
+        @overload
211
+        def mangled_entry_points(
212
+            *, group: str
213
+        ) -> importlib.metadata.EntryPoints: ...
214
+
215
+        def mangled_entry_points(
216
+            **params: Any,
217
+        ) -> importlib.metadata.EntryPoints:
218
+            return new_entry_points.select(**params)
219
+
220
+    elif sys.version_info >= (3, 10):
221
+        # Compatibility concerns within importlib.metadata: depending on
222
+        # whether the .select() API is used, the result is either the dict
223
+        # of groups of points (as in < 3.10), or the EntryPoints iterable
224
+        # (as in >= 3.12).  So our wrapper needs to duplicate that
225
+        # interface.  FUN.
226
+        new_entry_points_dict = {
227
+            k: list(v) for k, v in true_entry_points.items()
228
+        }
229
+        for ep in additional_entry_points:
230
+            new_entry_points_dict.setdefault(ep.group, []).append(ep)
231
+        new_entry_points = importlib.metadata.EntryPoints([
232
+            ep for group in new_entry_points_dict.values() for ep in group
233
+        ])
234
+
235
+        @overload
236
+        def mangled_entry_points(
237
+            *, group: None = None
238
+        ) -> dict[
239
+            str,
240
+            list[importlib.metadata.EntryPoint]
241
+            | tuple[importlib.metadata.EntryPoint, ...],
242
+        ]: ...
243
+
244
+        @overload
245
+        def mangled_entry_points(
246
+            *, group: str
247
+        ) -> importlib.metadata.EntryPoints: ...
248
+
249
+        def mangled_entry_points(
250
+            **params: Any,
251
+        ) -> (
252
+            importlib.metadata.EntryPoints
253
+            | dict[
254
+                str,
255
+                list[importlib.metadata.EntryPoint]
256
+                | tuple[importlib.metadata.EntryPoint, ...],
257
+            ]
258
+        ):
259
+            return (
260
+                new_entry_points.select(**params)
261
+                if params
262
+                else new_entry_points_dict
263
+            )
264
+
265
+    else:
266
+        new_entry_points: dict[
267
+            str,
268
+            list[importlib.metadata.EntryPoint]
269
+            | tuple[importlib.metadata.EntryPoint, ...],
270
+        ] = {
271
+            group_name: list(group)
272
+            for group_name, group in true_entry_points.items()
273
+        }
274
+        for ep in additional_entry_points:
275
+            new_entry_points.setdefault(ep.group, [])
276
+            new_entry_points[ep.group].append(ep)
277
+        new_entry_points = {
278
+            group_name: tuple(group)
279
+            for group_name, group in new_entry_points.items()
280
+        }
281
+
282
+        @overload
283
+        def mangled_entry_points(
284
+            *, group: None = None
285
+        ) -> dict[str, tuple[importlib.metadata.EntryPoint, ...]]: ...
286
+
287
+        @overload
288
+        def mangled_entry_points(
289
+            *, group: str
290
+        ) -> tuple[importlib.metadata.EntryPoint, ...]: ...
291
+
292
+        def mangled_entry_points(
293
+            *, group: str | None = None
294
+        ) -> (
295
+            dict[str, tuple[importlib.metadata.EntryPoint, ...]]
296
+            | tuple[importlib.metadata.EntryPoint, ...]
297
+        ):
298
+            return (
299
+                new_entry_points.get(group, ())
300
+                if group is not None
301
+                else new_entry_points
302
+            )
303
+
304
+    registry = socketprovider.SocketProvider.registry
305
+    new_registry = registry.copy()
306
+    keys = [ep.load().key for ep in additional_entry_points]
307
+    aliases = [a for ep in additional_entry_points for a in ep.load().aliases]
308
+    if remove_conflicting_entries:  # pragma: no cover [unused]
309
+        for name in [*keys, *aliases]:
310
+            new_registry.pop(name, None)
311
+
312
+    with pytest.MonkeyPatch.context() as monkeypatch:
313
+        monkeypatch.setattr(
314
+            socketprovider.SocketProvider, "registry", new_registry
315
+        )
316
+        monkeypatch.setattr(
317
+            importlib.metadata, "entry_points", mangled_entry_points
318
+        )
319
+        yield (*keys, *aliases)
320
+
321
+
322
+@contextlib.contextmanager
323
+def isolated_config(
324
+    monkeypatch: pytest.MonkeyPatch,
325
+    runner: tests.machinery.CliRunner,
326
+    main_config_str: str | None = None,
327
+) -> Iterator[None]:
328
+    """Provide an isolated configuration setup, as a context.
329
+
330
+    This context manager sets up (and changes into) a temporary
331
+    directory, which holds the user configuration specified in
332
+    `main_config_str`, if any.  The manager also ensures that the
333
+    environment variables `HOME` and `USERPROFILE` are set, and that
334
+    `DERIVEPASSPHRASE_PATH` is unset.  Upon exiting the context, the
335
+    changes are undone and the temporary directory is removed.
336
+
337
+    Args:
338
+        monkeypatch:
339
+            A monkeypatch fixture object.
340
+        runner:
341
+            A `click` CLI runner harness.
342
+        main_config_str:
343
+            Optional TOML file contents, to be used as the user
344
+            configuration.
345
+
346
+    Returns:
347
+        A context manager, without a return value.
348
+
349
+    """
350
+    prog_name = cli_helpers.PROG_NAME
351
+    env_name = prog_name.replace(" ", "_").upper() + "_PATH"
352
+    # TODO(the-13th-letter): Rewrite using parenthesized with-statements.
353
+    # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
354
+    with contextlib.ExitStack() as stack:
355
+        stack.enter_context(runner.isolated_filesystem())
356
+        stack.enter_context(
357
+            cli_machinery.StandardCLILogging.ensure_standard_logging()
358
+        )
359
+        stack.enter_context(
360
+            cli_machinery.StandardCLILogging.ensure_standard_warnings_logging()
361
+        )
362
+        cwd = str(pathlib.Path.cwd().resolve())
363
+        monkeypatch.setenv("HOME", cwd)
364
+        monkeypatch.setenv("APPDATA", cwd)
365
+        monkeypatch.setenv("LOCALAPPDATA", cwd)
366
+        monkeypatch.delenv(env_name, raising=False)
367
+        config_dir = cli_helpers.config_filename(subsystem=None)
368
+        config_dir.mkdir(parents=True, exist_ok=True)
369
+        if isinstance(main_config_str, str):
370
+            cli_helpers.config_filename("user configuration").write_text(
371
+                main_config_str, encoding="UTF-8"
372
+            )
373
+        try:
374
+            yield
375
+        finally:
376
+            cli_helpers.config_filename("write lock").unlink(missing_ok=True)
377
+
378
+
379
+@contextlib.contextmanager
380
+def isolated_vault_config(
381
+    monkeypatch: pytest.MonkeyPatch,
382
+    runner: tests.machinery.CliRunner,
383
+    vault_config: Any,
384
+    main_config_str: str | None = None,
385
+) -> Iterator[None]:
386
+    """Provide an isolated vault configuration setup, as a context.
387
+
388
+    Uses [`isolated_config`][] internally.  Beyond those actions, this
389
+    manager also loads the specified vault configuration into the
390
+    context.
391
+
392
+    Args:
393
+        monkeypatch:
394
+            A monkeypatch fixture object.
395
+        runner:
396
+            A `click` CLI runner harness.
397
+        vault_config:
398
+            A valid vault configuration, to be integrated into the
399
+            context.
400
+        main_config_str:
401
+            Optional TOML file contents, to be used as the user
402
+            configuration.
403
+
404
+    Returns:
405
+        A context manager, without a return value.
406
+
407
+    """
408
+    with isolated_config(
409
+        monkeypatch=monkeypatch, runner=runner, main_config_str=main_config_str
410
+    ):
411
+        config_filename = cli_helpers.config_filename(subsystem="vault")
412
+        with config_filename.open("w", encoding="UTF-8") as outfile:
413
+            json.dump(vault_config, outfile)
414
+        yield
415
+
416
+
417
+@contextlib.contextmanager
418
+def isolated_vault_exporter_config(
419
+    monkeypatch: pytest.MonkeyPatch,
420
+    runner: tests.machinery.CliRunner,
421
+    vault_config: str | bytes | None = None,
422
+    vault_key: str | None = None,
423
+) -> Iterator[None]:
424
+    """Provide an isolated vault configuration setup, as a context.
425
+
426
+    Works similarly to [`isolated_config`][], except that no user
427
+    configuration is accepted or integrated into the context.  This
428
+    manager also accepts a serialized vault-native configuration and
429
+    a vault encryption key to integrate into the context.
430
+
431
+    Args:
432
+        monkeypatch:
433
+            A monkeypatch fixture object.
434
+        runner:
435
+            A `click` CLI runner harness.
436
+        vault_config:
437
+            An optional serialized vault-native configuration, to be
438
+            integrated into the context.  If a text string, then the
439
+            contents are written to the file `.vault`.  If a byte
440
+            string, then it is treated as base64-encoded zip file
441
+            contents, which---once inside the `.vault` directory---will
442
+            be extracted into the current directory.
443
+        vault_key:
444
+            An optional encryption key presumably for the stored
445
+            vault-native configuration.  If given, then the environment
446
+            variable `VAULT_KEY` will be populated with this key while
447
+            the context is active.
448
+
449
+    Returns:
450
+        A context manager, without a return value.
451
+
452
+    """
453
+    # TODO(the-13th-letter): Remove the fallback implementation.
454
+    # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.10
455
+    if TYPE_CHECKING:
456
+        chdir: Callable[..., AbstractContextManager]
457
+    else:
458
+        try:
459
+            chdir = contextlib.chdir  # type: ignore[attr]
460
+        except AttributeError:
461
+
462
+            @contextlib.contextmanager
463
+            def chdir(
464
+                newpath: str | bytes | os.PathLike,
465
+            ) -> Iterator[None]:  # pragma: no branch
466
+                oldpath = pathlib.Path.cwd().resolve()
467
+                os.chdir(newpath)
468
+                yield
469
+                os.chdir(oldpath)
470
+
471
+    with runner.isolated_filesystem():
472
+        cwd = str(pathlib.Path.cwd().resolve())
473
+        monkeypatch.setenv("HOME", cwd)
474
+        monkeypatch.setenv("USERPROFILE", cwd)
475
+        monkeypatch.delenv(
476
+            cli_helpers.PROG_NAME.replace(" ", "_").upper() + "_PATH",
477
+            raising=False,
478
+        )
479
+        monkeypatch.delenv("VAULT_PATH", raising=False)
480
+        monkeypatch.delenv("VAULT_KEY", raising=False)
481
+        monkeypatch.delenv("LOGNAME", raising=False)
482
+        monkeypatch.delenv("USER", raising=False)
483
+        monkeypatch.delenv("USERNAME", raising=False)
484
+        if vault_key is not None:
485
+            monkeypatch.setenv("VAULT_KEY", vault_key)
486
+        vault_config_path = pathlib.Path(".vault").resolve()
487
+        # TODO(the-13th-letter): Rewrite using structural pattern matching.
488
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
489
+        if isinstance(vault_config, str):
490
+            vault_config_path.write_text(f"{vault_config}\n", encoding="UTF-8")
491
+        elif isinstance(vault_config, bytes):
492
+            vault_config_path.mkdir(parents=True, mode=0o700, exist_ok=True)
493
+            # TODO(the-13th-letter): Rewrite using parenthesized
494
+            # with-statements.
495
+            # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
496
+            with contextlib.ExitStack() as stack:
497
+                stack.enter_context(chdir(vault_config_path))
498
+                tmpzipfile = stack.enter_context(
499
+                    tempfile.NamedTemporaryFile(suffix=".zip")
500
+                )
501
+                for line in vault_config.splitlines():
502
+                    tmpzipfile.write(base64.standard_b64decode(line))
503
+                tmpzipfile.flush()
504
+                tmpzipfile.seek(0, 0)
505
+                with zipfile.ZipFile(tmpzipfile.file) as zipfileobj:
506
+                    zipfileobj.extractall()
507
+        elif vault_config is None:
508
+            pass
509
+        else:  # pragma: no cover
510
+            assert_never(vault_config)
511
+        try:
512
+            yield
513
+        finally:
514
+            cli_helpers.config_filename("write lock").unlink(missing_ok=True)
... ...
@@ -8,7 +8,7 @@ import base64
8 8
 
9 9
 import pytest
10 10
 
11
-import tests
11
+import tests.data
12 12
 from derivepassphrase import ssh_agent
13 13
 
14 14
 OPENSSH_MAGIC = b"openssh-key-v1\x00"
... ...
@@ -138,14 +138,14 @@ class Parametrize:
138 138
     """Common test parametrizations."""
139 139
 
140 140
     TEST_KEYS = pytest.mark.parametrize(
141
-        ["keyname", "key"], tests.ALL_KEYS.items(), ids=tests.ALL_KEYS.keys()
141
+        ["keyname", "key"], tests.data.ALL_KEYS.items(), ids=tests.data.ALL_KEYS.keys()
142 142
     )
143 143
 
144 144
 
145 145
 @Parametrize.TEST_KEYS
146 146
 def test_100_test_keys_public_keys_are_internally_consistent(
147 147
     keyname: str,
148
-    key: tests.SSHTestKey,
148
+    key: tests.data.SSHTestKey,
149 149
 ) -> None:
150 150
     """The test key public key data structures are internally consistent."""
151 151
     del keyname
... ...
@@ -161,7 +161,7 @@ def test_100_test_keys_public_keys_are_internally_consistent(
161 161
 @Parametrize.TEST_KEYS
162 162
 def test_101_test_keys_private_keys_are_consistent_with_public_keys(
163 163
     keyname: str,
164
-    key: tests.SSHTestKey,
164
+    key: tests.data.SSHTestKey,
165 165
 ) -> None:
166 166
     """The test key private key data are consistent with their public parts."""
167 167
     del keyname
... ...
@@ -195,7 +195,7 @@ def test_101_test_keys_private_keys_are_consistent_with_public_keys(
195 195
 @Parametrize.TEST_KEYS
196 196
 def test_102_test_keys_private_keys_are_internally_consistent(
197 197
     keyname: str,
198
-    key: tests.SSHTestKey,
198
+    key: tests.data.SSHTestKey,
199 199
 ) -> None:
200 200
     """The test key private key data structures are internally consistent."""
201 201
     del keyname
... ...
@@ -34,7 +34,11 @@ import pytest
34 34
 from hypothesis import stateful, strategies
35 35
 from typing_extensions import Any, NamedTuple, TypeAlias
36 36
 
37
-import tests
37
+import tests.data
38
+import tests.data.callables
39
+import tests.machinery
40
+import tests.machinery.hypothesis
41
+import tests.machinery.pytest
38 42
 from derivepassphrase import _types, cli, ssh_agent, vault
39 43
 from derivepassphrase._internals import (
40 44
     cli_helpers,
... ...
@@ -51,22 +55,22 @@ if TYPE_CHECKING:
51 55
 
52 56
     from typing_extensions import Literal
53 57
 
54
-DUMMY_SERVICE = tests.DUMMY_SERVICE
55
-DUMMY_PASSPHRASE = tests.DUMMY_PASSPHRASE
56
-DUMMY_CONFIG_SETTINGS = tests.DUMMY_CONFIG_SETTINGS
57
-DUMMY_RESULT_PASSPHRASE = tests.DUMMY_RESULT_PASSPHRASE
58
-DUMMY_RESULT_KEY1 = tests.DUMMY_RESULT_KEY1
59
-DUMMY_PHRASE_FROM_KEY1_RAW = tests.DUMMY_PHRASE_FROM_KEY1_RAW
60
-DUMMY_PHRASE_FROM_KEY1 = tests.DUMMY_PHRASE_FROM_KEY1
58
+DUMMY_SERVICE = tests.data.DUMMY_SERVICE
59
+DUMMY_PASSPHRASE = tests.data.DUMMY_PASSPHRASE
60
+DUMMY_CONFIG_SETTINGS = tests.data.DUMMY_CONFIG_SETTINGS
61
+DUMMY_RESULT_PASSPHRASE = tests.data.DUMMY_RESULT_PASSPHRASE
62
+DUMMY_RESULT_KEY1 = tests.data.DUMMY_RESULT_KEY1
63
+DUMMY_PHRASE_FROM_KEY1_RAW = tests.data.DUMMY_PHRASE_FROM_KEY1_RAW
64
+DUMMY_PHRASE_FROM_KEY1 = tests.data.DUMMY_PHRASE_FROM_KEY1
61 65
 
62
-DUMMY_KEY1 = tests.DUMMY_KEY1
63
-DUMMY_KEY1_B64 = tests.DUMMY_KEY1_B64
64
-DUMMY_KEY2 = tests.DUMMY_KEY2
65
-DUMMY_KEY2_B64 = tests.DUMMY_KEY2_B64
66
-DUMMY_KEY3 = tests.DUMMY_KEY3
67
-DUMMY_KEY3_B64 = tests.DUMMY_KEY3_B64
66
+DUMMY_KEY1 = tests.data.DUMMY_KEY1
67
+DUMMY_KEY1_B64 = tests.data.DUMMY_KEY1_B64
68
+DUMMY_KEY2 = tests.data.DUMMY_KEY2
69
+DUMMY_KEY2_B64 = tests.data.DUMMY_KEY2_B64
70
+DUMMY_KEY3 = tests.data.DUMMY_KEY3
71
+DUMMY_KEY3_B64 = tests.data.DUMMY_KEY3_B64
68 72
 
69
-TEST_CONFIGS = tests.TEST_CONFIGS
73
+TEST_CONFIGS = tests.data.TEST_CONFIGS
70 74
 
71 75
 
72 76
 class IncompatibleConfiguration(NamedTuple):
... ...
@@ -274,7 +278,7 @@ def is_harmless_config_import_warning(record: tuple[str, int, str]) -> bool:
274 278
             "because a key is also set:"
275 279
         ),
276 280
     ]
277
-    return any(tests.warning_emitted(w, [record]) for w in possible_warnings)
281
+    return any(tests.machinery.warning_emitted(w, [record]) for w in possible_warnings)
278 282
 
279 283
 
280 284
 def assert_vault_config_is_indented_and_line_broken(
... ...
@@ -323,8 +327,8 @@ def vault_config_exporter_shell_interpreter(  # noqa: C901
323 327
     *,
324 328
     prog_name_list: list[str] | None = None,
325 329
     command: click.BaseCommand | None = None,
326
-    runner: tests.CliRunner | None = None,
327
-) -> Iterator[tests.ReadableResult]:
330
+    runner: tests.machinery.CliRunner | None = None,
331
+) -> Iterator[tests.machinery.ReadableResult]:
328 332
     """A rudimentary sh(1) interpreter for `--export-as=sh` output.
329 333
 
330 334
     Assumes a script as emitted by `derivepassphrase vault
... ...
@@ -341,7 +345,7 @@ def vault_config_exporter_shell_interpreter(  # noqa: C901
341 345
     if command is None:  # pragma: no cover
342 346
         command = cli.derivepassphrase_vault
343 347
     if runner is None:  # pragma: no cover
344
-        runner = tests.CliRunner(mix_stderr=False)
348
+        runner = tests.machinery.CliRunner(mix_stderr=False)
345 349
     n = len(prog_name_list)
346 350
     it = iter(script)
347 351
     while True:
... ...
@@ -1218,7 +1222,7 @@ class Parametrize(types.SimpleNamespace):
1218 1222
         [
1219 1223
             conf.config
1220 1224
             for conf in TEST_CONFIGS
1221
-            if tests.is_valid_test_config(conf)
1225
+            if tests.data.is_valid_test_config(conf)
1222 1226
         ],
1223 1227
     )
1224 1228
     KEY_OVERRIDING_IN_CONFIG = pytest.mark.parametrize(
... ...
@@ -2015,14 +2019,14 @@ class TestAllCLI:
2015 2019
         TODO: Do we actually need this?  What should we check for?
2016 2020
 
2017 2021
         """
2018
-        runner = tests.CliRunner(mix_stderr=False)
2022
+        runner = tests.machinery.CliRunner(mix_stderr=False)
2019 2023
         # TODO(the-13th-letter): Rewrite using parenthesized
2020 2024
         # with-statements.
2021 2025
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
2022 2026
         with contextlib.ExitStack() as stack:
2023 2027
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
2024 2028
             stack.enter_context(
2025
-                tests.isolated_config(
2029
+                tests.machinery.pytest.isolated_config(
2026 2030
                     monkeypatch=monkeypatch,
2027 2031
                     runner=runner,
2028 2032
                 )
... ...
@@ -2044,14 +2048,14 @@ class TestAllCLI:
2044 2048
         TODO: Do we actually need this?  What should we check for?
2045 2049
 
2046 2050
         """
2047
-        runner = tests.CliRunner(mix_stderr=False)
2051
+        runner = tests.machinery.CliRunner(mix_stderr=False)
2048 2052
         # TODO(the-13th-letter): Rewrite using parenthesized
2049 2053
         # with-statements.
2050 2054
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
2051 2055
         with contextlib.ExitStack() as stack:
2052 2056
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
2053 2057
             stack.enter_context(
2054
-                tests.isolated_config(
2058
+                tests.machinery.pytest.isolated_config(
2055 2059
                     monkeypatch=monkeypatch,
2056 2060
                     runner=runner,
2057 2061
                 )
... ...
@@ -2075,14 +2079,14 @@ class TestAllCLI:
2075 2079
         TODO: Do we actually need this?  What should we check for?
2076 2080
 
2077 2081
         """
2078
-        runner = tests.CliRunner(mix_stderr=False)
2082
+        runner = tests.machinery.CliRunner(mix_stderr=False)
2079 2083
         # TODO(the-13th-letter): Rewrite using parenthesized
2080 2084
         # with-statements.
2081 2085
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
2082 2086
         with contextlib.ExitStack() as stack:
2083 2087
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
2084 2088
             stack.enter_context(
2085
-                tests.isolated_config(
2089
+                tests.machinery.pytest.isolated_config(
2086 2090
                     monkeypatch=monkeypatch,
2087 2091
                     runner=runner,
2088 2092
                 )
... ...
@@ -2106,14 +2110,14 @@ class TestAllCLI:
2106 2110
         TODO: Do we actually need this?  What should we check for?
2107 2111
 
2108 2112
         """
2109
-        runner = tests.CliRunner(mix_stderr=False)
2113
+        runner = tests.machinery.CliRunner(mix_stderr=False)
2110 2114
         # TODO(the-13th-letter): Rewrite using parenthesized
2111 2115
         # with-statements.
2112 2116
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
2113 2117
         with contextlib.ExitStack() as stack:
2114 2118
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
2115 2119
             stack.enter_context(
2116
-                tests.isolated_config(
2120
+                tests.machinery.pytest.isolated_config(
2117 2121
                     monkeypatch=monkeypatch,
2118 2122
                     runner=runner,
2119 2123
                 )
... ...
@@ -2139,14 +2143,14 @@ class TestAllCLI:
2139 2143
         non_eager_arguments: list[str],
2140 2144
     ) -> None:
2141 2145
         """Eager options terminate option and argument processing."""
2142
-        runner = tests.CliRunner(mix_stderr=False)
2146
+        runner = tests.machinery.CliRunner(mix_stderr=False)
2143 2147
         # TODO(the-13th-letter): Rewrite using parenthesized
2144 2148
         # with-statements.
2145 2149
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
2146 2150
         with contextlib.ExitStack() as stack:
2147 2151
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
2148 2152
             stack.enter_context(
2149
-                tests.isolated_config(
2153
+                tests.machinery.pytest.isolated_config(
2150 2154
                     monkeypatch=monkeypatch,
2151 2155
                     runner=runner,
2152 2156
                 )
... ...
@@ -2174,14 +2178,14 @@ class TestAllCLI:
2174 2178
 
2175 2179
         """
2176 2180
         color = False
2177
-        runner = tests.CliRunner(mix_stderr=False)
2181
+        runner = tests.machinery.CliRunner(mix_stderr=False)
2178 2182
         # TODO(the-13th-letter): Rewrite using parenthesized
2179 2183
         # with-statements.
2180 2184
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
2181 2185
         with contextlib.ExitStack() as stack:
2182 2186
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
2183 2187
             stack.enter_context(
2184
-                tests.isolated_config(
2188
+                tests.machinery.pytest.isolated_config(
2185 2189
                     monkeypatch=monkeypatch,
2186 2190
                     runner=runner,
2187 2191
                 )
... ...
@@ -2215,14 +2219,14 @@ class TestAllCLI:
2215 2219
         subcommands.
2216 2220
 
2217 2221
         """
2218
-        runner = tests.CliRunner(mix_stderr=False)
2222
+        runner = tests.machinery.CliRunner(mix_stderr=False)
2219 2223
         # TODO(the-13th-letter): Rewrite using parenthesized
2220 2224
         # with-statements.
2221 2225
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
2222 2226
         with contextlib.ExitStack() as stack:
2223 2227
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
2224 2228
             stack.enter_context(
2225
-                tests.isolated_config(
2229
+                tests.machinery.pytest.isolated_config(
2226 2230
                     monkeypatch=monkeypatch,
2227 2231
                     runner=runner,
2228 2232
                 )
... ...
@@ -2256,14 +2260,14 @@ class TestAllCLI:
2256 2260
         of subcommands.
2257 2261
 
2258 2262
         """
2259
-        runner = tests.CliRunner(mix_stderr=False)
2263
+        runner = tests.machinery.CliRunner(mix_stderr=False)
2260 2264
         # TODO(the-13th-letter): Rewrite using parenthesized
2261 2265
         # with-statements.
2262 2266
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
2263 2267
         with contextlib.ExitStack() as stack:
2264 2268
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
2265 2269
             stack.enter_context(
2266
-                tests.isolated_config(
2270
+                tests.machinery.pytest.isolated_config(
2267 2271
                     monkeypatch=monkeypatch,
2268 2272
                     runner=runner,
2269 2273
                 )
... ...
@@ -2304,14 +2308,14 @@ class TestAllCLI:
2304 2308
         configuration formats, and a list of available PEP 508 extras.
2305 2309
 
2306 2310
         """
2307
-        runner = tests.CliRunner(mix_stderr=False)
2311
+        runner = tests.machinery.CliRunner(mix_stderr=False)
2308 2312
         # TODO(the-13th-letter): Rewrite using parenthesized
2309 2313
         # with-statements.
2310 2314
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
2311 2315
         with contextlib.ExitStack() as stack:
2312 2316
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
2313 2317
             stack.enter_context(
2314
-                tests.isolated_config(
2318
+                tests.machinery.pytest.isolated_config(
2315 2319
                     monkeypatch=monkeypatch,
2316 2320
                     runner=runner,
2317 2321
                 )
... ...
@@ -2359,14 +2363,14 @@ class TestAllCLI:
2359 2363
         first paragraph.
2360 2364
 
2361 2365
         """
2362
-        runner = tests.CliRunner(mix_stderr=False)
2366
+        runner = tests.machinery.CliRunner(mix_stderr=False)
2363 2367
         # TODO(the-13th-letter): Rewrite using parenthesized
2364 2368
         # with-statements.
2365 2369
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
2366 2370
         with contextlib.ExitStack() as stack:
2367 2371
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
2368 2372
             stack.enter_context(
2369
-                tests.isolated_config(
2373
+                tests.machinery.pytest.isolated_config(
2370 2374
                     monkeypatch=monkeypatch,
2371 2375
                     runner=runner,
2372 2376
                 )
... ...
@@ -2411,14 +2415,14 @@ class TestCLI:
2411 2415
         self,
2412 2416
     ) -> None:
2413 2417
         """The `--help` option emits help text."""
2414
-        runner = tests.CliRunner(mix_stderr=False)
2418
+        runner = tests.machinery.CliRunner(mix_stderr=False)
2415 2419
         # TODO(the-13th-letter): Rewrite using parenthesized
2416 2420
         # with-statements.
2417 2421
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
2418 2422
         with contextlib.ExitStack() as stack:
2419 2423
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
2420 2424
             stack.enter_context(
2421
-                tests.isolated_config(
2425
+                tests.machinery.pytest.isolated_config(
2422 2426
                     monkeypatch=monkeypatch,
2423 2427
                     runner=runner,
2424 2428
                 )
... ...
@@ -2441,14 +2445,14 @@ class TestCLI:
2441 2445
         self,
2442 2446
     ) -> None:
2443 2447
         """The `--version` option emits version information."""
2444
-        runner = tests.CliRunner(mix_stderr=False)
2448
+        runner = tests.machinery.CliRunner(mix_stderr=False)
2445 2449
         # TODO(the-13th-letter): Rewrite using parenthesized
2446 2450
         # with-statements.
2447 2451
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
2448 2452
         with contextlib.ExitStack() as stack:
2449 2453
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
2450 2454
             stack.enter_context(
2451
-                tests.isolated_config(
2455
+                tests.machinery.pytest.isolated_config(
2452 2456
                     monkeypatch=monkeypatch,
2453 2457
                     runner=runner,
2454 2458
                 )
... ...
@@ -2473,20 +2477,20 @@ class TestCLI:
2473 2477
         """Named character classes can be disabled on the command-line."""
2474 2478
         option = f"--{charset_name}"
2475 2479
         charset = vault.Vault.CHARSETS[charset_name].decode("ascii")
2476
-        runner = tests.CliRunner(mix_stderr=False)
2480
+        runner = tests.machinery.CliRunner(mix_stderr=False)
2477 2481
         # TODO(the-13th-letter): Rewrite using parenthesized
2478 2482
         # with-statements.
2479 2483
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
2480 2484
         with contextlib.ExitStack() as stack:
2481 2485
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
2482 2486
             stack.enter_context(
2483
-                tests.isolated_config(
2487
+                tests.machinery.pytest.isolated_config(
2484 2488
                     monkeypatch=monkeypatch,
2485 2489
                     runner=runner,
2486 2490
                 )
2487 2491
             )
2488 2492
             monkeypatch.setattr(
2489
-                cli_helpers, "prompt_for_passphrase", tests.auto_prompt
2493
+                cli_helpers, "prompt_for_passphrase", tests.data.callables.auto_prompt
2490 2494
             )
2491 2495
             result = runner.invoke(
2492 2496
                 cli.derivepassphrase_vault,
... ...
@@ -2504,20 +2508,20 @@ class TestCLI:
2504 2508
         self,
2505 2509
     ) -> None:
2506 2510
         """Character repetition can be disabled on the command-line."""
2507
-        runner = tests.CliRunner(mix_stderr=False)
2511
+        runner = tests.machinery.CliRunner(mix_stderr=False)
2508 2512
         # TODO(the-13th-letter): Rewrite using parenthesized
2509 2513
         # with-statements.
2510 2514
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
2511 2515
         with contextlib.ExitStack() as stack:
2512 2516
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
2513 2517
             stack.enter_context(
2514
-                tests.isolated_config(
2518
+                tests.machinery.pytest.isolated_config(
2515 2519
                     monkeypatch=monkeypatch,
2516 2520
                     runner=runner,
2517 2521
                 )
2518 2522
             )
2519 2523
             monkeypatch.setattr(
2520
-                cli_helpers, "prompt_for_passphrase", tests.auto_prompt
2524
+                cli_helpers, "prompt_for_passphrase", tests.data.callables.auto_prompt
2521 2525
             )
2522 2526
             result = runner.invoke(
2523 2527
                 cli.derivepassphrase_vault,
... ...
@@ -2538,26 +2542,26 @@ class TestCLI:
2538 2542
     @Parametrize.CONFIG_WITH_KEY
2539 2543
     def test_204a_key_from_config(
2540 2544
         self,
2541
-        running_ssh_agent: tests.RunningSSHAgentInfo,
2545
+        running_ssh_agent: tests.data.RunningSSHAgentInfo,
2542 2546
         config: _types.VaultConfig,
2543 2547
     ) -> None:
2544 2548
         """A stored configured SSH key will be used."""
2545 2549
         del running_ssh_agent
2546
-        runner = tests.CliRunner(mix_stderr=False)
2550
+        runner = tests.machinery.CliRunner(mix_stderr=False)
2547 2551
         # TODO(the-13th-letter): Rewrite using parenthesized
2548 2552
         # with-statements.
2549 2553
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
2550 2554
         with contextlib.ExitStack() as stack:
2551 2555
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
2552 2556
             stack.enter_context(
2553
-                tests.isolated_vault_config(
2557
+                tests.machinery.pytest.isolated_vault_config(
2554 2558
                     monkeypatch=monkeypatch,
2555 2559
                     runner=runner,
2556 2560
                     vault_config=config,
2557 2561
                 )
2558 2562
             )
2559 2563
             monkeypatch.setattr(
2560
-                vault.Vault, "phrase_from_key", tests.phrase_from_key
2564
+                vault.Vault, "phrase_from_key", tests.data.callables.phrase_from_key
2561 2565
             )
2562 2566
             result = runner.invoke(
2563 2567
                 cli.derivepassphrase_vault,
... ...
@@ -2578,18 +2582,18 @@ class TestCLI:
2578 2582
 
2579 2583
     def test_204b_key_from_command_line(
2580 2584
         self,
2581
-        running_ssh_agent: tests.RunningSSHAgentInfo,
2585
+        running_ssh_agent: tests.data.RunningSSHAgentInfo,
2582 2586
     ) -> None:
2583 2587
         """An SSH key requested on the command-line will be used."""
2584 2588
         del running_ssh_agent
2585
-        runner = tests.CliRunner(mix_stderr=False)
2589
+        runner = tests.machinery.CliRunner(mix_stderr=False)
2586 2590
         # TODO(the-13th-letter): Rewrite using parenthesized
2587 2591
         # with-statements.
2588 2592
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
2589 2593
         with contextlib.ExitStack() as stack:
2590 2594
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
2591 2595
             stack.enter_context(
2592
-                tests.isolated_vault_config(
2596
+                tests.machinery.pytest.isolated_vault_config(
2593 2597
                     monkeypatch=monkeypatch,
2594 2598
                     runner=runner,
2595 2599
                     vault_config={
... ...
@@ -2598,10 +2602,10 @@ class TestCLI:
2598 2602
                 )
2599 2603
             )
2600 2604
             monkeypatch.setattr(
2601
-                cli_helpers, "get_suitable_ssh_keys", tests.suitable_ssh_keys
2605
+                cli_helpers, "get_suitable_ssh_keys", tests.data.callables.suitable_ssh_keys
2602 2606
             )
2603 2607
             monkeypatch.setattr(
2604
-                vault.Vault, "phrase_from_key", tests.phrase_from_key
2608
+                vault.Vault, "phrase_from_key", tests.data.callables.phrase_from_key
2605 2609
             )
2606 2610
             result = runner.invoke(
2607 2611
                 cli.derivepassphrase_vault,
... ...
@@ -2623,29 +2627,29 @@ class TestCLI:
2623 2627
     @Parametrize.KEY_INDEX
2624 2628
     def test_204c_key_override_on_command_line(
2625 2629
         self,
2626
-        running_ssh_agent: tests.RunningSSHAgentInfo,
2630
+        running_ssh_agent: tests.data.RunningSSHAgentInfo,
2627 2631
         config: dict[str, Any],
2628 2632
         key_index: int,
2629 2633
     ) -> None:
2630 2634
         """A command-line SSH key will override the configured key."""
2631 2635
         del running_ssh_agent
2632
-        runner = tests.CliRunner(mix_stderr=False)
2636
+        runner = tests.machinery.CliRunner(mix_stderr=False)
2633 2637
         # TODO(the-13th-letter): Rewrite using parenthesized
2634 2638
         # with-statements.
2635 2639
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
2636 2640
         with contextlib.ExitStack() as stack:
2637 2641
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
2638 2642
             stack.enter_context(
2639
-                tests.isolated_vault_config(
2643
+                tests.machinery.pytest.isolated_vault_config(
2640 2644
                     monkeypatch=monkeypatch,
2641 2645
                     runner=runner,
2642 2646
                     vault_config=config,
2643 2647
                 )
2644 2648
             )
2645 2649
             monkeypatch.setattr(
2646
-                ssh_agent.SSHAgentClient, "list_keys", tests.list_keys
2650
+                ssh_agent.SSHAgentClient, "list_keys", tests.data.callables.list_keys
2647 2651
             )
2648
-            monkeypatch.setattr(ssh_agent.SSHAgentClient, "sign", tests.sign)
2652
+            monkeypatch.setattr(ssh_agent.SSHAgentClient, "sign", tests.data.callables.sign)
2649 2653
             result = runner.invoke(
2650 2654
                 cli.derivepassphrase_vault,
2651 2655
                 ["-k", "--", DUMMY_SERVICE],
... ...
@@ -2660,18 +2664,18 @@ class TestCLI:
2660 2664
 
2661 2665
     def test_205_service_phrase_if_key_in_global_config(
2662 2666
         self,
2663
-        running_ssh_agent: tests.RunningSSHAgentInfo,
2667
+        running_ssh_agent: tests.data.RunningSSHAgentInfo,
2664 2668
     ) -> None:
2665 2669
         """A command-line passphrase will override the configured key."""
2666 2670
         del running_ssh_agent
2667
-        runner = tests.CliRunner(mix_stderr=False)
2671
+        runner = tests.machinery.CliRunner(mix_stderr=False)
2668 2672
         # TODO(the-13th-letter): Rewrite using parenthesized
2669 2673
         # with-statements.
2670 2674
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
2671 2675
         with contextlib.ExitStack() as stack:
2672 2676
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
2673 2677
             stack.enter_context(
2674
-                tests.isolated_vault_config(
2678
+                tests.machinery.pytest.isolated_vault_config(
2675 2679
                     monkeypatch=monkeypatch,
2676 2680
                     runner=runner,
2677 2681
                     vault_config={
... ...
@@ -2686,9 +2690,9 @@ class TestCLI:
2686 2690
                 )
2687 2691
             )
2688 2692
             monkeypatch.setattr(
2689
-                ssh_agent.SSHAgentClient, "list_keys", tests.list_keys
2693
+                ssh_agent.SSHAgentClient, "list_keys", tests.data.callables.list_keys
2690 2694
             )
2691
-            monkeypatch.setattr(ssh_agent.SSHAgentClient, "sign", tests.sign)
2695
+            monkeypatch.setattr(ssh_agent.SSHAgentClient, "sign", tests.data.callables.sign)
2692 2696
             result = runner.invoke(
2693 2697
                 cli.derivepassphrase_vault,
2694 2698
                 ["--", DUMMY_SERVICE],
... ...
@@ -2707,30 +2711,30 @@ class TestCLI:
2707 2711
     @Parametrize.KEY_OVERRIDING_IN_CONFIG
2708 2712
     def test_206_setting_phrase_thus_overriding_key_in_config(
2709 2713
         self,
2710
-        running_ssh_agent: tests.RunningSSHAgentInfo,
2714
+        running_ssh_agent: tests.data.RunningSSHAgentInfo,
2711 2715
         caplog: pytest.LogCaptureFixture,
2712 2716
         config: _types.VaultConfig,
2713 2717
         command_line: list[str],
2714 2718
     ) -> None:
2715 2719
         """Configuring a passphrase atop an SSH key works, but warns."""
2716 2720
         del running_ssh_agent
2717
-        runner = tests.CliRunner(mix_stderr=False)
2721
+        runner = tests.machinery.CliRunner(mix_stderr=False)
2718 2722
         # TODO(the-13th-letter): Rewrite using parenthesized
2719 2723
         # with-statements.
2720 2724
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
2721 2725
         with contextlib.ExitStack() as stack:
2722 2726
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
2723 2727
             stack.enter_context(
2724
-                tests.isolated_vault_config(
2728
+                tests.machinery.pytest.isolated_vault_config(
2725 2729
                     monkeypatch=monkeypatch,
2726 2730
                     runner=runner,
2727 2731
                     vault_config=config,
2728 2732
                 )
2729 2733
             )
2730 2734
             monkeypatch.setattr(
2731
-                ssh_agent.SSHAgentClient, "list_keys", tests.list_keys
2735
+                ssh_agent.SSHAgentClient, "list_keys", tests.data.callables.list_keys
2732 2736
             )
2733
-            monkeypatch.setattr(ssh_agent.SSHAgentClient, "sign", tests.sign)
2737
+            monkeypatch.setattr(ssh_agent.SSHAgentClient, "sign", tests.data.callables.sign)
2734 2738
             result = runner.invoke(
2735 2739
                 cli.derivepassphrase_vault,
2736 2740
                 command_line,
... ...
@@ -2742,10 +2746,10 @@ class TestCLI:
2742 2746
         assert result.stderr, "expected known error output"
2743 2747
         err_lines = result.stderr.splitlines(False)
2744 2748
         assert err_lines[0].startswith("Passphrase:")
2745
-        assert tests.warning_emitted(
2749
+        assert tests.machinery.warning_emitted(
2746 2750
             "Setting a service passphrase is ineffective ",
2747 2751
             caplog.record_tuples,
2748
-        ) or tests.warning_emitted(
2752
+        ) or tests.machinery.warning_emitted(
2749 2753
             "Setting a global passphrase is ineffective ",
2750 2754
             caplog.record_tuples,
2751 2755
         ), "expected known warning message"
... ...
@@ -2770,14 +2774,14 @@ class TestCLI:
2770 2774
     ) -> None:
2771 2775
         """Service notes are printed, if they exist."""
2772 2776
         hypothesis.assume("Error:" not in notes)
2773
-        runner = tests.CliRunner(mix_stderr=False)
2777
+        runner = tests.machinery.CliRunner(mix_stderr=False)
2774 2778
         # TODO(the-13th-letter): Rewrite using parenthesized
2775 2779
         # with-statements.
2776 2780
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
2777 2781
         with contextlib.ExitStack() as stack:
2778 2782
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
2779 2783
             stack.enter_context(
2780
-                tests.isolated_vault_config(
2784
+                tests.machinery.pytest.isolated_vault_config(
2781 2785
                     monkeypatch=monkeypatch,
2782 2786
                     runner=runner,
2783 2787
                     vault_config={
... ...
@@ -2816,14 +2820,14 @@ class TestCLI:
2816 2820
         option: str,
2817 2821
     ) -> None:
2818 2822
         """Requesting invalidly many characters from a class fails."""
2819
-        runner = tests.CliRunner(mix_stderr=False)
2823
+        runner = tests.machinery.CliRunner(mix_stderr=False)
2820 2824
         # TODO(the-13th-letter): Rewrite using parenthesized
2821 2825
         # with-statements.
2822 2826
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
2823 2827
         with contextlib.ExitStack() as stack:
2824 2828
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
2825 2829
             stack.enter_context(
2826
-                tests.isolated_config(
2830
+                tests.machinery.pytest.isolated_config(
2827 2831
                     monkeypatch=monkeypatch,
2828 2832
                     runner=runner,
2829 2833
                 )
... ...
@@ -2848,21 +2852,21 @@ class TestCLI:
2848 2852
         check_success: bool,
2849 2853
     ) -> None:
2850 2854
         """We require or forbid a service argument, depending on options."""
2851
-        runner = tests.CliRunner(mix_stderr=False)
2855
+        runner = tests.machinery.CliRunner(mix_stderr=False)
2852 2856
         # TODO(the-13th-letter): Rewrite using parenthesized
2853 2857
         # with-statements.
2854 2858
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
2855 2859
         with contextlib.ExitStack() as stack:
2856 2860
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
2857 2861
             stack.enter_context(
2858
-                tests.isolated_vault_config(
2862
+                tests.machinery.pytest.isolated_vault_config(
2859 2863
                     monkeypatch=monkeypatch,
2860 2864
                     runner=runner,
2861 2865
                     vault_config={"global": {"phrase": "abc"}, "services": {}},
2862 2866
                 )
2863 2867
             )
2864 2868
             monkeypatch.setattr(
2865
-                cli_helpers, "prompt_for_passphrase", tests.auto_prompt
2869
+                cli_helpers, "prompt_for_passphrase", tests.data.callables.auto_prompt
2866 2870
             )
2867 2871
             result = runner.invoke(
2868 2872
                 cli.derivepassphrase_vault,
... ...
@@ -2890,7 +2894,7 @@ class TestCLI:
2890 2894
             with contextlib.ExitStack() as stack:
2891 2895
                 monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
2892 2896
                 stack.enter_context(
2893
-                    tests.isolated_vault_config(
2897
+                    tests.machinery.pytest.isolated_vault_config(
2894 2898
                         monkeypatch=monkeypatch,
2895 2899
                         runner=runner,
2896 2900
                         vault_config={
... ...
@@ -2900,7 +2904,7 @@ class TestCLI:
2900 2904
                     )
2901 2905
                 )
2902 2906
                 monkeypatch.setattr(
2903
-                    cli_helpers, "prompt_for_passphrase", tests.auto_prompt
2907
+                    cli_helpers, "prompt_for_passphrase", tests.data.callables.auto_prompt
2904 2908
                 )
2905 2909
                 result = runner.invoke(
2906 2910
                     cli.derivepassphrase_vault,
... ...
@@ -2923,25 +2927,25 @@ class TestCLI:
2923 2927
         def is_expected_warning(record: tuple[str, int, str]) -> bool:
2924 2928
             return is_harmless_config_import_warning(
2925 2929
                 record
2926
-            ) or tests.warning_emitted(
2930
+            ) or tests.machinery.warning_emitted(
2927 2931
                 "An empty SERVICE is not supported by vault(1)", [record]
2928 2932
             )
2929 2933
 
2930
-        runner = tests.CliRunner(mix_stderr=False)
2934
+        runner = tests.machinery.CliRunner(mix_stderr=False)
2931 2935
         # TODO(the-13th-letter): Rewrite using parenthesized
2932 2936
         # with-statements.
2933 2937
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
2934 2938
         with contextlib.ExitStack() as stack:
2935 2939
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
2936 2940
             stack.enter_context(
2937
-                tests.isolated_vault_config(
2941
+                tests.machinery.pytest.isolated_vault_config(
2938 2942
                     monkeypatch=monkeypatch,
2939 2943
                     runner=runner,
2940 2944
                     vault_config={"services": {}},
2941 2945
                 )
2942 2946
             )
2943 2947
             monkeypatch.setattr(
2944
-                cli_helpers, "prompt_for_passphrase", tests.auto_prompt
2948
+                cli_helpers, "prompt_for_passphrase", tests.data.callables.auto_prompt
2945 2949
             )
2946 2950
             result = runner.invoke(
2947 2951
                 cli.derivepassphrase_vault,
... ...
@@ -2981,14 +2985,14 @@ class TestCLI:
2981 2985
         service: bool | None,
2982 2986
     ) -> None:
2983 2987
         """Incompatible options are detected."""
2984
-        runner = tests.CliRunner(mix_stderr=False)
2988
+        runner = tests.machinery.CliRunner(mix_stderr=False)
2985 2989
         # TODO(the-13th-letter): Rewrite using parenthesized
2986 2990
         # with-statements.
2987 2991
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
2988 2992
         with contextlib.ExitStack() as stack:
2989 2993
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
2990 2994
             stack.enter_context(
2991
-                tests.isolated_config(
2995
+                tests.machinery.pytest.isolated_config(
2992 2996
                     monkeypatch=monkeypatch,
2993 2997
                     runner=runner,
2994 2998
                 )
... ...
@@ -3010,14 +3014,14 @@ class TestCLI:
3010 3014
         config: Any,
3011 3015
     ) -> None:
3012 3016
         """Importing a configuration works."""
3013
-        runner = tests.CliRunner(mix_stderr=False)
3017
+        runner = tests.machinery.CliRunner(mix_stderr=False)
3014 3018
         # TODO(the-13th-letter): Rewrite using parenthesized
3015 3019
         # with-statements.
3016 3020
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
3017 3021
         with contextlib.ExitStack() as stack:
3018 3022
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
3019 3023
             stack.enter_context(
3020
-                tests.isolated_vault_config(
3024
+                tests.machinery.pytest.isolated_vault_config(
3021 3025
                     monkeypatch=monkeypatch,
3022 3026
                     runner=runner,
3023 3027
                     vault_config={"services": {}},
... ...
@@ -3047,18 +3051,18 @@ class TestCLI:
3047 3051
         ],
3048 3052
     )
3049 3053
     @hypothesis.given(
3050
-        conf=tests.smudged_vault_test_config(
3054
+        conf=tests.machinery.hypothesis.smudged_vault_test_config(
3051 3055
             strategies.sampled_from([
3052 3056
                 conf
3053
-                for conf in tests.TEST_CONFIGS
3054
-                if tests.is_valid_test_config(conf)
3057
+                for conf in tests.data.TEST_CONFIGS
3058
+                if tests.data.is_valid_test_config(conf)
3055 3059
             ])
3056 3060
         )
3057 3061
     )
3058 3062
     def test_213a_import_config_success(
3059 3063
         self,
3060 3064
         caplog: pytest.LogCaptureFixture,
3061
-        conf: tests.VaultTestConfig,
3065
+        conf: tests.data.VaultTestConfig,
3062 3066
     ) -> None:
3063 3067
         """Importing a smudged configuration works.
3064 3068
 
... ...
@@ -3070,14 +3074,14 @@ class TestCLI:
3070 3074
         _types.clean_up_falsy_vault_config_values(config2)
3071 3075
         # Reset caplog between hypothesis runs.
3072 3076
         caplog.clear()
3073
-        runner = tests.CliRunner(mix_stderr=False)
3077
+        runner = tests.machinery.CliRunner(mix_stderr=False)
3074 3078
         # TODO(the-13th-letter): Rewrite using parenthesized
3075 3079
         # with-statements.
3076 3080
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
3077 3081
         with contextlib.ExitStack() as stack:
3078 3082
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
3079 3083
             stack.enter_context(
3080
-                tests.isolated_vault_config(
3084
+                tests.machinery.pytest.isolated_vault_config(
3081 3085
                     monkeypatch=monkeypatch,
3082 3086
                     runner=runner,
3083 3087
                     vault_config={"services": {}},
... ...
@@ -3104,14 +3108,14 @@ class TestCLI:
3104 3108
         self,
3105 3109
     ) -> None:
3106 3110
         """Importing an invalid config fails."""
3107
-        runner = tests.CliRunner(mix_stderr=False)
3111
+        runner = tests.machinery.CliRunner(mix_stderr=False)
3108 3112
         # TODO(the-13th-letter): Rewrite using parenthesized
3109 3113
         # with-statements.
3110 3114
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
3111 3115
         with contextlib.ExitStack() as stack:
3112 3116
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
3113 3117
             stack.enter_context(
3114
-                tests.isolated_config(
3118
+                tests.machinery.pytest.isolated_config(
3115 3119
                     monkeypatch=monkeypatch,
3116 3120
                     runner=runner,
3117 3121
                 )
... ...
@@ -3130,14 +3134,14 @@ class TestCLI:
3130 3134
         self,
3131 3135
     ) -> None:
3132 3136
         """Importing an invalid config fails."""
3133
-        runner = tests.CliRunner(mix_stderr=False)
3137
+        runner = tests.machinery.CliRunner(mix_stderr=False)
3134 3138
         # TODO(the-13th-letter): Rewrite using parenthesized
3135 3139
         # with-statements.
3136 3140
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
3137 3141
         with contextlib.ExitStack() as stack:
3138 3142
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
3139 3143
             stack.enter_context(
3140
-                tests.isolated_config(
3144
+                tests.machinery.pytest.isolated_config(
3141 3145
                     monkeypatch=monkeypatch,
3142 3146
                     runner=runner,
3143 3147
                 )
... ...
@@ -3156,7 +3160,7 @@ class TestCLI:
3156 3160
         self,
3157 3161
     ) -> None:
3158 3162
         """Importing an invalid config fails."""
3159
-        runner = tests.CliRunner(mix_stderr=False)
3163
+        runner = tests.machinery.CliRunner(mix_stderr=False)
3160 3164
         # `isolated_vault_config` ensures the configuration is valid
3161 3165
         # JSON.  So, to pass an actual broken configuration, we must
3162 3166
         # open the configuration file ourselves afterwards, inside the
... ...
@@ -3168,7 +3172,7 @@ class TestCLI:
3168 3172
         with contextlib.ExitStack() as stack:
3169 3173
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
3170 3174
             stack.enter_context(
3171
-                tests.isolated_vault_config(
3175
+                tests.machinery.pytest.isolated_vault_config(
3172 3176
                     monkeypatch=monkeypatch,
3173 3177
                     runner=runner,
3174 3178
                     vault_config={"services": {}},
... ...
@@ -3197,14 +3201,14 @@ class TestCLI:
3197 3201
         config: Any,
3198 3202
     ) -> None:
3199 3203
         """Exporting a configuration works."""
3200
-        runner = tests.CliRunner(mix_stderr=False)
3204
+        runner = tests.machinery.CliRunner(mix_stderr=False)
3201 3205
         # TODO(the-13th-letter): Rewrite using parenthesized
3202 3206
         # with-statements.
3203 3207
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
3204 3208
         with contextlib.ExitStack() as stack:
3205 3209
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
3206 3210
             stack.enter_context(
3207
-                tests.isolated_vault_config(
3211
+                tests.machinery.pytest.isolated_vault_config(
3208 3212
                     monkeypatch=monkeypatch,
3209 3213
                     runner=runner,
3210 3214
                     vault_config=config,
... ...
@@ -3237,14 +3241,14 @@ class TestCLI:
3237 3241
         export_options: list[str],
3238 3242
     ) -> None:
3239 3243
         """Exporting the default, empty config works."""
3240
-        runner = tests.CliRunner(mix_stderr=False)
3244
+        runner = tests.machinery.CliRunner(mix_stderr=False)
3241 3245
         # TODO(the-13th-letter): Rewrite using parenthesized
3242 3246
         # with-statements.
3243 3247
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
3244 3248
         with contextlib.ExitStack() as stack:
3245 3249
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
3246 3250
             stack.enter_context(
3247
-                tests.isolated_config(
3251
+                tests.machinery.pytest.isolated_config(
3248 3252
                     monkeypatch=monkeypatch,
3249 3253
                     runner=runner,
3250 3254
                 )
... ...
@@ -3269,14 +3273,14 @@ class TestCLI:
3269 3273
         export_options: list[str],
3270 3274
     ) -> None:
3271 3275
         """Exporting an invalid config fails."""
3272
-        runner = tests.CliRunner(mix_stderr=False)
3276
+        runner = tests.machinery.CliRunner(mix_stderr=False)
3273 3277
         # TODO(the-13th-letter): Rewrite using parenthesized
3274 3278
         # with-statements.
3275 3279
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
3276 3280
         with contextlib.ExitStack() as stack:
3277 3281
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
3278 3282
             stack.enter_context(
3279
-                tests.isolated_vault_config(
3283
+                tests.machinery.pytest.isolated_vault_config(
3280 3284
                     monkeypatch=monkeypatch,
3281 3285
                     runner=runner,
3282 3286
                     vault_config={},
... ...
@@ -3298,14 +3302,14 @@ class TestCLI:
3298 3302
         export_options: list[str],
3299 3303
     ) -> None:
3300 3304
         """Exporting an invalid config fails."""
3301
-        runner = tests.CliRunner(mix_stderr=False)
3305
+        runner = tests.machinery.CliRunner(mix_stderr=False)
3302 3306
         # TODO(the-13th-letter): Rewrite using parenthesized
3303 3307
         # with-statements.
3304 3308
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
3305 3309
         with contextlib.ExitStack() as stack:
3306 3310
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
3307 3311
             stack.enter_context(
3308
-                tests.isolated_config(
3312
+                tests.machinery.pytest.isolated_config(
3309 3313
                     monkeypatch=monkeypatch,
3310 3314
                     runner=runner,
3311 3315
                 )
... ...
@@ -3329,14 +3333,14 @@ class TestCLI:
3329 3333
         export_options: list[str],
3330 3334
     ) -> None:
3331 3335
         """Exporting an invalid config fails."""
3332
-        runner = tests.CliRunner(mix_stderr=False)
3336
+        runner = tests.machinery.CliRunner(mix_stderr=False)
3333 3337
         # TODO(the-13th-letter): Rewrite using parenthesized
3334 3338
         # with-statements.
3335 3339
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
3336 3340
         with contextlib.ExitStack() as stack:
3337 3341
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
3338 3342
             stack.enter_context(
3339
-                tests.isolated_config(
3343
+                tests.machinery.pytest.isolated_config(
3340 3344
                     monkeypatch=monkeypatch,
3341 3345
                     runner=runner,
3342 3346
                 )
... ...
@@ -3352,21 +3356,21 @@ class TestCLI:
3352 3356
             "expected error exit and known error message"
3353 3357
         )
3354 3358
 
3355
-    @tests.skip_if_on_the_annoying_os
3359
+    @tests.machinery.pytest.skip_if_on_the_annoying_os
3356 3360
     @Parametrize.EXPORT_FORMAT_OPTIONS
3357 3361
     def test_214e_export_settings_settings_directory_not_a_directory(
3358 3362
         self,
3359 3363
         export_options: list[str],
3360 3364
     ) -> None:
3361 3365
         """Exporting an invalid config fails."""
3362
-        runner = tests.CliRunner(mix_stderr=False)
3366
+        runner = tests.machinery.CliRunner(mix_stderr=False)
3363 3367
         # TODO(the-13th-letter): Rewrite using parenthesized
3364 3368
         # with-statements.
3365 3369
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
3366 3370
         with contextlib.ExitStack() as stack:
3367 3371
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
3368 3372
             stack.enter_context(
3369
-                tests.isolated_config(
3373
+                tests.machinery.pytest.isolated_config(
3370 3374
                     monkeypatch=monkeypatch,
3371 3375
                     runner=runner,
3372 3376
                 )
... ...
@@ -3417,14 +3421,14 @@ class TestCLI:
3417 3421
             if notes_placement == "before"
3418 3422
             else f"{result_phrase}\n\n{notes}\n\n"
3419 3423
         )
3420
-        runner = tests.CliRunner(mix_stderr=True)
3424
+        runner = tests.machinery.CliRunner(mix_stderr=True)
3421 3425
         # TODO(the-13th-letter): Rewrite using parenthesized
3422 3426
         # with-statements.
3423 3427
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
3424 3428
         with contextlib.ExitStack() as stack:
3425 3429
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
3426 3430
             stack.enter_context(
3427
-                tests.isolated_vault_config(
3431
+                tests.machinery.pytest.isolated_vault_config(
3428 3432
                     monkeypatch=monkeypatch,
3429 3433
                     runner=runner,
3430 3434
                     vault_config=vault_config,
... ...
@@ -3470,14 +3474,14 @@ class TestCLI:
3470 3474
 """
3471 3475
         # Reset caplog between hypothesis runs.
3472 3476
         caplog.clear()
3473
-        runner = tests.CliRunner(mix_stderr=False)
3477
+        runner = tests.machinery.CliRunner(mix_stderr=False)
3474 3478
         # TODO(the-13th-letter): Rewrite using parenthesized
3475 3479
         # with-statements.
3476 3480
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
3477 3481
         with contextlib.ExitStack() as stack:
3478 3482
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
3479 3483
             stack.enter_context(
3480
-                tests.isolated_vault_config(
3484
+                tests.machinery.pytest.isolated_vault_config(
3481 3485
                     monkeypatch=monkeypatch,
3482 3486
                     runner=runner,
3483 3487
                     vault_config={
... ...
@@ -3509,7 +3513,7 @@ class TestCLI:
3509 3513
             )
3510 3514
             assert result.clean_exit(), "expected clean exit"
3511 3515
             assert all(map(is_warning_line, result.stderr.splitlines(True)))
3512
-            assert modern_editor_interface or tests.warning_emitted(
3516
+            assert modern_editor_interface or tests.machinery.warning_emitted(
3513 3517
                 "A backup copy of the old notes was saved",
3514 3518
                 caplog.record_tuples,
3515 3519
             ), "expected known warning message in stderr"
... ...
@@ -3560,14 +3564,14 @@ class TestCLI:
3560 3564
             return "       " + notes.strip() + "\n\n\n\n\n\n"
3561 3565
 
3562 3566
         edit_funcs = {"empty": empty, "space": space}
3563
-        runner = tests.CliRunner(mix_stderr=False)
3567
+        runner = tests.machinery.CliRunner(mix_stderr=False)
3564 3568
         # TODO(the-13th-letter): Rewrite using parenthesized
3565 3569
         # with-statements.
3566 3570
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
3567 3571
         with contextlib.ExitStack() as stack:
3568 3572
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
3569 3573
             stack.enter_context(
3570
-                tests.isolated_vault_config(
3574
+                tests.machinery.pytest.isolated_vault_config(
3571 3575
                     monkeypatch=monkeypatch,
3572 3576
                     runner=runner,
3573 3577
                     vault_config={
... ...
@@ -3649,14 +3653,14 @@ class TestCLI:
3649 3653
         hypothesis.assume(str(notes_marker) not in notes.strip())
3650 3654
         # Reset caplog between hypothesis runs.
3651 3655
         caplog.clear()
3652
-        runner = tests.CliRunner(mix_stderr=False)
3656
+        runner = tests.machinery.CliRunner(mix_stderr=False)
3653 3657
         # TODO(the-13th-letter): Rewrite using parenthesized
3654 3658
         # with-statements.
3655 3659
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
3656 3660
         with contextlib.ExitStack() as stack:
3657 3661
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
3658 3662
             stack.enter_context(
3659
-                tests.isolated_vault_config(
3663
+                tests.machinery.pytest.isolated_vault_config(
3660 3664
                     monkeypatch=monkeypatch,
3661 3665
                     runner=runner,
3662 3666
                     vault_config={
... ...
@@ -3690,7 +3694,7 @@ class TestCLI:
3690 3694
             assert not result.stderr or all(
3691 3695
                 map(is_warning_line, result.stderr.splitlines(True))
3692 3696
             )
3693
-            assert not caplog.record_tuples or tests.warning_emitted(
3697
+            assert not caplog.record_tuples or tests.machinery.warning_emitted(
3694 3698
                 "A backup copy of the old notes was saved",
3695 3699
                 caplog.record_tuples,
3696 3700
             ), "expected known warning message in stderr"
... ...
@@ -3726,14 +3730,14 @@ class TestCLI:
3726 3730
         Aborting is only supported with the modern editor interface.
3727 3731
 
3728 3732
         """
3729
-        runner = tests.CliRunner(mix_stderr=False)
3733
+        runner = tests.machinery.CliRunner(mix_stderr=False)
3730 3734
         # TODO(the-13th-letter): Rewrite using parenthesized
3731 3735
         # with-statements.
3732 3736
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
3733 3737
         with contextlib.ExitStack() as stack:
3734 3738
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
3735 3739
             stack.enter_context(
3736
-                tests.isolated_vault_config(
3740
+                tests.machinery.pytest.isolated_vault_config(
3737 3741
                     monkeypatch=monkeypatch,
3738 3742
                     runner=runner,
3739 3743
                     vault_config={
... ...
@@ -3774,14 +3778,14 @@ class TestCLI:
3774 3778
         Aborting is only supported with the modern editor interface.
3775 3779
 
3776 3780
         """
3777
-        runner = tests.CliRunner(mix_stderr=False)
3781
+        runner = tests.machinery.CliRunner(mix_stderr=False)
3778 3782
         # TODO(the-13th-letter): Rewrite using parenthesized
3779 3783
         # with-statements.
3780 3784
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
3781 3785
         with contextlib.ExitStack() as stack:
3782 3786
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
3783 3787
             stack.enter_context(
3784
-                tests.isolated_vault_config(
3788
+                tests.machinery.pytest.isolated_vault_config(
3785 3789
                     monkeypatch=monkeypatch,
3786 3790
                     runner=runner,
3787 3791
                     vault_config={
... ...
@@ -3845,14 +3849,14 @@ class TestCLI:
3845 3849
         }
3846 3850
         # Reset caplog between hypothesis runs.
3847 3851
         caplog.clear()
3848
-        runner = tests.CliRunner(mix_stderr=False)
3852
+        runner = tests.machinery.CliRunner(mix_stderr=False)
3849 3853
         # TODO(the-13th-letter): Rewrite using parenthesized
3850 3854
         # with-statements.
3851 3855
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
3852 3856
         with contextlib.ExitStack() as stack:
3853 3857
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
3854 3858
             stack.enter_context(
3855
-                tests.isolated_vault_config(
3859
+                tests.machinery.pytest.isolated_vault_config(
3856 3860
                     monkeypatch=monkeypatch,
3857 3861
                     runner=runner,
3858 3862
                     vault_config=vault_config,
... ...
@@ -3893,7 +3897,7 @@ class TestCLI:
3893 3897
                 for line in result.stderr.splitlines(True)
3894 3898
                 if line.startswith(f"{cli.PROG_NAME}: ")
3895 3899
             )
3896
-            assert tests.warning_emitted(
3900
+            assert tests.machinery.warning_emitted(
3897 3901
                 "Specifying --notes without --config is ineffective.  "
3898 3902
                 "No notes will be edited.",
3899 3903
                 caplog.record_tuples,
... ...
@@ -3922,21 +3926,21 @@ class TestCLI:
3922 3926
         the config more readable.
3923 3927
 
3924 3928
         """
3925
-        runner = tests.CliRunner(mix_stderr=False)
3929
+        runner = tests.machinery.CliRunner(mix_stderr=False)
3926 3930
         # TODO(the-13th-letter): Rewrite using parenthesized
3927 3931
         # with-statements.
3928 3932
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
3929 3933
         with contextlib.ExitStack() as stack:
3930 3934
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
3931 3935
             stack.enter_context(
3932
-                tests.isolated_vault_config(
3936
+                tests.machinery.pytest.isolated_vault_config(
3933 3937
                     monkeypatch=monkeypatch,
3934 3938
                     runner=runner,
3935 3939
                     vault_config={"global": {"phrase": "abc"}, "services": {}},
3936 3940
                 )
3937 3941
             )
3938 3942
             monkeypatch.setattr(
3939
-                cli_helpers, "get_suitable_ssh_keys", tests.suitable_ssh_keys
3943
+                cli_helpers, "get_suitable_ssh_keys", tests.data.callables.suitable_ssh_keys
3940 3944
             )
3941 3945
             result = runner.invoke(
3942 3946
                 cli.derivepassphrase_vault,
... ...
@@ -3962,21 +3966,21 @@ class TestCLI:
3962 3966
         err_text: str,
3963 3967
     ) -> None:
3964 3968
         """Storing invalid settings via `--config` fails."""
3965
-        runner = tests.CliRunner(mix_stderr=False)
3969
+        runner = tests.machinery.CliRunner(mix_stderr=False)
3966 3970
         # TODO(the-13th-letter): Rewrite using parenthesized
3967 3971
         # with-statements.
3968 3972
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
3969 3973
         with contextlib.ExitStack() as stack:
3970 3974
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
3971 3975
             stack.enter_context(
3972
-                tests.isolated_vault_config(
3976
+                tests.machinery.pytest.isolated_vault_config(
3973 3977
                     monkeypatch=monkeypatch,
3974 3978
                     runner=runner,
3975 3979
                     vault_config={"global": {"phrase": "abc"}, "services": {}},
3976 3980
                 )
3977 3981
             )
3978 3982
             monkeypatch.setattr(
3979
-                cli_helpers, "get_suitable_ssh_keys", tests.suitable_ssh_keys
3983
+                cli_helpers, "get_suitable_ssh_keys", tests.data.callables.suitable_ssh_keys
3980 3984
             )
3981 3985
             result = runner.invoke(
3982 3986
                 cli.derivepassphrase_vault,
... ...
@@ -3990,18 +3994,18 @@ class TestCLI:
3990 3994
 
3991 3995
     def test_225a_store_config_fail_manual_no_ssh_key_selection(
3992 3996
         self,
3993
-        running_ssh_agent: tests.RunningSSHAgentInfo,
3997
+        running_ssh_agent: tests.data.RunningSSHAgentInfo,
3994 3998
     ) -> None:
3995 3999
         """Not selecting an SSH key during `--config --key` fails."""
3996 4000
         del running_ssh_agent
3997
-        runner = tests.CliRunner(mix_stderr=False)
4001
+        runner = tests.machinery.CliRunner(mix_stderr=False)
3998 4002
         # TODO(the-13th-letter): Rewrite using parenthesized
3999 4003
         # with-statements.
4000 4004
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
4001 4005
         with contextlib.ExitStack() as stack:
4002 4006
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
4003 4007
             stack.enter_context(
4004
-                tests.isolated_vault_config(
4008
+                tests.machinery.pytest.isolated_vault_config(
4005 4009
                     monkeypatch=monkeypatch,
4006 4010
                     runner=runner,
4007 4011
                     vault_config={"global": {"phrase": "abc"}, "services": {}},
... ...
@@ -4017,7 +4021,7 @@ class TestCLI:
4017 4021
             # Also patch the list of suitable SSH keys, lest we be at
4018 4022
             # the mercy of whatever SSH agent may be running.
4019 4023
             monkeypatch.setattr(
4020
-                cli_helpers, "get_suitable_ssh_keys", tests.suitable_ssh_keys
4024
+                cli_helpers, "get_suitable_ssh_keys", tests.data.callables.suitable_ssh_keys
4021 4025
             )
4022 4026
             result = runner.invoke(
4023 4027
                 cli.derivepassphrase_vault,
... ...
@@ -4030,18 +4034,18 @@ class TestCLI:
4030 4034
 
4031 4035
     def test_225b_store_config_fail_manual_no_ssh_agent(
4032 4036
         self,
4033
-        running_ssh_agent: tests.RunningSSHAgentInfo,
4037
+        running_ssh_agent: tests.data.RunningSSHAgentInfo,
4034 4038
     ) -> None:
4035 4039
         """Not running an SSH agent during `--config --key` fails."""
4036 4040
         del running_ssh_agent
4037
-        runner = tests.CliRunner(mix_stderr=False)
4041
+        runner = tests.machinery.CliRunner(mix_stderr=False)
4038 4042
         # TODO(the-13th-letter): Rewrite using parenthesized
4039 4043
         # with-statements.
4040 4044
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
4041 4045
         with contextlib.ExitStack() as stack:
4042 4046
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
4043 4047
             stack.enter_context(
4044
-                tests.isolated_vault_config(
4048
+                tests.machinery.pytest.isolated_vault_config(
4045 4049
                     monkeypatch=monkeypatch,
4046 4050
                     runner=runner,
4047 4051
                     vault_config={"global": {"phrase": "abc"}, "services": {}},
... ...
@@ -4059,18 +4063,18 @@ class TestCLI:
4059 4063
 
4060 4064
     def test_225c_store_config_fail_manual_bad_ssh_agent_connection(
4061 4065
         self,
4062
-        running_ssh_agent: tests.RunningSSHAgentInfo,
4066
+        running_ssh_agent: tests.data.RunningSSHAgentInfo,
4063 4067
     ) -> None:
4064 4068
         """Not running a reachable SSH agent during `--config --key` fails."""
4065 4069
         running_ssh_agent.require_external_address()
4066
-        runner = tests.CliRunner(mix_stderr=False)
4070
+        runner = tests.machinery.CliRunner(mix_stderr=False)
4067 4071
         # TODO(the-13th-letter): Rewrite using parenthesized
4068 4072
         # with-statements.
4069 4073
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
4070 4074
         with contextlib.ExitStack() as stack:
4071 4075
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
4072 4076
             stack.enter_context(
4073
-                tests.isolated_vault_config(
4077
+                tests.machinery.pytest.isolated_vault_config(
4074 4078
                     monkeypatch=monkeypatch,
4075 4079
                     runner=runner,
4076 4080
                     vault_config={"global": {"phrase": "abc"}, "services": {}},
... ...
@@ -4093,20 +4097,20 @@ class TestCLI:
4093 4097
         try_race_free_implementation: bool,
4094 4098
     ) -> None:
4095 4099
         """Using a read-only configuration file with `--config` fails."""
4096
-        runner = tests.CliRunner(mix_stderr=False)
4100
+        runner = tests.machinery.CliRunner(mix_stderr=False)
4097 4101
         # TODO(the-13th-letter): Rewrite using parenthesized
4098 4102
         # with-statements.
4099 4103
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
4100 4104
         with contextlib.ExitStack() as stack:
4101 4105
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
4102 4106
             stack.enter_context(
4103
-                tests.isolated_vault_config(
4107
+                tests.machinery.pytest.isolated_vault_config(
4104 4108
                     monkeypatch=monkeypatch,
4105 4109
                     runner=runner,
4106 4110
                     vault_config={"global": {"phrase": "abc"}, "services": {}},
4107 4111
                 )
4108 4112
             )
4109
-            tests.make_file_readonly(
4113
+            tests.data.callables.make_file_readonly(
4110 4114
                 cli_helpers.config_filename(subsystem="vault"),
4111 4115
                 try_race_free_implementation=try_race_free_implementation,
4112 4116
             )
... ...
@@ -4123,14 +4127,14 @@ class TestCLI:
4123 4127
         self,
4124 4128
     ) -> None:
4125 4129
         """OS-erroring with `--config` fails."""
4126
-        runner = tests.CliRunner(mix_stderr=False)
4130
+        runner = tests.machinery.CliRunner(mix_stderr=False)
4127 4131
         # TODO(the-13th-letter): Rewrite using parenthesized
4128 4132
         # with-statements.
4129 4133
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
4130 4134
         with contextlib.ExitStack() as stack:
4131 4135
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
4132 4136
             stack.enter_context(
4133
-                tests.isolated_vault_config(
4137
+                tests.machinery.pytest.isolated_vault_config(
4134 4138
                     monkeypatch=monkeypatch,
4135 4139
                     runner=runner,
4136 4140
                     vault_config={"global": {"phrase": "abc"}, "services": {}},
... ...
@@ -4156,14 +4160,14 @@ class TestCLI:
4156 4160
         self,
4157 4161
     ) -> None:
4158 4162
         """Issuing conflicting settings to `--config` fails."""
4159
-        runner = tests.CliRunner(mix_stderr=False)
4163
+        runner = tests.machinery.CliRunner(mix_stderr=False)
4160 4164
         # TODO(the-13th-letter): Rewrite using parenthesized
4161 4165
         # with-statements.
4162 4166
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
4163 4167
         with contextlib.ExitStack() as stack:
4164 4168
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
4165 4169
             stack.enter_context(
4166
-                tests.isolated_vault_config(
4170
+                tests.machinery.pytest.isolated_vault_config(
4167 4171
                     monkeypatch=monkeypatch,
4168 4172
                     runner=runner,
4169 4173
                     vault_config={"global": {"phrase": "abc"}, "services": {}},
... ...
@@ -4186,18 +4190,18 @@ class TestCLI:
4186 4190
 
4187 4191
     def test_225g_store_config_fail_manual_ssh_agent_no_keys_loaded(
4188 4192
         self,
4189
-        running_ssh_agent: tests.RunningSSHAgentInfo,
4193
+        running_ssh_agent: tests.data.RunningSSHAgentInfo,
4190 4194
     ) -> None:
4191 4195
         """Not holding any SSH keys during `--config --key` fails."""
4192 4196
         del running_ssh_agent
4193
-        runner = tests.CliRunner(mix_stderr=False)
4197
+        runner = tests.machinery.CliRunner(mix_stderr=False)
4194 4198
         # TODO(the-13th-letter): Rewrite using parenthesized
4195 4199
         # with-statements.
4196 4200
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
4197 4201
         with contextlib.ExitStack() as stack:
4198 4202
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
4199 4203
             stack.enter_context(
4200
-                tests.isolated_vault_config(
4204
+                tests.machinery.pytest.isolated_vault_config(
4201 4205
                     monkeypatch=monkeypatch,
4202 4206
                     runner=runner,
4203 4207
                     vault_config={"global": {"phrase": "abc"}, "services": {}},
... ...
@@ -4222,18 +4226,18 @@ class TestCLI:
4222 4226
 
4223 4227
     def test_225h_store_config_fail_manual_ssh_agent_runtime_error(
4224 4228
         self,
4225
-        running_ssh_agent: tests.RunningSSHAgentInfo,
4229
+        running_ssh_agent: tests.data.RunningSSHAgentInfo,
4226 4230
     ) -> None:
4227 4231
         """The SSH agent erroring during `--config --key` fails."""
4228 4232
         del running_ssh_agent
4229
-        runner = tests.CliRunner(mix_stderr=False)
4233
+        runner = tests.machinery.CliRunner(mix_stderr=False)
4230 4234
         # TODO(the-13th-letter): Rewrite using parenthesized
4231 4235
         # with-statements.
4232 4236
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
4233 4237
         with contextlib.ExitStack() as stack:
4234 4238
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
4235 4239
             stack.enter_context(
4236
-                tests.isolated_vault_config(
4240
+                tests.machinery.pytest.isolated_vault_config(
4237 4241
                     monkeypatch=monkeypatch,
4238 4242
                     runner=runner,
4239 4243
                     vault_config={"global": {"phrase": "abc"}, "services": {}},
... ...
@@ -4255,18 +4259,18 @@ class TestCLI:
4255 4259
 
4256 4260
     def test_225i_store_config_fail_manual_ssh_agent_refuses(
4257 4261
         self,
4258
-        running_ssh_agent: tests.RunningSSHAgentInfo,
4262
+        running_ssh_agent: tests.data.RunningSSHAgentInfo,
4259 4263
     ) -> None:
4260 4264
         """The SSH agent refusing during `--config --key` fails."""
4261 4265
         del running_ssh_agent
4262
-        runner = tests.CliRunner(mix_stderr=False)
4266
+        runner = tests.machinery.CliRunner(mix_stderr=False)
4263 4267
         # TODO(the-13th-letter): Rewrite using parenthesized
4264 4268
         # with-statements.
4265 4269
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
4266 4270
         with contextlib.ExitStack() as stack:
4267 4271
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
4268 4272
             stack.enter_context(
4269
-                tests.isolated_vault_config(
4273
+                tests.machinery.pytest.isolated_vault_config(
4270 4274
                     monkeypatch=monkeypatch,
4271 4275
                     runner=runner,
4272 4276
                     vault_config={"global": {"phrase": "abc"}, "services": {}},
... ...
@@ -4290,14 +4294,14 @@ class TestCLI:
4290 4294
 
4291 4295
     def test_226_no_arguments(self) -> None:
4292 4296
         """Calling `derivepassphrase vault` without any arguments fails."""
4293
-        runner = tests.CliRunner(mix_stderr=False)
4297
+        runner = tests.machinery.CliRunner(mix_stderr=False)
4294 4298
         # TODO(the-13th-letter): Rewrite using parenthesized
4295 4299
         # with-statements.
4296 4300
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
4297 4301
         with contextlib.ExitStack() as stack:
4298 4302
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
4299 4303
             stack.enter_context(
4300
-                tests.isolated_config(
4304
+                tests.machinery.pytest.isolated_config(
4301 4305
                     monkeypatch=monkeypatch,
4302 4306
                     runner=runner,
4303 4307
                 )
... ...
@@ -4313,14 +4317,14 @@ class TestCLI:
4313 4317
         self,
4314 4318
     ) -> None:
4315 4319
         """Deriving a passphrase without a passphrase or key fails."""
4316
-        runner = tests.CliRunner(mix_stderr=False)
4320
+        runner = tests.machinery.CliRunner(mix_stderr=False)
4317 4321
         # TODO(the-13th-letter): Rewrite using parenthesized
4318 4322
         # with-statements.
4319 4323
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
4320 4324
         with contextlib.ExitStack() as stack:
4321 4325
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
4322 4326
             stack.enter_context(
4323
-                tests.isolated_config(
4327
+                tests.machinery.pytest.isolated_config(
4324 4328
                     monkeypatch=monkeypatch,
4325 4329
                     runner=runner,
4326 4330
                 )
... ...
@@ -4344,14 +4348,14 @@ class TestCLI:
4344 4348
         [issue #6]: https://github.com/the-13th-letter/derivepassphrase/issues/6
4345 4349
 
4346 4350
         """
4347
-        runner = tests.CliRunner(mix_stderr=False)
4351
+        runner = tests.machinery.CliRunner(mix_stderr=False)
4348 4352
         # TODO(the-13th-letter): Rewrite using parenthesized
4349 4353
         # with-statements.
4350 4354
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
4351 4355
         with contextlib.ExitStack() as stack:
4352 4356
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
4353 4357
             stack.enter_context(
4354
-                tests.isolated_config(
4358
+                tests.machinery.pytest.isolated_config(
4355 4359
                     monkeypatch=monkeypatch,
4356 4360
                     runner=runner,
4357 4361
                 )
... ...
@@ -4390,14 +4394,14 @@ class TestCLI:
4390 4394
         [issue #6]: https://github.com/the-13th-letter/derivepassphrase/issues/6
4391 4395
 
4392 4396
         """
4393
-        runner = tests.CliRunner(mix_stderr=False)
4397
+        runner = tests.machinery.CliRunner(mix_stderr=False)
4394 4398
         # TODO(the-13th-letter): Rewrite using parenthesized
4395 4399
         # with-statements.
4396 4400
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
4397 4401
         with contextlib.ExitStack() as stack:
4398 4402
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
4399 4403
             stack.enter_context(
4400
-                tests.isolated_config(
4404
+                tests.machinery.pytest.isolated_config(
4401 4405
                     monkeypatch=monkeypatch,
4402 4406
                     runner=runner,
4403 4407
                 )
... ...
@@ -4429,14 +4433,14 @@ class TestCLI:
4429 4433
         self,
4430 4434
     ) -> None:
4431 4435
         """Storing the configuration reacts even to weird errors."""
4432
-        runner = tests.CliRunner(mix_stderr=False)
4436
+        runner = tests.machinery.CliRunner(mix_stderr=False)
4433 4437
         # TODO(the-13th-letter): Rewrite using parenthesized
4434 4438
         # with-statements.
4435 4439
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
4436 4440
         with contextlib.ExitStack() as stack:
4437 4441
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
4438 4442
             stack.enter_context(
4439
-                tests.isolated_config(
4443
+                tests.machinery.pytest.isolated_config(
4440 4444
                     monkeypatch=monkeypatch,
4441 4445
                     runner=runner,
4442 4446
                 )
... ...
@@ -4468,14 +4472,14 @@ class TestCLI:
4468 4472
         warning_message: str,
4469 4473
     ) -> None:
4470 4474
         """Using unnormalized Unicode passphrases warns."""
4471
-        runner = tests.CliRunner(mix_stderr=False)
4475
+        runner = tests.machinery.CliRunner(mix_stderr=False)
4472 4476
         # TODO(the-13th-letter): Rewrite using parenthesized
4473 4477
         # with-statements.
4474 4478
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
4475 4479
         with contextlib.ExitStack() as stack:
4476 4480
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
4477 4481
             stack.enter_context(
4478
-                tests.isolated_vault_config(
4482
+                tests.machinery.pytest.isolated_vault_config(
4479 4483
                     monkeypatch=monkeypatch,
4480 4484
                     runner=runner,
4481 4485
                     vault_config={
... ...
@@ -4493,7 +4497,7 @@ class TestCLI:
4493 4497
                 input=input,
4494 4498
             )
4495 4499
         assert result.clean_exit(), "expected clean exit"
4496
-        assert tests.warning_emitted(warning_message, caplog.record_tuples), (
4500
+        assert tests.machinery.warning_emitted(warning_message, caplog.record_tuples), (
4497 4501
             "expected known warning message in stderr"
4498 4502
         )
4499 4503
 
... ...
@@ -4506,14 +4510,14 @@ class TestCLI:
4506 4510
         error_message: str,
4507 4511
     ) -> None:
4508 4512
         """Using unknown Unicode normalization forms fails."""
4509
-        runner = tests.CliRunner(mix_stderr=False)
4513
+        runner = tests.machinery.CliRunner(mix_stderr=False)
4510 4514
         # TODO(the-13th-letter): Rewrite using parenthesized
4511 4515
         # with-statements.
4512 4516
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
4513 4517
         with contextlib.ExitStack() as stack:
4514 4518
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
4515 4519
             stack.enter_context(
4516
-                tests.isolated_vault_config(
4520
+                tests.machinery.pytest.isolated_vault_config(
4517 4521
                     monkeypatch=monkeypatch,
4518 4522
                     runner=runner,
4519 4523
                     vault_config={
... ...
@@ -4543,14 +4547,14 @@ class TestCLI:
4543 4547
         command_line: list[str],
4544 4548
     ) -> None:
4545 4549
         """Using unknown Unicode normalization forms in the config fails."""
4546
-        runner = tests.CliRunner(mix_stderr=False)
4550
+        runner = tests.machinery.CliRunner(mix_stderr=False)
4547 4551
         # TODO(the-13th-letter): Rewrite using parenthesized
4548 4552
         # with-statements.
4549 4553
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
4550 4554
         with contextlib.ExitStack() as stack:
4551 4555
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
4552 4556
             stack.enter_context(
4553
-                tests.isolated_vault_config(
4557
+                tests.machinery.pytest.isolated_vault_config(
4554 4558
                     monkeypatch=monkeypatch,
4555 4559
                     runner=runner,
4556 4560
                     vault_config={
... ...
@@ -4583,14 +4587,14 @@ class TestCLI:
4583 4587
         self,
4584 4588
     ) -> None:
4585 4589
         """Loading a user configuration file in an invalid format fails."""
4586
-        runner = tests.CliRunner(mix_stderr=False)
4590
+        runner = tests.machinery.CliRunner(mix_stderr=False)
4587 4591
         # TODO(the-13th-letter): Rewrite using parenthesized
4588 4592
         # with-statements.
4589 4593
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
4590 4594
         with contextlib.ExitStack() as stack:
4591 4595
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
4592 4596
             stack.enter_context(
4593
-                tests.isolated_vault_config(
4597
+                tests.machinery.pytest.isolated_vault_config(
4594 4598
                     monkeypatch=monkeypatch,
4595 4599
                     runner=runner,
4596 4600
                     vault_config={"services": {}},
... ...
@@ -4611,14 +4615,14 @@ class TestCLI:
4611 4615
         self,
4612 4616
     ) -> None:
4613 4617
         """Loading a user configuration file in an invalid format fails."""
4614
-        runner = tests.CliRunner(mix_stderr=False)
4618
+        runner = tests.machinery.CliRunner(mix_stderr=False)
4615 4619
         # TODO(the-13th-letter): Rewrite using parenthesized
4616 4620
         # with-statements.
4617 4621
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
4618 4622
         with contextlib.ExitStack() as stack:
4619 4623
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
4620 4624
             stack.enter_context(
4621
-                tests.isolated_vault_config(
4625
+                tests.machinery.pytest.isolated_vault_config(
4622 4626
                     monkeypatch=monkeypatch,
4623 4627
                     runner=runner,
4624 4628
                     vault_config={"services": {}},
... ...
@@ -4645,14 +4649,14 @@ class TestCLI:
4645 4649
         caplog: pytest.LogCaptureFixture,
4646 4650
     ) -> None:
4647 4651
         """Querying the SSH agent without `AF_UNIX` support fails."""
4648
-        runner = tests.CliRunner(mix_stderr=False)
4652
+        runner = tests.machinery.CliRunner(mix_stderr=False)
4649 4653
         # TODO(the-13th-letter): Rewrite using parenthesized
4650 4654
         # with-statements.
4651 4655
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
4652 4656
         with contextlib.ExitStack() as stack:
4653 4657
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
4654 4658
             stack.enter_context(
4655
-                tests.isolated_vault_config(
4659
+                tests.machinery.pytest.isolated_vault_config(
4656 4660
                     monkeypatch=monkeypatch,
4657 4661
                     runner=runner,
4658 4662
                     vault_config={"global": {"phrase": "abc"}, "services": {}},
... ...
@@ -4673,7 +4677,7 @@ class TestCLI:
4673 4677
         assert result.error_exit(
4674 4678
             error="does not support communicating with it"
4675 4679
         ), "expected error exit and known error message"
4676
-        assert tests.warning_emitted(
4680
+        assert tests.machinery.warning_emitted(
4677 4681
             "Cannot connect to an SSH agent via UNIX domain sockets",
4678 4682
             caplog.record_tuples,
4679 4683
         ), "expected known warning message in stderr"
... ...
@@ -4688,14 +4692,14 @@ class TestCLIUtils:
4688 4692
         config: Any,
4689 4693
     ) -> None:
4690 4694
         """[`cli_helpers.load_config`][] works for valid configurations."""
4691
-        runner = tests.CliRunner(mix_stderr=False)
4695
+        runner = tests.machinery.CliRunner(mix_stderr=False)
4692 4696
         # TODO(the-13th-letter): Rewrite using parenthesized
4693 4697
         # with-statements.
4694 4698
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
4695 4699
         with contextlib.ExitStack() as stack:
4696 4700
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
4697 4701
             stack.enter_context(
4698
-                tests.isolated_vault_config(
4702
+                tests.machinery.pytest.isolated_vault_config(
4699 4703
                     monkeypatch=monkeypatch,
4700 4704
                     runner=runner,
4701 4705
                     vault_config=config,
... ...
@@ -4710,14 +4714,14 @@ class TestCLIUtils:
4710 4714
         self,
4711 4715
     ) -> None:
4712 4716
         """[`cli_helpers.save_config`][] fails for bad configurations."""
4713
-        runner = tests.CliRunner(mix_stderr=False)
4717
+        runner = tests.machinery.CliRunner(mix_stderr=False)
4714 4718
         # TODO(the-13th-letter): Rewrite using parenthesized
4715 4719
         # with-statements.
4716 4720
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
4717 4721
         with contextlib.ExitStack() as stack:
4718 4722
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
4719 4723
             stack.enter_context(
4720
-                tests.isolated_vault_config(
4724
+                tests.machinery.pytest.isolated_vault_config(
4721 4725
                     monkeypatch=monkeypatch,
4722 4726
                     runner=runner,
4723 4727
                     vault_config={},
... ...
@@ -4760,7 +4764,7 @@ class TestCLIUtils:
4760 4764
             click.echo(items[index])
4761 4765
             click.echo("(Note: Vikings strictly optional.)")
4762 4766
 
4763
-        runner = tests.CliRunner(mix_stderr=True)
4767
+        runner = tests.machinery.CliRunner(mix_stderr=True)
4764 4768
         result = runner.invoke(driver, [], input="9")
4765 4769
         assert result.clean_exit(
4766 4770
             output="""\
... ...
@@ -4857,7 +4861,7 @@ Your selection? (1-10, leave empty to abort): """,
4857 4861
             else:
4858 4862
                 click.echo("Great!")
4859 4863
 
4860
-        runner = tests.CliRunner(mix_stderr=True)
4864
+        runner = tests.machinery.CliRunner(mix_stderr=True)
4861 4865
         result = runner.invoke(
4862 4866
             driver, ["Will replace with spam. Confirm, y/n?"], input="y"
4863 4867
         )
... ...
@@ -5072,14 +5076,14 @@ Will replace with spam, okay? (Please say "y" or "n".): Boo.
5072 5076
                 config, outfile=outfile, prog_name_list=prog_name_list
5073 5077
             )
5074 5078
             script = outfile.getvalue()
5075
-        runner = tests.CliRunner(mix_stderr=False)
5079
+        runner = tests.machinery.CliRunner(mix_stderr=False)
5076 5080
         # TODO(the-13th-letter): Rewrite using parenthesized
5077 5081
         # with-statements.
5078 5082
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
5079 5083
         with contextlib.ExitStack() as stack:
5080 5084
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
5081 5085
             stack.enter_context(
5082
-                tests.isolated_vault_config(
5086
+                tests.machinery.pytest.isolated_vault_config(
5083 5087
                     monkeypatch=monkeypatch,
5084 5088
                     runner=runner,
5085 5089
                     vault_config={"services": {}},
... ...
@@ -5090,7 +5094,7 @@ Will replace with spam, okay? (Please say "y" or "n".): Boo.
5090 5094
             assert cli_helpers.load_config() == config
5091 5095
 
5092 5096
     @hypothesis.given(
5093
-        global_config_settable=tests.vault_full_service_config(),
5097
+        global_config_settable=tests.machinery.hypothesis.vault_full_service_config(),
5094 5098
         global_config_importable=strategies.fixed_dictionaries(
5095 5099
             {},
5096 5100
             optional={
... ...
@@ -5185,7 +5189,7 @@ Will replace with spam, okay? (Please say "y" or "n".): Boo.
5185 5189
             min_size=4,
5186 5190
             max_size=64,
5187 5191
         ),
5188
-        service_config_settable=tests.vault_full_service_config(),
5192
+        service_config_settable=tests.machinery.hypothesis.vault_full_service_config(),
5189 5193
         service_config_importable=strategies.fixed_dictionaries(
5190 5194
             {},
5191 5195
             optional={
... ...
@@ -5342,14 +5346,14 @@ Will replace with spam, okay? (Please say "y" or "n".): Boo.
5342 5346
             finally:
5343 5347
                 shutil.rmtree(path)
5344 5348
 
5345
-        runner = tests.CliRunner(mix_stderr=False)
5349
+        runner = tests.machinery.CliRunner(mix_stderr=False)
5346 5350
         # TODO(the-13th-letter): Rewrite using parenthesized
5347 5351
         # with-statements.
5348 5352
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
5349 5353
         with contextlib.ExitStack() as stack:
5350 5354
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
5351 5355
             stack.enter_context(
5352
-                tests.isolated_vault_config(
5356
+                tests.machinery.pytest.isolated_vault_config(
5353 5357
                     monkeypatch=monkeypatch,
5354 5358
                     runner=runner,
5355 5359
                     vault_config={"services": {}},
... ...
@@ -5372,7 +5376,7 @@ Will replace with spam, okay? (Please say "y" or "n".): Boo.
5372 5376
             system_tempdir = os.fsdecode(tempfile.gettempdir())
5373 5377
             our_tempdir = cli_helpers.get_tempdir()
5374 5378
             assert system_tempdir == os.fsdecode(our_tempdir) or (
5375
-                # TODO(the-13th-letter): `tests.isolated_config`
5379
+                # TODO(the-13th-letter): `tests.machinery.pytest.isolated_config`
5376 5380
                 # guarantees that `Path.cwd() == config_filename(None)`.
5377 5381
                 # So this sub-branch ought to never trigger in our
5378 5382
                 # tests.
... ...
@@ -5389,14 +5393,14 @@ Will replace with spam, okay? (Please say "y" or "n".): Boo.
5389 5393
         configuration directory.
5390 5394
 
5391 5395
         """
5392
-        runner = tests.CliRunner(mix_stderr=False)
5396
+        runner = tests.machinery.CliRunner(mix_stderr=False)
5393 5397
         # TODO(the-13th-letter): Rewrite using parenthesized
5394 5398
         # with-statements.
5395 5399
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
5396 5400
         with contextlib.ExitStack() as stack:
5397 5401
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
5398 5402
             stack.enter_context(
5399
-                tests.isolated_vault_config(
5403
+                tests.machinery.pytest.isolated_vault_config(
5400 5404
                     monkeypatch=monkeypatch,
5401 5405
                     runner=runner,
5402 5406
                     vault_config={"services": {}},
... ...
@@ -5444,14 +5448,14 @@ Will replace with spam, okay? (Please say "y" or "n".): Boo.
5444 5448
     ) -> None:
5445 5449
         """Repeatedly removing the same parts of a configuration works."""
5446 5450
         for start_config in [config, result_config]:
5447
-            runner = tests.CliRunner(mix_stderr=False)
5451
+            runner = tests.machinery.CliRunner(mix_stderr=False)
5448 5452
             # TODO(the-13th-letter): Rewrite using parenthesized
5449 5453
             # with-statements.
5450 5454
             # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
5451 5455
             with contextlib.ExitStack() as stack:
5452 5456
                 monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
5453 5457
                 stack.enter_context(
5454
-                    tests.isolated_vault_config(
5458
+                    tests.machinery.pytest.isolated_vault_config(
5455 5459
                         monkeypatch=monkeypatch,
5456 5460
                         runner=runner,
5457 5461
                         vault_config=start_config,
... ...
@@ -5494,13 +5498,13 @@ Will replace with spam, okay? (Please say "y" or "n".): Boo.
5494 5498
     @Parametrize.CONNECTION_HINTS
5495 5499
     def test_227_get_suitable_ssh_keys(
5496 5500
         self,
5497
-        running_ssh_agent: tests.RunningSSHAgentInfo,
5501
+        running_ssh_agent: tests.data.RunningSSHAgentInfo,
5498 5502
         conn_hint: str,
5499 5503
     ) -> None:
5500 5504
         """[`cli_helpers.get_suitable_ssh_keys`][] works."""
5501 5505
         with pytest.MonkeyPatch.context() as monkeypatch:
5502 5506
             monkeypatch.setattr(
5503
-                ssh_agent.SSHAgentClient, "list_keys", tests.list_keys
5507
+                ssh_agent.SSHAgentClient, "list_keys", tests.data.callables.list_keys
5504 5508
             )
5505 5509
             hint: ssh_agent.SSHAgentClient | _types.SSHAgentSocket | None
5506 5510
             # TODO(the-13th-letter): Rewrite using structural pattern
... ...
@@ -5595,14 +5599,14 @@ class TestCLITransition:
5595 5599
         config: Any,
5596 5600
     ) -> None:
5597 5601
         """Loading the old settings file works."""
5598
-        runner = tests.CliRunner(mix_stderr=False)
5602
+        runner = tests.machinery.CliRunner(mix_stderr=False)
5599 5603
         # TODO(the-13th-letter): Rewrite using parenthesized
5600 5604
         # with-statements.
5601 5605
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
5602 5606
         with contextlib.ExitStack() as stack:
5603 5607
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
5604 5608
             stack.enter_context(
5605
-                tests.isolated_config(
5609
+                tests.machinery.pytest.isolated_config(
5606 5610
                     monkeypatch=monkeypatch,
5607 5611
                     runner=runner,
5608 5612
                 )
... ...
@@ -5618,14 +5622,14 @@ class TestCLITransition:
5618 5622
         config: Any,
5619 5623
     ) -> None:
5620 5624
         """Migrating the old settings file works."""
5621
-        runner = tests.CliRunner(mix_stderr=False)
5625
+        runner = tests.machinery.CliRunner(mix_stderr=False)
5622 5626
         # TODO(the-13th-letter): Rewrite using parenthesized
5623 5627
         # with-statements.
5624 5628
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
5625 5629
         with contextlib.ExitStack() as stack:
5626 5630
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
5627 5631
             stack.enter_context(
5628
-                tests.isolated_config(
5632
+                tests.machinery.pytest.isolated_config(
5629 5633
                     monkeypatch=monkeypatch,
5630 5634
                     runner=runner,
5631 5635
                 )
... ...
@@ -5641,14 +5645,14 @@ class TestCLITransition:
5641 5645
         config: Any,
5642 5646
     ) -> None:
5643 5647
         """Migrating the old settings file atop a directory fails."""
5644
-        runner = tests.CliRunner(mix_stderr=False)
5648
+        runner = tests.machinery.CliRunner(mix_stderr=False)
5645 5649
         # TODO(the-13th-letter): Rewrite using parenthesized
5646 5650
         # with-statements.
5647 5651
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
5648 5652
         with contextlib.ExitStack() as stack:
5649 5653
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
5650 5654
             stack.enter_context(
5651
-                tests.isolated_config(
5655
+                tests.machinery.pytest.isolated_config(
5652 5656
                     monkeypatch=monkeypatch,
5653 5657
                     runner=runner,
5654 5658
                 )
... ...
@@ -5671,14 +5675,14 @@ class TestCLITransition:
5671 5675
         config: Any,
5672 5676
     ) -> None:
5673 5677
         """Migrating an invalid old settings file fails."""
5674
-        runner = tests.CliRunner(mix_stderr=False)
5678
+        runner = tests.machinery.CliRunner(mix_stderr=False)
5675 5679
         # TODO(the-13th-letter): Rewrite using parenthesized
5676 5680
         # with-statements.
5677 5681
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
5678 5682
         with contextlib.ExitStack() as stack:
5679 5683
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
5680 5684
             stack.enter_context(
5681
-                tests.isolated_config(
5685
+                tests.machinery.pytest.isolated_config(
5682 5686
                     monkeypatch=monkeypatch,
5683 5687
                     runner=runner,
5684 5688
                 )
... ...
@@ -5697,33 +5701,33 @@ class TestCLITransition:
5697 5701
     ) -> None:
5698 5702
         """Forwarding arguments from "export" to "export vault" works."""
5699 5703
         pytest.importorskip("cryptography", minversion="38.0")
5700
-        runner = tests.CliRunner(mix_stderr=False)
5704
+        runner = tests.machinery.CliRunner(mix_stderr=False)
5701 5705
         # TODO(the-13th-letter): Rewrite using parenthesized
5702 5706
         # with-statements.
5703 5707
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
5704 5708
         with contextlib.ExitStack() as stack:
5705 5709
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
5706 5710
             stack.enter_context(
5707
-                tests.isolated_vault_exporter_config(
5711
+                tests.machinery.pytest.isolated_vault_exporter_config(
5708 5712
                     monkeypatch=monkeypatch,
5709 5713
                     runner=runner,
5710
-                    vault_config=tests.VAULT_V03_CONFIG,
5711
-                    vault_key=tests.VAULT_MASTER_KEY,
5714
+                    vault_config=tests.data.VAULT_V03_CONFIG,
5715
+                    vault_key=tests.data.VAULT_MASTER_KEY,
5712 5716
                 )
5713 5717
             )
5714
-            monkeypatch.setenv("VAULT_KEY", tests.VAULT_MASTER_KEY)
5718
+            monkeypatch.setenv("VAULT_KEY", tests.data.VAULT_MASTER_KEY)
5715 5719
             result = runner.invoke(
5716 5720
                 cli.derivepassphrase,
5717 5721
                 ["export", "VAULT_PATH"],
5718 5722
             )
5719 5723
         assert result.clean_exit(empty_stderr=False), "expected clean exit"
5720
-        assert tests.deprecation_warning_emitted(
5724
+        assert tests.machinery.deprecation_warning_emitted(
5721 5725
             "A subcommand will be required here in v1.0", caplog.record_tuples
5722 5726
         )
5723
-        assert tests.deprecation_warning_emitted(
5727
+        assert tests.machinery.deprecation_warning_emitted(
5724 5728
             'Defaulting to subcommand "vault"', caplog.record_tuples
5725 5729
         )
5726
-        assert json.loads(result.stdout) == tests.VAULT_V03_CONFIG_DATA
5730
+        assert json.loads(result.stdout) == tests.data.VAULT_V03_CONFIG_DATA
5727 5731
 
5728 5732
     def test_201_forward_export_vault_empty_commandline(
5729 5733
         self,
... ...
@@ -5731,14 +5735,14 @@ class TestCLITransition:
5731 5735
     ) -> None:
5732 5736
         """Deferring from "export" to "export vault" works."""
5733 5737
         pytest.importorskip("cryptography", minversion="38.0")
5734
-        runner = tests.CliRunner(mix_stderr=False)
5738
+        runner = tests.machinery.CliRunner(mix_stderr=False)
5735 5739
         # TODO(the-13th-letter): Rewrite using parenthesized
5736 5740
         # with-statements.
5737 5741
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
5738 5742
         with contextlib.ExitStack() as stack:
5739 5743
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
5740 5744
             stack.enter_context(
5741
-                tests.isolated_config(
5745
+                tests.machinery.pytest.isolated_config(
5742 5746
                     monkeypatch=monkeypatch,
5743 5747
                     runner=runner,
5744 5748
                 )
... ...
@@ -5747,10 +5751,10 @@ class TestCLITransition:
5747 5751
                 cli.derivepassphrase,
5748 5752
                 ["export"],
5749 5753
             )
5750
-        assert tests.deprecation_warning_emitted(
5754
+        assert tests.machinery.deprecation_warning_emitted(
5751 5755
             "A subcommand will be required here in v1.0", caplog.record_tuples
5752 5756
         )
5753
-        assert tests.deprecation_warning_emitted(
5757
+        assert tests.machinery.deprecation_warning_emitted(
5754 5758
             'Defaulting to subcommand "vault"', caplog.record_tuples
5755 5759
         )
5756 5760
         assert result.error_exit(error="Missing argument 'PATH'"), (
... ...
@@ -5766,20 +5770,20 @@ class TestCLITransition:
5766 5770
         """Forwarding arguments from top-level to "vault" works."""
5767 5771
         option = f"--{charset_name}"
5768 5772
         charset = vault.Vault.CHARSETS[charset_name].decode("ascii")
5769
-        runner = tests.CliRunner(mix_stderr=False)
5773
+        runner = tests.machinery.CliRunner(mix_stderr=False)
5770 5774
         # TODO(the-13th-letter): Rewrite using parenthesized
5771 5775
         # with-statements.
5772 5776
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
5773 5777
         with contextlib.ExitStack() as stack:
5774 5778
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
5775 5779
             stack.enter_context(
5776
-                tests.isolated_config(
5780
+                tests.machinery.pytest.isolated_config(
5777 5781
                     monkeypatch=monkeypatch,
5778 5782
                     runner=runner,
5779 5783
                 )
5780 5784
             )
5781 5785
             monkeypatch.setattr(
5782
-                cli_helpers, "prompt_for_passphrase", tests.auto_prompt
5786
+                cli_helpers, "prompt_for_passphrase", tests.data.callables.auto_prompt
5783 5787
             )
5784 5788
             result = runner.invoke(
5785 5789
                 cli.derivepassphrase,
... ...
@@ -5788,10 +5792,10 @@ class TestCLITransition:
5788 5792
                 catch_exceptions=False,
5789 5793
             )
5790 5794
         assert result.clean_exit(empty_stderr=False), "expected clean exit"
5791
-        assert tests.deprecation_warning_emitted(
5795
+        assert tests.machinery.deprecation_warning_emitted(
5792 5796
             "A subcommand will be required here in v1.0", caplog.record_tuples
5793 5797
         )
5794
-        assert tests.deprecation_warning_emitted(
5798
+        assert tests.machinery.deprecation_warning_emitted(
5795 5799
             'Defaulting to subcommand "vault"', caplog.record_tuples
5796 5800
         )
5797 5801
         for c in charset:
... ...
@@ -5804,14 +5808,14 @@ class TestCLITransition:
5804 5808
         caplog: pytest.LogCaptureFixture,
5805 5809
     ) -> None:
5806 5810
         """Deferring from top-level to "vault" works."""
5807
-        runner = tests.CliRunner(mix_stderr=False)
5811
+        runner = tests.machinery.CliRunner(mix_stderr=False)
5808 5812
         # TODO(the-13th-letter): Rewrite using parenthesized
5809 5813
         # with-statements.
5810 5814
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
5811 5815
         with contextlib.ExitStack() as stack:
5812 5816
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
5813 5817
             stack.enter_context(
5814
-                tests.isolated_config(
5818
+                tests.machinery.pytest.isolated_config(
5815 5819
                     monkeypatch=monkeypatch,
5816 5820
                     runner=runner,
5817 5821
                 )
... ...
@@ -5822,10 +5826,10 @@ class TestCLITransition:
5822 5826
                 input=DUMMY_PASSPHRASE,
5823 5827
                 catch_exceptions=False,
5824 5828
             )
5825
-        assert tests.deprecation_warning_emitted(
5829
+        assert tests.machinery.deprecation_warning_emitted(
5826 5830
             "A subcommand will be required here in v1.0", caplog.record_tuples
5827 5831
         )
5828
-        assert tests.deprecation_warning_emitted(
5832
+        assert tests.machinery.deprecation_warning_emitted(
5829 5833
             'Defaulting to subcommand "vault"', caplog.record_tuples
5830 5834
         )
5831 5835
         assert result.error_exit(
... ...
@@ -5838,14 +5842,14 @@ class TestCLITransition:
5838 5842
     ) -> None:
5839 5843
         """Exporting from (and migrating) the old settings file works."""
5840 5844
         caplog.set_level(logging.INFO)
5841
-        runner = tests.CliRunner(mix_stderr=False)
5845
+        runner = tests.machinery.CliRunner(mix_stderr=False)
5842 5846
         # TODO(the-13th-letter): Rewrite using parenthesized
5843 5847
         # with-statements.
5844 5848
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
5845 5849
         with contextlib.ExitStack() as stack:
5846 5850
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
5847 5851
             stack.enter_context(
5848
-                tests.isolated_config(
5852
+                tests.machinery.pytest.isolated_config(
5849 5853
                     monkeypatch=monkeypatch,
5850 5854
                     runner=runner,
5851 5855
                 )
... ...
@@ -5866,10 +5870,10 @@ class TestCLITransition:
5866 5870
                 catch_exceptions=False,
5867 5871
             )
5868 5872
         assert result.clean_exit(), "expected clean exit"
5869
-        assert tests.deprecation_warning_emitted(
5873
+        assert tests.machinery.deprecation_warning_emitted(
5870 5874
             "v0.1-style config file", caplog.record_tuples
5871 5875
         ), "expected known warning message in stderr"
5872
-        assert tests.deprecation_info_emitted(
5876
+        assert tests.machinery.deprecation_info_emitted(
5873 5877
             "Successfully migrated to ", caplog.record_tuples
5874 5878
         ), "expected known warning message in stderr"
5875 5879
 
... ...
@@ -5878,14 +5882,14 @@ class TestCLITransition:
5878 5882
         caplog: pytest.LogCaptureFixture,
5879 5883
     ) -> None:
5880 5884
         """Exporting from (and not migrating) the old settings file fails."""
5881
-        runner = tests.CliRunner(mix_stderr=False)
5885
+        runner = tests.machinery.CliRunner(mix_stderr=False)
5882 5886
         # TODO(the-13th-letter): Rewrite using parenthesized
5883 5887
         # with-statements.
5884 5888
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
5885 5889
         with contextlib.ExitStack() as stack:
5886 5890
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
5887 5891
             stack.enter_context(
5888
-                tests.isolated_config(
5892
+                tests.machinery.pytest.isolated_config(
5889 5893
                     monkeypatch=monkeypatch,
5890 5894
                     runner=runner,
5891 5895
                 )
... ...
@@ -5916,10 +5920,10 @@ class TestCLITransition:
5916 5920
                 catch_exceptions=False,
5917 5921
             )
5918 5922
         assert result.clean_exit(), "expected clean exit"
5919
-        assert tests.deprecation_warning_emitted(
5923
+        assert tests.machinery.deprecation_warning_emitted(
5920 5924
             "v0.1-style config file", caplog.record_tuples
5921 5925
         ), "expected known warning message in stderr"
5922
-        assert tests.warning_emitted(
5926
+        assert tests.machinery.warning_emitted(
5923 5927
             "Failed to migrate to ", caplog.record_tuples
5924 5928
         ), "expected known warning message in stderr"
5925 5929
 
... ...
@@ -5928,14 +5932,14 @@ class TestCLITransition:
5928 5932
     ) -> None:
5929 5933
         """Completing service names from the old settings file works."""
5930 5934
         config = {"services": {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy()}}
5931
-        runner = tests.CliRunner(mix_stderr=False)
5935
+        runner = tests.machinery.CliRunner(mix_stderr=False)
5932 5936
         # TODO(the-13th-letter): Rewrite using parenthesized
5933 5937
         # with-statements.
5934 5938
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
5935 5939
         with contextlib.ExitStack() as stack:
5936 5940
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
5937 5941
             stack.enter_context(
5938
-                tests.isolated_vault_config(
5942
+                tests.machinery.pytest.isolated_vault_config(
5939 5943
                     monkeypatch=monkeypatch,
5940 5944
                     runner=runner,
5941 5945
                     vault_config=config,
... ...
@@ -5990,7 +5994,7 @@ def build_reduced_vault_config_settings(
5990 5994
 
5991 5995
 SERVICES_STRATEGY = strategies.builds(
5992 5996
     build_reduced_vault_config_settings,
5993
-    tests.vault_full_service_config(),
5997
+    tests.machinery.hypothesis.vault_full_service_config(),
5994 5998
     strategies.sets(
5995 5999
         strategies.sampled_from(VALID_PROPERTIES),
5996 6000
         max_size=7,
... ...
@@ -6084,13 +6088,13 @@ class ConfigManagementStateMachine(stateful.RuleBasedStateMachine):
6084 6088
     def __init__(self) -> None:
6085 6089
         """Initialize self, set up context managers and enter them."""
6086 6090
         super().__init__()
6087
-        self.runner = tests.CliRunner(mix_stderr=False)
6091
+        self.runner = tests.machinery.CliRunner(mix_stderr=False)
6088 6092
         self.exit_stack = contextlib.ExitStack().__enter__()
6089 6093
         self.monkeypatch = self.exit_stack.enter_context(
6090 6094
             pytest.MonkeyPatch().context()
6091 6095
         )
6092 6096
         self.isolated_config = self.exit_stack.enter_context(
6093
-            tests.isolated_vault_config(
6097
+            tests.machinery.pytest.isolated_vault_config(
6094 6098
                 monkeypatch=self.monkeypatch,
6095 6099
                 runner=self.runner,
6096 6100
                 vault_config={"services": {}},
... ...
@@ -6511,7 +6515,7 @@ def run_actions_handler(
6511 6515
                 timeout=timeout,
6512 6516
             ),
6513 6517
         )
6514
-        runner = tests.CliRunner(mix_stderr=False)
6518
+        runner = tests.machinery.CliRunner(mix_stderr=False)
6515 6519
         try:
6516 6520
             result = runner.invoke(
6517 6521
                 cli.derivepassphrase_vault,
... ...
@@ -6542,7 +6546,7 @@ def run_actions_handler(
6542 6546
 
6543 6547
 
6544 6548
 @hypothesis.settings(
6545
-    stateful_step_count=tests.get_concurrency_step_count(),
6549
+    stateful_step_count=tests.machinery.hypothesis.get_concurrency_step_count(),
6546 6550
     deadline=None,
6547 6551
 )
6548 6552
 class FakeConfigurationMutexStateMachine(stateful.RuleBasedStateMachine):
... ...
@@ -6723,7 +6727,7 @@ class FakeConfigurationMutexStateMachine(stateful.RuleBasedStateMachine):
6723 6727
             settings = FakeConfigurationMutexStateMachine.TestCase.settings
6724 6728
         except AttributeError:  # pragma: no cover
6725 6729
             settings = None
6726
-        self.step_count = tests.get_concurrency_step_count(settings)
6730
+        self.step_count = tests.machinery.hypothesis.get_concurrency_step_count(settings)
6727 6731
 
6728 6732
     @stateful.initialize(
6729 6733
         target=configuration,
... ...
@@ -7002,7 +7006,7 @@ class FakeConfigurationMutexStateMachine(stateful.RuleBasedStateMachine):
7002 7006
         mp = multiprocessing.get_context()
7003 7007
         # Coverage tracking writes coverage data to the current working
7004 7008
         # directory, but because the subprocesses are spawned within the
7005
-        # `tests.isolated_vault_config` context manager, their starting
7009
+        # `tests.machinery.pytest.isolated_vault_config` context manager, their starting
7006 7010
         # working directory is the isolated one, not the original one.
7007 7011
         orig_cwd = pathlib.Path.cwd()
7008 7012
 
... ...
@@ -7021,10 +7025,10 @@ class FakeConfigurationMutexStateMachine(stateful.RuleBasedStateMachine):
7021 7025
 
7022 7026
         stack = contextlib.ExitStack()
7023 7027
         with stack:
7024
-            runner = tests.CliRunner(mix_stderr=False)
7028
+            runner = tests.machinery.CliRunner(mix_stderr=False)
7025 7029
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
7026 7030
             stack.enter_context(
7027
-                tests.isolated_vault_config(
7031
+                tests.machinery.pytest.isolated_vault_config(
7028 7032
                     monkeypatch=monkeypatch,
7029 7033
                     runner=runner,
7030 7034
                     vault_config={"services": {}},
... ...
@@ -7045,10 +7049,10 @@ class FakeConfigurationMutexStateMachine(stateful.RuleBasedStateMachine):
7045 7049
                 )
7046 7050
 
7047 7051
         with stack:  # noqa: PLR1702
7048
-            runner = tests.CliRunner(mix_stderr=False)
7052
+            runner = tests.machinery.CliRunner(mix_stderr=False)
7049 7053
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
7050 7054
             stack.enter_context(
7051
-                tests.isolated_vault_config(
7055
+                tests.machinery.pytest.isolated_vault_config(
7052 7056
                     monkeypatch=monkeypatch,
7053 7057
                     runner=runner,
7054 7058
                     vault_config={"services": {}},
... ...
@@ -7133,7 +7137,7 @@ class FakeConfigurationMutexStateMachine(stateful.RuleBasedStateMachine):
7133 7137
                             break
7134 7138
                 finally:
7135 7139
                     # The subprocesses have this
7136
-                    # `tests.isolated_vault_config` directory as their
7140
+                    # `tests.machinery.pytest.isolated_vault_config` directory as their
7137 7141
                     # startup and working directory, so systems like
7138 7142
                     # coverage tracking write their data files to this
7139 7143
                     # directory.  We need to manually move them back to
... ...
@@ -7228,7 +7232,7 @@ class FakeConfigurationMutexStateMachine(stateful.RuleBasedStateMachine):
7228 7232
             raise AssertionError()
7229 7233
 
7230 7234
 
7231
-TestFakedConfigurationMutex = tests.skip_if_no_multiprocessing_support(
7235
+TestFakedConfigurationMutex = tests.machinery.pytest.skip_if_no_multiprocessing_support(
7232 7236
     FakeConfigurationMutexStateMachine.TestCase
7233 7237
 )
7234 7238
 """The [`unittest.TestCase`][] class that will actually be run."""
... ...
@@ -7350,14 +7354,14 @@ class TestShellCompletion:
7350 7354
         completions: AbstractSet[str],
7351 7355
     ) -> None:
7352 7356
         """Our completion machinery works for vault service names."""
7353
-        runner = tests.CliRunner(mix_stderr=False)
7357
+        runner = tests.machinery.CliRunner(mix_stderr=False)
7354 7358
         # TODO(the-13th-letter): Rewrite using parenthesized
7355 7359
         # with-statements.
7356 7360
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
7357 7361
         with contextlib.ExitStack() as stack:
7358 7362
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
7359 7363
             stack.enter_context(
7360
-                tests.isolated_vault_config(
7364
+                tests.machinery.pytest.isolated_vault_config(
7361 7365
                     monkeypatch=monkeypatch,
7362 7366
                     runner=runner,
7363 7367
                     vault_config=config,
... ...
@@ -7382,14 +7386,14 @@ class TestShellCompletion:
7382 7386
         results: list[str | click.shell_completion.CompletionItem],
7383 7387
     ) -> None:
7384 7388
         """Custom completion functions work for all shells."""
7385
-        runner = tests.CliRunner(mix_stderr=False)
7389
+        runner = tests.machinery.CliRunner(mix_stderr=False)
7386 7390
         # TODO(the-13th-letter): Rewrite using parenthesized
7387 7391
         # with-statements.
7388 7392
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
7389 7393
         with contextlib.ExitStack() as stack:
7390 7394
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
7391 7395
             stack.enter_context(
7392
-                tests.isolated_vault_config(
7396
+                tests.machinery.pytest.isolated_vault_config(
7393 7397
                     monkeypatch=monkeypatch,
7394 7398
                     runner=runner,
7395 7399
                     vault_config=config,
... ...
@@ -7444,14 +7448,14 @@ class TestShellCompletion:
7444 7448
     ) -> None:
7445 7449
         """Completion skips incompletable items."""
7446 7450
         vault_config = config if mode == "config" else {"services": {}}
7447
-        runner = tests.CliRunner(mix_stderr=False)
7451
+        runner = tests.machinery.CliRunner(mix_stderr=False)
7448 7452
         # TODO(the-13th-letter): Rewrite using parenthesized
7449 7453
         # with-statements.
7450 7454
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
7451 7455
         with contextlib.ExitStack() as stack:
7452 7456
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
7453 7457
             stack.enter_context(
7454
-                tests.isolated_vault_config(
7458
+                tests.machinery.pytest.isolated_vault_config(
7455 7459
                     monkeypatch=monkeypatch,
7456 7460
                     runner=runner,
7457 7461
                     vault_config=vault_config,
... ...
@@ -7471,10 +7475,10 @@ class TestShellCompletion:
7471 7475
                     input=json.dumps(config),
7472 7476
                 )
7473 7477
             assert result.clean_exit(), "expected clean exit"
7474
-            assert tests.warning_emitted(
7478
+            assert tests.machinery.warning_emitted(
7475 7479
                 "contains an ASCII control character", caplog.record_tuples
7476 7480
             ), "expected known warning message in stderr"
7477
-            assert tests.warning_emitted(
7481
+            assert tests.machinery.warning_emitted(
7478 7482
                 "not be available for completion", caplog.record_tuples
7479 7483
             ), "expected known warning message in stderr"
7480 7484
             assert cli_helpers.load_config() == config
... ...
@@ -7485,14 +7489,14 @@ class TestShellCompletion:
7485 7489
         self,
7486 7490
     ) -> None:
7487 7491
         """Service name completion quietly fails on missing configuration."""
7488
-        runner = tests.CliRunner(mix_stderr=False)
7492
+        runner = tests.machinery.CliRunner(mix_stderr=False)
7489 7493
         # TODO(the-13th-letter): Rewrite using parenthesized
7490 7494
         # with-statements.
7491 7495
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
7492 7496
         with contextlib.ExitStack() as stack:
7493 7497
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
7494 7498
             stack.enter_context(
7495
-                tests.isolated_vault_config(
7499
+                tests.machinery.pytest.isolated_vault_config(
7496 7500
                     monkeypatch=monkeypatch,
7497 7501
                     runner=runner,
7498 7502
                     vault_config={
... ...
@@ -7515,14 +7519,14 @@ class TestShellCompletion:
7515 7519
         exc_type: type[Exception],
7516 7520
     ) -> None:
7517 7521
         """Service name completion quietly fails on configuration errors."""
7518
-        runner = tests.CliRunner(mix_stderr=False)
7522
+        runner = tests.machinery.CliRunner(mix_stderr=False)
7519 7523
         # TODO(the-13th-letter): Rewrite using parenthesized
7520 7524
         # with-statements.
7521 7525
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
7522 7526
         with contextlib.ExitStack() as stack:
7523 7527
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
7524 7528
             stack.enter_context(
7525
-                tests.isolated_vault_config(
7529
+                tests.machinery.pytest.isolated_vault_config(
7526 7530
                     monkeypatch=monkeypatch,
7527 7531
                     runner=runner,
7528 7532
                     vault_config={
... ...
@@ -15,7 +15,9 @@ import hypothesis
15 15
 import pytest
16 16
 from hypothesis import strategies
17 17
 
18
-import tests
18
+import tests.data
19
+import tests.machinery
20
+import tests.machinery.pytest
19 21
 from derivepassphrase import _types, cli, exporter
20 22
 from derivepassphrase.exporter import storeroom, vault_native
21 23
 
... ...
@@ -47,27 +49,27 @@ class Parametrize(types.SimpleNamespace):
47 49
         ["config", "format", "config_data"],
48 50
         [
49 51
             pytest.param(
50
-                tests.VAULT_V02_CONFIG,
52
+                tests.data.VAULT_V02_CONFIG,
51 53
                 "v0.2",
52
-                tests.VAULT_V02_CONFIG_DATA,
54
+                tests.data.VAULT_V02_CONFIG_DATA,
53 55
                 id="V02_CONFIG-v0.2",
54 56
             ),
55 57
             pytest.param(
56
-                tests.VAULT_V02_CONFIG,
58
+                tests.data.VAULT_V02_CONFIG,
57 59
                 "v0.3",
58 60
                 exporter.NotAVaultConfigError,
59 61
                 id="V02_CONFIG-v0.3",
60 62
             ),
61 63
             pytest.param(
62
-                tests.VAULT_V03_CONFIG,
64
+                tests.data.VAULT_V03_CONFIG,
63 65
                 "v0.2",
64 66
                 exporter.NotAVaultConfigError,
65 67
                 id="V03_CONFIG-v0.2",
66 68
             ),
67 69
             pytest.param(
68
-                tests.VAULT_V03_CONFIG,
70
+                tests.data.VAULT_V03_CONFIG,
69 71
                 "v0.3",
70
-                tests.VAULT_V03_CONFIG_DATA,
72
+                tests.data.VAULT_V03_CONFIG_DATA,
71 73
                 id="V03_CONFIG-v0.3",
72 74
             ),
73 75
         ],
... ...
@@ -76,15 +78,15 @@ class Parametrize(types.SimpleNamespace):
76 78
         ["config", "parser_class", "config_data"],
77 79
         [
78 80
             pytest.param(
79
-                tests.VAULT_V02_CONFIG,
81
+                tests.data.VAULT_V02_CONFIG,
80 82
                 vault_native.VaultNativeV02ConfigParser,
81
-                tests.VAULT_V02_CONFIG_DATA,
83
+                tests.data.VAULT_V02_CONFIG_DATA,
82 84
                 id="0.2",
83 85
             ),
84 86
             pytest.param(
85
-                tests.VAULT_V03_CONFIG,
87
+                tests.data.VAULT_V03_CONFIG,
86 88
                 vault_native.VaultNativeV03ConfigParser,
87
-                tests.VAULT_V03_CONFIG_DATA,
89
+                tests.data.VAULT_V03_CONFIG_DATA,
88 90
                 id="0.3",
89 91
             ),
90 92
         ],
... ...
@@ -134,14 +136,14 @@ class Parametrize(types.SimpleNamespace):
134 136
         "key",
135 137
         [
136 138
             None,
137
-            pytest.param(tests.VAULT_MASTER_KEY, id="str"),
138
-            pytest.param(tests.VAULT_MASTER_KEY.encode("ascii"), id="bytes"),
139
+            pytest.param(tests.data.VAULT_MASTER_KEY, id="str"),
140
+            pytest.param(tests.data.VAULT_MASTER_KEY.encode("ascii"), id="bytes"),
139 141
             pytest.param(
140
-                bytearray(tests.VAULT_MASTER_KEY.encode("ascii")),
142
+                bytearray(tests.data.VAULT_MASTER_KEY.encode("ascii")),
141 143
                 id="bytearray",
142 144
             ),
143 145
             pytest.param(
144
-                memoryview(tests.VAULT_MASTER_KEY.encode("ascii")),
146
+                memoryview(tests.data.VAULT_MASTER_KEY.encode("ascii")),
145 147
                 id="memoryview",
146 148
             ),
147 149
         ],
... ...
@@ -151,22 +153,22 @@ class Parametrize(types.SimpleNamespace):
151 153
         ["zipped_config", "error_text"],
152 154
         [
153 155
             pytest.param(
154
-                tests.VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED,
156
+                tests.data.VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED,
155 157
                 "Object key mismatch",
156 158
                 id="VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED",
157 159
             ),
158 160
             pytest.param(
159
-                tests.VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED2,
161
+                tests.data.VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED2,
160 162
                 "Directory index is not actually an index",
161 163
                 id="VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED2",
162 164
             ),
163 165
             pytest.param(
164
-                tests.VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED3,
166
+                tests.data.VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED3,
165 167
                 "Directory index is not actually an index",
166 168
                 id="VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED3",
167 169
             ),
168 170
             pytest.param(
169
-                tests.VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED4,
171
+                tests.data.VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED4,
170 172
                 "Object key mismatch",
171 173
                 id="VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED4",
172 174
             ),
... ...
@@ -185,51 +187,51 @@ class TestCLI:
185 187
         [`exporter.get_vault_path`][] for details.
186 188
 
187 189
         """
188
-        runner = tests.CliRunner(mix_stderr=False)
190
+        runner = tests.machinery.CliRunner(mix_stderr=False)
189 191
         # TODO(the-13th-letter): Rewrite using parenthesized
190 192
         # with-statements.
191 193
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
192 194
         with contextlib.ExitStack() as stack:
193 195
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
194 196
             stack.enter_context(
195
-                tests.isolated_vault_exporter_config(
197
+                tests.machinery.pytest.isolated_vault_exporter_config(
196 198
                     monkeypatch=monkeypatch,
197 199
                     runner=runner,
198
-                    vault_config=tests.VAULT_V03_CONFIG,
199
-                    vault_key=tests.VAULT_MASTER_KEY,
200
+                    vault_config=tests.data.VAULT_V03_CONFIG,
201
+                    vault_key=tests.data.VAULT_MASTER_KEY,
200 202
                 )
201 203
             )
202
-            monkeypatch.setenv("VAULT_KEY", tests.VAULT_MASTER_KEY)
204
+            monkeypatch.setenv("VAULT_KEY", tests.data.VAULT_MASTER_KEY)
203 205
             result = runner.invoke(
204 206
                 cli.derivepassphrase_export_vault,
205 207
                 ["VAULT_PATH"],
206 208
             )
207 209
         assert result.clean_exit(empty_stderr=True), "expected clean exit"
208
-        assert json.loads(result.stdout) == tests.VAULT_V03_CONFIG_DATA
210
+        assert json.loads(result.stdout) == tests.data.VAULT_V03_CONFIG_DATA
209 211
 
210 212
     def test_201_key_parameter(self) -> None:
211 213
         """The `--key` option is supported."""
212
-        runner = tests.CliRunner(mix_stderr=False)
214
+        runner = tests.machinery.CliRunner(mix_stderr=False)
213 215
         # TODO(the-13th-letter): Rewrite using parenthesized
214 216
         # with-statements.
215 217
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
216 218
         with contextlib.ExitStack() as stack:
217 219
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
218 220
             stack.enter_context(
219
-                tests.isolated_vault_exporter_config(
221
+                tests.machinery.pytest.isolated_vault_exporter_config(
220 222
                     monkeypatch=monkeypatch,
221 223
                     runner=runner,
222
-                    vault_config=tests.VAULT_V03_CONFIG,
224
+                    vault_config=tests.data.VAULT_V03_CONFIG,
223 225
                 )
224 226
             )
225 227
             result = runner.invoke(
226 228
                 cli.derivepassphrase_export_vault,
227
-                ["-k", tests.VAULT_MASTER_KEY, ".vault"],
229
+                ["-k", tests.data.VAULT_MASTER_KEY, ".vault"],
228 230
             )
229 231
         assert result.clean_exit(empty_stderr=True), "expected clean exit"
230
-        assert json.loads(result.stdout) == tests.VAULT_V03_CONFIG_DATA
232
+        assert json.loads(result.stdout) == tests.data.VAULT_V03_CONFIG_DATA
231 233
 
232
-    @tests.Parametrize.VAULT_CONFIG_FORMATS_DATA
234
+    @tests.machinery.pytest.Parametrize.VAULT_CONFIG_FORMATS_DATA
233 235
     def test_210_load_vault_v02_v03_storeroom(
234 236
         self,
235 237
         config: str | bytes,
... ...
@@ -242,14 +244,14 @@ class TestCLI:
242 244
         vault` to only attempt decoding in that named format.
243 245
 
244 246
         """
245
-        runner = tests.CliRunner(mix_stderr=False)
247
+        runner = tests.machinery.CliRunner(mix_stderr=False)
246 248
         # TODO(the-13th-letter): Rewrite using parenthesized
247 249
         # with-statements.
248 250
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
249 251
         with contextlib.ExitStack() as stack:
250 252
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
251 253
             stack.enter_context(
252
-                tests.isolated_vault_exporter_config(
254
+                tests.machinery.pytest.isolated_vault_exporter_config(
253 255
                     monkeypatch=monkeypatch,
254 256
                     runner=runner,
255 257
                     vault_config=config,
... ...
@@ -257,7 +259,7 @@ class TestCLI:
257 259
             )
258 260
             result = runner.invoke(
259 261
                 cli.derivepassphrase_export_vault,
260
-                ["-f", format, "-k", tests.VAULT_MASTER_KEY, "VAULT_PATH"],
262
+                ["-f", format, "-k", tests.data.VAULT_MASTER_KEY, "VAULT_PATH"],
261 263
             )
262 264
         assert result.clean_exit(empty_stderr=True), "expected clean exit"
263 265
         assert json.loads(result.stdout) == config_data
... ...
@@ -270,18 +272,18 @@ class TestCLI:
270 272
         caplog: pytest.LogCaptureFixture,
271 273
     ) -> None:
272 274
         """Fail when trying to decode non-existant files/directories."""
273
-        runner = tests.CliRunner(mix_stderr=False)
275
+        runner = tests.machinery.CliRunner(mix_stderr=False)
274 276
         # TODO(the-13th-letter): Rewrite using parenthesized
275 277
         # with-statements.
276 278
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
277 279
         with contextlib.ExitStack() as stack:
278 280
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
279 281
             stack.enter_context(
280
-                tests.isolated_vault_exporter_config(
282
+                tests.machinery.pytest.isolated_vault_exporter_config(
281 283
                     monkeypatch=monkeypatch,
282 284
                     runner=runner,
283
-                    vault_config=tests.VAULT_V03_CONFIG,
284
-                    vault_key=tests.VAULT_MASTER_KEY,
285
+                    vault_config=tests.data.VAULT_V03_CONFIG,
286
+                    vault_key=tests.data.VAULT_MASTER_KEY,
285 287
                 )
286 288
             )
287 289
             result = runner.invoke(
... ...
@@ -295,25 +297,25 @@ class TestCLI:
295 297
             ),
296 298
             record_tuples=caplog.record_tuples,
297 299
         ), "expected error exit and known error message"
298
-        assert tests.CANNOT_LOAD_CRYPTOGRAPHY not in result.stderr
300
+        assert tests.data.CANNOT_LOAD_CRYPTOGRAPHY not in result.stderr
299 301
 
300 302
     def test_302_vault_config_invalid(
301 303
         self,
302 304
         caplog: pytest.LogCaptureFixture,
303 305
     ) -> None:
304 306
         """Fail to parse invalid vault configurations (files)."""
305
-        runner = tests.CliRunner(mix_stderr=False)
307
+        runner = tests.machinery.CliRunner(mix_stderr=False)
306 308
         # TODO(the-13th-letter): Rewrite using parenthesized
307 309
         # with-statements.
308 310
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
309 311
         with contextlib.ExitStack() as stack:
310 312
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
311 313
             stack.enter_context(
312
-                tests.isolated_vault_exporter_config(
314
+                tests.machinery.pytest.isolated_vault_exporter_config(
313 315
                     monkeypatch=monkeypatch,
314 316
                     runner=runner,
315 317
                     vault_config="",
316
-                    vault_key=tests.VAULT_MASTER_KEY,
318
+                    vault_key=tests.data.VAULT_MASTER_KEY,
317 319
                 )
318 320
             )
319 321
             result = runner.invoke(
... ...
@@ -324,25 +326,25 @@ class TestCLI:
324 326
             error="Cannot parse '.vault' as a valid vault-native config",
325 327
             record_tuples=caplog.record_tuples,
326 328
         ), "expected error exit and known error message"
327
-        assert tests.CANNOT_LOAD_CRYPTOGRAPHY not in result.stderr
329
+        assert tests.data.CANNOT_LOAD_CRYPTOGRAPHY not in result.stderr
328 330
 
329 331
     def test_302a_vault_config_invalid_just_a_directory(
330 332
         self,
331 333
         caplog: pytest.LogCaptureFixture,
332 334
     ) -> None:
333 335
         """Fail to parse invalid vault configurations (directories)."""
334
-        runner = tests.CliRunner(mix_stderr=False)
336
+        runner = tests.machinery.CliRunner(mix_stderr=False)
335 337
         # TODO(the-13th-letter): Rewrite using parenthesized
336 338
         # with-statements.
337 339
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
338 340
         with contextlib.ExitStack() as stack:
339 341
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
340 342
             stack.enter_context(
341
-                tests.isolated_vault_exporter_config(
343
+                tests.machinery.pytest.isolated_vault_exporter_config(
342 344
                     monkeypatch=monkeypatch,
343 345
                     runner=runner,
344 346
                     vault_config="",
345
-                    vault_key=tests.VAULT_MASTER_KEY,
347
+                    vault_key=tests.data.VAULT_MASTER_KEY,
346 348
                 )
347 349
             )
348 350
             p = pathlib.Path(".vault")
... ...
@@ -356,25 +358,25 @@ class TestCLI:
356 358
             error="Cannot parse '.vault' as a valid vault-native config",
357 359
             record_tuples=caplog.record_tuples,
358 360
         ), "expected error exit and known error message"
359
-        assert tests.CANNOT_LOAD_CRYPTOGRAPHY not in result.stderr
361
+        assert tests.data.CANNOT_LOAD_CRYPTOGRAPHY not in result.stderr
360 362
 
361 363
     def test_403_invalid_vault_config_bad_signature(
362 364
         self,
363 365
         caplog: pytest.LogCaptureFixture,
364 366
     ) -> None:
365 367
         """Fail to parse vault configurations with invalid integrity checks."""
366
-        runner = tests.CliRunner(mix_stderr=False)
368
+        runner = tests.machinery.CliRunner(mix_stderr=False)
367 369
         # TODO(the-13th-letter): Rewrite using parenthesized
368 370
         # with-statements.
369 371
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
370 372
         with contextlib.ExitStack() as stack:
371 373
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
372 374
             stack.enter_context(
373
-                tests.isolated_vault_exporter_config(
375
+                tests.machinery.pytest.isolated_vault_exporter_config(
374 376
                     monkeypatch=monkeypatch,
375 377
                     runner=runner,
376
-                    vault_config=tests.VAULT_V02_CONFIG,
377
-                    vault_key=tests.VAULT_MASTER_KEY,
378
+                    vault_config=tests.data.VAULT_V02_CONFIG,
379
+                    vault_key=tests.data.VAULT_MASTER_KEY,
378 380
                 )
379 381
             )
380 382
             result = runner.invoke(
... ...
@@ -385,25 +387,25 @@ class TestCLI:
385 387
             error="Cannot parse '.vault' as a valid vault-native config",
386 388
             record_tuples=caplog.record_tuples,
387 389
         ), "expected error exit and known error message"
388
-        assert tests.CANNOT_LOAD_CRYPTOGRAPHY not in result.stderr
390
+        assert tests.data.CANNOT_LOAD_CRYPTOGRAPHY not in result.stderr
389 391
 
390 392
     def test_500_vault_config_invalid_internal(
391 393
         self,
392 394
         caplog: pytest.LogCaptureFixture,
393 395
     ) -> None:
394 396
         """The decoded vault configuration data is valid."""
395
-        runner = tests.CliRunner(mix_stderr=False)
397
+        runner = tests.machinery.CliRunner(mix_stderr=False)
396 398
         # TODO(the-13th-letter): Rewrite using parenthesized
397 399
         # with-statements.
398 400
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
399 401
         with contextlib.ExitStack() as stack:
400 402
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
401 403
             stack.enter_context(
402
-                tests.isolated_vault_exporter_config(
404
+                tests.machinery.pytest.isolated_vault_exporter_config(
403 405
                     monkeypatch=monkeypatch,
404 406
                     runner=runner,
405
-                    vault_config=tests.VAULT_V03_CONFIG,
406
-                    vault_key=tests.VAULT_MASTER_KEY,
407
+                    vault_config=tests.data.VAULT_V03_CONFIG,
408
+                    vault_key=tests.data.VAULT_MASTER_KEY,
407 409
                 )
408 410
             )
409 411
 
... ...
@@ -423,7 +425,7 @@ class TestCLI:
423 425
             error="Invalid vault config: ",
424 426
             record_tuples=caplog.record_tuples,
425 427
         ), "expected error exit and known error message"
426
-        assert tests.CANNOT_LOAD_CRYPTOGRAPHY not in result.stderr
428
+        assert tests.data.CANNOT_LOAD_CRYPTOGRAPHY not in result.stderr
427 429
 
428 430
 
429 431
 class TestStoreroom:
... ...
@@ -444,23 +446,23 @@ class TestStoreroom:
444 446
         them as well.
445 447
 
446 448
         """
447
-        runner = tests.CliRunner(mix_stderr=False)
449
+        runner = tests.machinery.CliRunner(mix_stderr=False)
448 450
         # TODO(the-13th-letter): Rewrite using parenthesized
449 451
         # with-statements.
450 452
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
451 453
         with contextlib.ExitStack() as stack:
452 454
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
453 455
             stack.enter_context(
454
-                tests.isolated_vault_exporter_config(
456
+                tests.machinery.pytest.isolated_vault_exporter_config(
455 457
                     monkeypatch=monkeypatch,
456 458
                     runner=runner,
457
-                    vault_config=tests.VAULT_STOREROOM_CONFIG_ZIPPED,
458
-                    vault_key=tests.VAULT_MASTER_KEY,
459
+                    vault_config=tests.data.VAULT_STOREROOM_CONFIG_ZIPPED,
460
+                    vault_key=tests.data.VAULT_MASTER_KEY,
459 461
                 )
460 462
             )
461 463
             assert (
462 464
                 handler(path, key, format="storeroom")
463
-                == tests.VAULT_STOREROOM_CONFIG_DATA
465
+                == tests.data.VAULT_STOREROOM_CONFIG_DATA
464 466
             )
465 467
 
466 468
     def test_400_decrypt_bucket_item_unknown_version(self) -> None:
... ...
@@ -487,7 +489,7 @@ class TestStoreroom:
487 489
         wrong shape.
488 490
 
489 491
         """
490
-        runner = tests.CliRunner(mix_stderr=False)
492
+        runner = tests.machinery.CliRunner(mix_stderr=False)
491 493
         master_keys = _types.StoreroomMasterKeys(
492 494
             encryption_key=bytes(storeroom.KEY_SIZE),
493 495
             signing_key=bytes(storeroom.KEY_SIZE),
... ...
@@ -499,10 +501,10 @@ class TestStoreroom:
499 501
         with contextlib.ExitStack() as stack:
500 502
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
501 503
             stack.enter_context(
502
-                tests.isolated_vault_exporter_config(
504
+                tests.machinery.pytest.isolated_vault_exporter_config(
503 505
                     monkeypatch=monkeypatch,
504 506
                     runner=runner,
505
-                    vault_config=tests.VAULT_STOREROOM_CONFIG_ZIPPED,
507
+                    vault_config=tests.data.VAULT_STOREROOM_CONFIG_ZIPPED,
506 508
                 )
507 509
             )
508 510
             p = pathlib.Path(".vault", "20")
... ...
@@ -524,18 +526,18 @@ class TestStoreroom:
524 526
         These include unknown versions, and data of the wrong shape.
525 527
 
526 528
         """
527
-        runner = tests.CliRunner(mix_stderr=False)
529
+        runner = tests.machinery.CliRunner(mix_stderr=False)
528 530
         # TODO(the-13th-letter): Rewrite using parenthesized
529 531
         # with-statements.
530 532
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
531 533
         with contextlib.ExitStack() as stack:
532 534
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
533 535
             stack.enter_context(
534
-                tests.isolated_vault_exporter_config(
536
+                tests.machinery.pytest.isolated_vault_exporter_config(
535 537
                     monkeypatch=monkeypatch,
536 538
                     runner=runner,
537
-                    vault_config=tests.VAULT_STOREROOM_CONFIG_ZIPPED,
538
-                    vault_key=tests.VAULT_MASTER_KEY,
539
+                    vault_config=tests.data.VAULT_STOREROOM_CONFIG_ZIPPED,
540
+                    vault_key=tests.data.VAULT_MASTER_KEY,
539 541
                 )
540 542
             )
541 543
             p = pathlib.Path(".vault", ".keys")
... ...
@@ -566,18 +568,18 @@ class TestStoreroom:
566 568
             subdirectories.
567 569
 
568 570
         """
569
-        runner = tests.CliRunner(mix_stderr=False)
571
+        runner = tests.machinery.CliRunner(mix_stderr=False)
570 572
         # TODO(the-13th-letter): Rewrite using parenthesized
571 573
         # with-statements.
572 574
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
573 575
         with contextlib.ExitStack() as stack:
574 576
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
575 577
             stack.enter_context(
576
-                tests.isolated_vault_exporter_config(
578
+                tests.machinery.pytest.isolated_vault_exporter_config(
577 579
                     monkeypatch=monkeypatch,
578 580
                     runner=runner,
579 581
                     vault_config=zipped_config,
580
-                    vault_key=tests.VAULT_MASTER_KEY,
582
+                    vault_key=tests.data.VAULT_MASTER_KEY,
581 583
                 )
582 584
             )
583 585
             stack.enter_context(pytest.raises(RuntimeError, match=error_text))
... ...
@@ -668,7 +670,7 @@ class TestVaultNativeConfig:
668 670
         """The PBKDF2 helper function works."""
669 671
         assert (
670 672
             vault_native.VaultNativeConfigParser._pbkdf2(
671
-                tests.VAULT_MASTER_KEY.encode("utf-8"), 32, iterations
673
+                tests.data.VAULT_MASTER_KEY.encode("utf-8"), 32, iterations
672 674
             )
673 675
             == result
674 676
         )
... ...
@@ -692,18 +694,18 @@ class TestVaultNativeConfig:
692 694
             no longer does.
693 695
 
694 696
         """
695
-        runner = tests.CliRunner(mix_stderr=False)
697
+        runner = tests.machinery.CliRunner(mix_stderr=False)
696 698
         # TODO(the-13th-letter): Rewrite using parenthesized
697 699
         # with-statements.
698 700
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
699 701
         with contextlib.ExitStack() as stack:
700 702
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
701 703
             stack.enter_context(
702
-                tests.isolated_vault_exporter_config(
704
+                tests.machinery.pytest.isolated_vault_exporter_config(
703 705
                     monkeypatch=monkeypatch,
704 706
                     runner=runner,
705 707
                     vault_config=config,
706
-                    vault_key=tests.VAULT_MASTER_KEY,
708
+                    vault_key=tests.data.VAULT_MASTER_KEY,
707 709
                 )
708 710
             )
709 711
             if isinstance(config_data, type):
... ...
@@ -728,23 +730,23 @@ class TestVaultNativeConfig:
728 730
         them as well.
729 731
 
730 732
         """
731
-        runner = tests.CliRunner(mix_stderr=False)
733
+        runner = tests.machinery.CliRunner(mix_stderr=False)
732 734
         # TODO(the-13th-letter): Rewrite using parenthesized
733 735
         # with-statements.
734 736
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
735 737
         with contextlib.ExitStack() as stack:
736 738
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
737 739
             stack.enter_context(
738
-                tests.isolated_vault_exporter_config(
740
+                tests.machinery.pytest.isolated_vault_exporter_config(
739 741
                     monkeypatch=monkeypatch,
740 742
                     runner=runner,
741
-                    vault_config=tests.VAULT_V03_CONFIG,
742
-                    vault_key=tests.VAULT_MASTER_KEY,
743
+                    vault_config=tests.data.VAULT_V03_CONFIG,
744
+                    vault_key=tests.data.VAULT_MASTER_KEY,
743 745
                 )
744 746
             )
745 747
             assert (
746 748
                 handler(path, key, format="v0.3")
747
-                == tests.VAULT_V03_CONFIG_DATA
749
+                == tests.data.VAULT_V03_CONFIG_DATA
748 750
             )
749 751
 
750 752
     @Parametrize.VAULT_NATIVE_PARSER_CLASS_DATA
... ...
@@ -763,21 +765,21 @@ class TestVaultNativeConfig:
763 765
 
764 766
             return func
765 767
 
766
-        runner = tests.CliRunner(mix_stderr=False)
768
+        runner = tests.machinery.CliRunner(mix_stderr=False)
767 769
         # TODO(the-13th-letter): Rewrite using parenthesized
768 770
         # with-statements.
769 771
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
770 772
         with contextlib.ExitStack() as stack:
771 773
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
772 774
             stack.enter_context(
773
-                tests.isolated_vault_exporter_config(
775
+                tests.machinery.pytest.isolated_vault_exporter_config(
774 776
                     monkeypatch=monkeypatch,
775 777
                     runner=runner,
776 778
                     vault_config=config,
777 779
                 )
778 780
             )
779 781
             parser = parser_class(
780
-                base64.b64decode(config), tests.VAULT_MASTER_KEY
782
+                base64.b64decode(config), tests.data.VAULT_MASTER_KEY
781 783
             )
782 784
             assert parser() == config_data
783 785
             # Now stub out all functions used to calculate the above result.
... ...
@@ -16,7 +16,9 @@ import hypothesis
16 16
 import pytest
17 17
 from hypothesis import strategies
18 18
 
19
-import tests
19
+import tests.data
20
+import tests.machinery
21
+import tests.machinery.pytest
20 22
 from derivepassphrase import cli, exporter
21 23
 
22 24
 if TYPE_CHECKING:
... ...
@@ -205,14 +207,14 @@ class Test001ExporterUtils:
205 207
             ("USER", user),
206 208
             ("USERNAME", username),
207 209
         ]
208
-        runner = tests.CliRunner(mix_stderr=False)
210
+        runner = tests.machinery.CliRunner(mix_stderr=False)
209 211
         # TODO(the-13th-letter): Rewrite using parenthesized
210 212
         # with-statements.
211 213
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
212 214
         with contextlib.ExitStack() as stack:
213 215
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
214 216
             stack.enter_context(
215
-                tests.isolated_vault_exporter_config(
217
+                tests.machinery.pytest.isolated_vault_exporter_config(
216 218
                     monkeypatch=monkeypatch, runner=runner
217 219
                 )
218 220
             )
... ...
@@ -232,14 +234,14 @@ class Test001ExporterUtils:
232 234
         Handle relative paths, absolute paths, and missing paths.
233 235
 
234 236
         """
235
-        runner = tests.CliRunner(mix_stderr=False)
237
+        runner = tests.machinery.CliRunner(mix_stderr=False)
236 238
         # TODO(the-13th-letter): Rewrite using parenthesized
237 239
         # with-statements.
238 240
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
239 241
         with contextlib.ExitStack() as stack:
240 242
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
241 243
             stack.enter_context(
242
-                tests.isolated_vault_exporter_config(
244
+                tests.machinery.pytest.isolated_vault_exporter_config(
243 245
                     monkeypatch=monkeypatch, runner=runner
244 246
                 )
245 247
             )
... ...
@@ -377,18 +379,18 @@ class Test002CLI:
377 379
 
378 380
     def test_300_invalid_format(self) -> None:
379 381
         """Reject invalid vault configuration format names."""
380
-        runner = tests.CliRunner(mix_stderr=False)
382
+        runner = tests.machinery.CliRunner(mix_stderr=False)
381 383
         # TODO(the-13th-letter): Rewrite using parenthesized
382 384
         # with-statements.
383 385
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
384 386
         with contextlib.ExitStack() as stack:
385 387
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
386 388
             stack.enter_context(
387
-                tests.isolated_vault_exporter_config(
389
+                tests.machinery.pytest.isolated_vault_exporter_config(
388 390
                     monkeypatch=monkeypatch,
389 391
                     runner=runner,
390
-                    vault_config=tests.VAULT_V03_CONFIG,
391
-                    vault_key=tests.VAULT_MASTER_KEY,
392
+                    vault_config=tests.data.VAULT_V03_CONFIG,
393
+                    vault_key=tests.data.VAULT_MASTER_KEY,
392 394
                 )
393 395
             )
394 396
             result = runner.invoke(
... ...
@@ -401,8 +403,8 @@ class Test002CLI:
401 403
                 "expected error exit and known error message"
402 404
             )
403 405
 
404
-    @tests.skip_if_cryptography_support
405
-    @tests.Parametrize.VAULT_CONFIG_FORMATS_DATA
406
+    @tests.machinery.pytest.skip_if_cryptography_support
407
+    @tests.machinery.pytest.Parametrize.VAULT_CONFIG_FORMATS_DATA
406 408
     def test_999_no_cryptography_error_message(
407 409
         self,
408 410
         caplog: pytest.LogCaptureFixture,
... ...
@@ -412,18 +414,18 @@ class Test002CLI:
412 414
     ) -> None:
413 415
         """Abort export call if no cryptography is available."""
414 416
         del config_data
415
-        runner = tests.CliRunner(mix_stderr=False)
417
+        runner = tests.machinery.CliRunner(mix_stderr=False)
416 418
         # TODO(the-13th-letter): Rewrite using parenthesized
417 419
         # with-statements.
418 420
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
419 421
         with contextlib.ExitStack() as stack:
420 422
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
421 423
             stack.enter_context(
422
-                tests.isolated_vault_exporter_config(
424
+                tests.machinery.pytest.isolated_vault_exporter_config(
423 425
                     monkeypatch=monkeypatch,
424 426
                     runner=runner,
425 427
                     vault_config=config,
426
-                    vault_key=tests.VAULT_MASTER_KEY,
428
+                    vault_key=tests.data.VAULT_MASTER_KEY,
427 429
                 )
428 430
             )
429 431
             result = runner.invoke(
... ...
@@ -432,6 +434,6 @@ class Test002CLI:
432 434
                 catch_exceptions=False,
433 435
             )
434 436
         assert result.error_exit(
435
-            error=tests.CANNOT_LOAD_CRYPTOGRAPHY,
437
+            error=tests.data.CANNOT_LOAD_CRYPTOGRAPHY,
436 438
             record_tuples=caplog.record_tuples,
437 439
         ), "expected error exit and known error message"
... ...
@@ -25,7 +25,11 @@ import hypothesis
25 25
 import pytest
26 26
 from hypothesis import stateful, strategies
27 27
 
28
-import tests
28
+import tests.data
29
+import tests.data.callables
30
+import tests.machinery
31
+import tests.machinery.hypothesis
32
+import tests.machinery.pytest
29 33
 from derivepassphrase import _types, ssh_agent, vault
30 34
 from derivepassphrase._internals import cli_helpers
31 35
 from derivepassphrase.ssh_agent import socketprovider
... ...
@@ -46,9 +50,9 @@ class Parametrize(types.SimpleNamespace):
46 50
             pytest.param(
47 51
                 [
48 52
                     importlib.metadata.EntryPoint(
49
-                        name=tests.faulty_entry_callable.key,
53
+                        name=tests.data.faulty_entry_callable.key,
50 54
                         group=socketprovider.SocketProvider.ENTRY_POINT_GROUP_NAME,
51
-                        value="tests: faulty_entry_callable",
55
+                        value="tests.data: faulty_entry_callable",
52 56
                     ),
53 57
                 ],
54 58
                 id="not-callable",
... ...
@@ -56,9 +60,9 @@ class Parametrize(types.SimpleNamespace):
56 60
             pytest.param(
57 61
                 [
58 62
                     importlib.metadata.EntryPoint(
59
-                        name=tests.faulty_entry_name_exists.key,
63
+                        name=tests.data.faulty_entry_name_exists.key,
60 64
                         group=socketprovider.SocketProvider.ENTRY_POINT_GROUP_NAME,
61
-                        value="tests: faulty_entry_name_exists",
65
+                        value="tests.data: faulty_entry_name_exists",
62 66
                     ),
63 67
                 ],
64 68
                 id="name-already-exists",
... ...
@@ -66,9 +70,9 @@ class Parametrize(types.SimpleNamespace):
66 70
             pytest.param(
67 71
                 [
68 72
                     importlib.metadata.EntryPoint(
69
-                        name=tests.faulty_entry_alias_exists.key,
73
+                        name=tests.data.faulty_entry_alias_exists.key,
70 74
                         group=socketprovider.SocketProvider.ENTRY_POINT_GROUP_NAME,
71
-                        value="tests: faulty_entry_alias_exists",
75
+                        value="tests.data: faulty_entry_alias_exists",
72 76
                     ),
73 77
                 ],
74 78
                 id="alias-already-exists",
... ...
@@ -81,14 +85,14 @@ class Parametrize(types.SimpleNamespace):
81 85
             pytest.param(
82 86
                 [
83 87
                     importlib.metadata.EntryPoint(
84
-                        name=tests.posix_entry.key,
88
+                        name=tests.data.posix_entry.key,
85 89
                         group=socketprovider.SocketProvider.ENTRY_POINT_GROUP_NAME,
86
-                        value="tests: posix_entry",
90
+                        value="tests.data: posix_entry",
87 91
                     ),
88 92
                     importlib.metadata.EntryPoint(
89
-                        name=tests.the_annoying_os_entry.key,
93
+                        name=tests.data.the_annoying_os_entry.key,
90 94
                         group=socketprovider.SocketProvider.ENTRY_POINT_GROUP_NAME,
91
-                        value="tests: the_annoying_os_entry",
95
+                        value="tests.data: the_annoying_os_entry",
92 96
                     ),
93 97
                 ],
94 98
                 id="existing-entries",
... ...
@@ -96,14 +100,14 @@ class Parametrize(types.SimpleNamespace):
96 100
             pytest.param(
97 101
                 [
98 102
                     importlib.metadata.EntryPoint(
99
-                        name=tests.provider_entry1.key,
103
+                        name=tests.data.callables.provider_entry1.key,
100 104
                         group=socketprovider.SocketProvider.ENTRY_POINT_GROUP_NAME,
101
-                        value="tests: provider_entry1",
105
+                        value="tests.data.callables: provider_entry1",
102 106
                     ),
103 107
                     importlib.metadata.EntryPoint(
104
-                        name=tests.provider_entry2.key,
108
+                        name=tests.data.callables.provider_entry2.key,
105 109
                         group=socketprovider.SocketProvider.ENTRY_POINT_GROUP_NAME,
106
-                        value="tests: provider_entry2",
110
+                        value="tests.data.callables: provider_entry2",
107 111
                     ),
108 112
                 ],
109 113
                 id="new-entries",
... ...
@@ -253,7 +257,7 @@ class Parametrize(types.SimpleNamespace):
253 257
                 id="key-not-loaded",
254 258
             ),
255 259
             pytest.param(
256
-                tests.SUPPORTED_KEYS["ed25519"].public_key_data,
260
+                tests.data.SUPPORTED_KEYS["ed25519"].public_key_data,
257 261
                 True,
258 262
                 _types.SSH_AGENT.FAILURE,
259 263
                 b"",
... ...
@@ -267,10 +271,10 @@ class Parametrize(types.SimpleNamespace):
267 271
         ["key", "single"],
268 272
         [
269 273
             (value.public_key_data, False)
270
-            for value in tests.SUPPORTED_KEYS.values()
274
+            for value in tests.data.SUPPORTED_KEYS.values()
271 275
         ]
272
-        + [(tests.list_keys_singleton()[0].key, True)],
273
-        ids=[*tests.SUPPORTED_KEYS.keys(), "singleton"],
276
+        + [(tests.data.callables.list_keys_singleton()[0].key, True)],
277
+        ids=[*tests.data.SUPPORTED_KEYS.keys(), "singleton"],
274 278
     )
275 279
     SH_EXPORT_LINES = pytest.mark.parametrize(
276 280
         ["line", "env_name", "value"],
... ...
@@ -366,7 +370,7 @@ class Parametrize(types.SimpleNamespace):
366 370
                     b"".join([
367 371
                         b"\x0d",
368 372
                         ssh_agent.SSHAgentClient.string(
369
-                            tests.ALL_KEYS["rsa"].public_key_data
373
+                            tests.data.ALL_KEYS["rsa"].public_key_data
370 374
                         ),
371 375
                         ssh_agent.SSHAgentClient.string(vault.Vault.UUID),
372 376
                         b"\x00\x00\x00\x02",
... ...
@@ -379,7 +383,7 @@ class Parametrize(types.SimpleNamespace):
379 383
                     b"".join([
380 384
                         b"\x0d",
381 385
                         ssh_agent.SSHAgentClient.string(
382
-                            tests.ALL_KEYS["ed25519"].public_key_data
386
+                            tests.data.ALL_KEYS["ed25519"].public_key_data
383 387
                         ),
384 388
                         b"\x00\x00\x00\x08\x00\x01\x02\x03\x04\x05\x06\x07",
385 389
                         b"\x00\x00\x00\x00",
... ...
@@ -392,7 +396,7 @@ class Parametrize(types.SimpleNamespace):
392 396
                     b"".join([
393 397
                         b"\x0d",
394 398
                         ssh_agent.SSHAgentClient.string(
395
-                            tests.ALL_KEYS["dsa1024"].public_key_data
399
+                            tests.data.ALL_KEYS["dsa1024"].public_key_data
396 400
                         ),
397 401
                         ssh_agent.SSHAgentClient.string(vault.Vault.UUID),
398 402
                         b"\x00\x00\x00\x00",
... ...
@@ -415,8 +419,8 @@ class Parametrize(types.SimpleNamespace):
415 419
     )
416 420
     PUBLIC_KEY_DATA = pytest.mark.parametrize(
417 421
         "public_key_struct",
418
-        list(tests.SUPPORTED_KEYS.values()),
419
-        ids=list(tests.SUPPORTED_KEYS.keys()),
422
+        list(tests.data.SUPPORTED_KEYS.values()),
423
+        ids=list(tests.data.SUPPORTED_KEYS.keys()),
420 424
     )
421 425
     REQUEST_ERROR_RESPONSES = pytest.mark.parametrize(
422 426
         ["request_code", "response_code", "exc_type", "exc_pattern"],
... ...
@@ -484,13 +488,13 @@ class Parametrize(types.SimpleNamespace):
484 488
     )
485 489
     SUPPORTED_SSH_TEST_KEYS = pytest.mark.parametrize(
486 490
         ["ssh_test_key_type", "ssh_test_key"],
487
-        list(tests.SUPPORTED_KEYS.items()),
488
-        ids=tests.SUPPORTED_KEYS.keys(),
491
+        list(tests.data.SUPPORTED_KEYS.items()),
492
+        ids=tests.data.SUPPORTED_KEYS.keys(),
489 493
     )
490 494
     UNSUITABLE_SSH_TEST_KEYS = pytest.mark.parametrize(
491 495
         ["ssh_test_key_type", "ssh_test_key"],
492
-        list(tests.UNSUITABLE_KEYS.items()),
493
-        ids=tests.UNSUITABLE_KEYS.keys(),
496
+        list(tests.data.UNSUITABLE_KEYS.items()),
497
+        ids=tests.data.UNSUITABLE_KEYS.keys(),
494 498
     )
495 499
     RESOLVE_CHAINS = pytest.mark.parametrize(
496 500
         ["terminal", "chain"],
... ...
@@ -513,10 +517,10 @@ class TestTestingMachineryStubbedSSHAgentSocket:
513 517
         with contextlib.ExitStack() as stack:
514 518
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
515 519
             monkeypatch.setenv(
516
-                "SSH_AUTH_SOCK", tests.StubbedSSHAgentSocketWithAddress.ADDRESS
520
+                "SSH_AUTH_SOCK", tests.machinery.StubbedSSHAgentSocketWithAddress.ADDRESS
517 521
             )
518 522
             agent = stack.enter_context(
519
-                tests.StubbedSSHAgentSocketWithAddress()
523
+                tests.machinery.StubbedSSHAgentSocketWithAddress()
520 524
             )
521 525
             assert "query" not in agent.enabled_extensions
522 526
             query_request = (
... ...
@@ -541,10 +545,10 @@ class TestTestingMachineryStubbedSSHAgentSocket:
541 545
         with contextlib.ExitStack() as stack:
542 546
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
543 547
             monkeypatch.setenv(
544
-                "SSH_AUTH_SOCK", tests.StubbedSSHAgentSocketWithAddress.ADDRESS
548
+                "SSH_AUTH_SOCK", tests.machinery.StubbedSSHAgentSocketWithAddress.ADDRESS
545 549
             )
546 550
             agent = stack.enter_context(
547
-                tests.StubbedSSHAgentSocketWithAddressAndDeterministicDSA()
551
+                tests.machinery.StubbedSSHAgentSocketWithAddressAndDeterministicDSA()
548 552
             )
549 553
             assert "query" in agent.enabled_extensions
550 554
             query_request = (
... ...
@@ -574,7 +578,7 @@ class TestTestingMachineryStubbedSSHAgentSocket:
574 578
     def test_101_request_identities(self) -> None:
575 579
         """The agent implements a known list of identities."""
576 580
         unstring_prefix = ssh_agent.SSHAgentClient.unstring_prefix
577
-        with tests.StubbedSSHAgentSocket() as agent:
581
+        with tests.machinery.StubbedSSHAgentSocket() as agent:
578 582
             query_request = (
579 583
                 # SSH string header
580 584
                 b"\x00\x00\x00\x01"
... ...
@@ -597,7 +601,7 @@ class TestTestingMachineryStubbedSSHAgentSocket:
597 601
                 _comment, message = unstring_prefix(message)
598 602
                 assert key
599 603
                 assert key in {
600
-                    k.public_key_data for k in tests.ALL_KEYS.values()
604
+                    k.public_key_data for k in tests.data.ALL_KEYS.values()
601 605
                 }
602 606
             assert not message
603 607
 
... ...
@@ -605,11 +609,11 @@ class TestTestingMachineryStubbedSSHAgentSocket:
605 609
     def test_102_sign(
606 610
         self,
607 611
         ssh_test_key_type: str,
608
-        ssh_test_key: tests.SSHTestKey,
612
+        ssh_test_key: tests.data.SSHTestKey,
609 613
     ) -> None:
610 614
         """The agent signs known key/message pairs."""
611 615
         del ssh_test_key_type
612
-        spec = tests.SSHTestKeyDeterministicSignatureClass.SPEC
616
+        spec = tests.data.SSHTestKeyDeterministicSignatureClass.SPEC
613 617
         assert ssh_test_key.expected_signatures[spec].signature is not None
614 618
         string = ssh_agent.SSHAgentClient.string
615 619
         query_request = string(
... ...
@@ -628,21 +632,21 @@ class TestTestingMachineryStubbedSSHAgentSocket:
628 632
             # expected payload: the binary signature as recorded in the test key data structure
629 633
             + string(ssh_test_key.expected_signatures[spec].signature)
630 634
         )
631
-        with tests.StubbedSSHAgentSocket() as agent:
635
+        with tests.machinery.StubbedSSHAgentSocket() as agent:
632 636
             agent.sendall(query_request)
633 637
             assert agent.recv(1000) == query_response
634 638
 
635 639
     def test_120_close_multiple(self) -> None:
636 640
         """The agent can be closed repeatedly."""
637
-        with tests.StubbedSSHAgentSocket() as agent:
641
+        with tests.machinery.StubbedSSHAgentSocket() as agent:
638 642
             pass
639
-        with tests.StubbedSSHAgentSocket() as agent:
643
+        with tests.machinery.StubbedSSHAgentSocket() as agent:
640 644
             pass
641 645
         del agent
642 646
 
643 647
     def test_121_closed_agents_cannot_be_interacted_with(self) -> None:
644 648
         """The agent can be closed repeatedly."""
645
-        with tests.StubbedSSHAgentSocket() as agent:
649
+        with tests.machinery.StubbedSSHAgentSocket() as agent:
646 650
             pass
647 651
         query_request = (
648 652
             # SSH string header
... ...
@@ -655,18 +659,18 @@ class TestTestingMachineryStubbedSSHAgentSocket:
655 659
         query_response = b""
656 660
         with pytest.raises(
657 661
             ValueError,
658
-            match=re.escape(tests.StubbedSSHAgentSocket._SOCKET_IS_CLOSED),
662
+            match=re.escape(tests.machinery.StubbedSSHAgentSocket._SOCKET_IS_CLOSED),
659 663
         ):
660 664
             agent.sendall(query_request)
661 665
         assert agent.recv(100) == query_response
662 666
 
663 667
     def test_122_no_recv_without_sendall(self) -> None:
664 668
         """The agent requires a message before sending a response."""
665
-        with tests.StubbedSSHAgentSocket() as agent:  # noqa: SIM117
669
+        with tests.machinery.StubbedSSHAgentSocket() as agent:  # noqa: SIM117
666 670
             with pytest.raises(
667 671
                 AssertionError,
668 672
                 match=re.escape(
669
-                    tests.StubbedSSHAgentSocket._PROTOCOL_VIOLATION
673
+                    tests.machinery.StubbedSSHAgentSocket._PROTOCOL_VIOLATION
670 674
                 ),
671 675
             ):
672 676
                 agent.recv(100)
... ...
@@ -683,7 +687,7 @@ class TestTestingMachineryStubbedSSHAgentSocket:
683 687
             # response code: SSH_AGENT_FAILURE
684 688
             b"\x05"
685 689
         )
686
-        with tests.StubbedSSHAgentSocket() as agent:
690
+        with tests.machinery.StubbedSSHAgentSocket() as agent:
687 691
             agent.sendall(message)
688 692
             assert agent.recv(100) == query_response
689 693
 
... ...
@@ -699,7 +703,7 @@ class TestTestingMachineryStubbedSSHAgentSocket:
699 703
             # response code: SSH_AGENT_FAILURE
700 704
             b"\x05"
701 705
         )
702
-        with tests.StubbedSSHAgentSocket() as agent:
706
+        with tests.machinery.StubbedSSHAgentSocket() as agent:
703 707
             agent.sendall(message)
704 708
             assert agent.recv(100) == query_response
705 709
 
... ...
@@ -721,7 +725,7 @@ class TestTestingMachineryStubbedSSHAgentSocket:
721 725
                 stack.enter_context(
722 726
                     pytest.raises(exception, match=re.escape(match))
723 727
                 )
724
-            tests.StubbedSSHAgentSocketWithAddress()
728
+            tests.machinery.StubbedSSHAgentSocketWithAddress()
725 729
 
726 730
 
727 731
 class TestStaticFunctionality:
... ...
@@ -777,7 +781,7 @@ class TestStaticFunctionality:
777 781
     @Parametrize.PUBLIC_KEY_DATA
778 782
     def test_100_key_decoding(
779 783
         self,
780
-        public_key_struct: tests.SSHTestKey,
784
+        public_key_struct: tests.data.SSHTestKey,
781 785
     ) -> None:
782 786
         """The [`tests.ALL_KEYS`][] public key data looks sane."""
783 787
         keydata = base64.b64decode(
... ...
@@ -793,10 +797,10 @@ class TestStaticFunctionality:
793 797
     ) -> None:
794 798
         """[`tests.parse_sh_export_line`][] works."""
795 799
         if value is not None:
796
-            assert tests.parse_sh_export_line(line, env_name=env_name) == value
800
+            assert tests.data.callables.parse_sh_export_line(line, env_name=env_name) == value
797 801
         else:
798 802
             with pytest.raises(ValueError, match="Cannot parse sh line:"):
799
-                tests.parse_sh_export_line(line, env_name=env_name)
803
+                tests.data.callables.parse_sh_export_line(line, env_name=env_name)
800 804
 
801 805
     def test_200_constructor_posix_no_ssh_auth_sock(
802 806
         self,
... ...
@@ -1041,14 +1045,14 @@ class TestStaticFunctionality:
1041 1045
         """Finding all SSH agent socket providers works."""
1042 1046
         resolve = socketprovider.SocketProvider.resolve
1043 1047
         old_registry = socketprovider.SocketProvider.registry
1044
-        with tests.faked_entry_point_list(
1048
+        with tests.machinery.pytest.faked_entry_point_list(
1045 1049
             additional_entry_points, remove_conflicting_entries=False
1046 1050
         ) as names:
1047 1051
             socketprovider.SocketProvider._find_all_ssh_agent_socket_providers()
1048 1052
             for name in names:
1049 1053
                 assert name in socketprovider.SocketProvider.registry
1050 1054
                 assert resolve(name) in {
1051
-                    tests.provider_entry_provider,
1055
+                    tests.data.callables.provider_entry_provider,
1052 1056
                     *old_registry.values(),
1053 1057
                 }
1054 1058
 
... ...
@@ -1060,7 +1064,7 @@ class TestStaticFunctionality:
1060 1064
         """Finding faulty SSH agent socket providers raises errors."""
1061 1065
         with contextlib.ExitStack() as stack:
1062 1066
             stack.enter_context(
1063
-                tests.faked_entry_point_list(
1067
+                tests.machinery.pytest.faked_entry_point_list(
1064 1068
                     additional_entry_points, remove_conflicting_entries=False
1065 1069
                 )
1066 1070
             )
... ...
@@ -1231,7 +1235,7 @@ class TestAgentInteraction:
1231 1235
         self,
1232 1236
         ssh_agent_client_with_test_keys_loaded: ssh_agent.SSHAgentClient,
1233 1237
         ssh_test_key_type: str,
1234
-        ssh_test_key: tests.SSHTestKey,
1238
+        ssh_test_key: tests.data.SSHTestKey,
1235 1239
     ) -> None:
1236 1240
         """Signing data with specific SSH keys works.
1237 1241
 
... ...
@@ -1244,11 +1248,11 @@ class TestAgentInteraction:
1244 1248
         key_comment_pairs = {bytes(k): bytes(c) for k, c in client.list_keys()}
1245 1249
         public_key_data = ssh_test_key.public_key_data
1246 1250
         assert (
1247
-            tests.SSHTestKeyDeterministicSignatureClass.SPEC
1251
+            tests.data.SSHTestKeyDeterministicSignatureClass.SPEC
1248 1252
             in ssh_test_key.expected_signatures
1249 1253
         )
1250 1254
         sig = ssh_test_key.expected_signatures[
1251
-            tests.SSHTestKeyDeterministicSignatureClass.SPEC
1255
+            tests.data.SSHTestKeyDeterministicSignatureClass.SPEC
1252 1256
         ]
1253 1257
         expected_signature = sig.signature
1254 1258
         derived_passphrase = sig.derived_passphrase
... ...
@@ -1276,7 +1280,7 @@ class TestAgentInteraction:
1276 1280
         self,
1277 1281
         ssh_agent_client_with_test_keys_loaded: ssh_agent.SSHAgentClient,
1278 1282
         ssh_test_key_type: str,
1279
-        ssh_test_key: tests.SSHTestKey,
1283
+        ssh_test_key: tests.data.SSHTestKey,
1280 1284
     ) -> None:
1281 1285
         """Using an unsuitable key with [`vault.Vault`][] fails.
1282 1286
 
... ...
@@ -1320,10 +1324,10 @@ class TestAgentInteraction:
1320 1324
 
1321 1325
         def key_is_suitable(key: bytes) -> bool:
1322 1326
             """Stub out [`vault.Vault.key_is_suitable`][]."""
1323
-            always = {v.public_key_data for v in tests.SUPPORTED_KEYS.values()}
1327
+            always = {v.public_key_data for v in tests.data.SUPPORTED_KEYS.values()}
1324 1328
             dsa = {
1325 1329
                 v.public_key_data
1326
-                for k, v in tests.UNSUITABLE_KEYS.items()
1330
+                for k, v in tests.data.UNSUITABLE_KEYS.items()
1327 1331
                 if k.startswith(("dsa", "ecdsa"))
1328 1332
             }
1329 1333
             return key in always or (
... ...
@@ -1339,22 +1343,22 @@ class TestAgentInteraction:
1339 1343
             monkeypatch.setattr(
1340 1344
                 ssh_agent.SSHAgentClient,
1341 1345
                 "list_keys",
1342
-                tests.list_keys_singleton,
1346
+                tests.data.callables.list_keys_singleton,
1343 1347
             )
1344 1348
             keys = [
1345 1349
                 pair.key
1346
-                for pair in tests.list_keys_singleton()
1350
+                for pair in tests.data.callables.list_keys_singleton()
1347 1351
                 if key_is_suitable(pair.key)
1348 1352
             ]
1349 1353
             index = "1"
1350 1354
             text = "Use this key? yes\n"
1351 1355
         else:
1352 1356
             monkeypatch.setattr(
1353
-                ssh_agent.SSHAgentClient, "list_keys", tests.list_keys
1357
+                ssh_agent.SSHAgentClient, "list_keys", tests.data.callables.list_keys
1354 1358
             )
1355 1359
             keys = [
1356 1360
                 pair.key
1357
-                for pair in tests.list_keys()
1361
+                for pair in tests.data.callables.list_keys()
1358 1362
                 if key_is_suitable(pair.key)
1359 1363
             ]
1360 1364
             index = str(1 + keys.index(key))
... ...
@@ -1370,7 +1374,7 @@ class TestAgentInteraction:
1370 1374
 
1371 1375
         # TODO(the-13th-letter): (Continued from above.)  Update input
1372 1376
         # data to use `index`/`input` directly and unconditionally.
1373
-        runner = tests.CliRunner(mix_stderr=True)
1377
+        runner = tests.machinery.CliRunner(mix_stderr=True)
1374 1378
         result = runner.invoke(
1375 1379
             driver,
1376 1380
             [],
... ...
@@ -1382,7 +1386,7 @@ class TestAgentInteraction:
1382 1386
 
1383 1387
     def test_300_constructor_bad_running_agent(
1384 1388
         self,
1385
-        running_ssh_agent: tests.RunningSSHAgentInfo,
1389
+        running_ssh_agent: tests.data.RunningSSHAgentInfo,
1386 1390
     ) -> None:
1387 1391
         """Fail if the agent address is invalid."""
1388 1392
         with pytest.MonkeyPatch.context() as monkeypatch:
... ...
@@ -1426,7 +1430,7 @@ class TestAgentInteraction:
1426 1430
 
1427 1431
     def test_303_explicit_socket(
1428 1432
         self,
1429
-        spawn_ssh_agent: tests.SpawnedSSHAgentInfo,
1433
+        spawn_ssh_agent: tests.data.SpawnedSSHAgentInfo,
1430 1434
     ) -> None:
1431 1435
         conn = spawn_ssh_agent.client._connection
1432 1436
         ssh_agent.SSHAgentClient(socket=conn)
... ...
@@ -1434,7 +1438,7 @@ class TestAgentInteraction:
1434 1438
     @Parametrize.TRUNCATED_AGENT_RESPONSES
1435 1439
     def test_310_truncated_server_response(
1436 1440
         self,
1437
-        running_ssh_agent: tests.RunningSSHAgentInfo,
1441
+        running_ssh_agent: tests.data.RunningSSHAgentInfo,
1438 1442
         response: bytes,
1439 1443
     ) -> None:
1440 1444
         """Fail on truncated responses from the SSH agent."""
... ...
@@ -1458,7 +1462,7 @@ class TestAgentInteraction:
1458 1462
     @Parametrize.LIST_KEYS_ERROR_RESPONSES
1459 1463
     def test_320_list_keys_error_responses(
1460 1464
         self,
1461
-        running_ssh_agent: tests.RunningSSHAgentInfo,
1465
+        running_ssh_agent: tests.data.RunningSSHAgentInfo,
1462 1466
         response_code: _types.SSH_AGENT,
1463 1467
         response: bytes | bytearray,
1464 1468
         exc_type: type[Exception],
... ...
@@ -1518,7 +1522,7 @@ class TestAgentInteraction:
1518 1522
     @Parametrize.SIGN_ERROR_RESPONSES
1519 1523
     def test_330_sign_error_responses(
1520 1524
         self,
1521
-        running_ssh_agent: tests.RunningSSHAgentInfo,
1525
+        running_ssh_agent: tests.data.RunningSSHAgentInfo,
1522 1526
         key: bytes | bytearray,
1523 1527
         check: bool,
1524 1528
         response_code: _types.SSH_AGENT,
... ...
@@ -1578,7 +1582,7 @@ class TestAgentInteraction:
1578 1582
             com = b"no comment"
1579 1583
             loaded_keys = [
1580 1584
                 Pair(v.public_key_data, com).toreadonly()
1581
-                for v in tests.SUPPORTED_KEYS.values()
1585
+                for v in tests.data.SUPPORTED_KEYS.values()
1582 1586
             ]
1583 1587
             monkeypatch.setattr(client, "list_keys", lambda: loaded_keys)
1584 1588
             with pytest.raises(exc_type, match=exc_pattern):
... ...
@@ -1587,7 +1591,7 @@ class TestAgentInteraction:
1587 1591
     @Parametrize.REQUEST_ERROR_RESPONSES
1588 1592
     def test_340_request_error_responses(
1589 1593
         self,
1590
-        running_ssh_agent: tests.RunningSSHAgentInfo,
1594
+        running_ssh_agent: tests.data.RunningSSHAgentInfo,
1591 1595
         request_code: _types.SSH_AGENTC,
1592 1596
         response_code: _types.SSH_AGENT,
1593 1597
         exc_type: type[Exception],
... ...
@@ -1616,7 +1620,7 @@ class TestAgentInteraction:
1616 1620
     def test_350_query_extensions_malformed_responses(
1617 1621
         self,
1618 1622
         monkeypatch: pytest.MonkeyPatch,
1619
-        running_ssh_agent: tests.RunningSSHAgentInfo,
1623
+        running_ssh_agent: tests.data.RunningSSHAgentInfo,
1620 1624
         response_data: bytes,
1621 1625
     ) -> None:
1622 1626
         """Fail on malformed responses while querying extensions."""
... ...
@@ -13,7 +13,9 @@ import pytest
13 13
 from hypothesis import strategies
14 14
 from typing_extensions import Any
15 15
 
16
-import tests
16
+import tests.data
17
+import tests.data.callables
18
+import tests.machinery.hypothesis
17 19
 from derivepassphrase import _types
18 20
 
19 21
 
... ...
@@ -72,13 +74,13 @@ class Parametrize(types.SimpleNamespace):
72 74
         "test_config",
73 75
         [
74 76
             conf
75
-            for conf in tests.TEST_CONFIGS
77
+            for conf in tests.data.TEST_CONFIGS
76 78
             if conf.validation_settings in {None, (True,)}
77 79
         ],
78
-        ids=tests._test_config_ids,
80
+        ids=tests.data._test_config_ids,
79 81
     )
80 82
     VAULT_TEST_CONFIGS = pytest.mark.parametrize(
81
-        "test_config", tests.TEST_CONFIGS, ids=tests._test_config_ids
83
+        "test_config", tests.data.TEST_CONFIGS, ids=tests.data._test_config_ids
82 84
     )
83 85
 
84 86
 
... ...
@@ -102,7 +104,7 @@ def test_100_js_truthiness(value: Any) -> None:
102 104
 
103 105
 
104 106
 @Parametrize.VALID_VAULT_TEST_CONFIGS
105
-def test_200_is_vault_config(test_config: tests.VaultTestConfig) -> None:
107
+def test_200_is_vault_config(test_config: tests.data.VaultTestConfig) -> None:
106 108
     """Is this vault configuration recognized as valid/invalid?
107 109
 
108 110
     Check all test configurations that do not need custom validation
... ...
@@ -123,16 +125,16 @@ def test_200_is_vault_config(test_config: tests.VaultTestConfig) -> None:
123 125
 
124 126
 
125 127
 @hypothesis.given(
126
-    test_config=tests.smudged_vault_test_config(
128
+    test_config=tests.machinery.hypothesis.smudged_vault_test_config(
127 129
         config=strategies.sampled_from([
128 130
             conf
129
-            for conf in tests.TEST_CONFIGS
130
-            if tests.is_valid_test_config(conf)
131
+            for conf in tests.data.TEST_CONFIGS
132
+            if tests.data.is_valid_test_config(conf)
131 133
         ])
132 134
     )
133 135
 )
134 136
 def test_200a_is_vault_config_smudged(
135
-    test_config: tests.VaultTestConfig,
137
+    test_config: tests.data.VaultTestConfig,
136 138
 ) -> None:
137 139
     """Is this vault configuration recognized as valid/invalid?
138 140
 
... ...
@@ -157,7 +159,7 @@ def test_200a_is_vault_config_smudged(
157 159
 
158 160
 
159 161
 @Parametrize.VAULT_TEST_CONFIGS
160
-def test_400_validate_vault_config(test_config: tests.VaultTestConfig) -> None:
162
+def test_400_validate_vault_config(test_config: tests.data.VaultTestConfig) -> None:
161 163
     """Validate this vault configuration.
162 164
 
163 165
     Check all test configurations, including those with non-standard
... ...
@@ -188,16 +190,16 @@ def test_400_validate_vault_config(test_config: tests.VaultTestConfig) -> None:
188 190
 
189 191
 
190 192
 @hypothesis.given(
191
-    test_config=tests.smudged_vault_test_config(
193
+    test_config=tests.machinery.hypothesis.smudged_vault_test_config(
192 194
         config=strategies.sampled_from([
193 195
             conf
194
-            for conf in tests.TEST_CONFIGS
195
-            if tests.is_smudgable_vault_test_config(conf)
196
+            for conf in tests.data.TEST_CONFIGS
197
+            if tests.data.is_smudgable_vault_test_config(conf)
196 198
         ])
197 199
     )
198 200
 )
199 201
 def test_400a_validate_vault_config_smudged(
200
-    test_config: tests.VaultTestConfig,
202
+    test_config: tests.data.VaultTestConfig,
201 203
 ) -> None:
202 204
     """Validate this vault configuration.
203 205
 
... ...
@@ -17,7 +17,7 @@ import pytest
17 17
 from hypothesis import strategies
18 18
 from typing_extensions import TypeVar
19 19
 
20
-import tests
20
+import tests.machinery.hypothesis
21 21
 from derivepassphrase import vault
22 22
 
23 23
 if TYPE_CHECKING:
... ...
@@ -503,7 +503,7 @@ class TestVault:
503 503
             min_size=1,
504 504
             max_size=32,
505 505
         ),
506
-        config=tests.vault_full_service_config(),
506
+        config=tests.machinery.hypothesis.vault_full_service_config(),
507 507
         services=strategies.lists(
508 508
             strategies.binary(min_size=1, max_size=32),
509 509
             min_size=2,
... ...
@@ -629,7 +629,7 @@ class TestVault:
629 629
         phrase=strategies.one_of(
630 630
             strategies.binary(min_size=1), strategies.text(min_size=1)
631 631
         ),
632
-        config=tests.vault_full_service_config(),
632
+        config=tests.machinery.hypothesis.vault_full_service_config(),
633 633
         service=strategies.text(min_size=1),
634 634
     )
635 635
     @hypothesis.example(
636 636