synta 0.2.1

ASN.1 parser, decoder, and encoder library with DER/BER support and C FFI
Documentation
"""Tests for synta.pkcs11 that do not require a live PKCS#11 token.

Covers module import, class presence, URI-redaction behaviour observable
through __repr__, and error handling on invalid input.
"""

import pytest

pkcs11 = pytest.importorskip(
    "synta.pkcs11",
    reason="synta built without pkcs11-mgmt feature",
)


# ── Module structure ──────────────────────────────────────────────────────────


def test_module_has_expected_names():
    assert hasattr(pkcs11, "SlotInfo")
    assert hasattr(pkcs11, "KeyInfo")
    assert hasattr(pkcs11, "Pkcs11Token")
    assert hasattr(pkcs11, "list_slots")


def test_all_lists_public_names():
    all_ = pkcs11.__all__
    for name in ("SlotInfo", "KeyInfo", "Pkcs11Token", "list_slots"):
        assert name in all_, f"{name!r} missing from __all__"


# ── Pkcs11Token construction ──────────────────────────────────────────────────


def test_rejects_non_pkcs11_uri():
    with pytest.raises(ValueError, match="invalid PKCS#11 URI"):
        pkcs11.Pkcs11Token("file:///tmp/key.pem")


def test_rejects_empty_string():
    with pytest.raises((ValueError, Exception)):
        pkcs11.Pkcs11Token("")


def test_rejects_http_uri():
    with pytest.raises((ValueError, Exception)):
        pkcs11.Pkcs11Token("https://example.com/token")


# ── PIN redaction in __repr__ ─────────────────────────────────────────────────


def _make_token_repr(uri: str) -> str:
    """Construct a Pkcs11Token and return its repr without loading a module.

    Pkcs11Token.__new__ loads the PKCS#11 module, which will fail when no
    module is available.  We catch that error and inspect the repr only when
    construction succeeds (e.g. in CI with softhsm2), or skip otherwise.
    """
    try:
        tok = pkcs11.Pkcs11Token(uri)
        return repr(tok)
    except ValueError as e:
        msg = str(e)
        # Module not found — construction failed before repr is observable.
        if "module" in msg.lower() or "pkcs11" in msg.lower():
            pytest.skip(f"no PKCS#11 module available: {e}")
        raise


def test_repr_redacts_pin_value():
    r = _make_token_repr("pkcs11:token=T;object=k?pin-value=s3cr3t")
    assert "s3cr3t" not in r, f"PIN leaked in repr: {r!r}"
    assert "***" in r


def test_repr_no_pin_unchanged():
    r = _make_token_repr("pkcs11:token=T;object=k")
    assert "***" not in r


def test_repr_multi_query_pin_redacted():
    r = _make_token_repr("pkcs11:token=T?pin-value=abc&foo=bar")
    assert "abc" not in r, f"PIN leaked: {r!r}"
    assert "foo=bar" not in r or "***" in r


# ── SlotInfo / KeyInfo repr ───────────────────────────────────────────────────


def test_slot_info_repr_format():
    # SlotInfo is a frozen pyclass; we can instantiate it indirectly via
    # list_slots() when a module is available, or check repr format on a
    # known good object if construction is possible.
    # Since we can't construct SlotInfo directly, just verify it's a class.
    assert callable(pkcs11.SlotInfo)


def test_key_info_repr_format():
    assert callable(pkcs11.KeyInfo)


# ── list_slots with bad module path ──────────────────────────────────────────


def test_list_slots_bad_module_raises():
    with pytest.raises((ValueError, OSError, Exception)):
        pkcs11.list_slots(module="/nonexistent/path/token.so")


def test_list_slots_relative_module_raises():
    with pytest.raises((ValueError, Exception)):
        pkcs11.list_slots(module="relative/path.so")