Use an enum for SSH agent configurations in the test suite
Marco Ricci

Marco Ricci commited on 2026-06-15 21:36:53
Zeige 2 geänderte Dateien mit 132 Einfügungen und 58 Löschungen.


Instead of having all these magic strings floating around all over the
test suite, use a dedicated enum for consistency, discoverability, and
typo-resistance.
... ...
@@ -337,75 +337,70 @@ class SpawnHandler(NamedTuple):
337 337
     """A handler for a spawned or interfaced SSH agent.
338 338
 
339 339
     Attributes:
340
-        key:
341
-            The key under which this handler is registered.
340
+        instance:
341
+            The SSH agent instance type under which this handler is
342
+            registered.
343
+
344
+            This is a full enum, not just a string.  In particular, the
345
+            enum can be queried for the type of agent represented by
346
+            this entry.
342 347
         executable:
343 348
             The (optional) full path to the executable.
344 349
         spawn_func:
345 350
             The spawn function.
346
-        agent_type:
347
-            The type of the spawned or interfaced SSH agent.
348 351
         agent_name:
349 352
             The (optional) display name for this handler.
350 353
 
351 354
     """
352 355
 
353
-    key: str
356
+    instance: data.KnownSSHAgentInstance
354 357
     executable: str | None
355 358
     spawn_func: SSHAgentSpawnFunc | SSHAgentInterfaceFunc
356
-    agent_type: data.KnownSSHAgent
357 359
     agent_name: str | None
358 360
 
359 361
 
