use std::sync::Arc;
use async_trait::async_trait;
use arcly_http_core::auth::{JwtConfig, JwtService};
use arcly_http_identity::consent::{ConsentGrant, ConsentStore, MemoryConsentStore};
use arcly_http_identity::error::{IdentityError, Result as IdResult};
use arcly_http_identity::identity::{IdentityConfig, IdentityService};
use arcly_http_identity::model::{LoginOutcome, RegisterRequest};
use arcly_http_identity::password::{Argon2idHasher, BreachChecker, PasswordPolicy};
use arcly_http_identity::session::{MemoryChallengeStore, MemoryRefreshStore};
use arcly_http_identity::store::MemoryStore;
fn jwt() -> Arc<JwtService> {
Arc::new(JwtService::new(JwtConfig {
secret: "htier-test-secret".into(),
..Default::default()
}))
}
fn builder(store: Arc<MemoryStore>) -> arcly_http_identity::identity::IdentityServiceBuilder {
IdentityService::builder(
store.clone(),
store.clone(),
Arc::new(Argon2idHasher::new()),
jwt(),
Arc::new(MemoryRefreshStore::new()),
Arc::new(MemoryChallengeStore::new()),
)
.config(IdentityConfig {
bind_tenant: false,
..Default::default()
})
}
struct StubBreach(&'static str);
#[async_trait]
impl BreachChecker for StubBreach {
async fn is_breached(&self, password: &str) -> IdResult<bool> {
Ok(password == self.0)
}
}
#[tokio::test]
async fn register_enforces_password_policy() {
let store = Arc::new(MemoryStore::new());
let svc = builder(store)
.password_policy(PasswordPolicy {
min_len: 12,
..PasswordPolicy::default()
})
.build();
let err = svc
.register(RegisterRequest {
email: "a@corp.com".into(),
password: Some("short".into()),
tenant: None,
attributes: Default::default(),
})
.await
.unwrap_err();
assert!(matches!(err, IdentityError::PasswordRejected(_)));
assert!(svc
.register(RegisterRequest {
email: "a@corp.com".into(),
password: Some("a-good-long-passphrase".into()),
tenant: None,
attributes: Default::default(),
})
.await
.is_ok());
}
#[tokio::test]
async fn register_rejects_breached_password() {
let store = Arc::new(MemoryStore::new());
let svc = builder(store)
.breach_checker(Arc::new(StubBreach("hunter2-hunter2")))
.build();
let err = svc
.register(RegisterRequest {
email: "b@corp.com".into(),
password: Some("hunter2-hunter2".into()),
tenant: None,
attributes: Default::default(),
})
.await
.unwrap_err();
assert!(matches!(err, IdentityError::PasswordRejected(_)));
}
#[tokio::test]
async fn consent_gate_blocks_mint_until_granted() {
let store = Arc::new(MemoryStore::new());
let consent = Arc::new(MemoryConsentStore::new());
let svc = builder(store)
.require_consents(consent.clone(), vec!["tos".into()])
.build();
let user = svc
.register(RegisterRequest {
email: "c@corp.com".into(),
password: Some("a-good-long-passphrase".into()),
tenant: None,
attributes: Default::default(),
})
.await
.unwrap();
let err = svc
.login(None, "c@corp.com", "a-good-long-passphrase")
.await
.unwrap_err();
match err {
IdentityError::ConsentRequired(purposes) => assert_eq!(purposes, vec!["tos".to_string()]),
other => panic!("expected ConsentRequired, got {other:?}"),
}
consent
.record(
&user.id,
ConsentGrant {
purpose: "tos".into(),
version: "1".into(),
granted: true,
timestamp: 0,
},
)
.await
.unwrap();
let out = svc
.login(None, "c@corp.com", "a-good-long-passphrase")
.await
.unwrap();
assert!(matches!(out, LoginOutcome::Authenticated(_)));
}