Cybersecurity-Projects/PROJECTS/foundations/password-manager/tests/conftest.py

140 lines
5.9 KiB
Python

"""
©AngelaMos | 2026
conftest.py
Shared pytest fixtures for the test suite
A "fixture" is a setup function pytest runs before a test that needs
it. The test asks for a fixture by listing its name as a parameter,
and pytest wires the result in automatically — no manual `setUp` or
`tearDown` like older unittest-style frameworks. Fixtures defined in
conftest.py are auto-discovered and available to every test file in
the same directory, with no `import` statement needed
────────────────────────────────────────────────────────────────────
Why conftest.py specifically
────────────────────────────────────────────────────────────────────
pytest treats `conftest.py` as a magic filename. Anything defined
there is shared across every test file in the directory (and
subdirectories). This is the canonical place to put
- Shared fixtures (`@pytest.fixture` functions)
- Shared test constants (like TEST_KDF_PARAMETERS below)
- Test-suite-wide hooks (e.g. autouse fixtures)
Tests in test_crypto.py or test_vault.py can reference `vault_path`,
`master_password`, or `fresh_vault` by listing them as parameters,
and pytest finds them here automatically
────────────────────────────────────────────────────────────────────
Two costs we balance when choosing fixture scope
────────────────────────────────────────────────────────────────────
1. Time — Argon2 derivation is intentionally slow (~0.3 sec/call
in production). If every test created a new vault with real
parameters, the suite would take minutes
2. Isolation — tests must NOT share writable state, or one test
can corrupt another (especially dangerous with disk-based
tests where leftover files leak across runs)
We solve both: a "fast" KDF parameter set just for tests (so
derivation runs in milliseconds, not seconds) and a fresh temporary
vault path per test (`tmp_path` is a built-in pytest fixture that
auto-creates and auto-cleans a unique temp directory each time)
"""
# Standard library: object-oriented filesystem paths — used here as
# a type hint for the `tmp_path` fixture pytest hands us.
from pathlib import Path
# Third-party: the test runner. We need it here to declare fixtures
# with the `@pytest.fixture` decorator.
import pytest
# Local: the KDF parameter record — fixtures build a fast variant of
# this so tests do not wait seconds on real Argon2 work.
from password_manager.crypto import KdfParameters
# Local: the main vault class — one fixture spins up a freshly
# created vault that individual tests can mutate.
from password_manager.vault import UnlockedVault
# =============================================================================
# Fast Argon2 parameters — for tests only, never use these in production
# =============================================================================
# These values are well below OWASP recommendations but they cut test
# runtime from minutes to milliseconds. The cryptographic correctness
# of the code is the same regardless of parameter strength — Argon2
# does the same operations, just fewer of them
#
# memory_cost = 8 KiB is the absolute floor that Argon2 accepts when
# parallelism = 1 (the algorithm requires memory_cost >= 8 *
# parallelism — see the Argon2 spec). If you change parallelism here,
# bump memory_cost too or Argon2 will reject the parameter set
TEST_KDF_PARAMETERS = KdfParameters(
time_cost = 1,
memory_cost = 8,
parallelism = 1,
)
# =============================================================================
# Fixtures
# =============================================================================
# Each `@pytest.fixture` below defines a setup helper. Pytest sees a
# test like `def test_x(vault_path): ...` and matches the parameter
# name against a fixture name — when it finds one, the fixture runs
# first and its return value is wired into the test
@pytest.fixture
def vault_path(tmp_path: Path) -> Path:
"""
Provide a path to a fresh, non-existent vault file
`tmp_path` is a built-in pytest fixture that creates a fresh
empty temp directory for each test. Combined with our filename,
every test gets a unique vault path that is auto-cleaned after
the test ends. No global state, no cleanup code in test bodies
"""
return tmp_path / "test-vault.json"
@pytest.fixture
def master_password() -> str:
"""
A reasonable test master password
Pulled into a fixture so every test uses the same value and a
future refactor can swap it in one place. The xkcd-inspired
string is long enough that the empty-password check below
cannot accidentally accept it
"""
return "correct horse battery staple"
@pytest.fixture
def fresh_vault(
vault_path: Path,
master_password: str,
) -> UnlockedVault:
"""
Create an empty UnlockedVault using fast test KDF parameters
Notice we pass kdf_parameters explicitly into create() — no
monkey-patching of `KdfParameters.defaults` anywhere. The
constructor takes what it needs as an argument, the test pays
no global-state tax, and the production code path is untouched
`fresh_vault` depends on TWO other fixtures (`vault_path` and
`master_password`). pytest figures out the dependency chain
automatically — it builds them in the right order so this
fixture receives an already-resolved path and password
"""
return UnlockedVault.create(
vault_path,
master_password,
kdf_parameters = TEST_KDF_PARAMETERS,
)