360 362
 spawn_handlers: dict[str, SpawnHandler] = {
361
-    "unix-pageant": SpawnHandler(
362
-        "unix-pageant",
363
+    data.KnownSSHAgentInstance.unix_pageant: SpawnHandler(
364
+        data.KnownSSHAgentInstance.unix_pageant,
363 365
         None,
364 366
         spawn_pageant_on_posix,
365
-        data.KnownSSHAgent.UNIXPageant,
366 367
         "Pageant (UNIX)",
367 368
     ),
368
-    "openssh": SpawnHandler(
369
-        "openssh",
369
+    data.KnownSSHAgentInstance.openssh: SpawnHandler(
370
+        data.KnownSSHAgentInstance.openssh,
370 371
         None,
371 372
         spawn_openssh_agent_on_posix,
372
-        data.KnownSSHAgent.OpenSSHAgent,
373 373
         "ssh-agent (OpenSSH)",
374 374
     ),
375
-    "pageant": SpawnHandler(
376
-        "pageant",
375
+    data.KnownSSHAgentInstance.pageant: SpawnHandler(
376
+        data.KnownSSHAgentInstance.pageant,
377 377
         None,
378 378
         interface_pageant_on_the_annoying_os,
379
-        data.KnownSSHAgent.Pageant,
380 379
         "Pageant",
381 380
     ),
382
-    "openssh-on-windows": SpawnHandler(
383
-        "openssh-on-windows",
381
+    data.KnownSSHAgentInstance.openssh_on_windows: SpawnHandler(
382
+        data.KnownSSHAgentInstance.openssh_on_windows,
384 383
         None,
385 384
         interface_openssh_agent_on_the_annoying_os,
386
-        data.KnownSSHAgent.OpenSSHAgentOnWindows,
387 385
         "ssh-agent (OpenSSH on Windows)",
388 386
     ),
389
-    "stub_agent_with_address": SpawnHandler(
390
-        "stub_agent_with_address",
387
+    data.KnownSSHAgentInstance.stub_agent_with_address: SpawnHandler(
388
+        data.KnownSSHAgentInstance.stub_agent_with_address,
391 389
         None,
392 390
         spawn_noop,
393
-        data.KnownSSHAgent.StubbedSSHAgent,
394 391
         "stub_agent_with_address (derivepassphrase test suite)",
395 392
     ),
396
-    "stub_agent_with_address_and_deterministic_dsa": SpawnHandler(
397
-        "stub_agent_with_address_and_deterministic_dsa",
393
+    data.KnownSSHAgentInstance.stub_agent_with_address_and_deterministic_dsa: SpawnHandler(
394
+        data.KnownSSHAgentInstance.stub_agent_with_address_and_deterministic_dsa,
398 395
         None,
399 396
         spawn_noop,
400
-        data.KnownSSHAgent.StubbedSSHAgent,
401 397
         "stub_agent_with_address_and_deterministic_dsa "
402 398
         "(derivepassphrase test suite)",
403 399
     ),
404
-    "(system-agent)": SpawnHandler(
405
-        "(system-agent)",
400
+    data.KnownSSHAgentInstance.system_agent: SpawnHandler(
401
+        data.KnownSSHAgentInstance.system_agent,
406 402
         None,
407 403
         spawn_noop,
408
-        data.KnownSSHAgent.UNKNOWN,
409 404
         None,
410 405
     ),
411 406
 }
... ...
@@ -415,7 +410,7 @@ The standard registry of agent spawning functions.
415 410
 
416 411
 
417 412
 def external_agent_restriction(
418
-    agent_type: data.KnownSSHAgent,
413
+    agent_type: data.KnownSSHAgentType,
419 414
     env_var: str,
420 415
     *,
421 416
     default: bool = True,
... ...
@@ -454,20 +449,20 @@ def external_agent_restriction(
454 449
         make use of this function internally.
455 450
 
456 451
     """
457
-    if agent_type == data.KnownSSHAgent.StubbedSSHAgent:
452
+    if agent_type == data.KnownSSHAgentType.StubbedSSHAgent:
458 453
         return default
459 454
     if not os.environ.get(env_var):
460 455
         return default
461 456
     marked_agents = {
462
-        data.KnownSSHAgent(x)
457
+        data.KnownSSHAgentType(x)
463 458
         for x in os.environ[env_var].split(",")
464
-        if x in data.KnownSSHAgent.__members__
459
+        if x in data.KnownSSHAgentType.__members__
465 460
     }
466 461
     return agent_type in marked_agents
467 462
 
468 463
 
469 464
 def is_agent_permitted(
470
-    agent_type: data.KnownSSHAgent,
465
+    agent_type: data.KnownSSHAgentType,
471 466
 ) -> bool:  # pragma: no cover [external]
472 467
     """May the given SSH agent be spawned by the test harness?
473 468
 
... ...
@@ -489,7 +484,7 @@ def is_agent_permitted(
489 484
 
490 485
 
491 486
 def is_agent_non_reentrant(
492
-    agent_type: data.KnownSSHAgent,
487
+    agent_type: data.KnownSSHAgentType,
493 488
 ) -> bool:  # pragma: no cover [external]
494 489
     """Is the given SSH agent assumed to be non-reentrant?
495 490
 
... ...
@@ -523,7 +518,7 @@ skip marks.)  Used by some test fixtures.
523 518
 for key, handler in spawn_handlers.items():
524 519
     marks = [
525 520
         pytest.mark.skipif(
526
-            not is_agent_permitted(handler.agent_type),
521
+            not is_agent_permitted(handler.instance.agent_type),
527 522
             reason="SSH agent excluded via PERMITTED_AGENTS "
528 523
             "environment variable.",
529 524
         ),
... ...
@@ -532,14 +527,14 @@ for key, handler in spawn_handlers.items():
532 527
     # issue with GnuPG 2.4.8 on The Annoying OS when `gpg-agent`
533 528
     # masquerades as OpenSSH's `ssh-agent`.
534 529
     if is_agent_non_reentrant(
535
-        handler.agent_type
530
+        handler.instance.agent_type
536 531
     ):  # pragma: no cover [external]
537 532
         marks.append(pytest.mark.non_reentrant_agent)
538 533
     # Mark non-isolated agents as, well, using non-isolated agents.
539 534
     # Assume by default that an agent is potentially non-isolated unless
540 535
     # we can be absolutely sure it is isolated, by design.  (The "stub"
541 536
     # agents fall into the latter category.)
542
-    if handler.agent_type != data.KnownSSHAgent.StubbedSSHAgent:
537
+    if handler.instance.agent_type != data.KnownSSHAgentType.StubbedSSHAgent:
543 538
         marks.append(pytest_machinery.non_isolated_agent_use)
544 539
     # Some agents require UNIX domain socket support.
545 540
     if key in {"unix-pageant", "openssh"}:
... ...
@@ -596,7 +591,7 @@ class CannotSpawnError(RuntimeError):
596 591
 def spawn_named_agent(  # noqa: C901
597 592
     executable: str | None,
598 593
     spawn_func: SSHAgentSpawnFunc | SSHAgentInterfaceFunc,
599
-    agent_type: data.KnownSSHAgent,
594
+    agent_type: data.KnownSSHAgentType,
600 595
     agent_name: str,
601 596
 ) -> Iterator[data.SpawnedSSHAgentInfo]:  # pragma: no cover [external]
602 597
     """Spawn the named SSH agent and check that it is operational.
... ...
@@ -656,7 +651,7 @@ def spawn_named_agent(  # noqa: C901
656 651
     ssh_auth_sock = agent_env.pop("SSH_AUTH_SOCK", None)
657 652
     proc_or_socket_data = spawn_func(executable=executable, env=agent_env)
658 653
     with exit_stack:
659
-        if agent_type == data.KnownSSHAgent.StubbedSSHAgent:
654
+        if agent_type == data.KnownSSHAgentType.StubbedSSHAgent:
660 655
             ssh_auth_sock = None
661 656
         elif spawn_func is spawn_noop:
662 657
             ssh_auth_sock = os.environ.get("SSH_AUTH_SOCK")
... ...
@@ -688,7 +683,7 @@ def spawn_named_agent(  # noqa: C901
688 683
         else:
689 684
             ssh_auth_sock = proc_or_socket_data[1]
690 685
         monkeypatch = exit_stack.enter_context(pytest.MonkeyPatch.context())
691
-        if agent_type == data.KnownSSHAgent.StubbedSSHAgent:
686
+        if agent_type == data.KnownSSHAgentType.StubbedSSHAgent:
692 687
             assert ssh_auth_sock is None
693 688
             monkeypatch.setenv(
694 689
                 "SSH_AUTH_SOCK",
... ...
@@ -737,7 +732,7 @@ def spawn_named_agent(  # noqa: C901
737 732
         # agent is not one of our test agents, and if the check fails,
738 733
         # skip this agent.
739 734
         if (
740
-            agent_type != data.KnownSSHAgent.StubbedSSHAgent
735
+            agent_type != data.KnownSSHAgentType.StubbedSSHAgent
741 736
         ):  # pragma: no cover [external]
742 737
             try:
743 738
                 client.list_keys()  # sanity test
... ...
@@ -799,10 +794,10 @@ def running_ssh_agent(  # pragma: no cover [external]
799 794
     del request
800 795
 
801 796
     def prepare_environment(
802
-        agent_type: data.KnownSSHAgent,
797
+        agent_type: data.KnownSSHAgentType,
803 798
     ) -> Iterator[data.RunningSSHAgentInfo]:
804 799
         with pytest.MonkeyPatch.context() as monkeypatch:
805
-            if agent_type == data.KnownSSHAgent.StubbedSSHAgent:
800
+            if agent_type == data.KnownSSHAgentType.StubbedSSHAgent:
806 801
                 monkeypatch.setattr(
807 802
                     ssh_agent.SSHAgentClient,
808 803
                     "SOCKET_PROVIDERS",
... ...
@@ -814,7 +809,7 @@ def running_ssh_agent(  # pragma: no cover [external]
814 809
                 )
815 810
                 yield data.RunningSSHAgentInfo(
816 811
                     machinery.StubbedSSHAgentSocketWithAddressAndDeterministicDSA,
817
-                    data.KnownSSHAgent.StubbedSSHAgent,
812
+                    data.KnownSSHAgentType.StubbedSSHAgent,
818 813
                 )
819 814
             else:
820 815
                 yield data.RunningSSHAgentInfo(
... ...
@@ -835,16 +830,16 @@ def running_ssh_agent(  # pragma: no cover [external]
835 830
         else:
836 831
             monkeypatch.delenv("SSH_AUTH_SOCK", raising=False)
837 832
         for handler in spawn_handlers.values():
838
-            if not is_agent_permitted(handler.agent_type):
833
+            if not is_agent_permitted(handler.instance.agent_type):
839 834
                 continue
840 835
             try:
841 836
                 for _agent_info in spawn_named_agent(
842 837
                     executable=handler.executable,
843 838
                     spawn_func=handler.spawn_func,
844
-                    agent_type=handler.agent_type,
845
-                    agent_name=handler.agent_name or handler.key,
839
+                    agent_type=handler.instance.agent_type,
840
+                    agent_name=handler.agent_name or handler.instance,
846 841
                 ):
847
-                    yield from prepare_environment(handler.agent_type)
842
+                    yield from prepare_environment(handler.instance.agent_type)
848 843
             except (KeyError, OSError, CannotSpawnError):
849 844
                 continue
850 845
             return
... ...
@@ -890,8 +885,8 @@ def spawn_ssh_agent(
890 885
             yield from spawn_named_agent(
891 886
                 executable=request.param.executable,
892 887
                 spawn_func=request.param.spawn_func,
893
-                agent_type=request.param.agent_type,
894
-                agent_name=request.param.agent_name or request.param.key,
888
+                agent_type=request.param.instance.agent_type,
889
+                agent_name=request.param.agent_name or request.param.instance,
895 890
             )
896 891
         except KeyError as exc:
897 892
             pytest.skip(
... ...
@@ -900,7 +895,7 @@ def spawn_ssh_agent(
900 895
                 else exc.args[0]
901 896
             )
902 897
         except (OSError, CannotSpawnError) as exc:
903
-            name = request.param.agent_name or request.param.key
898
+            name = request.param.agent_name or request.param.instance
904 899
             pytest.skip(f"Cannot spawn {name}: {exc}")
905 900
         return
906 901
 
... ...
@@ -989,13 +984,19 @@ def _load_key_optimistically(
989 984
             })
990 985
             if (
991 986
                 agent_type
992
-                in {data.KnownSSHAgent.UNIXPageant, data.KnownSSHAgent.Pageant}
987
+                in {
988
+                    data.KnownSSHAgentType.UNIXPageant,
989
+                    data.KnownSSHAgentType.Pageant,
990
+                }
993 991
                 and key_struct.public_key_data in current_loaded_keys
994 992
             ):
995 993
                 pass
996 994
             elif (
997 995
                 agent_type
998
-                in {data.KnownSSHAgent.UNIXPageant, data.KnownSSHAgent.Pageant}
996
+                in {
997
+                    data.KnownSSHAgentType.UNIXPageant,
998
+                    data.KnownSSHAgentType.Pageant,
999
+                }
999 1000
                 and not isolated
1000 1001
             ):
1001 1002
                 request_code, payload = _prepare_payload(
... ...
@@ -1073,7 +1074,7 @@ def ssh_agent_client_with_test_keys_loaded(
1073 1074
             if _load_key_optimistically(spawn_ssh_agent, key_struct)
1074 1075
         }
1075 1076
         if (
1076
-            agent_type != data.KnownSSHAgent.StubbedSSHAgent
1077
+            agent_type != data.KnownSSHAgentType.StubbedSSHAgent
1077 1078
             and not successfully_loaded_keys
1078 1079
         ):  # pragma: no cover [external]
1079 1080
             pytest.skip("Failed to load any test keys at all into the agent.")
... ...
@@ -23,7 +23,7 @@ import base64
23 23
 import enum
24 24
 from typing import TYPE_CHECKING, cast
25 25
 
26
-from typing_extensions import NamedTuple
26
+from typing_extensions import NamedTuple, assert_never
27 27
 
28 28
 from derivepassphrase import _types, ssh_agent, vault
29 29
 from derivepassphrase.ssh_agent import socketprovider
... ...
@@ -157,8 +157,8 @@ class SSHTestKey(NamedTuple):
157 157
         )
158 158
 
159 159
 
160
-class KnownSSHAgent(str, enum.Enum):
161
-    """Known SSH agents.
160
+class KnownSSHAgentType(str, enum.Enum):
161
+    """Known SSH agent types.
162 162
 
163 163
     Attributes:
164 164
         UNKNOWN (str):
... ...
@@ -194,6 +194,79 @@ class KnownSSHAgent(str, enum.Enum):
194 194
     """"""
195 195
 
196 196
 
197
+class KnownSSHAgentInstance(str, enum.Enum):
198
+    """Known SSH agent instances, as test cases/configurations.
199
+
200
+    Attributes:
201
+        pageant:
202
+            An instance of [`Pageant`][KnownSSHAgentType.Pageant].
203
+        openssh:
204
+            An instance of
205
+            [`OpenSSHAgent`][KnownSSHAgentType.OpenSSHAgent].
206
+        unix_pageant:
207
+            An instance of
208
+            [`UNIXPageant`][KnownSSHAgentType.UNIXPageant].
209
+        openssh_on_windows:
210
+            An instance of
211
+            [`OpenSSHAgentOnWindows`][KnownSSHAgentType.OpenSSHAgentOnWindows].
212
+        stub_agent_with_address:
213
+            An instance of
214
+            [`StubbedSSHAgent`][KnownSSHAgentType.StubbedSSHAgent] with
215
+            a specific "socket address".
216
+        stub_agent_with_address_and_deterministic_dsa:
217
+            An instance of
218
+            [`StubbedSSHAgent`][KnownSSHAgentType.StubbedSSHAgent] with
219
+            a specific "socket address" that also support deterministic
220
+            DSA signatures.
221
+        system_agent:
222
+            The system agent, of [unknown][KnownSSHAgentType.UNKNOWN]
223
+            type.
224
+
225
+    """
226
+
227
+    pageant = "pageant"
228
+    """"""
229
+    openssh = "openssh"
230
+    """"""
231
+    unix_pageant = "unix_pageant"
232
+    """"""
233
+    openssh_on_windows = "openssh_on_windows"
234
+    """"""
235
+    stub_agent_with_address = "stub_agent_with_address"
236
+    """"""
237
+    stub_agent_with_address_and_deterministic_dsa = (
238
+        "stub_agent_with_address_and_deterministic_dsa"
239
+    )
240
+    """"""
241
+    system_agent = "system_agent"
242
+    """"""
243
+
244
+    @property
245
+    def agent_type(self) -> KnownSSHAgentType:
246
+        """Return the agent type for this instance."""
247
+        # TODO(the-13th-letter): Rewrite using structural pattern
248
+        # matching.
249
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
250
+        if self == KnownSSHAgentInstance.pageant:
251
+            return KnownSSHAgentType.Pageant
252
+        if self == KnownSSHAgentInstance.openssh:
253
+            return KnownSSHAgentType.OpenSSHAgent
254
+        if self == KnownSSHAgentInstance.unix_pageant:
255
+            return KnownSSHAgentType.UNIXPageant
256
+        if self == KnownSSHAgentInstance.openssh_on_windows:
257
+            return KnownSSHAgentType.OpenSSHAgentOnWindows
258
+        if self == KnownSSHAgentInstance.stub_agent_with_address:
259
+            return KnownSSHAgentType.StubbedSSHAgent
260
+        if (
261
+            self
262
+            == KnownSSHAgentInstance.stub_agent_with_address_and_deterministic_dsa
263
+        ):
264
+            return KnownSSHAgentType.StubbedSSHAgent
265
+        if self == KnownSSHAgentInstance.system_agent:
266
+            return KnownSSHAgentType.UNKNOWN
267
+        assert_never(self)
268
+
269
+
197 270
 class SpawnedSSHAgentInfo(NamedTuple):
198 271
     """Info about a spawned SSH agent, as provided by some fixtures.
199 272
 
... ...
@@ -215,7 +288,7 @@ class SpawnedSSHAgentInfo(NamedTuple):
215 288
 
216 289
     """
217 290
 
218
-    agent_type: KnownSSHAgent
291
+    agent_type: KnownSSHAgentType
219 292
     """"""
220 293
     client: ssh_agent.SSHAgentClient
221 294
     """"""
... ...
@@ -241,7 +314,7 @@ class RunningSSHAgentInfo(NamedTuple):
241 314
 
242 315
     socket: str | type[_types.SSHAgentSocket]
243 316
     """"""
244
-    agent_type: KnownSSHAgent
317
+    agent_type: KnownSSHAgentType
245 318
     """"""
246 319
 
247 320
     def require_external_address(self) -> str:  # pragma: no cover
248 321