Skip to content

Commit 9f21162

Browse files
Haytwpfleger96
andcommitted
fix(nip-fi): close F1 scheduling witness and remove empty_for_test()
Thufir pass-3 escalation items, addressed: Item 1 — production scheduling loop witness. Extract the inner JWKS refresh scheduling loop (sleep/due-selection + run_jwks_refresh_step call) into run_jwks_refresh_loop<S>. The supervisor inner task now calls this function directly; removing that call site breaks the test. Add f1_supervisor_loop_drives_recovery_and_restores_admission (start_paused Tokio time): spawns run_jwks_refresh_loop with a real ProductionJwksSource<ToggleJwksFetcher>, drives warm → hard-dead → fast-retry → recovery, then asserts IssuerKeySource::key_set returns Some (admission restored). Red on three mutations: (1) remove run_jwks_refresh_loop from supervisor → cache never updates → None; (2) revert cadence logic to warm interval → 6 s advance misses 300 s deadline → None; (3) stub snapshot_available to false → cache never warms → None. Item 2 — remove empty_for_test(); seal AssertionKeySet construction. Change JwksRefreshSource::get_snapshot return type to bool (snapshot_available). Production impl maps ProductionJwksSource::get_snapshot(…).is_some(). Mocks return bool directly — no AssertionKeySet construction needed outside buzz-auth. Add ToggleJwksFetcher to buzz-auth behind cfg(any(test, feature="test-utils")) and re-export it: the only path for downstream crates to build a controllable ProductionJwksSource without implementing the sealed JwksFetcher trait themselves. Delete empty_for_test() from AssertionKeySet. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
1 parent 37042db commit 9f21162

5 files changed

Lines changed: 293 additions & 226 deletions

File tree

crates/buzz-auth/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ pub use access::MockAccessChecker;
6161
#[cfg(any(test, feature = "test-utils"))]
6262
pub use nip98_replay::AlwaysFreshReplayGuard;
6363
#[cfg(any(test, feature = "test-utils"))]
64+
pub use nip_fi::ToggleJwksFetcher;
65+
#[cfg(any(test, feature = "test-utils"))]
6466
pub use rate_limit::AlwaysAllowRateLimiter;
6567

6668
/// How the connection was authenticated.

crates/buzz-auth/src/nip_fi/jwks/mod.rs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -734,5 +734,66 @@ impl<F> std::fmt::Debug for ProductionJwksSource<F> {
734734
}
735735
}
736736

737+
/// A toggle-controlled [`JwksFetcher`] for use in downstream-crate integration
738+
/// tests. Returns a minimal but valid JWKS document when the toggle is `true`,
739+
/// and `NetworkError` when `false`.
740+
///
741+
/// This is the only path by which a crate outside `buzz-auth` can build a
742+
/// `ProductionJwksSource` with a controllable fetch outcome — `sealed::Sealed`
743+
/// is crate-private, so downstream crates cannot implement `JwksFetcher`
744+
/// directly. Because the returned JWKS is real (not a synthetic shortcut), the
745+
/// full `ProductionJwksSource` code path — parse, bound, cache, hard-deadline —
746+
/// exercises itself normally, and the resulting `AssertionKeySet` is valid for
747+
/// verifier lookups.
748+
///
749+
/// Only available with the `test-utils` feature enabled.
750+
#[cfg(any(test, feature = "test-utils"))]
751+
#[derive(Clone, Debug)]
752+
pub struct ToggleJwksFetcher {
753+
/// When `true` the fetcher returns a minimal valid JWKS body; when `false`
754+
/// it returns `JwksFetchError::NetworkError`.
755+
pub available: std::sync::Arc<std::sync::atomic::AtomicBool>,
756+
}
757+
758+
#[cfg(any(test, feature = "test-utils"))]
759+
impl ToggleJwksFetcher {
760+
/// Construct a new `ToggleJwksFetcher`. Pass `initial` as the starting
761+
/// availability state; the shared `available` flag can be flipped from the
762+
/// test after construction.
763+
pub fn new(initial: bool) -> Self {
764+
Self {
765+
available: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(initial)),
766+
}
767+
}
768+
}
769+
770+
#[cfg(any(test, feature = "test-utils"))]
771+
impl super::verifier::sealed::Sealed for ToggleJwksFetcher {}
772+
773+
#[cfg(any(test, feature = "test-utils"))]
774+
impl JwksFetcher for ToggleJwksFetcher {
775+
fn fetch_jwks<'a>(
776+
&'a self,
777+
_uri: &'a str,
778+
) -> impl std::future::Future<Output = Result<String, JwksFetchError>> + Send + 'a {
779+
// A minimal P-256 JWK. The coordinates are the same values used in
780+
// buzz-auth's own test suite (tests.rs `minimal_jwks_json`).
781+
const TOGGLE_JWKS: &str = concat!(
782+
r#"{"keys":[{"kty":"EC","crv":"P-256","#,
783+
r#""x":"f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU","#,
784+
r#""y":"x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0","#,
785+
r#""use":"sig","alg":"ES256","kid":"toggle-kid"}]}"#
786+
);
787+
let available = self.available.load(std::sync::atomic::Ordering::SeqCst);
788+
async move {
789+
if available {
790+
Ok(TOGGLE_JWKS.to_string())
791+
} else {
792+
Err(JwksFetchError::NetworkError)
793+
}
794+
}
795+
}
796+
}
797+
737798
#[cfg(test)]
738799
mod tests;

