From 0274f19d7554a8a4bbc037a27f9ac62502eaa75b Mon Sep 17 00:00:00 2001 From: Lewis Date: Sun, 26 Jul 2026 13:57:07 +0300 Subject: [PATCH] auth: EmailTokenPurpose from tranquil-types, shared cache key fns, MemoryCache in tests Lewis: May this revision serve well! --- crates/tranquil-oauth-server/Cargo.toml | 1 + .../endpoints/authorize/scope_resolution.rs | 40 ++---- crates/tranquil-pds/src/auth/email_token.rs | 106 +++------------- crates/tranquil-pds/src/auth/legacy_2fa.rs | 119 +++++------------- .../src/oauth/permission_set_resolver.rs | 57 +++------ 5 files changed, 68 insertions(+), 255 deletions(-) diff --git a/crates/tranquil-oauth-server/Cargo.toml b/crates/tranquil-oauth-server/Cargo.toml index 3cf9ce3..aca1519 100644 --- a/crates/tranquil-oauth-server/Cargo.toml +++ b/crates/tranquil-oauth-server/Cargo.toml @@ -37,6 +37,7 @@ webauthn-rs = { workspace = true } [dev-dependencies] async-trait = { workspace = true } +tranquil-infra = { workspace = true, features = ["testing"] } [features] bsky = [] diff --git a/crates/tranquil-oauth-server/src/endpoints/authorize/scope_resolution.rs b/crates/tranquil-oauth-server/src/endpoints/authorize/scope_resolution.rs index ac12090..d2246cf 100644 --- a/crates/tranquil-oauth-server/src/endpoints/authorize/scope_resolution.rs +++ b/crates/tranquil-oauth-server/src/endpoints/authorize/scope_resolution.rs @@ -33,36 +33,12 @@ pub async fn resolve_effective_scopes( #[cfg(test)] mod tests { use super::*; - use std::collections::HashMap; - use std::sync::Mutex; use std::time::Duration; - use tranquil_pds::cache::{Cache, CacheError}; + use tranquil_infra::MemoryCache; + use tranquil_pds::cache::Cache; - #[derive(Default)] - struct MapCache(Mutex>); - #[async_trait::async_trait] - impl Cache for MapCache { - async fn get(&self, k: &str) -> Option { - self.0.lock().unwrap().get(k).cloned() - } - async fn set(&self, k: &str, v: &str, _t: Duration) -> Result<(), CacheError> { - self.0.lock().unwrap().insert(k.into(), v.into()); - Ok(()) - } - async fn delete(&self, k: &str) -> Result<(), CacheError> { - self.0.lock().unwrap().remove(k); - Ok(()) - } - async fn get_bytes(&self, _k: &str) -> Option> { - None - } - async fn set_bytes(&self, _k: &str, _v: &[u8], _t: Duration) -> Result<(), CacheError> { - Ok(()) - } - } - - fn cache_with(nsid: &str, scopes: &str) -> MapCache { - let c = MapCache::default(); + async fn cache_with(nsid: &str, scopes: &str) -> MemoryCache { + let c = MemoryCache::new(); let key = tranquil_pds::cache_keys::permission_set_key( &tranquil_types::Nsid::new(nsid).unwrap(), None, @@ -74,7 +50,7 @@ mod tests { "refreshed_at": chrono::Utc::now().timestamp(), }) .to_string(); - c.0.lock().unwrap().insert(key, json); + let _ = c.set(&key, &json, Duration::from_secs(3600)).await; c } @@ -83,7 +59,8 @@ mod tests { let c = cache_with( "io.atcr.authFullApp", "repo:io.atcr.manifest?action=create identity:*", - ); + ) + .await; let eff = resolve_effective_scopes( &c, "atproto include:io.atcr.authFullApp", @@ -104,7 +81,8 @@ mod tests { let c = cache_with( "io.atcr.authFullApp", "repo:io.atcr.manifest?action=create identity:*", - ); + ) + .await; let granted = DbScope::new("atproto repo:* blob:*/* account:*?action=manage").unwrap(); let eff = resolve_effective_scopes( &c, diff --git a/crates/tranquil-pds/src/auth/email_token.rs b/crates/tranquil-pds/src/auth/email_token.rs index 2441a29..c597ed1 100644 --- a/crates/tranquil-pds/src/auth/email_token.rs +++ b/crates/tranquil-pds/src/auth/email_token.rs @@ -2,32 +2,14 @@ use serde::{Deserialize, Serialize}; use std::time::Duration; use crate::cache::Cache; +use crate::cache_keys::email_token_key; use crate::types::Did; use crate::util::{generate_token_code, normalize_token_code}; +pub use tranquil_types::EmailTokenPurpose; + const TOKEN_TTL_SECS: u64 = 900; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum EmailTokenPurpose { - UpdateEmail, - ConfirmEmail, - DeleteAccount, - ResetPassword, - PlcOperation, -} - -impl EmailTokenPurpose { - fn as_str(&self) -> &'static str { - match self { - Self::UpdateEmail => "update_email", - Self::ConfirmEmail => "confirm_email", - Self::DeleteAccount => "delete_account", - Self::ResetPassword => "reset_password", - Self::PlcOperation => "plc_operation", - } - } -} - #[derive(Debug, Clone, Serialize, Deserialize)] struct TokenData { token: String, @@ -42,10 +24,6 @@ pub enum TokenError { ExpiredToken, } -fn cache_key(did: &Did, purpose: EmailTokenPurpose) -> String { - format!("email_token:{}:{}", purpose.as_str(), did) -} - fn current_timestamp() -> u64 { u64::try_from(chrono::Utc::now().timestamp()).unwrap_or(0) } @@ -69,7 +47,7 @@ pub async fn create_email_token( cache .set( - &cache_key(did, purpose), + &email_token_key(did, purpose), &json, Duration::from_secs(TOKEN_TTL_SECS), ) @@ -89,7 +67,7 @@ pub async fn validate_email_token( return Err(TokenError::CacheUnavailable); } - let key = cache_key(did, purpose); + let key = email_token_key(did, purpose); let json = cache.get(&key).await.ok_or(TokenError::InvalidToken)?; let data: TokenData = serde_json::from_str(&json).map_err(|_| TokenError::InvalidToken)?; @@ -112,7 +90,7 @@ pub async fn validate_email_token( } pub async fn delete_email_token(cache: &dyn Cache, did: &Did, purpose: EmailTokenPurpose) { - let _ = cache.delete(&cache_key(did, purpose)).await; + let _ = cache.delete(&email_token_key(did, purpose)).await; } fn constant_time_eq(a: &[u8], b: &[u8]) -> bool { @@ -128,67 +106,11 @@ fn constant_time_eq(a: &[u8], b: &[u8]) -> bool { #[cfg(test)] mod tests { use super::*; - use crate::cache::CacheError; - use async_trait::async_trait; - use std::collections::HashMap; - use std::sync::Mutex; - - struct MockCache { - data: Mutex>, - } - - impl MockCache { - fn new() -> Self { - Self { - data: Mutex::new(HashMap::new()), - } - } - } - - #[async_trait] - impl Cache for MockCache { - async fn get(&self, key: &str) -> Option { - let data = self.data.lock().unwrap(); - let now = current_timestamp(); - data.get(key) - .filter(|(_, exp)| *exp > now) - .map(|(v, _)| v.clone()) - } - - async fn set(&self, key: &str, value: &str, ttl: Duration) -> Result<(), CacheError> { - let mut data = self.data.lock().unwrap(); - let expires = current_timestamp() + ttl.as_secs(); - data.insert(key.to_string(), (value.to_string(), expires)); - Ok(()) - } - - async fn delete(&self, key: &str) -> Result<(), CacheError> { - let mut data = self.data.lock().unwrap(); - data.remove(key); - Ok(()) - } - - async fn get_bytes(&self, _key: &str) -> Option> { - None - } - - async fn set_bytes( - &self, - _key: &str, - _value: &[u8], - _ttl: Duration, - ) -> Result<(), CacheError> { - Ok(()) - } - - fn is_available(&self) -> bool { - true - } - } + use tranquil_infra::MemoryCache; #[tokio::test] async fn test_create_and_validate_token() { - let cache = MockCache::new(); + let cache = MemoryCache::new(); let did = Did::new("did:plc:teq").expect("valid DID"); let token = create_email_token(&cache, &did, EmailTokenPurpose::UpdateEmail) @@ -205,7 +127,7 @@ mod tests { #[tokio::test] async fn test_token_consumed_after_use() { - let cache = MockCache::new(); + let cache = MemoryCache::new(); let did = Did::new("did:plc:teq").expect("valid DID"); let token = create_email_token(&cache, &did, EmailTokenPurpose::UpdateEmail) @@ -223,7 +145,7 @@ mod tests { #[tokio::test] async fn test_invalid_token_rejected() { - let cache = MockCache::new(); + let cache = MemoryCache::new(); let did = Did::new("did:plc:teq").expect("valid DID"); let _token = create_email_token(&cache, &did, EmailTokenPurpose::UpdateEmail) @@ -237,7 +159,7 @@ mod tests { #[tokio::test] async fn test_wrong_purpose_rejected() { - let cache = MockCache::new(); + let cache = MemoryCache::new(); let did = Did::new("did:plc:teq").expect("valid DID"); let token = create_email_token(&cache, &did, EmailTokenPurpose::UpdateEmail) @@ -252,7 +174,7 @@ mod tests { #[tokio::test] async fn test_token_format() { // The emitted token is the display form: uppercase `XXXXX-XXXXX`. - let cache = MockCache::new(); + let cache = MemoryCache::new(); let did = Did::new("did:plc:teq").expect("valid DID"); (0..50).for_each(|_| { let token = futures::executor::block_on(create_email_token( @@ -269,7 +191,7 @@ mod tests { #[tokio::test] async fn test_case_insensitive_validation() { - let cache = MockCache::new(); + let cache = MemoryCache::new(); let did = Did::new("did:plc:teq").expect("valid DID"); let token = create_email_token(&cache, &did, EmailTokenPurpose::UpdateEmail) @@ -284,7 +206,7 @@ mod tests { #[tokio::test] async fn test_hyphen_insensitive_validation() { - let cache = MockCache::new(); + let cache = MemoryCache::new(); let did = Did::new("did:plc:teq").expect("valid DID"); let token = create_email_token(&cache, &did, EmailTokenPurpose::UpdateEmail) diff --git a/crates/tranquil-pds/src/auth/legacy_2fa.rs b/crates/tranquil-pds/src/auth/legacy_2fa.rs index e8d3aba..b85aca6 100644 --- a/crates/tranquil-pds/src/auth/legacy_2fa.rs +++ b/crates/tranquil-pds/src/auth/legacy_2fa.rs @@ -3,6 +3,7 @@ use serde::{Deserialize, Serialize}; use std::time::Duration; use crate::cache::Cache; +use crate::cache_keys::{legacy_2fa_challenge_key, legacy_2fa_cooldown_key}; use crate::types::Did; use crate::util::{generate_token_code, normalize_token_code}; @@ -58,8 +59,8 @@ pub async fn create_challenge( } pub async fn clear_challenge(cache: &dyn Cache, did: &Did) { - let _ = cache.delete(&challenge_key(did)).await; - let _ = cache.delete(&cooldown_key(did)).await; + let _ = cache.delete(&legacy_2fa_challenge_key(did)).await; + let _ = cache.delete(&legacy_2fa_cooldown_key(did)).await; } async fn validate_challenge_internal( @@ -71,7 +72,7 @@ async fn validate_challenge_internal( return Err(ValidationError::CacheUnavailable); } - let challenge_k = challenge_key(did); + let challenge_k = legacy_2fa_challenge_key(did); let json = cache .get(&challenge_k) @@ -114,19 +115,11 @@ async fn validate_challenge_internal( } let _ = cache.delete(&challenge_k).await; - let _ = cache.delete(&cooldown_key(did)).await; + let _ = cache.delete(&legacy_2fa_cooldown_key(did)).await; Ok(()) } -fn challenge_key(did: &Did) -> String { - format!("legacy_2fa:{}", did) -} - -fn cooldown_key(did: &Did) -> String { - format!("legacy_2fa_cooldown:{}", did) -} - fn current_timestamp() -> u64 { u64::try_from(Utc::now().timestamp()).unwrap_or(0) } @@ -226,7 +219,7 @@ async fn create_challenge_code( return Err(ChallengeError::CacheUnavailable); } - let cooldown = cooldown_key(did); + let cooldown = legacy_2fa_cooldown_key(did); if cache.get(&cooldown).await.is_some() { return Err(ChallengeError::RateLimited); } @@ -244,7 +237,7 @@ async fn create_challenge_code( cache .set( - &challenge_key(did), + &legacy_2fa_challenge_key(did), &json, Duration::from_secs(CHALLENGE_TTL_SECS), ) @@ -280,67 +273,11 @@ impl From for Legacy2faFlowError { #[cfg(test)] mod tests { use super::*; - use crate::cache::CacheError; - use async_trait::async_trait; - use std::collections::HashMap; - use std::sync::Mutex; - - struct MockCache { - data: Mutex>, - } - - impl MockCache { - fn new() -> Self { - Self { - data: Mutex::new(HashMap::new()), - } - } - } - - #[async_trait] - impl Cache for MockCache { - async fn get(&self, key: &str) -> Option { - let data = self.data.lock().unwrap(); - let now = current_timestamp(); - data.get(key) - .filter(|(_, exp)| *exp > now) - .map(|(v, _)| v.clone()) - } - - async fn set(&self, key: &str, value: &str, ttl: Duration) -> Result<(), CacheError> { - let mut data = self.data.lock().unwrap(); - let expires = current_timestamp() + ttl.as_secs(); - data.insert(key.to_string(), (value.to_string(), expires)); - Ok(()) - } - - async fn delete(&self, key: &str) -> Result<(), CacheError> { - let mut data = self.data.lock().unwrap(); - data.remove(key); - Ok(()) - } - - async fn get_bytes(&self, _key: &str) -> Option> { - None - } - - async fn set_bytes( - &self, - _key: &str, - _value: &[u8], - _ttl: Duration, - ) -> Result<(), CacheError> { - Ok(()) - } - - fn is_available(&self) -> bool { - true - } - } + use tranquil_infra::MemoryCache; #[tokio::test] async fn test_create_and_validate_challenge() { - let cache = MockCache::new(); + let cache = MemoryCache::new(); let did = Did::new("did:plc:test123".to_string()).unwrap(); let code = create_challenge(&cache, &did).await.unwrap(); @@ -352,7 +289,7 @@ mod tests { #[tokio::test] async fn test_challenge_code_format() { - let cache = MockCache::new(); + let cache = MemoryCache::new(); let did = Did::new("did:plc:test123".to_string()).unwrap(); let code = create_challenge(&cache, &did).await.unwrap(); @@ -364,7 +301,7 @@ mod tests { #[tokio::test] async fn test_case_insensitive_validation() { - let cache = MockCache::new(); + let cache = MemoryCache::new(); let did = Did::new("did:plc:test123".to_string()).unwrap(); let code = create_challenge(&cache, &did).await.unwrap(); @@ -375,7 +312,7 @@ mod tests { #[tokio::test] async fn test_hyphen_insensitive_validation() { - let cache = MockCache::new(); + let cache = MemoryCache::new(); let did = Did::new("did:plc:test123".to_string()).unwrap(); let code = create_challenge(&cache, &did).await.unwrap(); @@ -386,7 +323,7 @@ mod tests { #[tokio::test] async fn test_invalid_code_rejected() { - let cache = MockCache::new(); + let cache = MemoryCache::new(); let did = Did::new("did:plc:test123".to_string()).unwrap(); let _code = create_challenge(&cache, &did).await.unwrap(); @@ -396,7 +333,7 @@ mod tests { #[tokio::test] async fn test_challenge_consumed_on_success() { - let cache = MockCache::new(); + let cache = MemoryCache::new(); let did = Did::new("did:plc:test123".to_string()).unwrap(); let code = create_challenge(&cache, &did).await.unwrap(); @@ -410,7 +347,7 @@ mod tests { #[tokio::test] async fn test_max_attempts_exceeded() { - let cache = MockCache::new(); + let cache = MemoryCache::new(); let did = Did::new("did:plc:test123".to_string()).unwrap(); let _code = create_challenge(&cache, &did).await.unwrap(); @@ -425,7 +362,7 @@ mod tests { #[tokio::test] async fn test_rate_limiting() { - let cache = MockCache::new(); + let cache = MemoryCache::new(); let did = Did::new("did:plc:test123".to_string()).unwrap(); let _first = create_challenge(&cache, &did).await.unwrap(); @@ -453,7 +390,7 @@ mod tests { #[tokio::test] async fn test_process_flow_not_required() { - let cache = MockCache::new(); + let cache = MemoryCache::new(); let did = Did::new("did:plc:test".to_string()).unwrap(); let ctx = Legacy2faContext { is_app_password: false, @@ -470,7 +407,7 @@ mod tests { #[tokio::test] async fn test_process_flow_not_required_because_app_password() { - let cache = MockCache::new(); + let cache = MemoryCache::new(); let did = Did::new("did:plc:test".to_string()).unwrap(); let ctx = Legacy2faContext { is_app_password: true, @@ -487,7 +424,7 @@ mod tests { #[tokio::test] async fn test_process_flow_blocked() { - let cache = MockCache::new(); + let cache = MemoryCache::new(); let did = Did::new("did:plc:test".to_string()).unwrap(); let ctx = Legacy2faContext { is_app_password: false, @@ -504,7 +441,7 @@ mod tests { #[tokio::test] async fn test_process_flow_challenge_sent_totp() { - let cache = MockCache::new(); + let cache = MemoryCache::new(); let did = Did::new("did:plc:test".to_string()).unwrap(); let ctx = Legacy2faContext { is_app_password: false, @@ -521,7 +458,7 @@ mod tests { #[tokio::test] async fn test_process_flow_challenge_sent_email_2fa_enabled() { - let cache = MockCache::new(); + let cache = MemoryCache::new(); let did = Did::new("did:plc:test2".to_string()).unwrap(); let ctx = Legacy2faContext { is_app_password: false, @@ -538,7 +475,7 @@ mod tests { #[tokio::test] async fn test_process_flow_verified() { - let cache = MockCache::new(); + let cache = MemoryCache::new(); let did = Did::new("did:plc:test".to_string()).unwrap(); let ctx = Legacy2faContext { is_app_password: false, @@ -557,7 +494,7 @@ mod tests { #[tokio::test] async fn test_attempts_persist_across_failures() { - let cache = MockCache::new(); + let cache = MemoryCache::new(); let did = Did::new("did:plc:test123".to_string()).unwrap(); let code = create_challenge(&cache, &did).await.unwrap(); @@ -590,7 +527,7 @@ mod tests { #[tokio::test] async fn test_totp_shaped_token_accepted_via_verifier() { - let cache = MockCache::new(); + let cache = MemoryCache::new(); let did = Did::new("did:plc:totp1".to_string()).unwrap(); let ctx = Legacy2faContext { is_app_password: false, @@ -607,7 +544,7 @@ mod tests { #[tokio::test] async fn test_totp_shaped_token_rejected_does_not_touch_email_challenge() { - let cache = MockCache::new(); + let cache = MemoryCache::new(); let did = Did::new("did:plc:totp2".to_string()).unwrap(); let ctx = Legacy2faContext { is_app_password: false, @@ -641,7 +578,7 @@ mod tests { #[tokio::test] async fn test_email_shaped_token_routes_to_email_path_when_totp_present() { - let cache = MockCache::new(); + let cache = MemoryCache::new(); let did = Did::new("did:plc:totp3".to_string()).unwrap(); let ctx = Legacy2faContext { is_app_password: false, @@ -662,7 +599,7 @@ mod tests { #[tokio::test] async fn test_backup_code_shaped_token_routes_to_verifier() { - let cache = MockCache::new(); + let cache = MemoryCache::new(); let did = Did::new("did:plc:totp4".to_string()).unwrap(); let ctx = Legacy2faContext { is_app_password: false, @@ -681,7 +618,7 @@ mod tests { #[tokio::test] async fn test_totp_shaped_token_ignored_when_no_totp() { - let cache = MockCache::new(); + let cache = MemoryCache::new(); let did = Did::new("did:plc:totp5".to_string()).unwrap(); let ctx = Legacy2faContext { is_app_password: false, diff --git a/crates/tranquil-pds/src/oauth/permission_set_resolver.rs b/crates/tranquil-pds/src/oauth/permission_set_resolver.rs index a9420bc..defe864 100644 --- a/crates/tranquil-pds/src/oauth/permission_set_resolver.rs +++ b/crates/tranquil-pds/src/oauth/permission_set_resolver.rs @@ -137,39 +137,12 @@ fn map_err(e: &ScopeExpansionError) -> ResolveFailure { #[cfg(test)] mod tests { use super::*; - use crate::cache::{Cache, CacheError}; - use std::collections::HashMap; - use std::sync::Mutex; use std::time::Duration; + use tranquil_infra::MemoryCache; - #[derive(Default)] - struct MapCache(Mutex>); + const SEED_TTL: Duration = Duration::from_secs(3600); - #[async_trait::async_trait] - impl Cache for MapCache { - async fn get(&self, key: &str) -> Option { - self.0.lock().unwrap().get(key).cloned() - } - async fn set(&self, key: &str, value: &str, _ttl: Duration) -> Result<(), CacheError> { - self.0 - .lock() - .unwrap() - .insert(key.to_string(), value.to_string()); - Ok(()) - } - async fn delete(&self, key: &str) -> Result<(), CacheError> { - self.0.lock().unwrap().remove(key); - Ok(()) - } - async fn get_bytes(&self, _key: &str) -> Option> { - None - } - async fn set_bytes(&self, _k: &str, _v: &[u8], _t: Duration) -> Result<(), CacheError> { - Ok(()) - } - } - - fn seed_at(cache: &MapCache, nsid: &str, scope: &str, refreshed_at: i64) { + async fn seed_at(cache: &MemoryCache, nsid: &str, scope: &str, refreshed_at: i64) { let key = crate::cache_keys::permission_set_key(&tranquil_types::Nsid::new(nsid).unwrap(), None); let val = serde_json::to_string(&CachedPermissionSet { @@ -179,21 +152,22 @@ mod tests { refreshed_at, }) .unwrap(); - cache.0.lock().unwrap().insert(key, val); + let _ = cache.set(&key, &val, SEED_TTL).await; } - fn seed(cache: &MapCache, nsid: &str, scope: &str) { - seed_at(cache, nsid, scope, now_secs()); + async fn seed(cache: &MemoryCache, nsid: &str, scope: &str) { + seed_at(cache, nsid, scope, now_secs()).await; } #[tokio::test] async fn cache_hit_expands_without_network() { - let cache = MapCache::default(); + let cache = MemoryCache::new(); seed( &cache, "io.atcr.authFullApp", "repo:io.atcr.manifest?action=create identity:*", - ); + ) + .await; let out = expand_scopes(&cache, "atproto include:io.atcr.authFullApp").await; assert!(out.failures.is_empty()); assert_eq!(out.passthrough, vec!["atproto".to_string()]); @@ -208,13 +182,14 @@ mod tests { #[tokio::test] async fn stale_entry_is_served_when_refresh_fails() { - let cache = MapCache::default(); + let cache = MemoryCache::new(); seed_at( &cache, "nonexistent.fake.permissionSet", "repo:nonexistent.fake.record?action=create", now_secs() - STALE_AFTER_SECS - 1, - ); + ) + .await; let out = expand_scopes(&cache, "include:nonexistent.fake.permissionSet").await; assert!( out.failures.is_empty(), @@ -230,7 +205,7 @@ mod tests { #[tokio::test] async fn entry_without_refreshed_at_is_treated_as_stale_but_usable() { - let cache = MapCache::default(); + let cache = MemoryCache::new(); let key = crate::cache_keys::permission_set_key( &tranquil_types::Nsid::new("nonexistent.fake.permissionSet").unwrap(), None, @@ -238,7 +213,7 @@ mod tests { // Shape written before `refreshed_at` existed. let legacy = r#"{"scope":"repo:nonexistent.fake.record?action=create","title":null,"detail":null}"#; - cache.0.lock().unwrap().insert(key, legacy.to_string()); + let _ = cache.set(&key, legacy, SEED_TTL).await; let out = expand_scopes(&cache, "include:nonexistent.fake.permissionSet").await; assert!(out.failures.is_empty()); assert_eq!(out.sets.len(), 1); @@ -246,7 +221,7 @@ mod tests { #[tokio::test] async fn passthrough_scopes_untouched() { - let cache = MapCache::default(); + let cache = MemoryCache::new(); let out = expand_scopes(&cache, "atproto repo:app.bsky.feed.post?action=create").await; assert!(out.failures.is_empty()); assert!(out.sets.is_empty()); @@ -255,7 +230,7 @@ mod tests { #[tokio::test] async fn cache_miss_unresolvable_is_a_failure() { - let cache = MapCache::default(); + let cache = MemoryCache::new(); let out = expand_scopes(&cache, "include:nonexistent.fake.permissionSet").await; assert_eq!(out.sets.len(), 0); assert_eq!(out.failures.len(), 1);