Skip to content

Share the card doubles behind one adapter - #20

Merged
mmlado merged 1 commit into
mainfrom
test/shared-card-fakes
Oct 7, 2026
Merged

mmlado merged 1 commit into
mainfrom
test/shared-card-fakes

Conversation

@mmlado

@mmlado mmlado commented Sep 29, 2026

Copy link
Copy Markdown
Collaborator

Share the card doubles behind one adapter

Test infrastructure only. No src changes.

Changes

  • tests/unit/_cardfakes.py: ResponseAdapter, QueueConn, RecordingConn, and the
    wiring helpers wire_session, wire_pcsc, run, run_both_modes.
  • QueueConn had three byte-identical copies (test_desfire, test_ev2, test_fido) and
    _RecordingConn a fourth in test_keyimport; all four now import from the shared module.
  • tests/unit/test_card_fakes_seam.py: an import-boundary guard and four tests driving a
    real SCP03 mutual authentication.

Constraints

Two transmit conventions exist and both are kept. RawConnection style,
transmit(list[int]) -> (list[int], sw1, sw2), is what CardSession drives and what most
doubles implement. The SCP doubles instead take APDU | bytes and return Response,
because open_channel takes a bare callable rather than a connection. ResponseAdapter
bridges the second into the first, which is what lets a secure-channel double sit behind
connect() and be driven by a real CardSession, with the redactor, the --apdu-log file
and 6Cxx/61xx chaining all running rather than bypassed.

The substitution point depends on the module. 63 call sites go through
AppContext.open_session, which wire_session patches. doctor and readers instead do
from ...transport.pcsc import connect, binding the names at import time, so they need
wire_pcsc against their own namespace. A module that starts importing that way later would
slip past a fixture patched only at open_session and reach a real reader in CI, so
test_only_known_modules_bind_pcsc_entry_points walks the AST of every src module and
asserts the set is exactly {cli/context.py, cli/commands/doctor.py, cli/commands/readers.py}.
A second guard asserts smartcard is imported nowhere but transport/pcsc.py.

Two tests exist to stop the double decaying into "answers 9000". A fake that returns
success for everything would make tests pass on command streams a real card rejects, which
is the main risk of testing against one. So one test asserts INITIALIZE UPDATE, EXTERNAL
AUTHENTICATE and a CLA 0x04 wrapped command all appear on the wire, and another asserts the
host challenge differs between runs. os.urandom is deliberately not patched: the card
derives its cryptogram from the challenge it receives, so the handshake is computed rather
than replayed. That is also why a recorded transcript cannot cover this path.

_cardfakes is importable by basename. There is no __init__.py under tests/, so
pytest's prepend import mode puts tests/unit on sys.path under both python -m pytest
and the bare pytest CI uses. This was checked under both entry points. The leading
underscore keeps the module out of collection. conftest.MockConnection and the transcript
stay as they are; they remain the right tool for the detector's recorded-card assertions.

Verification

ruff check, ruff format --check, mypy, sphinx-build -W --keep-going pass. The suite
goes 357 to 362, project coverage 61.16% to 61.75%, applets/piv/admin.py 29% to 53% and
transport/scp03.py 86% to 90%, with 64 fewer lines of duplicated test code.

No changelog entry: nothing in src changed.

🤖 Generated with Claude Code

The suite carries two transmit conventions: RawConnection style, which
CardSession drives, and a transmit-callable returning Response, used by the
SCP doubles because open_channel takes a bare callable. ResponseAdapter
bridges the second into the first, so a secure-channel double can sit behind
connect() and be driven by a real CardSession with the redactor, the APDU log
and 6Cxx/61xx chaining all running.

QueueConn existed in three byte-identical copies and _RecordingConn in a
fourth file; both now live in _cardfakes alongside the wiring helpers.

Two tests hold the arrangement up. An AST guard asserts that only cli.context,
cli.commands.doctor and cli.commands.readers bind PC/SC entry points, since a
module importing them later would bypass a fixture and reach a real reader in
CI. The rest drive `piv admin authenticate` through a real SCP03 mutual
authentication and assert the handshake appears on the wire with a fresh host
challenge each run, so the double cannot decay into answering 9000.

No src changes. admin.py goes 29% to 53%, scp03.py 86% to 90%.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@mmlado mmlado self-assigned this Sep 29, 2026
@mmlado
mmlado merged commit f0c04ea into main Oct 7, 2026
12 checks passed
@mmlado
mmlado deleted the test/shared-card-fakes branch October 7, 2026 16:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant