#[cfg(test)]
mod custom_bag_tests {
use super::super::*;
use crate::session::layer::SessionInner;
use tokio::sync::RwLock;
fn make_session() -> AuthSession {
let inner = SessionInner {
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(crate::session::layer::SessionHandle(Arc::new(RwLock::new(
inner,
))))
}
#[tokio::test]
async fn with_auth_state_borrows_and_projects() {
let session = make_session();
let is_guest = session.with_auth_state(|s| s.is_guest()).await;
assert!(is_guest);
}
#[tokio::test]
async fn with_data_sees_set_custom() {
let session = make_session();
session.set_custom("key", serde_json::json!("value")).await;
let observed = session
.with_data(|d| {
d.custom
.get("key")
.and_then(|v| v.as_str())
.map(str::to_string)
})
.await;
assert_eq!(observed.as_deref(), Some("value"));
}
#[tokio::test]
async fn mutate_custom_removes_keys_atomically() {
let session = make_session();
session.set_custom("a", serde_json::json!(1)).await;
session.set_custom("b", serde_json::json!(2)).await;
session.set_custom("c", serde_json::json!(3)).await;
session
.mutate_custom(|obj| {
obj.remove("a");
obj.remove("b");
})
.await;
let after = session.with_data(|d| d.custom.clone()).await;
let map = after.as_object().expect("custom is an object");
assert!(!map.contains_key("a"));
assert!(!map.contains_key("b"));
assert!(map.contains_key("c"));
}
#[tokio::test]
async fn mutate_custom_marks_modified_when_changed() {
let session = make_session();
session.set_custom("k", serde_json::json!("v")).await;
{
let mut g = session.0.0.write().await;
g.modified = false;
}
session
.mutate_custom(|obj| {
obj.remove("k");
})
.await;
let modified = { session.0.0.read().await.modified };
assert!(modified, "removing a key should mark session modified");
}
#[tokio::test]
async fn mutate_custom_no_op_does_not_mark_modified() {
let session = make_session();
session.set_custom("k", serde_json::json!("v")).await;
{
let mut g = session.0.0.write().await;
g.modified = false;
}
session.mutate_custom(|_obj| { }).await;
let modified = { session.0.0.read().await.modified };
assert!(!modified, "no-op mutate_custom should not mark modified");
}
}
#[cfg(test)]
mod extractor_tests {
use super::super::*;
use crate::session::layer::SessionInner;
use tokio::sync::RwLock;
fn make_session() -> AuthSession {
let inner = SessionInner {
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(crate::session::layer::SessionHandle(Arc::new(RwLock::new(
inner,
))))
}
#[tokio::test]
async fn snapshot_returns_some_for_authenticated_state() {
let session = make_session();
let user = axess_identity::testing::user("u-1");
let tenant = axess_identity::testing::tenant("t-1");
let now = chrono::Utc::now();
session.set_authenticated(user, tenant, now).await;
let snap = session
.snapshot()
.await
.expect("authenticated must yield Some");
assert_eq!(snap.user_id, user);
assert_eq!(snap.tenant_id, tenant);
assert_eq!(snap.authn_time, now);
assert_eq!(snap.session_id, session.session_id().await);
}
#[tokio::test]
async fn snapshot_returns_none_for_guest_state() {
let session = make_session();
assert!(session.snapshot().await.is_none());
}
#[tokio::test]
async fn record_attempt_at_increments_and_stamps_time() {
let session = make_session();
let user = axess_identity::testing::user("u");
let tenant = axess_identity::testing::tenant("t");
session
.begin_authenticating(user, tenant, Arc::from("password"), vec![])
.await;
let t1 = chrono::Utc::now();
for expected_count in 1u32..=3 {
session.record_attempt_at(t1).await;
let count = session
.with_auth_state(|s| match s {
AuthState::Authenticating {
attempt_count,
last_attempt,
..
} => Some((*attempt_count, *last_attempt)),
_ => None,
})
.await
.expect("Authenticating state");
assert_eq!(count.0, expected_count, "attempt_count must increment by 1");
assert_eq!(count.1, Some(t1), "last_attempt must be stamped");
}
}
#[tokio::test]
async fn set_identifying_transitions_state() {
let session = make_session();
assert!(session.with_auth_state(|s| s.is_guest()).await);
let user = axess_identity::testing::user("alice");
let tenant = axess_identity::testing::tenant("acme");
session.set_identifying(user, tenant).await;
let saw = session
.with_auth_state(|s| match s {
AuthState::Identifying { user_id, tenant_id } => Some((*user_id, *tenant_id)),
_ => None,
})
.await
.expect("expected Identifying state");
assert_eq!(saw.0, user);
assert_eq!(saw.1, tenant);
}
#[tokio::test]
async fn clear_custom_wipes_the_bag() {
let session = make_session();
session.set_custom("k1", serde_json::json!("v1")).await;
session.set_custom("k2", serde_json::json!(2)).await;
assert!(session.get_custom("k1").await.is_some());
session.clear_custom().await;
assert!(session.get_custom("k1").await.is_none());
assert!(session.get_custom("k2").await.is_none());
let bag = session.with_data(|d| d.custom.clone()).await;
let map = bag.as_object().expect("custom is object");
assert!(map.is_empty(), "clear_custom must yield an empty object");
}
#[tokio::test]
async fn remove_custom_returns_true_only_when_key_present() {
let session = make_session();
session.set_custom("present", serde_json::json!(1)).await;
let absent = session.remove_custom("never-set").await;
assert!(!absent, "remove_custom on absent key must return false");
let present = session.remove_custom("present").await;
assert!(present, "remove_custom on present key must return true");
let again = session.remove_custom("present").await;
assert!(!again);
}
#[tokio::test]
async fn set_custom_size_boundary_is_strict_greater_than() {
let session = make_session();
const CAP: usize = 256;
{
let mut g = session.0.0.write().await;
g.max_custom_bytes = CAP;
}
let target_value_len = CAP - 8;
let exactly_at_cap = "v".repeat(target_value_len);
let just_over_cap = "v".repeat(target_value_len + 1);
let ok = session
.set_custom("k", serde_json::json!(exactly_at_cap))
.await;
assert!(
ok,
"at exactly max_custom_bytes the write must succeed (> not >=)"
);
let rejected = session
.set_custom("k", serde_json::json!(just_over_cap))
.await;
assert!(!rejected, "one byte over max_custom_bytes must reject");
let stored = session
.get_custom("k")
.await
.and_then(|v| v.as_str().map(|s| s.to_string()));
assert_eq!(stored, Some(exactly_at_cap));
}
#[tokio::test]
async fn set_custom_with_zero_limit_accepts_any_size() {
let session = make_session();
{
let mut g = session.0.0.write().await;
g.max_custom_bytes = 0;
}
let large_value = "v".repeat(10_000);
let stored = session
.set_custom("k", serde_json::json!(large_value))
.await;
assert!(
stored,
"limit == 0 means unlimited; mutation `>= 0` would always reject"
);
}
#[tokio::test]
async fn is_authenticated_false_for_default_guest_session() {
let session = make_session();
assert!(!session.is_authenticated().await);
}
#[tokio::test]
async fn is_authenticated_true_after_set_authenticated() {
let session = make_session();
let user = axess_identity::testing::user("u-isauth");
let tenant = axess_identity::testing::tenant("t-isauth");
session
.set_authenticated(user, tenant, chrono::Utc::now())
.await;
assert!(session.is_authenticated().await);
}
#[tokio::test]
async fn auth_state_reflects_set_authenticated() {
let session = make_session();
let user = axess_identity::testing::user("u-state");
let tenant = axess_identity::testing::tenant("t-state");
session
.set_authenticated(user, tenant, chrono::Utc::now())
.await;
let state = session.auth_state().await;
assert!(matches!(state, AuthState::Authenticated { .. }));
}
#[tokio::test]
async fn data_reflects_set_custom() {
let session = make_session();
session.set_custom("k", serde_json::json!("v")).await;
let data = session.data().await;
assert_eq!(data.custom.get("k").and_then(|v| v.as_str()), Some("v"));
}
#[tokio::test]
async fn authenticated_ids_returns_some_for_authenticated() {
let session = make_session();
let user = axess_identity::testing::user("u-ids");
let tenant = axess_identity::testing::tenant("t-ids");
session
.set_authenticated(user, tenant, chrono::Utc::now())
.await;
let ids = session.authenticated_ids().await;
assert_eq!(ids, Some((user, tenant)));
}
#[tokio::test]
async fn authenticated_ids_returns_none_for_guest() {
let session = make_session();
assert_eq!(session.authenticated_ids().await, None);
}
#[tokio::test]
async fn set_pending_workflow_transitions_state() {
use crate::session::data::{WorkflowKind, WorkflowState};
let session = make_session();
let user = axess_identity::testing::user("u-pend");
let tenant = axess_identity::testing::tenant("t-pend");
let workflow = WorkflowState::new(WorkflowKind::Signup, 3, chrono::Utc::now());
session.set_pending_workflow(user, tenant, workflow).await;
let state = session.auth_state().await;
assert!(matches!(state, AuthState::PendingWorkflow { .. }));
}
#[tokio::test]
async fn clear_resets_to_guest_and_drops_custom() {
let session = make_session();
let user = axess_identity::testing::user("u-clr");
let tenant = axess_identity::testing::tenant("t-clr");
session
.set_authenticated(user, tenant, chrono::Utc::now())
.await;
session.set_custom("k", serde_json::json!("v")).await;
session.clear().await;
assert!(matches!(session.auth_state().await, AuthState::Guest));
assert!(session.get_custom("k").await.is_none());
}
#[tokio::test]
async fn regenerate_sets_the_regenerate_flag() {
let session = make_session();
session.regenerate().await;
let g = session.0.0.read().await;
assert!(g.regenerate);
}
#[tokio::test]
async fn take_custom_returns_and_removes_present_value() {
let session = make_session();
session.set_custom("k", serde_json::json!("v")).await;
let taken = session.take_custom("k").await;
assert_eq!(taken, Some(serde_json::json!("v")));
assert!(session.get_custom("k").await.is_none());
}
#[tokio::test]
async fn take_custom_returns_none_for_absent_key() {
let session = make_session();
assert!(session.take_custom("missing").await.is_none());
}
#[tokio::test]
async fn device_id_returns_session_data_value() {
let session = make_session();
assert!(session.device_id().await.is_none());
let device = axess_identity::testing::device("dev-extractor");
{
session.0.0.write().await.data.device_id = Some(device);
}
assert_eq!(
session.device_id().await,
Some(device),
"device_id must reflect SessionData; `-> None` mutation would always return None"
);
}
#[test]
fn session_missing_into_response_is_internal_server_error() {
use axum::response::IntoResponse;
let response = SessionMissing.into_response();
assert_eq!(
response.status(),
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
"SessionMissing must surface as 500, not Default::default()"
);
}
}