pxsolver-auth 1.3.0

API-key + per-domain allowlist + audit log
Documentation
#![allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]

use argon2::Argon2;
use argon2::password_hash::{PasswordHasher, SaltString};
use px_auth::{
    AllowlistEntry, ApiKeyRecord, AuditEvent, AuditOutcome, AuditSink, CheckAllowlist,
    FileAuditSink, VerifyKey, YamlAllowlistStore, YamlKeyStore,
};
use std::sync::Arc;

fn hash(secret: &str) -> String {
    let salt = SaltString::encode_b64(b"px-solver-test-salt-fixed").expect("salt");
    Argon2::default()
        .hash_password(secret.as_bytes(), &salt)
        .expect("hash")
        .to_string()
}

#[tokio::test]
async fn verify_key_accepts_correct_secret() {
    let record = ApiKeyRecord {
        id: "key1".into(),
        argon2_hash: hash("topsecret"),
        note: None,
    };
    let store = Arc::new(YamlKeyStore::from_records(vec![record.clone()]));
    let verifier = VerifyKey::new(store);
    let got = verifier.execute("key1", "topsecret").await.expect("verify");
    assert_eq!(got.id, "key1");
}

#[tokio::test]
async fn verify_key_rejects_wrong_secret() {
    let record = ApiKeyRecord {
        id: "key1".into(),
        argon2_hash: hash("right"),
        note: None,
    };
    let store = Arc::new(YamlKeyStore::from_records(vec![record]));
    let verifier = VerifyKey::new(store);
    assert!(verifier.execute("key1", "wrong").await.is_err());
}

#[tokio::test]
async fn verify_key_rejects_unknown_key() {
    let store = Arc::new(YamlKeyStore::from_records(vec![]));
    let verifier = VerifyKey::new(store);
    assert!(verifier.execute("nope", "anything").await.is_err());
}

#[tokio::test]
async fn allowlist_admits_reviewed_entry() {
    let store = Arc::new(YamlAllowlistStore::from_entries(vec![AllowlistEntry {
        domain: "pedidosya.com.ar".into(),
        tos_reviewed: true,
        justification: "research".into(),
        handler: None,
    }]));
    let check = CheckAllowlist::new(store);
    let entry = check.execute("pedidosya.com.ar").await.expect("allow");
    assert_eq!(entry.domain, "pedidosya.com.ar");
}

#[tokio::test]
async fn allowlist_rejects_unreviewed_entry() {
    let store = Arc::new(YamlAllowlistStore::from_entries(vec![AllowlistEntry {
        domain: "x.com".into(),
        tos_reviewed: false,
        justification: "test".into(),
        handler: None,
    }]));
    let check = CheckAllowlist::new(store);
    assert!(check.execute("x.com").await.is_err());
}

#[tokio::test]
async fn allowlist_rejects_unknown_domain() {
    let store = Arc::new(YamlAllowlistStore::from_entries(vec![]));
    let check = CheckAllowlist::new(store);
    assert!(check.execute("missing.com").await.is_err());
}

#[tokio::test]
async fn file_audit_sink_writes_redacted_jsonl() {
    let dir = tempfile::tempdir().expect("tempdir");
    let path = dir.path().join("audit.log");
    let sink = FileAuditSink::new(&path);
    let event = AuditEvent {
        timestamp_unix: 1_700_000_000,
        key_id: "key1".into(),
        target_domain: "pedidosya.com.ar".into(),
        outcome: AuditOutcome::Solved,
        latency_ms: 1234,
        handler: Some("perimeterx".into()),
    };
    sink.record(&event).await.expect("record");
    let body = std::fs::read_to_string(&path).expect("read");
    assert!(body.contains("\"target_domain\":\"pedidosya.com.ar\""));
    assert!(!body.contains("_px3"));
    assert!(!body.contains("_pxhd"));
}