crates/buzz-auth/src/nip_fi/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,8 @@ pub use jwks::{
3333
HttpJwksFetcher, IssuerJwksConfig, JwksFetchError, JwksFetcher, JwksSourceContract,
3434
ProductionJwksSource,
3535
};
36+
37+
#[cfg(any(test, feature = "test-utils"))]
38+
pub use jwks::ToggleJwksFetcher;
3639
pub use startup::{validate_nip_fi_config, NipFiMode, NipFiStartupError};
3740
pub use verifier::{AssertionKeySet, FederatedAssertionVerifier, IssuerKeySource, VerifierError};

crates/buzz-auth/src/nip_fi/verifier.rs

Lines changed: 0 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -143,46 +143,6 @@ impl AssertionKeySet {
143143
pub(crate) fn hard_deadline(&self) -> chrono::DateTime<chrono::Utc> {
144144
self.hard_deadline
145145
}
146-
147-
/// Construct a minimal `AssertionKeySet` for tests that only need a
148-
/// `Some(_)` return from a mock source — the key material and issuer are
149-
/// arbitrary and the set is not valid for actual token verification.
150-
///
151-
/// Named `_for_test` to signal intent; not gated by `cfg(test)` so it is
152-
/// visible to downstream crates' test builds.
153-
#[doc(hidden)]
154-
pub fn empty_for_test() -> Self {
155-
use jsonwebtoken::jwk::{
156-
AlgorithmParameters, EllipticCurve, Jwk, JwkSet, KeyAlgorithm, OctetKeyPairParameters,
157-
OctetKeyPairType,
158-
};
159-
// A syntactically minimal OKP key — its x coordinate is not real key
160-
// material, so this set cannot sign or verify any token. Only the
161-
// `Some(_)` vs `None` distinction matters in cadence-state tests.
162-
let jwk = Jwk {
163-
common: jsonwebtoken::jwk::CommonParameters {
164-
public_key_use: None,
165-
key_operations: None,
166-
key_algorithm: Some(KeyAlgorithm::EdDSA),
167-
key_id: Some("test-kid".to_string()),
168-
x509_url: None,
169-
x509_chain: None,
170-
x509_sha1_fingerprint: None,
171-
x509_sha256_fingerprint: None,
172-
},
173-
algorithm: AlgorithmParameters::OctetKeyPair(OctetKeyPairParameters {
174-
key_type: OctetKeyPairType::OctetKeyPair,
175-
curve: EllipticCurve::Ed25519,
176-
x: "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo".to_string(),
177-
}),
178-
};
179-
Self {
180-
issuer: "https://test.example".to_string(),
181-
generation: 1,
182-
jwks: JwkSet { keys: vec![jwk] },
183-
hard_deadline: chrono::Utc::now() + chrono::Duration::hours(1),
184-
}
185-
}
186146
}
187147

188148
impl fmt::Debug for AssertionKeySet {

0 commit comments

Comments
 (0)