Marco Ricci commited on 2025-12-25 14:33:08
Zeige 6 geänderte Dateien mit 239 Einfügungen und 56 Löschungen.
Introduce SSH agent spawning functions that interface with running SSH agents on The Annoying OS. Also officially activate interfacing with the system SSH agent on The Annoying OS, now that there are working SSH agent socket providers on The Annoying OS that do not require UNIX domain socket support. Because the socket provider registry drastically changed shape since 0123456789abcdef, also update the tests referencing registry entries to use the correct non-alias base entry, if needed. The two most common agents on The Annoying OS, PuTTY/Pageant and OpenSSH on Windows, do not support session- or subshell-scoped spawning as they do on UNIX. So technically, these new "spawning" functions are actually "interfacing" functions. Introduce a matching type `SSHAgentInterfaceFunc` for these functions, as aluded to in a previous commit. Because the type does not return a process object, consumers of `SSHAgentSpawnFunc | SSHAgentInterfaceFunc` need to be adapted accordingly, leading to a couple of code changes.
| ... | ... |
@@ -6,15 +6,15 @@ from __future__ import annotations |
| 6 | 6 |
|
| 7 | 7 |
import base64 |
| 8 | 8 |
import contextlib |
| 9 |
+import ctypes |
|
| 9 | 10 |
import datetime |
| 10 | 11 |
import importlib |
| 11 | 12 |
import importlib.util |
| 12 | 13 |
import os |
| 13 |
-import shutil |
|
| 14 | 14 |
import socket |
| 15 | 15 |
import subprocess |
| 16 | 16 |
import sys |
| 17 |
-from typing import TYPE_CHECKING, Protocol, TypeVar |
|
| 17 |
+from typing import TYPE_CHECKING, Any, Protocol, TypeVar |
|
| 18 | 18 |
|
| 19 | 19 |
import hypothesis |
| 20 | 20 |
import packaging.version |
| ... | ... |
@@ -25,6 +25,7 @@ import tests.data |
| 25 | 25 |
import tests.data.callables |
| 26 | 26 |
import tests.machinery |
| 27 | 27 |
from derivepassphrase import _types, ssh_agent |
| 28 |
+from derivepassphrase.ssh_agent import socketprovider |
|
| 28 | 29 |
|
| 29 | 30 |
if TYPE_CHECKING: |
| 30 | 31 |
from collections.abc import Iterator, Sequence |
| ... | ... |
@@ -184,6 +185,37 @@ class SSHAgentSpawnFunc(Protocol): |
| 184 | 185 |
""" |
| 185 | 186 |
|
| 186 | 187 |
|
| 188 |
+class SSHAgentInterfaceFunc(Protocol): |
|
| 189 |
+ """Interface an SSH agent, if possible.""" |
|
| 190 |
+ |
|
| 191 |
+ def __call__( |
|
| 192 |
+ self, |
|
| 193 |
+ executable: str | None, |
|
| 194 |
+ env: dict[str, Any], |
|
| 195 |
+ ) -> _types.SSHAgentSocket | None: |
|
| 196 |
+ """Interface the SSH agent. |
|
| 197 |
+ |
|
| 198 |
+ Args: |
|
| 199 |
+ env: |
|
| 200 |
+ A configuration environment to interface the agent. |
|
| 201 |
+ Will typically include some kind of address setting |
|
| 202 |
+ detailing how to communicate with the agent. The |
|
| 203 |
+ exact contents depend on the specific agent. |
|
| 204 |
+ |
|
| 205 |
+ Other Args: |
|
| 206 |
+ executable: |
|
| 207 |
+ (Ignored. Not relevant for interfacing functions.) |
|
| 208 |
+ |
|
| 209 |
+ Returns: |
|
| 210 |
+ An abstract socket connected to the specified SSH agent, if |
|
| 211 |
+ possible; otherwise, `None`. |
|
| 212 |
+ |
|
| 213 |
+ It is the caller's responsibility to clean up the socket |
|
| 214 |
+ (through use of the context manager protocol). |
|
| 215 |
+ |
|
| 216 |
+ """ |
|
| 217 |
+ |
|
| 218 |
+ |
|
| 187 | 219 |
def spawn_pageant_on_posix( # pragma: no cover [external] |
| 188 | 220 |
executable: str | None, env: dict[str, str] |
| 189 | 221 |
) -> subprocess.Popen[str] | None: |
| ... | ... |
@@ -296,8 +328,58 @@ def spawn_noop( # pragma: no cover [unused] |
| 296 | 328 |
"""Placeholder function. Does nothing.""" |
| 297 | 329 |
|
| 298 | 330 |
|
| 331 |
+def interface_pageant_on_the_annoying_os( |
|
| 332 |
+ executable: str | None, |
|
| 333 |
+ env: dict[str, Any], |
|
| 334 |
+) -> _types.SSHAgentSocket | None: # pragma: no cover [external] |
|
| 335 |
+ """Interface a Pageant instance on The Annoying OS, if possible. |
|
| 336 |
+ |
|
| 337 |
+ Args: |
|
| 338 |
+ env: |
|
| 339 |
+ (Ignored.) |
|
| 340 |
+ |
|
| 341 |
+ Returns: |
|
| 342 |
+ An abstract socket connected to Pageant, if possible; else |
|
| 343 |
+ `None`. |
|
| 344 |
+ |
|
| 345 |
+ It is the caller's responsibility to clean up the socket |
|
| 346 |
+ (through use of the context manager protocol). |
|
| 347 |
+ |
|
| 348 |
+ """ |
|
| 349 |
+ del executable, env |
|
| 350 |
+ try: |
|
| 351 |
+ return socketprovider.SocketProvider.windows_named_pipe_for_pageant() |
|
| 352 |
+ except (OSError, socketprovider.WindowsNamedPipesNotAvailableError): |
|
| 353 |
+ return None |
|
| 354 |
+ |
|
| 355 |
+ |
|
| 356 |
+def interface_openssh_agent_on_the_annoying_os( |
|
| 357 |
+ executable: str | None, |
|
| 358 |
+ env: dict[str, Any], |
|
| 359 |
+) -> _types.SSHAgentSocket | None: # pragma: no cover [external] |
|
| 360 |
+ """Interface an OpenSSH agent instance on The Annoying OS, if possible. |
|
| 361 |
+ |
|
| 362 |
+ Args: |
|
| 363 |
+ env: |
|
| 364 |
+ (Ignored.) |
|
| 365 |
+ |
|
| 366 |
+ Returns: |
|
| 367 |
+ An abstract socket connected to the OpenSSH agent, if possible; |
|
| 368 |
+ else `None`. |
|
| 369 |
+ |
|
| 370 |
+ It is the caller's responsibility to clean up the socket |
|
| 371 |
+ (through use of the context manager protocol). |
|
| 372 |
+ |
|
| 373 |
+ """ |
|
| 374 |
+ del executable, env |
|
| 375 |
+ try: |
|
| 376 |
+ return socketprovider.SocketProvider.windows_named_pipe_for_openssh() |
|
| 377 |
+ except (OSError, socketprovider.WindowsNamedPipesNotAvailableError): |
|
| 378 |
+ return None |
|
| 379 |
+ |
|
| 380 |
+ |
|
| 299 | 381 |
class SpawnHandler(NamedTuple): |
| 300 |
- """A handler for a spawned SSH agent. |
|
| 382 |
+ """A handler for a spawned or interfaced SSH agent. |
|
| 301 | 383 |
|
| 302 | 384 |
Attributes: |
| 303 | 385 |
key: |
| ... | ... |
@@ -307,7 +389,7 @@ class SpawnHandler(NamedTuple): |
| 307 | 389 |
spawn_func: |
| 308 | 390 |
The spawn function. |
| 309 | 391 |
agent_type: |
| 310 |
- The type of the spawned SSH agent. |
|
| 392 |
+ The type of the spawned or interfaced SSH agent. |
|
| 311 | 393 |
agent_name: |
| 312 | 394 |
The (optional) display name for this handler. |
| 313 | 395 |
|
| ... | ... |
@@ -315,7 +397,7 @@ class SpawnHandler(NamedTuple): |
| 315 | 397 |
|
| 316 | 398 |
key: str |
| 317 | 399 |
executable: str | None |
| 318 |
- spawn_func: SSHAgentSpawnFunc |
|
| 400 |
+ spawn_func: SSHAgentSpawnFunc | SSHAgentInterfaceFunc |
|
| 319 | 401 |
agent_type: tests.data.KnownSSHAgent |
| 320 | 402 |
agent_name: str | None |
| 321 | 403 |
|
| ... | ... |
@@ -335,7 +417,21 @@ spawn_handlers: dict[str, SpawnHandler] = {
|
| 335 | 417 |
tests.data.KnownSSHAgent.OpenSSHAgent, |
| 336 | 418 |
"ssh-agent (OpenSSH)", |
| 337 | 419 |
), |
| 338 |
- "stub_agent_with_address": ( |
|
| 420 |
+ "pageant": SpawnHandler( |
|
| 421 |
+ "pageant", |
|
| 422 |
+ None, |
|
| 423 |
+ interface_pageant_on_the_annoying_os, |
|
| 424 |
+ tests.data.KnownSSHAgent.Pageant, |
|
| 425 |
+ "Pageant", |
|
| 426 |
+ ), |
|
| 427 |
+ "openssh-on-windows": SpawnHandler( |
|
| 428 |
+ "openssh-on-windows", |
|
| 429 |
+ None, |
|
| 430 |
+ interface_openssh_agent_on_the_annoying_os, |
|
| 431 |
+ tests.data.KnownSSHAgent.OpenSSHAgentOnWindows, |
|
| 432 |
+ "ssh-agent (OpenSSH on Windows)", |
|
| 433 |
+ ), |
|
| 434 |
+ "stub_agent_with_address": SpawnHandler( |
|
| 339 | 435 |
"stub_agent_with_address", |
| 340 | 436 |
None, |
| 341 | 437 |
spawn_noop, |
| ... | ... |
@@ -405,15 +501,25 @@ for key, handler in spawn_handlers.items(): |
| 405 | 501 |
"environment variable.", |
| 406 | 502 |
), |
| 407 | 503 |
] |
| 408 |
- if key in {"unix-pageant", "openssh", "(system-agent)"}:
|
|
| 504 |
+ # Some agents require UNIX domain socket support. |
|
| 505 |
+ if key in {"unix-pageant", "openssh"}:
|
|
| 409 | 506 |
marks.append( |
| 410 | 507 |
pytest.mark.skipif( |
| 411 | 508 |
not hasattr(socket, "AF_UNIX"), |
| 412 | 509 |
reason="No Python or system support for UNIX domain sockets.", |
| 413 | 510 |
) |
| 414 | 511 |
) |
| 512 |
+ # Others require Windows named pipe support. |
|
| 513 |
+ if key in {"pageant", "openssh-on-windows"}:
|
|
| 514 |
+ marks.append( |
|
| 515 |
+ pytest.mark.skipif( |
|
| 516 |
+ not hasattr(ctypes, "WinDLL"), |
|
| 517 |
+ reason="No Python or system support for Windows named pipes.", |
|
| 518 |
+ ) |
|
| 519 |
+ ) |
|
| 415 | 520 |
spawn_handlers_params.append(pytest.param(handler, id=key, marks=marks)) |
| 416 | 521 |
|
| 522 |
+ |
|
| 417 | 523 |
Popen = TypeVar("Popen", bound=subprocess.Popen)
|
| 418 | 524 |
|
| 419 | 525 |
|
| ... | ... |
@@ -442,9 +548,9 @@ class CannotSpawnError(RuntimeError): |
| 442 | 548 |
"""Cannot spawn the SSH agent.""" |
| 443 | 549 |
|
| 444 | 550 |
|
| 445 |
-def spawn_named_agent( |
|
| 551 |
+def spawn_named_agent( # noqa: C901 |
|
| 446 | 552 |
executable: str | None, |
| 447 |
- spawn_func: SSHAgentSpawnFunc, |
|
| 553 |
+ spawn_func: SSHAgentSpawnFunc | SSHAgentInterfaceFunc, |
|
| 448 | 554 |
agent_type: tests.data.KnownSSHAgent, |
| 449 | 555 |
agent_name: str, |
| 450 | 556 |
) -> Iterator[tests.data.SpawnedSSHAgentInfo]: # pragma: no cover [external] |
| ... | ... |
@@ -504,24 +610,17 @@ def spawn_named_agent( |
| 504 | 610 |
exit_stack = contextlib.ExitStack() |
| 505 | 611 |
agent_env = os.environ.copy() |
| 506 | 612 |
ssh_auth_sock = agent_env.pop("SSH_AUTH_SOCK", None)
|
| 507 |
- proc = spawn_func( |
|
| 508 |
- executable=shutil.which(executable) |
|
| 509 |
- if executable is not None |
|
| 510 |
- else None, |
|
| 511 |
- env=agent_env, |
|
| 512 |
- ) |
|
| 613 |
+ proc_or_socket = spawn_func(executable=executable, env=agent_env) |
|
| 513 | 614 |
with exit_stack: |
| 514 |
- if ( |
|
| 515 |
- spawn_func is spawn_noop |
|
| 516 |
- and agent_type == tests.data.KnownSSHAgent.StubbedSSHAgent |
|
| 517 |
- ): |
|
| 615 |
+ if agent_type == tests.data.KnownSSHAgent.StubbedSSHAgent: |
|
| 518 | 616 |
ssh_auth_sock = None |
| 519 | 617 |
elif spawn_func is spawn_noop: |
| 520 |
- ssh_auth_sock = os.environ["SSH_AUTH_SOCK"] |
|
| 521 |
- elif proc is None: # pragma: no cover [external] |
|
| 522 |
- err_msg = f"Cannot spawn usable {agent_name}"
|
|
| 618 |
+ ssh_auth_sock = os.environ.get("SSH_AUTH_SOCK")
|
|
| 619 |
+ elif proc_or_socket is None: # pragma: no cover [external] |
|
| 620 |
+ err_msg = f"Cannot spawn or interface with usable {agent_name}"
|
|
| 523 | 621 |
raise CannotSpawnError(err_msg) |
| 524 |
- else: |
|
| 622 |
+ elif isinstance(proc_or_socket, subprocess.Popen): |
|
| 623 |
+ proc = proc_or_socket |
|
| 525 | 624 |
exit_stack.enter_context(terminate_on_exit(proc)) |
| 526 | 625 |
assert os.environ.get("SSH_AUTH_SOCK") == startup_ssh_auth_sock, (
|
| 527 | 626 |
f"SSH_AUTH_SOCK mismatch after spawning {agent_name}"
|
| ... | ... |
@@ -543,12 +642,8 @@ def spawn_named_agent( |
| 543 | 642 |
err_msg = f"Cannot parse agent output: {pid_line!r}"
|
| 544 | 643 |
raise CannotSpawnError(err_msg) |
| 545 | 644 |
monkeypatch = exit_stack.enter_context(pytest.MonkeyPatch.context()) |
| 546 |
- if ssh_auth_sock is not None: |
|
| 547 |
- monkeypatch.setenv("SSH_AUTH_SOCK", ssh_auth_sock)
|
|
| 548 |
- client = exit_stack.enter_context( |
|
| 549 |
- ssh_agent.SSHAgentClient.ensure_agent_subcontext() |
|
| 550 |
- ) |
|
| 551 |
- else: |
|
| 645 |
+ if agent_type == tests.data.KnownSSHAgent.StubbedSSHAgent: |
|
| 646 |
+ assert ssh_auth_sock is None |
|
| 552 | 647 |
monkeypatch.setenv( |
| 553 | 648 |
"SSH_AUTH_SOCK", |
| 554 | 649 |
tests.machinery.StubbedSSHAgentSocketWithAddress.ADDRESS, |
| ... | ... |
@@ -567,6 +662,20 @@ def spawn_named_agent( |
| 567 | 662 |
else tests.machinery.StubbedSSHAgentSocketWithAddress() |
| 568 | 663 |
) |
| 569 | 664 |
) |
| 665 |
+ elif ( |
|
| 666 |
+ not isinstance(proc_or_socket, subprocess.Popen) |
|
| 667 |
+ and proc_or_socket is not None |
|
| 668 |
+ ): |
|
| 669 |
+ socket: _types.SSHAgentSocket = proc_or_socket |
|
| 670 |
+ client = exit_stack.enter_context( |
|
| 671 |
+ ssh_agent.SSHAgentClient.ensure_agent_subcontext(conn=socket) |
|
| 672 |
+ ) |
|
| 673 |
+ else: |
|
| 674 |
+ if ssh_auth_sock: |
|
| 675 |
+ monkeypatch.setenv("SSH_AUTH_SOCK", ssh_auth_sock)
|
|
| 676 |
+ client = exit_stack.enter_context( |
|
| 677 |
+ ssh_agent.SSHAgentClient.ensure_agent_subcontext() |
|
| 678 |
+ ) |
|
| 570 | 679 |
# We sanity-test the connected SSH agent if it is not one of our |
| 571 | 680 |
# test agents, because allowing the user to run the test suite |
| 572 | 681 |
# with a clearly faulty agent would likely and unfairly |
| ... | ... |
@@ -168,6 +168,12 @@ class KnownSSHAgent(str, enum.Enum): |
| 168 | 168 |
UNIX/BSD/POSIX). |
| 169 | 169 |
OpenSSHAgent (str): |
| 170 | 170 |
The agent from OpenBSD's OpenSSH suite (on UNIX/BSD/POSIX). |
| 171 |
+ Pageant (str): |
|
| 172 |
+ The agent from Simon Tatham's PuTTY suite (on The Annoying |
|
| 173 |
+ OS). |
|
| 174 |
+ OpenSSHAgentOnWindows (str): |
|
| 175 |
+ The agent from OpenBSD's OpenSSH suite (PowerShell/Windows |
|
| 176 |
+ version, on The Annoying OS). |
|
| 171 | 177 |
StubbedSSHAgent (str): |
| 172 | 178 |
The stubbed, fake agent pseudo-socket defined in this test |
| 173 | 179 |
suite. |
| ... | ... |
@@ -180,6 +186,10 @@ class KnownSSHAgent(str, enum.Enum): |
| 180 | 186 |
"""""" |
| 181 | 187 |
OpenSSHAgent = "OpenSSHAgent" |
| 182 | 188 |
"""""" |
| 189 |
+ Pageant = "Pageant" |
|
| 190 |
+ """""" |
|
| 191 |
+ OpenSSHAgentOnWindows = "OpenSSHAgentOnWindows" |
|
| 192 |
+ """""" |
|
| 183 | 193 |
StubbedSSHAgent = "StubbedSSHAgent" |
| 184 | 194 |
"""""" |
| 185 | 195 |
|
| ... | ... |
@@ -343,22 +353,25 @@ class VaultTestConfig(NamedTuple): |
| 343 | 353 |
# ------------------------------ |
| 344 | 354 |
|
| 345 | 355 |
|
| 346 |
-posix_entry = _types.SSHAgentSocketProviderEntry( |
|
| 347 |
- socketprovider.SocketProvider.resolve("posix"), "posix", ()
|
|
| 356 |
+ssh_auth_sock_on_posix_entry = _types.SSHAgentSocketProviderEntry( |
|
| 357 |
+ socketprovider.SocketProvider.resolve("ssh_auth_sock_on_posix"),
|
|
| 358 |
+ "ssh_auth_sock_on_posix", |
|
| 359 |
+ (), |
|
| 348 | 360 |
) |
| 349 | 361 |
""" |
| 350 | 362 |
The standard [`_types.SSHAgentSocketProviderEntry`][] for the UNIX |
| 351 | 363 |
domain socket handler on POSIX systems. |
| 352 | 364 |
""" |
| 353 | 365 |
|
| 354 |
-the_annoying_os_entry = _types.SSHAgentSocketProviderEntry( |
|
| 355 |
- socketprovider.SocketProvider.resolve("the_annoying_os"),
|
|
| 356 |
- "the_annoying_os", |
|
| 366 |
+pageant_on_the_annoying_os_entry = _types.SSHAgentSocketProviderEntry( |
|
| 367 |
+ socketprovider.SocketProvider.resolve("pageant_on_the_annoying_os"),
|
|
| 368 |
+ "pageant_on_the_annoying_os", |
|
| 357 | 369 |
(), |
| 358 | 370 |
) |
| 359 | 371 |
""" |
| 360 | 372 |
The standard [`_types.SSHAgentSocketProviderEntry`][] for the Windows |
| 361 |
-named pipe handler on The Annoying Operating System. |
|
| 373 |
+named pipe handler (connecting to Pageant) on The Annoying Operating |
|
| 374 |
+System. |
|
| 362 | 375 |
""" |
| 363 | 376 |
|
| 364 | 377 |
faulty_entry_callable = _types.SSHAgentSocketProviderEntry( |
| ... | ... |
@@ -204,12 +204,18 @@ class SocketAddressAction(str, enum.Enum): |
| 204 | 204 |
|
| 205 | 205 |
For UNIX domain sockets, mangle the `SSH_AUTH_SOCK` |
| 206 | 206 |
environment variable. |
| 207 |
+ |
|
| 208 |
+ For Windows named pipes, skip the test. (Addresses are |
|
| 209 |
+ fixed, so cannot be mangled.) |
|
| 207 | 210 |
UNSET_ADDRESS: |
| 208 | 211 |
Unset the socket address. |
| 209 | 212 |
|
| 210 | 213 |
For UNIX domain sockets, unset the `SSH_AUTH_SOCK` |
| 211 | 214 |
environment variable. |
| 212 | 215 |
|
| 216 |
+ For Windows named pipes, skip the test. (Adresses are |
|
| 217 |
+ fixed, so cannot be unset.) |
|
| 218 |
+ |
|
| 213 | 219 |
""" |
| 214 | 220 |
|
| 215 | 221 |
MANGLE_ADDRESS = enum.auto() |
| ... | ... |
@@ -225,6 +231,20 @@ class SocketAddressAction(str, enum.Enum): |
| 225 | 231 |
# matching. |
| 226 | 232 |
# https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 |
| 227 | 233 |
if self == self.MANGLE_ADDRESS: |
| 234 |
+ |
|
| 235 |
+ def mangled_address(*_args: Any, **_kwargs: Any) -> NoReturn: |
|
| 236 |
+ raise OSError(errno.ENOENT, os.strerror(errno.ENOENT)) |
|
| 237 |
+ |
|
| 238 |
+ monkeypatch.setattr( |
|
| 239 |
+ socketprovider.WindowsNamedPipeHandle, |
|
| 240 |
+ "for_pageant", |
|
| 241 |
+ mangled_address, |
|
| 242 |
+ ) |
|
| 243 |
+ monkeypatch.setattr( |
|
| 244 |
+ socketprovider.WindowsNamedPipeHandle, |
|
| 245 |
+ "for_openssh", |
|
| 246 |
+ mangled_address, |
|
| 247 |
+ ) |
|
| 228 | 248 |
monkeypatch.setenv( |
| 229 | 249 |
"SSH_AUTH_SOCK", |
| 230 | 250 |
os.environ["SSH_AUTH_SOCK"] + "~" |
| ... | ... |
@@ -232,6 +252,20 @@ class SocketAddressAction(str, enum.Enum): |
| 232 | 252 |
else "/", |
| 233 | 253 |
) |
| 234 | 254 |
elif self == self.UNSET_ADDRESS: |
| 255 |
+ |
|
| 256 |
+ def no_address(*_args: Any, **_kwargs: Any) -> NoReturn: |
|
| 257 |
+ raise KeyError("SSH_AUTH_SOCK environment variable") # noqa: EM101, TRY003
|
|
| 258 |
+ |
|
| 259 |
+ monkeypatch.setattr( |
|
| 260 |
+ socketprovider.WindowsNamedPipeHandle, |
|
| 261 |
+ "for_pageant", |
|
| 262 |
+ no_address, |
|
| 263 |
+ ) |
|
| 264 |
+ monkeypatch.setattr( |
|
| 265 |
+ socketprovider.WindowsNamedPipeHandle, |
|
| 266 |
+ "for_openssh", |
|
| 267 |
+ no_address, |
|
| 268 |
+ ) |
|
| 235 | 269 |
monkeypatch.delenv("SSH_AUTH_SOCK", raising=False)
|
| 236 | 270 |
else: |
| 237 | 271 |
raise AssertionError() |
| ... | ... |
@@ -33,6 +33,7 @@ from derivepassphrase import _types, cli, ssh_agent |
| 33 | 33 |
from derivepassphrase._internals import ( |
| 34 | 34 |
cli_helpers, |
| 35 | 35 |
) |
| 36 |
+from derivepassphrase.ssh_agent import socketprovider |
|
| 36 | 37 |
from tests import data, machinery |
| 37 | 38 |
from tests.data import callables |
| 38 | 39 |
from tests.machinery import hypothesis as hypothesis_machinery |
| ... | ... |
@@ -796,6 +797,16 @@ class TestStoringConfigurationFailures: |
| 796 | 797 |
error_text="Cannot find any running SSH agent", |
| 797 | 798 |
patch_suitable_ssh_keys=False, |
| 798 | 799 |
) as monkeypatch: |
| 800 |
+ |
|
| 801 |
+ def no_agent(*_args: Any, **_kwargs: Any) -> NoReturn: |
|
| 802 |
+ raise KeyError("SSH_AUTH_SOCK environment variable") # noqa: EM101, TRY003
|
|
| 803 |
+ |
|
| 804 |
+ monkeypatch.setattr( |
|
| 805 |
+ socketprovider.WindowsNamedPipeHandle, "for_pageant", no_agent |
|
| 806 |
+ ) |
|
| 807 |
+ monkeypatch.setattr( |
|
| 808 |
+ socketprovider.WindowsNamedPipeHandle, "for_openssh", no_agent |
|
| 809 |
+ ) |
|
| 799 | 810 |
monkeypatch.delenv("SSH_AUTH_SOCK", raising=False)
|
| 800 | 811 |
|
| 801 | 812 |
def test_fail_because_bad_ssh_agent_connection( |
| ... | ... |
@@ -811,6 +822,20 @@ class TestStoringConfigurationFailures: |
| 811 | 822 |
cwd = pathlib.Path.cwd().resolve() |
| 812 | 823 |
monkeypatch.setenv("SSH_AUTH_SOCK", str(cwd))
|
| 813 | 824 |
|
| 825 |
+ def mangled_address(*_args: Any, **_kwargs: Any) -> NoReturn: |
|
| 826 |
+ raise OSError(errno.ENOENT, os.strerror(errno.ENOENT)) |
|
| 827 |
+ |
|
| 828 |
+ monkeypatch.setattr( |
|
| 829 |
+ socketprovider.WindowsNamedPipeHandle, |
|
| 830 |
+ "for_pageant", |
|
| 831 |
+ mangled_address, |
|
| 832 |
+ ) |
|
| 833 |
+ monkeypatch.setattr( |
|
| 834 |
+ socketprovider.WindowsNamedPipeHandle, |
|
| 835 |
+ "for_openssh", |
|
| 836 |
+ mangled_address, |
|
| 837 |
+ ) |
|
| 838 |
+ |
|
| 814 | 839 |
@Parametrize.TRY_RACE_FREE_IMPLEMENTATION |
| 815 | 840 |
def test_fail_because_read_only_file( |
| 816 | 841 |
self, try_race_free_implementation: bool |
| ... | ... |
@@ -80,14 +80,14 @@ class Parametrize(types.SimpleNamespace): |
| 80 | 80 |
pytest.param( |
| 81 | 81 |
[ |
| 82 | 82 |
importlib.metadata.EntryPoint( |
| 83 |
- name=data.posix_entry.key, |
|
| 83 |
+ name=data.ssh_auth_sock_on_posix_entry.key, |
|
| 84 | 84 |
group=socketprovider.SocketProvider.ENTRY_POINT_GROUP_NAME, |
| 85 |
- value="tests.data: posix_entry", |
|
| 85 |
+ value="tests.data: ssh_auth_sock_on_posix_entry", |
|
| 86 | 86 |
), |
| 87 | 87 |
importlib.metadata.EntryPoint( |
| 88 |
- name=data.the_annoying_os_entry.key, |
|
| 88 |
+ name=data.pageant_on_the_annoying_os_entry.key, |
|
| 89 | 89 |
group=socketprovider.SocketProvider.ENTRY_POINT_GROUP_NAME, |
| 90 |
- value="tests.data: the_annoying_os_entry", |
|
| 90 |
+ value="tests.data: pageant_on_the_annoying_os_entry", |
|
| 91 | 91 |
), |
| 92 | 92 |
], |
| 93 | 93 |
id="existing-entries", |
| ... | ... |
@@ -110,7 +110,7 @@ class Parametrize(types.SimpleNamespace): |
| 110 | 110 |
], |
| 111 | 111 |
) |
| 112 | 112 |
EXISTING_REGISTRY_ENTRIES = pytest.mark.parametrize( |
| 113 |
- "existing", ["posix", "the_annoying_os"] |
|
| 113 |
+ "existing", ["ssh_auth_sock_on_posix", "pageant_on_the_annoying_os"] |
|
| 114 | 114 |
) |
| 115 | 115 |
SSH_STRING_EXCEPTIONS = pytest.mark.parametrize( |
| 116 | 116 |
["input", "exc_type", "exc_pattern"], |
| ... | ... |
@@ -818,10 +818,10 @@ class TestSSHAgentSocketProviderRegistry: |
| 818 | 818 |
) -> None: |
| 819 | 819 |
"""Resolving a non-existant entry fails.""" |
| 820 | 820 |
new_registry = {
|
| 821 |
- "posix": socketprovider.SocketProvider.registry["posix"], |
|
| 822 |
- "the_annoying_os": socketprovider.SocketProvider.registry[ |
|
| 821 |
+ "posix": socketprovider.SocketProvider.resolve("posix"),
|
|
| 822 |
+ "the_annoying_os": socketprovider.SocketProvider.resolve( |
|
| 823 | 823 |
"the_annoying_os" |
| 824 |
- ], |
|
| 824 |
+ ), |
|
| 825 | 825 |
} |
| 826 | 826 |
with pytest.MonkeyPatch.context() as monkeypatch: |
| 827 | 827 |
monkeypatch.setattr( |
| ... | ... |
@@ -840,10 +840,10 @@ class TestSSHAgentSocketProviderRegistry: |
| 840 | 840 |
|
| 841 | 841 |
names = ["spam", "ham", "eggs", "parrot"] |
| 842 | 842 |
new_registry = {
|
| 843 |
- "posix": socketprovider.SocketProvider.registry["posix"], |
|
| 844 |
- "the_annoying_os": socketprovider.SocketProvider.registry[ |
|
| 843 |
+ "posix": socketprovider.SocketProvider.resolve("posix"),
|
|
| 844 |
+ "the_annoying_os": socketprovider.SocketProvider.resolve( |
|
| 845 | 845 |
"the_annoying_os" |
| 846 |
- ], |
|
| 846 |
+ ), |
|
| 847 | 847 |
} |
| 848 | 848 |
with pytest.MonkeyPatch.context() as monkeypatch: |
| 849 | 849 |
monkeypatch.setattr( |
| ... | ... |
@@ -873,12 +873,14 @@ class TestSSHAgentSocketProviderRegistry: |
| 873 | 873 |
|
| 874 | 874 |
provider = socketprovider.SocketProvider.resolve(existing) |
| 875 | 875 |
new_registry = {
|
| 876 |
- "posix": socketprovider.SocketProvider.registry["posix"], |
|
| 877 |
- "the_annoying_os": socketprovider.SocketProvider.registry[ |
|
| 878 |
- "the_annoying_os" |
|
| 879 |
- ], |
|
| 880 |
- "unix_domain": "posix", |
|
| 881 |
- "the_annoying_os_named_pipe": "the_annoying_os", |
|
| 876 |
+ "ssh_auth_sock_on_posix": socketprovider.SocketProvider.resolve( |
|
| 877 |
+ "ssh_auth_sock_on_posix" |
|
| 878 |
+ ), |
|
| 879 |
+ "pageant_on_the_annoying_os": socketprovider.SocketProvider.resolve( |
|
| 880 |
+ "pageant_on_the_annoying_os" |
|
| 881 |
+ ), |
|
| 882 |
+ "posix": "ssh_auth_sock_on_posix", |
|
| 883 |
+ "the_annoying_os": "pageant_on_the_annoying_os", |
|
| 882 | 884 |
} |
| 883 | 885 |
names = [ |
| 884 | 886 |
k |
| ... | ... |
@@ -151,11 +151,11 @@ class SSHAgentSocketProviderRegistryStateMachine( |
| 151 | 151 |
self.registry: dict[ |
| 152 | 152 |
str, _types.SSHAgentSocketProvider | str | None |
| 153 | 153 |
] = {
|
| 154 |
- "posix": self.orig_registry["posix"], |
|
| 155 |
- "the_annoying_os": self.orig_registry["the_annoying_os"], |
|
| 154 |
+ "ssh_auth_sock_on_posix": self.orig_registry["ssh_auth_sock_on_posix"], |
|
| 155 |
+ "pageant_on_the_annoying_os": self.orig_registry["pageant_on_the_annoying_os"], |
|
| 156 | 156 |
"native": self.orig_registry["native"], |
| 157 |
- "unix_domain": "posix", |
|
| 158 |
- "the_annoying_os_named_pipe": "the_annoying_os", |
|
| 157 |
+ "posix": "ssh_auth_sock_on_posix", |
|
| 158 |
+ "the_annoying_os": "pageant_on_the_annoying_os", |
|
| 159 | 159 |
} |
| 160 | 160 |
self.monkeypatch.setattr( |
| 161 | 161 |
socketprovider.SocketProvider, "registry", self.registry |
| 162 | 162 |