Skip to content

Commit 2ba7b72

Browse files
committed
move tests to pytest
1 parent 79feb31 commit 2ba7b72

16 files changed

+1942
-1906
lines changed

pyproject.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,14 @@ Documentation = "http://python-pkcs11.readthedocs.io/en/latest/"
3636
Issues = "https://github.com/pyauth/python-pkcs11/issues"
3737
Repository = "https://github.com/pyauth/python-pkcs11"
3838

39+
[tool.pytest.ini_options]
40+
markers = [
41+
"requires: marks tests require support for a certain PKCS11 mechanism.",
42+
"xfail_nfast: Expected failure on nFast.",
43+
"xfail_softhsm: Expected failure on SoftHSMv2.",
44+
"xfail_opencryptoki: Expected failure on OpenCryptoki.",
45+
]
46+
3947
[tool.ruff]
4048
line-length = 100
4149

@@ -46,6 +54,7 @@ extend-select = [
4654
"F", # pyflakes
4755
"I", # isort
4856
"G", # flake8-logging-format
57+
"PT", # flake8-pytest-style
4958
"RUF", # ruff specific checks
5059
]
5160

tests/__init__.py

Lines changed: 0 additions & 168 deletions
Original file line numberDiff line numberDiff line change
@@ -1,168 +0,0 @@
1-
"""
2-
PKCS#11 Tests
3-
4-
The following environment variables will influence the behaviour of test cases:
5-
- PKCS11_MODULE, mandatory, points to the library/DLL to use for testing
6-
- PKCS11_TOKEN_LABEL, mandatory, contains the token label
7-
- PKCS11_TOKEN_PIN, optional (default is None), contains the PIN/passphrase of the token
8-
- PKCS11_TOKEN_SO_PIN, optional (default is same as PKCS11_TOKEN_PIN), security officer PIN
9-
- OPENSSL_PATH, optional, path to openssl executable (i.e. the folder that contains it)
10-
11-
"""
12-
13-
import os
14-
import shutil
15-
import unittest
16-
from functools import wraps
17-
from warnings import warn
18-
19-
import pkcs11
20-
21-
try:
22-
LIB = os.environ["PKCS11_MODULE"]
23-
except KeyError as ex:
24-
raise RuntimeError("Must define `PKCS11_MODULE' to run tests.") from ex
25-
26-
27-
try:
28-
TOKEN = os.environ["PKCS11_TOKEN_LABEL"]
29-
except KeyError as ex:
30-
raise RuntimeError("Must define `PKCS11_TOKEN_LABEL' to run tests.") from ex
31-
32-
TOKEN_PIN = os.environ.get("PKCS11_TOKEN_PIN") # Can be None
33-
if TOKEN_PIN is None:
34-
warn("`PKCS11_TOKEN_PIN' env variable is unset.", stacklevel=2)
35-
36-
TOKEN_SO_PIN = os.environ.get("PKCS11_TOKEN_SO_PIN")
37-
if TOKEN_SO_PIN is None:
38-
TOKEN_SO_PIN = TOKEN_PIN
39-
warn(
40-
"`PKCS11_TOKEN_SO_PIN' env variable is unset. Using value from `PKCS11_TOKEN_PIN'",
41-
stacklevel=2,
42-
)
43-
44-
OPENSSL = shutil.which("openssl", path=os.environ.get("OPENSSL_PATH"))
45-
if OPENSSL is None:
46-
warn("Path to OpenSSL not found. Please adjust `PATH' or define `OPENSSL_PATH'", stacklevel=2)
47-
48-
49-
class TestCase(unittest.TestCase):
50-
"""Base test case, optionally creates a token and a session."""
51-
52-
with_token = True
53-
"""Creates a token for this test case."""
54-
with_session = True
55-
"""Creates a session for this test case."""
56-
57-
@classmethod
58-
def setUpClass(cls):
59-
super().setUpClass()
60-
cls.lib = lib = pkcs11.lib(LIB)
61-
62-
if cls.with_token or cls.with_session:
63-
cls.token = lib.get_token(token_label=TOKEN)
64-
65-
def setUp(self):
66-
super().setUp()
67-
68-
if self.with_session:
69-
self.session = self.token.open(user_pin=TOKEN_PIN)
70-
71-
def tearDown(self):
72-
if self.with_session:
73-
self.session.close()
74-
75-
super().tearDown()
76-
77-
78-
def requires(*mechanisms):
79-
"""
80-
Decorates a function or class as requiring mechanisms, else they are
81-
skipped.
82-
"""
83-
84-
def check_requirements(self):
85-
"""Determine what, if any, required mechanisms are unavailable."""
86-
unavailable = set(mechanisms) - self.token.slot.get_mechanisms()
87-
88-
if unavailable:
89-
raise unittest.SkipTest("Requires %s" % ", ".join(map(str, unavailable)))
90-
91-
def inner(func):
92-
@wraps(func)
93-
def wrapper(self, *args, **kwargs):
94-
check_requirements(self)
95-
96-
return func(self, *args, **kwargs)
97-
98-
return wrapper
99-
100-
return inner
101-
102-
103-
def xfail(condition):
104-
"""Mark a test that's expected to fail for a given condition."""
105-
106-
def inner(func):
107-
if condition:
108-
return unittest.expectedFailure(func)
109-
110-
else:
111-
return func
112-
113-
return inner
114-
115-
116-
class Is:
117-
"""
118-
Test what device we're using.
119-
"""
120-
121-
# trick: str.endswith() can accept tuples,
122-
# see https://stackoverflow.com/questions/18351951/check-if-string-ends-with-one-of-the-strings-from-a-list
123-
softhsm2 = LIB.lower().endswith(
124-
("libsofthsm2.so", "libsofthsm2.dylib", "softhsm2.dll", "softhsm2-x64.dll")
125-
)
126-
nfast = LIB.lower().endswith(("libcknfast.so", "cknfast.dll"))
127-
opencryptoki = LIB.endswith("libopencryptoki.so")
128-
travis = os.environ.get("TRAVIS") == "true"
129-
130-
131-
class Avail:
132-
"""
133-
Test if a resource is available
134-
"""
135-
136-
# openssl is searched across the exec path. Optionally, OPENSSL_PATH env variable can be defined
137-
# in case there is no direct path to it (i.e. PATH does not point to it)
138-
openssl = OPENSSL is not None
139-
140-
141-
class Only:
142-
"""
143-
Limit tests to given conditions
144-
"""
145-
146-
softhsm2 = unittest.skipUnless(Is.softhsm2, "SoftHSMv2 only")
147-
openssl = unittest.skipUnless(Avail.openssl, "openssl not found in the path")
148-
149-
150-
class Not:
151-
"""
152-
Ignore tests for given devices
153-
"""
154-
155-
softhsm2 = unittest.skipIf(Is.softhsm2, "Not supported by SoftHSMv2")
156-
nfast = unittest.skipIf(Is.nfast, "Not supported by nFast")
157-
opencryptoki = unittest.skipIf(Is.opencryptoki, "Not supported by OpenCryptoki")
158-
159-
160-
class FIXME:
161-
"""
162-
Tests is broken on this platform.
163-
"""
164-
165-
softhsm2 = xfail(Is.softhsm2)
166-
nfast = xfail(Is.nfast)
167-
opencryptoki = xfail(Is.opencryptoki)
168-
travis = xfail(Is.travis)

