Files
praxis/tests/test_vc_integration.py
T
Praxis CI 813bd586d6 docs(milestone): merge v0.3-mastery-scoring → main
v0.3 milestone merged to main. Mastery scoring + competency rubrics +
verifiable credentials (formative-tier) shipped. 13/13 REQ-IDs covered.
Next milestone: v0.4 (operator tier — cohort dashboard + auth + Postgres).

---ci---
project: praxis
phase: 2
milestone: v0.3
status: complete
milestone_complete: true
milestone_merged_to_main: true
---/ci---
2026-08-04 00:14:59 +00:00

179 lines
5.7 KiB
Python

"""VC integration test — issue → verify + key rotation (SLICE-09 TASK-09-06).
Issue a credential, verify it (valid: true, credentialTier: formative).
Revoke → verify (valid: false, status: revoked). Tamper payload → verify
fails. Key rotation: issue with key A, rotate to key B, issue with key B,
verify both (A against archived public key, B against active).
"""
from __future__ import annotations
import asyncio
import base64
import json
from pathlib import Path
import pytest
from db.migrate import apply_migrations
from db.store import PraxisStore
from server.vc import issuer, issuer_keys
from server.vc.verification import verify_credential, revoke_credential
def _await(coro):
return asyncio.run(coro)
@pytest.fixture
def store(tmp_path: Path) -> PraxisStore:
db = tmp_path / "test_vc_int.db"
apply_migrations(db)
return PraxisStore(db)
def test_issue_and_verify_valid(store: PraxisStore):
root = b"k" * 32
kp = _await(issuer_keys.init_issuer_key(store, root))
cred_id = _await(
issuer.issue_credential(
store=store,
signing_key=kp.signing_key,
key_id=kp.key_id,
learner_id="learner-1",
path="customer-service",
scenarios_passed=["cs_refund_ca_v01", "cs_escalation_ca_v02", "cs_billing_v01"],
rubric_score=4.2,
completed_weeks=6,
evidence=[{"type": "Evidence", "rubricMean": 4.2, "distinctScenarios": 3}],
)
)
result = _await(verify_credential(store, cred_id))
assert result is not None
assert result["valid"] is True
assert result["status"] == "active"
assert result["credentialTier"] == "formative"
assert result["mastery"]["completedWeeks"] == 6
assert result["mastery"]["path"] == "customer-service"
def test_revoke_then_verify_invalid(store: PraxisStore):
root = b"k" * 32
kp = _await(issuer_keys.init_issuer_key(store, root))
cred_id = _await(
issuer.issue_credential(
store=store,
signing_key=kp.signing_key,
key_id=kp.key_id,
learner_id="learner-1",
path="customer-service",
scenarios_passed=["s1", "s2", "s3"],
rubric_score=4.0,
completed_weeks=6,
evidence=[],
)
)
ok = _await(revoke_credential(store, cred_id))
assert ok is True
result = _await(verify_credential(store, cred_id))
assert result is not None
assert result["valid"] is False
assert result["status"] == "revoked"
def test_tamper_payload_verify_fails(store: PraxisStore):
root = b"k" * 32
kp = _await(issuer_keys.init_issuer_key(store, root))
cred_id = _await(
issuer.issue_credential(
store=store,
signing_key=kp.signing_key,
key_id=kp.key_id,
learner_id="learner-1",
path="customer-service",
scenarios_passed=["s1", "s2", "s3"],
rubric_score=3.9,
completed_weeks=6,
evidence=[],
)
)
row = _await(store.get_credential(cred_id))
secured = json.loads(row["vc_payload_json"])
secured["credentialSubject"]["scenariosPassed"] = ["forged"]
vk = _await(issuer_keys.get_public_key_for_verification(store, kp.key_id))
assert issuer.verify_proof(secured, vk) is False
def test_key_rotation_old_vc_still_verifies(store: PraxisStore):
root = b"k" * 32
kp_a = _await(issuer_keys.init_issuer_key(store, root))
cred_a = _await(
issuer.issue_credential(
store=store,
signing_key=kp_a.signing_key,
key_id=kp_a.key_id,
learner_id="learner-1",
path="customer-service",
scenarios_passed=["s1", "s2", "s3"],
rubric_score=4.1,
completed_weeks=6,
evidence=[],
)
)
kp_b = _await(issuer_keys.rotate_key(store, root))
cred_b = _await(
issuer.issue_credential(
store=store,
signing_key=kp_b.signing_key,
key_id=kp_b.key_id,
learner_id="learner-2",
path="customer-service",
scenarios_passed=["s1", "s2", "s3"],
rubric_score=4.3,
completed_weeks=6,
evidence=[],
)
)
res_a = _await(verify_credential(store, cred_a))
res_b = _await(verify_credential(store, cred_b))
assert res_a["valid"] is True
assert res_b["valid"] is True
row_a = _await(store.get_credential(cred_a))
secured_a = json.loads(row_a["vc_payload_json"])
vm_a = secured_a["proof"]["verificationMethod"]
row_b = _await(store.get_credential(cred_b))
secured_b = json.loads(row_b["vc_payload_json"])
vm_b = secured_b["proof"]["verificationMethod"]
assert vm_a != vm_b
old_row = _await(store.get_public_key_row(kp_a.key_id))
assert old_row["status"] == "superseded"
def test_verify_returns_none_for_unknown_id(store: PraxisStore):
result = _await(verify_credential(store, "vc-doesnotexist"))
assert result is None
def test_valid_until_is_three_years_out(store: PraxisStore):
root = b"k" * 32
kp = _await(issuer_keys.init_issuer_key(store, root))
cred_id = _await(
issuer.issue_credential(
store=store,
signing_key=kp.signing_key,
key_id=kp.key_id,
learner_id="learner-1",
path="customer-service",
scenarios_passed=["s1", "s2", "s3"],
rubric_score=4.0,
completed_weeks=6,
evidence=[],
)
)
row = _await(store.get_credential(cred_id))
secured = json.loads(row["vc_payload_json"])
vf = secured["validFrom"]
vu = secured["validUntil"]
assert vf[:4] == "2026"
assert vu[:4] == "2029"
assert vu > vf