pas-external 0.7.0

Ppoppo Accounts System (PAS) external SDK — OAuth2 PKCE, JWT verification port, Axum middleware, session liveness
Documentation
//! In-memory [`IdTokenVerifier`] adapter for tests + boundary fixtures.
//!
//! Gated behind `cfg(any(test, feature = "test-support"))` so production
//! builds never link the test machinery. Mirrors
//! [`crate::token::MemoryBearerVerifier`] structurally — same in-memory
//! pattern, different port.
//!
//! Two ways to fail a verification:
//!
//! - **Lookup miss**: the token string isn't in the `assertions` map.
//!   Surfaces as [`IdVerifyError::SignatureInvalid`] (the boundary
//!   semantic of "this token doesn't verify against our keyset" maps
//!   identically to "this token is unknown to the test fixture").
//! - **Default failure**: any token returns the configured error.
//!   Used to simulate keyset outages, expired tokens, M66 nonce
//!   mismatches, etc. without pre-registering every variant.
//!
//! **Nonce semantics**: the test verifier ignores the per-call nonce
//! by default. Test fixtures control success/failure via the
//! `assertions` map and `default_failure`; callers that want to
//! simulate a nonce mismatch insert
//! `IdVerifyError::NonceMismatch` as the `default_failure`.

use std::collections::HashMap;
use std::marker::PhantomData;

use async_trait::async_trait;
use ppoppo_token::id_token::Nonce;

use super::port::{IdAssertion, IdTokenVerifier, IdVerifyError, ScopePiiReader};
use crate::token::Expectations;

/// In-memory verifier — pre-registered token → [`IdAssertion<S>`] map.
pub struct MemoryIdTokenVerifier<S: ScopePiiReader> {
    assertions: HashMap<String, IdAssertion<S>>,
    default_failure: Option<IdVerifyError>,
    /// Optional expectations stored for symmetry with
    /// [`super::PasIdTokenVerifier`]. The in-memory verifier doesn't
    /// consult them by default — the test fixture controls
    /// success/failure via the `assertions` map and `default_failure`.
    /// Callers that want to simulate iss/aud mismatches insert an
    /// `IssuerInvalid` / `AudienceInvalid` `default_failure`.
    #[allow(dead_code)]
    expectations: Option<Expectations>,
    _scope: PhantomData<S>,
}

impl<S: ScopePiiReader> Default for MemoryIdTokenVerifier<S> {
    /// Manual `Default` impl — `#[derive(Default)]` would generate an
    /// `S: Default` bound, but engine scope markers (`Openid`, `Email`,
    /// etc.) intentionally do not implement `Default` (they're unit
    /// witnesses, not constructible state). Manual impl drops the
    /// extra bound; the struct fields are all `Default` regardless of S.
    fn default() -> Self {
        Self {
            assertions: HashMap::new(),
            default_failure: None,
            expectations: None,
            _scope: PhantomData,
        }
    }
}

impl<S: ScopePiiReader> MemoryIdTokenVerifier<S> {
    #[must_use]
    pub fn new() -> Self {
        Self::default()
    }

    /// Attach Expectations for symmetry with PasIdTokenVerifier
    /// construction. The in-memory adapter does not currently consult
    /// them; it's a hook point for future "simulate iss/aud check"
    /// semantics.
    #[must_use]
    pub fn with_expectations(mut self, expectations: Expectations) -> Self {
        self.expectations = Some(expectations);
        self
    }

    /// Register a successful verification: when `id_token` is
    /// presented, return `assertion`.
    pub fn insert(
        &mut self,
        id_token: impl Into<String>,
        assertion: IdAssertion<S>,
    ) -> &mut Self {
        self.assertions.insert(id_token.into(), assertion);
        self
    }

    /// Configure a global default failure — every id_token (including
    /// those in the `assertions` map) returns this error. Useful for
    /// simulating a keyset outage or M66 nonce-mismatch in consumer
    /// integration tests.
    pub fn fail_with(&mut self, err: IdVerifyError) -> &mut Self {
        self.default_failure = Some(err);
        self
    }
}

#[async_trait]
impl<S: ScopePiiReader> IdTokenVerifier<S> for MemoryIdTokenVerifier<S> {
    async fn verify(
        &self,
        id_token: &str,
        _expected_nonce: &Nonce,
    ) -> Result<IdAssertion<S>, IdVerifyError> {
        if let Some(err) = self.default_failure.clone() {
            return Err(err);
        }
        self.assertions
            .get(id_token)
            .cloned()
            .ok_or(IdVerifyError::SignatureInvalid)
    }
}