tests/conftest.py

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import os
2+
import secrets
3+
import shutil
4+
import string
5+
import subprocess
6+
from pathlib import Path
7+
from typing import Iterator
8+
from unittest import mock
9+
from warnings import warn
10+
11+
import pytest
12+
from _pytest.fixtures import SubRequest
13+
14+
import pkcs11
15+
16+
ALLOWED_RANDOM_CHARS = string.ascii_letters + string.digits
17+
LIB_PATH = os.environ.get("PKCS11_MODULE", "/usr/lib/softhsm/libsofthsm2.so")
18+
19+
# trick: str.endswith() can accept tuples,
20+
# see https://stackoverflow.com/questions/18351951/check-if-string-ends-with-one-of-the-strings-from-a-list
21+
IS_SOFTHSM = LIB_PATH.lower().endswith(
22+
("libsofthsm2.so", "libsofthsm2.dylib", "softhsm2.dll", "softhsm2-x64.dll")
23+
)
24+
IS_NFAST = LIB_PATH.lower().endswith(("libcknfast.so", "cknfast.dll"))
25+
IS_OPENCRYPTOKI = LIB_PATH.endswith("libopencryptoki.so")
26+
27+
OPENSSL = shutil.which("openssl", path=os.environ.get("OPENSSL_PATH"))
28+
if OPENSSL is None:
29+
warn("Path to OpenSSL not found. Please adjust `PATH' or define `OPENSSL_PATH'", stacklevel=2)
30+
31+
32+
def pytest_collection_modifyitems(items) -> None:
33+
for item in items:
34+
markers = [marker.name for marker in item.iter_markers()]
35+
if "xfail_nfast" in markers and IS_NFAST:
36+
item.add_marker(
37+
pytest.mark.xfail(IS_NFAST, reason="Expected failure with nFast.", strict=True)
38+
)
39+
if "xfail_softhsm" in markers and IS_SOFTHSM:
40+
item.add_marker(
41+
pytest.mark.xfail(
42+
IS_SOFTHSM, reason="Expected failure with SoftHSMvs.", strict=True
43+
)
44+
)
45+
if "xfail_opencryptoki" in markers:
46+
item.add_marker(
47+
pytest.mark.xfail(
48+
IS_OPENCRYPTOKI, reason="Expected failure with OpenCryptoki.", strict=True
49+
)
50+
)
51+
52+
53+
def get_random_string(length):
54+
return "".join(secrets.choice(ALLOWED_RANDOM_CHARS) for i in range(length))
55+
56+
57+
@pytest.fixture(scope="session")
58+
def lib():
59+
return pkcs11.lib(LIB_PATH)
60+
61+
62+
@pytest.fixture
63+
def softhsm_setup(tmp_path: Path) -> Iterator[Path]: # pragma: hsm
64+
"""Fixture to set up a unique SoftHSM2 configuration."""
65+
softhsm_dir = tmp_path / "softhsm"
66+
token_dir = softhsm_dir / "tokens"
67+
token_dir.mkdir(exist_ok=True, parents=True)
68+
69+
softhsm2_conf = tmp_path / "softhsm2.conf"
70+
print("# SoftHSMv2 conf:", softhsm2_conf)
71+
72+
with open(softhsm2_conf, "w", encoding="utf-8") as stream:
73+
stream.write(f"""# SoftHSM v2 configuration file
74+
75+
directories.tokendir = {token_dir}
76+
objectstore.backend = file
77+
78+
# ERROR, WARNING, INFO, DEBUG
79+
log.level = DEBUG
80+
81+
# If CKF_REMOVABLE_DEVICE flag should be set
82+
slots.removable = false
83+
84+
# Enable and disable PKCS#11 mechanisms using slots.mechanisms.
85+
slots.mechanisms = ALL
86+
87+
# If the library should reset the state on fork
88+
library.reset_on_fork = false""")
89+
90+
with mock.patch.dict(os.environ, {"SOFTHSM2_CONF": str(softhsm2_conf)}):
91+
yield softhsm_dir
92+
93+
94+
@pytest.fixture
95+
def so_pin() -> str:
96+
return get_random_string(12)
97+
98+
99+
@pytest.fixture
100+
def pin() -> str:
101+
return get_random_string(12)
102+
103+
104+
@pytest.fixture
105+
def softhsm_token(request: "SubRequest", lib, so_pin: str, pin: str) -> pkcs11.Token:
106+
"""Get a unique token for the current test."""
107+
request.getfixturevalue("softhsm_setup")
108+
token = get_random_string(8)
109+
110+
args = (
111+
"softhsm2-util",
112+
"--init-token",
113+
"--free",
114+
"--label",
115+
token,
116+
"--so-pin",
117+
so_pin,
118+
"--pin",
119+
pin,
120+
)
121+
print("+", " ".join(args))
122+
subprocess.run(args, check=True)
123+
124+
# Reinitialize library if already loaded (tokens are only seen after (re-)initialization).
125+
lib.reinitialize()
126+
127+
return lib.get_token(token_label=token)
128+
129+
130+
@pytest.fixture
131+
def softhsm_session(softhsm_token: pkcs11.Token, pin: str) -> Iterator[pkcs11.Session]:
132+
session = softhsm_token.open(user_pin=pin)
133+
yield session
134+
session.close()
135+
136+
137+
@pytest.fixture
138+
def token(softhsm_token: pkcs11.Token) -> pkcs11.Token:
139+
return softhsm_token
140+
141+
142+
@pytest.fixture
143+
def session(
144+
request: "SubRequest", softhsm_session: pkcs11.Session, softhsm_token: pkcs11.Token
145+
) -> pkcs11.Session:
146+
# Skip test if session does not support required mechanisms
147+
requirements = [mark.args[0] for mark in request.node.iter_markers(name="requires")]
148+
if requirements:
149+
unavailable = set(requirements) - softhsm_token.slot.get_mechanisms()
150+
151+
if unavailable:
152+
pytest.skip("Requires %s" % ", ".join(map(str, unavailable)))
153+
154+
return softhsm_session

0 commit comments

Comments
 (0)