Run the vault utility `key_to_phrase` tests against the stub SSH agent
Marco Ricci

Marco Ricci commited on 2026-06-22 15:03:49
Zeige 1 geänderte Dateien mit 37 Einfügungen und 114 Löschungen.


The `key_to_phrase` vault utitilies test set checks the various error
conditions that can occur when obtaining an equivalent vault master
passphrase from a master SSH key.  The test has 10 parametrization
instances, and used to run against the
`ssh_agent_client_with_test_keys_loaded` fixture, which currently has
8 parametrization instances, making for a total of 80 tests attempted.
However, the tests either focus on construction failures or they inject
fake agent responses into the client, so in all 80 cases, there is no
actual I/O occurring with the agent.  For the 5 external agents (the
first 50 cases), this is wasteful, and for the 3 internal agents (the
remaining 30 cases), the results are identical among them.  So cut this
down to testing against 1 of the internal agents (the stubbed SSH agent
with address support), for a total of 10 test cases, none of which
involve external I/O.

As a bonus, because the remaining internal agent is guaranteed
reentrant, we can do away with the parametrization instance distinction
between construction failures and other failures, and with the special
handling of potentially non-reentrant agents.

To make this work, because some tests simulate broken support for
system-specific SSH agent socket providers, our setup code needs to
force the use of the respective provider.  As a result, only the
`UNSET_PROVIDERS`, `UNSET_AF_UNIX_AND_ENSURE_USE`,
`UNSET_NATIVE_AND_ENSURE_USE` and `UNSET_WINDLL_AND_ENSURE_USE`
constants from our `SystemSupportAction` enum are still in use, the
`UNSET_AF_UNIX`, `UNSET_NATIVE` and `UNSET_WINDLL` constants are not.
We retire those constants, and remove the "skip the test if that
provider is not currently in use" machinery associated with those
constants.  The remaining "ensure use" machinery is short enough that it
can then be inlined at the call sites.
... ...
@@ -69,6 +69,21 @@ DUMMY_KEY3_B64 = data.DUMMY_KEY3_B64
69 69
 TEST_CONFIGS = data.TEST_CONFIGS
70 70
 
71 71
 
