use super::super::AuthnService;
use super::*;
use crate::authn::ids::{TenantId, UserId};
use crate::authn::types::{EntityState, LockoutPolicy, Tenant, User};
use crate::session::data::SessionData;
use crate::session::extractor::AuthSession;
use crate::session::layer::{SessionHandle, SessionInner};
use crate::session::store::MemorySessionRegistry;
use crate::testing::mock_authn::{MockFactorStore, MockIdentityStore};
use std::sync::Arc;
use tokio::sync::RwLock;
fn fixture_user_id() -> UserId {
axess_identity::testing::user("u-login")
}
fn fixture_tenant_id() -> TenantId {
axess_identity::testing::tenant("t-login")
}
fn make_session() -> AuthSession {
let inner = SessionInner {
id: crate::session::id::SessionId::new(&axess_rng::SystemRng),
data: SessionData::default(),
modified: false,
regenerate: false,
pre_cycle_id: None,
pending_fingerprint: None,
max_custom_bytes: 64 * 1024,
};
AuthSession(SessionHandle(Arc::new(RwLock::new(inner))))
}
async fn authenticated_session() -> AuthSession {
let session = make_session();
session
.set_authenticated(fixture_user_id(), fixture_tenant_id(), chrono::Utc::now())
.await;
session
}
fn build_user(user_id: &UserId, tenant_id: &TenantId, identifier: &str) -> User {
let now = chrono::Utc::now();
User {
id: *user_id,
tenant_id: *tenant_id,
identifier: identifier.into(),
display_name: identifier.into(),
status: EntityState::Active,
webauthn_id: None,
created_by: UserId::system(),
created_at: now,
updated_by: UserId::system(),
updated_at: now,
}
}
fn build_tenant(tenant_id: TenantId, identifier: &str) -> Tenant {
let now = chrono::Utc::now();
Tenant {
id: tenant_id,
identifier: identifier.into(),
display_name: identifier.into(),
status: EntityState::Active,
created_by: UserId::system(),
created_at: now,
updated_by: UserId::system(),
updated_at: now,
}
}
fn build_service_with_identity(
identity: MockIdentityStore,
) -> AuthnService<MockIdentityStore, MockFactorStore> {
AuthnService::new(identity, MockFactorStore::new())
}
#[tokio::test]
async fn check_session_unauthenticated_returns_false() {
let service = build_service_with_identity(MockIdentityStore::new());
let session = make_session();
assert!(!service.check_session(&session).await);
}
#[tokio::test]
async fn check_session_authenticated_no_registry_returns_true() {
let service = build_service_with_identity(MockIdentityStore::new());
let session = authenticated_session().await;
assert!(service.check_session(&session).await);
}
#[tokio::test]
async fn check_session_registry_rejection_returns_false() {
let service = build_service_with_identity(MockIdentityStore::new())
.with_registry(MemorySessionRegistry::new());
let session = authenticated_session().await;
assert!(
!service.check_session(&session).await,
"registry rejection must invalidate the session"
);
}
#[tokio::test]
async fn check_session_registry_acceptance_returns_true() {
let registry = MemorySessionRegistry::new();
let session = authenticated_session().await;
let sid = session.session_id().await;
crate::session::store::SessionRegistry::register(®istry, &fixture_user_id(), &sid)
.await
.unwrap();
let service = build_service_with_identity(MockIdentityStore::new()).with_registry(registry);
assert!(service.check_session(&session).await);
}
#[tokio::test]
async fn logout_invalidates_user_in_registry() {
use crate::session::store::SessionRegistry;
let identity = MockIdentityStore::new()
.with_tenant(build_tenant(fixture_tenant_id(), "t-login"))
.with_user(build_user(
&fixture_user_id(),
&fixture_tenant_id(),
"u-login",
))
.with_lockout_policy(LockoutPolicy::default());
let registry = MemorySessionRegistry::new();
let session = authenticated_session().await;
let sid = session.session_id().await;
registry.register(&fixture_user_id(), &sid).await.unwrap();
assert!(registry.is_valid(&fixture_user_id(), &sid).await.unwrap());
let service = build_service_with_identity(identity).with_registry(registry.clone());
service.logout(&session).await.expect("logout must succeed");
assert!(
!registry.is_valid(&fixture_user_id(), &sid).await.unwrap(),
"logout must invalidate the user in the registry; \
the `Ok(())` mutation skips this side effect"
);
}
#[tokio::test]
async fn begin_login_rejects_oversized_identifier() {
use crate::validation::MAX_IDENTIFIER_BYTES;
let service = build_service_with_identity(MockIdentityStore::new());
let session = make_session();
let huge_identifier = "x".repeat(MAX_IDENTIFIER_BYTES + 1);
let outcome = service
.begin_login(&huge_identifier, "t-login", &session, None)
.await
.expect("oversized identifier must reject cleanly");
assert!(
matches!(outcome, LoginOutcome::InvalidCredentials),
"identifier longer than MAX_IDENTIFIER_BYTES must reject; \
`> → ==/>=` mutants would admit at the boundary. got: {outcome:?}"
);
}
#[tokio::test]
async fn begin_login_rejects_empty_identifier() {
let service = build_service_with_identity(MockIdentityStore::new());
let session = make_session();
let outcome = service
.begin_login("", "t-login", &session, None)
.await
.expect("empty identifier must reject cleanly");
assert!(
matches!(outcome, LoginOutcome::InvalidCredentials),
"empty identifier must reject; `|| → &&` mutant would admit"
);
}
#[tokio::test]
async fn begin_login_rejects_oversized_tenant_identifier() {
use crate::validation::MAX_IDENTIFIER_BYTES;
let service = build_service_with_identity(MockIdentityStore::new());
let session = make_session();
let huge_tenant = "y".repeat(MAX_IDENTIFIER_BYTES + 1);
let outcome = service
.begin_login("alice", &huge_tenant, &session, None)
.await
.expect("oversized tenant must reject cleanly");
assert!(
matches!(outcome, LoginOutcome::InvalidCredentials),
"tenant_identifier longer than MAX_IDENTIFIER_BYTES must reject"
);
}
#[tokio::test]
async fn begin_login_rejects_empty_tenant_identifier() {
let service = build_service_with_identity(MockIdentityStore::new());
let session = make_session();
let outcome = service
.begin_login("alice", "", &session, None)
.await
.expect("empty tenant must reject cleanly");
assert!(
matches!(outcome, LoginOutcome::InvalidCredentials),
"empty tenant_identifier must reject"
);
}
#[tokio::test]
async fn begin_login_finds_existing_user_via_timing_equalized_path() {
let user_id = fixture_user_id();
let tenant_id = fixture_tenant_id();
let tenant_identifier = "t-login";
let user_identifier = "alice@example.test";
let identity = MockIdentityStore::new()
.with_tenant(build_tenant(tenant_id, tenant_identifier))
.with_user(build_user(&user_id, &tenant_id, user_identifier))
.with_lockout_policy(LockoutPolicy::default());
let service = build_service_with_identity(identity);
let session = make_session();
let result = service
.begin_login(user_identifier, tenant_identifier, &session, None)
.await;
assert!(
!matches!(result, Ok(LoginOutcome::InvalidCredentials)),
"real user must NOT collapse to InvalidCredentials; \
`find_user → Ok(None)` mutation would invert this. got: {result:?}"
);
assert!(
matches!(result, Err(AuthnError::NoFlow)),
"with no methods registered, begin_login must reach the \
NoFlow gate past the user-lookup. got: {result:?}"
);
}