# Testing Guide
**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*
- [Test Suite Overview](#test-suite-overview)
- [Running Tests](#running-tests)
- [All tests at once](#all-tests-at-once)
- [Individual crate tests](#individual-crate-tests)
- [Merkle Tree Certificate tests](#merkle-tree-certificate-tests)
- [Python binding tests](#python-binding-tests)
- [C FFI tests](#c-ffi-tests)
- [RFC 5280 compliance (x509-limbo)](#rfc-5280-compliance-x509-limbo)
- [PKCS#11 / HSM Integration Test (kryoptic)](#pkcs11-hsm-integration-test-kryoptic)
- [Prerequisites](#prerequisites)
- [Build-time discovery](#build-time-discovery)
- [Running the test](#running-the-test)
- [Graceful skip behaviour](#graceful-skip-behaviour)
- [PKCS#11 Python Management Tests](#pkcs11-python-management-tests)
- [Tests that pass without a live PKCS#11 token (9 tests)](#tests-that-pass-without-a-live-pkcs11-token-9-tests)
- [Tests that skip gracefully when no PKCS#11 module is available (3 tests)](#tests-that-skip-gracefully-when-no-pkcs11-module-is-available-3-tests)
- [Running the Python management tests](#running-the-python-management-tests)
- [Test Vectors](#test-vectors)
- [Serde Tests](#serde-tests)
- [Writing New Tests](#writing-new-tests)
- [Rust unit / integration tests](#rust-unit-integration-tests)
- [Python tests](#python-tests)
- [C FFI tests](#c-ffi-tests-1)
- [Code Coverage](#code-coverage)
- [Fuzzing](#fuzzing)
- [Continuous Integration](#continuous-integration)
- [See Also](#see-also)
This guide explains how to run, extend, and debug the test suites across all
Synta crates and language bindings. For CI configuration see
[Contributing](contribution.md#running-ci-locally); for architecture background
see [System Architecture](system-architecture.md).
---
## Test Suite Overview
| Core DER/BER unit tests | `tests/*.rs` | Decoder, encoder, all ASN.1 types, tag/length, roundtrip, serde, BER indefinite-length |
| Certificate tests | `synta-certificate/tests/` | X.509/CRL/CSR/OCSP parsing, PKCS#7/12, algorithm IDs, owned types |
| Codegen tests | `synta-codegen/tests/` | ASN.1 schema parsing and Rust code generation |
| Kerberos tests | `synta-krb5/tests/` | Kerberos V5 ASN.1 structures |
| MTC tests | `synta-mtc/tests/` | Merkle Tree Certificate validation and property tests |
| x509-limbo | `synta-x509-verification/tests/limbo/` | RFC 5280 path validation compliance (~39 MB harness) |
| Python binding tests | `tests/python/` | Full Python API surface via pytest |
| C FFI tests | `tests/c/` | C API bindings via `make` |
---
## Running Tests
### All tests at once
```bash
./contrib/ci/local-ci.sh test
```
This runs `cargo test --workspace --all-features` on the stable, beta, and
nightly toolchains in sequence.
### Individual crate tests
```bash
# Core ASN.1 parser
cargo test -p synta
# X.509 / PKI
cargo test -p synta-certificate
# Code generator
cargo test -p synta-codegen --all-features
# Kerberos types
cargo test -p synta-krb5
# Merkle Tree Certificates
cargo test -p synta-mtc
# RFC 5280 path validation (x509-limbo)
cargo test -p synta-x509-verification
```
### Merkle Tree Certificate tests
```bash
cargo test -p synta-mtc
```
`synta-mtc` has several test targets with different purposes:
| `tests/security_audit.rs` | `cargo test -p synta-mtc --test security_audit` | 27 security-focused tests (DoS, manipulation, separation) |
| `tests/property_tests.rs` | `cargo test -p synta-mtc --test property_tests` | 13 property-based tests (custom harness) |
| `tests/test_vectors.rs` | `cargo test -p synta-mtc --test test_vectors` | 17 test vectors |
| `tests/roundtrip.rs` | `cargo test -p synta-mtc --test roundtrip` | ASN.1 encode/decode round-trips |
| `tests/integration.rs` | `cargo test -p synta-mtc --test integration` | End-to-end validator tests |
| `src/` (unit) | `cargo test -p synta-mtc --lib` | 203 unit tests inline with source |
Key functions with dedicated test coverage:
- `verify_subtree_inclusion_proof` — compact proof-path consistency (spec §4.3.3);
translates absolute leaf index to subtree-relative before calling
`verify_inclusion_proof`.
- Serial number enforcement — `CertificateValidator` checks that
`TBSCertificate.serialNumber == log_entry_index` and that the serial is at
least 1 before computing the leaf hash.
- Subtree alignment — `constraint::validate_subtree_range` rejects subtrees
where `start % BIT_CEIL(end - start) != 0` (spec §4.1).
### Python binding tests
The Python binding must be compiled first. Use `maturin develop` (recommended
for development) or the CI build step:
```bash
# Quick development cycle
cd synta-python
uv venv
uv pip install maturin pytest
uv run maturin develop
uv run pytest ../tests/python/ -v
# Or use the CI helper (also builds the release .so)
./contrib/ci/local-ci.sh python-test
```
Alternatively, if you have a compiled `.so` in `python/`:
```bash
PYTHONPATH=python python3 -m pytest tests/python/ -v
```
### C FFI tests
Build the shared library first, then run the C test suite:
```bash
cargo build --release -p synta-ffi
make -C tests/c test
```
For memory-error checking:
```bash
make -C tests/c valgrind
```
### RFC 5280 compliance (x509-limbo)
The x509-limbo test vectors are large (~39 MB). They are checked out
automatically by the test runner if the `tests/limbo/` directory contains the
harness JSON:
```bash
./contrib/ci/local-ci.sh test-limbo
```
---
### PKCS#11 / HSM Integration Test (kryoptic)
The `pkcs11_kryoptic` integration test signs and verifies a CA certificate
using a real PKCS#11 token. It exercises `BackendPrivateKey::from_pkcs11_uri`
end-to-end and confirms that the resulting signature is valid against the
public key extracted from the token.
#### Prerequisites
- **kryoptic** — a software PKCS#11 token implementation
(`libkryoptic_pkcs11.so` must be discoverable at build time)
- **pkcs11-tool** at `/usr/bin/pkcs11-tool` (from the `opensc` package)
- **modutil** — optional, required for the NSS backend path
(from the `nss-tools` package on Fedora/RHEL)
#### Build-time discovery
`synta-certificate/build.rs` probes for kryoptic at build time and sets two
constants:
- `SYNTA_TEST_KRYOPTIC_LIB` — absolute path to `libkryoptic_pkcs11.so`
- `SYNTA_TEST_PKCS11_PROVIDER` — absolute path to the OpenSSL pkcs11-provider shared object
When `SYNTA_TEST_KRYOPTIC_LIB` is empty (kryoptic not found), every test
function prints a skip message and returns without failure.
`SYNTA_TEST_PKCS11_PROVIDER` is checked only in the OpenSSL backend path; it
does not affect the NSS backend test. The library search path can be
overridden at build time via the `LT_SYS_LIBRARY_PATH` environment variable.
#### Running the test
```bash
# OpenSSL backend
cargo test -p synta-certificate --test pkcs11_kryoptic --features openssl -- --test-threads=1 --nocapture
# NSS backend
cargo test -p synta-certificate --test pkcs11_kryoptic --features nss -- --test-threads=1 --nocapture
```
`--test-threads=1` is **required**. Both `OPENSSL_CONF` (OpenSSL backend) and
NSS module state (NSS backend) are process-global. Running test functions in
parallel on separate threads would cause them to race on that global state,
producing spurious failures.
#### Graceful skip behaviour
When kryoptic or `pkcs11-tool` are absent, each `#[test]` function prints a
human-readable skip message (visible with `--nocapture`) and returns
`Ok(())` rather than failing. This means the test binary exits with status 0,
and CI jobs that run on machines without an HSM do not fail.
---
### PKCS#11 Python Management Tests
`tests/python/test_pkcs11.py` contains 12 tests for the `synta.pkcs11`
Python submodule (the `pkcs11-mgmt` feature). All tests guard their entire
test module with:
```python
pkcs11 = pytest.importorskip(
"synta.pkcs11",
reason="synta compiled without pkcs11-mgmt feature",
)
```
If the submodule is absent (feature not compiled in), the entire file is
skipped rather than failing.
#### Tests that pass without a live PKCS#11 token (9 tests)
These tests exercise module structure and error handling entirely in-process:
| `test_module_all` | `synta.pkcs11.__all__` contains exactly `["SlotInfo", "KeyInfo", "Pkcs11Token", "list_slots"]` |
| `test_slotinfo_class` | `SlotInfo` is callable and present in the module |
| `test_keyinfo_class` | `KeyInfo` is callable and present in the module |
| `test_pkcs11token_class` | `Pkcs11Token` is callable and present in the module |
| `test_list_slots_callable` | `list_slots` is callable |
| `test_token_rejects_non_pkcs11_uri` | `Pkcs11Token("not-a-pkcs11-uri")` raises `ValueError` |
| `test_list_slots_bad_module_path` | `list_slots(module="/nonexistent/path.so")` raises `ValueError` |
| `test_list_slots_relative_module_path` | `list_slots(module="relative/path.so")` raises `ValueError` (relative paths are rejected) |
| `test_token_bad_module_path` | `Pkcs11Token("pkcs11:token=X", module="/nonexistent.so")` raises `ValueError` |
#### Tests that skip gracefully when no PKCS#11 module is available (3 tests)
These tests construct a `Pkcs11Token` with a real URI and inspect its
`__repr__` output. They call `pytest.skip()` at runtime if no PKCS#11
module can be loaded (e.g. `PKCS11_MODULE_PATH` is unset and no candidate
system path exists):
| `test_pin_redaction_in_repr` | `__repr__` of a `Pkcs11Token` with `?pin-value=1234` shows `pin-value=***` |
| `test_pin_redaction_no_pin` | `__repr__` of a `Pkcs11Token` without a PIN shows no `pin-value` at all |
| `test_pin_redaction_error_message` | Error messages raised when the token is not found do not contain the raw PIN value |
#### Running the Python management tests
```bash
# Using the CI helper (also builds the .so):
./contrib/ci/local-ci.sh python-test
# Directly with pytest (requires a compiled .so in python/):
PYTHONPATH=python python3 -m pytest tests/python/test_pkcs11.py -v
# With a live PKCS#11 module (e.g. SoftHSM2):
PKCS11_MODULE_PATH=/usr/lib64/pkcs11/libsofthsm2.so \
PYTHONPATH=python python3 -m pytest tests/python/test_pkcs11.py -v
```
---
## Test Vectors
Binary DER/BER test vectors live under `tests/vectors/`. They are organised
by standard:
```
tests/vectors/
├── pkcs/ # PKCS#7, #8, #10, #12 DER binaries
├── cryptography/ # Vectors from the cryptography Python library
├── certs/ # X.509 certificate DER files
└── README.md # Origin and licence notes for each vector set
```
When adding a new vector:
1. Place the raw binary under the appropriate subdirectory.
2. Write a test that decodes it and asserts expected field values.
3. Document the origin and licence in `tests/vectors/README.md`.
---
## Serde Tests
Serde support is feature-gated (`--features serde`). Run serde tests in
isolation to avoid interference from non-serde builds:
```bash
./contrib/ci/local-ci.sh test-serde
# Equivalent to:
cargo test -p synta --features serde
```
---
## Writing New Tests
### Rust unit / integration tests
Place unit tests in a `#[cfg(test)]` module at the bottom of the source file
they test. Integration tests go in `tests/` at the crate root.
```rust
#[cfg(test)]
mod tests {
use super::*;
use synta::{Decoder, Encoding};
#[test]
fn decode_my_type() {
let der = hex::decode("3003020101").unwrap();
let mut dec = Decoder::new(&der, Encoding::Der);
let val = dec.decode::<MyType>().unwrap();
assert_eq!(val.field, 1);
}
}
```
### Python tests
Python tests use `pytest` and live in `tests/python/`. Always set
`PYTHONPATH=python` (or use the `maturin develop` environment):
```python
import synta
def test_decode_certificate(der_bytes):
cert = synta.Certificate.from_der(der_bytes)
assert cert.serial_number is not None
```
### C FFI tests
C tests are small programs in `tests/c/`. Each must link against `libcsynta`
(the Makefile handles this) and follow the OpenSSL-style ownership convention:
- `synta_*_parse_der()` returns an owned handle.
- `synta_*_free()` frees it; call it exactly once.
- `get0_*()` getters return borrowed pointers valid while the parent is alive.
---
## Code Coverage
Coverage reports can be generated with `cargo-llvm-cov`:
```bash
cargo llvm-cov --workspace --html --open
# Or for a lcov report:
cargo llvm-cov --workspace --lcov --output-path coverage/lcov.info
```
Existing `.profraw` files in the repository root and `synta-certificate/` are
from previous coverage runs and can be safely deleted.
---
## Fuzzing
The `synta-fuzz` crate provides a structured ASN.1 fuzzer:
```bash
cargo +nightly fuzz run fuzz_target -- -max_len=65536
```
Seeds for the fuzzer live in `tests/fuzz/corpus/`. Add any DER files that
exercise new code paths to expand the corpus.
---
## Continuous Integration
The full test pipeline is described in [Contributing](contribution.md#running-ci-locally).
Key jobs for testing:
| `test` | `cargo test --workspace` | stable + beta + nightly |
| `test-certificate` | `cargo test -p synta-certificate` | PKI types |
| `test-codegen` | `cargo test -p synta-codegen --all-features` | ASN.1 codegen |
| `test-krb5` | `cargo test -p synta-krb5` | Kerberos types |
| `test-mtc` | `cargo test -p synta-mtc` | MTC validation |
| `test-limbo` | `cargo test -p synta-x509-verification` | x509-limbo RFC 5280 compliance |
| `test-serde` | `cargo test -p synta --features serde` | Serde roundtrip |
| `python-test` | `pytest tests/python/` | Python bindings |
| `c-test` | `make -C tests/c test` | C FFI |
---
## See Also
- [Contributing](contribution.md) — commit conventions, CI setup, code style
- [System Architecture](system-architecture.md) — crate dependency graph
- [Codebase Summary](codebase-summary.md) — file inventory and test suite table
- [Limitations](limitations.md) — known unsupported ASN.1 constructs