72
+@pytest.fixture
73
+def use_stub_agent_with_address() -> Generator[None, None, None]:
74
+    """Enforce use of the stubbed SSH agent with socket address."""
75
+    with pytest.MonkeyPatch.context() as monkeypatch:
76
+        monkeypatch.setattr(
77
+            ssh_agent.SSHAgentClient,
78
+            "SOCKET_PROVIDERS",
79
+            (_types.BuiltinSSHAgentSocketProvider.STUB_AGENT_WITH_ADDRESS,),
80
+        )
81
+        monkeypatch.setenv(
82
+            "SSH_AUTH_SOCK", machinery.StubbedSSHAgentSocketWithAddress.ADDRESS
83
+        )
84
+        yield
85
+
86
+
72 87
 def vault_config_exporter_shell_interpreter(  # noqa: C901
73 88
     script: str | Iterable[str],
74 89
     /,
... ...
@@ -278,38 +293,26 @@ class SystemSupportAction(str, enum.Enum):
278 293
     """Test fixture settings for [`ssh_agent.SSHAgentClient`][] system support.
279 294
 
280 295
     Attributes:
281
-        UNSET_AF_UNIX:
282
-            Ensure lack of support for UNIX domain sockets.
283 296
         UNSET_AF_UNIX_AND_ENSURE_USE:
284 297
             Ensure lack of support for UNIX domain sockets, and that the
285 298
             agent will use this socket provider.
286
-        UNSET_NATIVE:
287
-            Ensure both `UNSET_AF_UNIX` and `UNSET_WINDLL`.
288 299
         UNSET_NATIVE_AND_ENSURE_USE:
289 300
             Ensure both `UNSET_AF_UNIX` and `UNSET_WINDLL`, and that the
290 301
             agent will use the native socket provider.
291 302
         UNSET_PROVIDER_LIST:
292 303
             Ensure an empty list of SSH agent socket providers.
293
-        UNSET_WINDLL:
294
-            Ensure lack of support for Windows named pipes.
295 304
         UNSET_WINDLL_AND_ENSURE_USE:
296 305
             Ensure lack of support for Windows named pipes, and that the
297 306
             agent will use this socket provider.
298 307
 
299 308
     """
300 309
 
301
-    UNSET_AF_UNIX = enum.auto()
302
-    """"""
303 310
     UNSET_AF_UNIX_AND_ENSURE_USE = enum.auto()
304 311
     """"""
305
-    UNSET_NATIVE = enum.auto()
306
-    """"""
307 312
     UNSET_NATIVE_AND_ENSURE_USE = enum.auto()
308 313
     """"""
309 314
     UNSET_PROVIDER_LIST = enum.auto()
310 315
     """"""
311
-    UNSET_WINDLL = enum.auto()
312
-    """"""
313 316
     UNSET_WINDLL_AND_ENSURE_USE = enum.auto()
314 317
     """"""
315 318
 
... ...
@@ -329,91 +332,33 @@ class SystemSupportAction(str, enum.Enum):
329 332
             monkeypatch.setattr(
330 333
                 ssh_agent.SSHAgentClient, "SOCKET_PROVIDERS", []
331 334
             )
332
-        elif self in {self.UNSET_NATIVE, self.UNSET_NATIVE_AND_ENSURE_USE}:
333
-            self.check_or_ensure_use(
334
-                _types.BuiltinSSHAgentSocketProvider.NATIVE,
335
-                monkeypatch=monkeypatch,
336
-                ensure_use=(self == self.UNSET_NATIVE_AND_ENSURE_USE),
335
+        elif self == self.UNSET_NATIVE_AND_ENSURE_USE:
336
+            monkeypatch.setattr(
337
+                ssh_agent.SSHAgentClient,
338
+                "SOCKET_PROVIDERS",
339
+                [_types.BuiltinSSHAgentSocketProvider.NATIVE],
337 340
             )
338 341
             monkeypatch.delattr(socket, "AF_UNIX", raising=False)
339 342
             monkeypatch.delattr(ctypes, "WinDLL", raising=False)
340 343
             monkeypatch.delattr(ctypes, "windll", raising=False)
341
-        elif self in {self.UNSET_AF_UNIX, self.UNSET_AF_UNIX_AND_ENSURE_USE}:
342
-            self.check_or_ensure_use(
343
-                _types.BuiltinSSHAgentSocketProvider.POSIX,
344
-                monkeypatch=monkeypatch,
345
-                ensure_use=(self == self.UNSET_AF_UNIX_AND_ENSURE_USE),
344
+        elif self == self.UNSET_AF_UNIX_AND_ENSURE_USE:
345
+            monkeypatch.setattr(
346
+                ssh_agent.SSHAgentClient,
347
+                "SOCKET_PROVIDERS",
348
+                [_types.BuiltinSSHAgentSocketProvider.SSH_AUTH_SOCK_ON_POSIX],
346 349
             )
347 350
             monkeypatch.delattr(socket, "AF_UNIX", raising=False)
348
-        elif self in {self.UNSET_WINDLL, self.UNSET_WINDLL_AND_ENSURE_USE}:
349
-            self.check_or_ensure_use(
350
-                _types.BuiltinSSHAgentSocketProvider.WINDOWS,
351
-                monkeypatch=monkeypatch,
352
-                ensure_use=(self == self.UNSET_WINDLL_AND_ENSURE_USE),
351
+        elif self == self.UNSET_WINDLL_AND_ENSURE_USE:
352
+            monkeypatch.setattr(
353
+                ssh_agent.SSHAgentClient,
354
+                "SOCKET_PROVIDERS",
355
+                [_types.BuiltinSSHAgentSocketProvider.WINDOWS_NAMED_PIPE],
353 356
             )
354 357
             monkeypatch.delattr(ctypes, "WinDLL", raising=False)
355 358
             monkeypatch.delattr(ctypes, "windll", raising=False)
356 359
         else:
357 360
             raise AssertionError()
358 361
 
359
-    @staticmethod
360
-    def check_or_ensure_use(
361
-        provider: str, /, *, monkeypatch: pytest.MonkeyPatch, ensure_use: bool
362
-    ) -> None:
363
-        """Check that the named SSH agent socket provider will be used.
364
-
365
-        Either ensure that the socket provider will definitely be used,
366
-        or, upon detecting that it won't be used, skip the test.
367
-
368
-        Args:
369
-            provider:
370
-                The provider to check for.
371
-            ensure_use:
372
-                If true, ensure that the socket provider will definitely
373
-                be used.  If false, then check for whether it will be
374
-                used, and skip this test if not.
375
-            monkeypatch:
376
-                The monkeypatch context within which the fixture
377
-                adjustments should be executed.
378
-
379
-        """
380
-        if ensure_use:
381
-            monkeypatch.setattr(
382
-                ssh_agent.SSHAgentClient, "SOCKET_PROVIDERS", [provider]
383
-            )
384
-        else:  # pragma: no cover [external]
385
-            # This branch operates completely on instrumented or on
386
-            # externally defined, non-deterministic state.
387
-            intended: (
388
-                _types.SSHAgentSocketProvider
389
-                | socketprovider.NoSuchProviderError
390
-                | None
391
-            )
392
-            try:
393
-                intended = socketprovider.SocketProvider.lookup(provider)
394
-            except socketprovider.NoSuchProviderError as exc:
395
-                intended = exc
396
-            actual: (
397
-                _types.SSHAgentSocketProvider
398
-                | socketprovider.NoSuchProviderError
399
-                | None
400
-            )
401
-            for name in ssh_agent.SSHAgentClient.SOCKET_PROVIDERS:
402
-                try:
403
-                    actual = socketprovider.SocketProvider.lookup(name)
404
-                except socketprovider.NoSuchProviderError as exc:
405
-                    actual = exc
406
-                if actual is None:
407
-                    continue
408
-                break
409
-            else:
410
-                actual = None
411
-            if intended != actual:
412
-                pytest.skip(
413
-                    f"{provider!r} SSH agent socket provider "
414
-                    f"is not currently in use"
415
-                )
416
-
417 362
 
418 363
 class Parametrize(types.SimpleNamespace):
419 364
     """Common test parametrizations."""
... ...
@@ -477,7 +422,6 @@ class Parametrize(types.SimpleNamespace):
477 422
             "sign_action",
478 423
             "pattern",
479 424
             "warnings_patterns",
480
-            "tests_construction_failure",
481 425
         ],
482 426
         [
483 427
             pytest.param(
... ...
@@ -487,7 +431,6 @@ class Parametrize(types.SimpleNamespace):
487 431
                 SignAction.FAIL,
488 432
                 "not loaded into the agent",
489 433
                 [],
490
-                False,
491 434
                 id="key-not-loaded",
492 435
             ),
493 436
             pytest.param(
... ...
@@ -497,7 +440,6 @@ class Parametrize(types.SimpleNamespace):
497 440
                 SignAction.FAIL,
498 441
                 "SSH agent failed to or refused to",
499 442
                 [],
500
-                False,
501 443
                 id="list-keys-refused",
502 444
             ),
503 445
             pytest.param(
... ...
@@ -507,7 +449,6 @@ class Parametrize(types.SimpleNamespace):
507 449
                 SignAction.FAIL,
508 450
                 "SSH agent failed to or refused to",
509 451
                 [],
510
-                False,
511 452
                 id="list-keys-protocol-error",
512 453
             ),
513 454
             pytest.param(
... ...
@@ -517,7 +458,6 @@ class Parametrize(types.SimpleNamespace):
517 458
                 SignAction.FAIL,
518 459
                 "Cannot connect to the SSH agent",
519 460
                 [],
520
-                True,
521 461
                 id="agent-address-mangled",
522 462
             ),
523 463
             pytest.param(
... ...
@@ -527,17 +467,15 @@ class Parametrize(types.SimpleNamespace):
527 467
                 SignAction.FAIL,
528 468
                 "Cannot find any running SSH agent",
529 469
                 [],
530
-                True,
531 470
                 id="agent-address-missing",
532 471
             ),
533 472
             pytest.param(
534 473
                 None,
535 474
                 None,
536
-                SystemSupportAction.UNSET_NATIVE,
475
+                SystemSupportAction.UNSET_NATIVE_AND_ENSURE_USE,
537 476
                 SignAction.FAIL,
538 477
                 "does not support communicating with it",
539 478
                 [],
540
-                True,
541 479
                 id="no-native-agent-available",
542 480
             ),
543 481
             pytest.param(
... ...
@@ -547,7 +485,6 @@ class Parametrize(types.SimpleNamespace):
547 485
                 SignAction.FAIL,
548 486
                 "does not support communicating with it",
549 487
                 [],
550
-                True,
551 488
                 id="no-agents-in-agent-provider-list",
552 489
             ),
553 490
             pytest.param(
... ...
@@ -557,7 +494,6 @@ class Parametrize(types.SimpleNamespace):
557 494
                 SignAction.FAIL,
558 495
                 "does not support communicating with it",
559 496
                 ["Cannot connect to an SSH agent via UNIX domain sockets"],
560
-                True,
561 497
                 id="no-unix-domain-sockets",
562 498
             ),
563 499
             pytest.param(
... ...
@@ -567,7 +503,6 @@ class Parametrize(types.SimpleNamespace):
567 503
                 SignAction.FAIL,
568 504
                 "does not support communicating with it",
569 505
                 ["Cannot connect to an SSH agent via Windows named pipes"],
570
-                True,
571 506
                 id="no-windows-named-pipes",
572 507
             ),
573 508
             pytest.param(
... ...
@@ -577,7 +512,6 @@ class Parametrize(types.SimpleNamespace):
577 512
                 SignAction.FAIL_RUNTIME,
578 513
                 "violates the communication protocol",
579 514
                 [],
580
-                False,
581 515
                 id="sign-violates-protocol",
582 516
             ),
583 517
         ],
... ...
@@ -1474,20 +1408,18 @@ class TestMisc:
1474 1408
                 )
1475 1409
 
1476 1410
     @Parametrize.KEY_TO_PHRASE_SETTINGS
1477
-    def test_key_to_phrase(  # noqa: C901
1411
+    def test_key_to_phrase(
1478 1412
         self,
1479
-        request: pytest.FixtureRequest,
1480
-        ssh_agent_client_with_test_keys_loaded: ssh_agent.SSHAgentClient,
1413
+        use_stub_agent_with_address: None,
1481 1414
         list_keys_action: ListKeysAction | None,
1482 1415
         system_support_action: SystemSupportAction | None,
1483 1416
         address_action: SocketAddressAction | None,
1484 1417
         sign_action: SignAction,
1485 1418
         pattern: str,
1486 1419
         warnings_patterns: list[str],
1487
-        tests_construction_failure: bool,
1488 1420
     ) -> None:
1489 1421
         """All errors in [`cli_helpers.key_to_phrase`][] are handled."""
1490
-
1422
+        del use_stub_agent_with_address
1491 1423
         captured_warnings: list[str] = []
1492 1424
 
1493 1425
         class ErrCallback(BaseException):
... ...
@@ -1503,12 +1435,10 @@ class TestMisc:
1503 1435
             if args:  # pragma: no branch
1504 1436
                 captured_warnings.append(str(args[0]))
1505 1437
 
1506
-        with pytest.MonkeyPatch.context() as monkeypatch:
1507
-            loaded_keys = list(
1508
-                ssh_agent_client_with_test_keys_loaded.list_keys()
1509
-            )
1438
+        with ssh_agent.SSHAgentClient.ensure_agent_subcontext() as client:
1439
+            loaded_keys = list(client.list_keys())
1510 1440
         loaded_key = base64.standard_b64encode(loaded_keys[0][0])
1511
-
1441
+        with pytest.MonkeyPatch.context() as monkeypatch:
1512 1442
             monkeypatch.setattr(ssh_agent.SSHAgentClient, "sign", sign_action)
1513 1443
             if list_keys_action:
1514 1444
                 monkeypatch.setattr(
... ...
@@ -1519,13 +1449,6 @@ class TestMisc:
1519 1449
             if system_support_action:
1520 1450
                 system_support_action(monkeypatch)
1521 1451
 
1522
-            if not tests_construction_failure:
1523
-                pytest_machinery.ensure_singleton_client_if_non_reentrant_agent(
1524
-                    request=request,
1525
-                    monkeypatch=monkeypatch,
1526
-                    client=ssh_agent_client_with_test_keys_loaded,
1527
-                )
1528
-
1529 1452
             with pytest.raises(ErrCallback, match=pattern) as excinfo:
1530 1453
                 cli_helpers.key_to_phrase(
1531 1454
                     loaded_key, error_callback=err, warning_callback=warn
1532 1455