#![cfg(test)]
use super::*;
#[test]
fn deserialised_session_without_version_field_defaults_to_one() {
let json = serde_json::json!({
"auth_state": { "kind": "Guest" },
"fingerprint": null,
"custom": {}
});
let parsed: SessionData = serde_json::from_value(json).expect("parse");
assert_eq!(parsed.version, 1);
}
#[test]
fn migrate_returns_false_when_version_already_current() {
let mut data = SessionData::default();
assert_eq!(data.version, SESSION_DATA_VERSION);
let migrated = data.migrate();
assert!(
!migrated,
"migrate must be a no-op when already at current version"
);
assert_eq!(data.version, SESSION_DATA_VERSION);
}
#[test]
fn migrate_returns_true_and_bumps_version_when_stale() {
let mut data = SessionData {
version: 1,
..Default::default()
};
let migrated = data.migrate();
assert!(migrated, "migrate must return true on upgrade");
assert_eq!(
data.version, SESSION_DATA_VERSION,
"migrate must bump version to current"
);
}
#[test]
fn deserialised_v1_session_gets_none_device_id() {
let json = serde_json::json!({
"version": 1,
"auth_state": { "kind": "Guest" },
"fingerprint": null,
"custom": {}
});
let parsed: SessionData = serde_json::from_value(json).expect("parse v1");
assert_eq!(parsed.device_id, None);
assert_eq!(parsed.version, 1, "deserialisation must NOT auto-migrate");
}
#[test]
fn migrate_v1_to_v2_bumps_version_and_signals_resave() {
let mut data = SessionData {
version: 1,
..Default::default()
};
let migrated = data.migrate();
assert!(migrated, "v1 → v2 must signal a resave");
assert_eq!(data.version, SESSION_DATA_VERSION);
assert_eq!(data.device_id, None);
}
#[test]
fn migrate_does_not_regress_future_version() {
let mut data = SessionData {
version: SESSION_DATA_VERSION.saturating_add(1),
..Default::default()
};
let before = data.version;
let migrated = data.migrate();
assert!(!migrated, "migrate must be no-op for future-version data");
assert_eq!(
data.version, before,
"migrate must NOT clobber future-version field"
);
}
#[test]
fn user_id_returns_none_only_for_guest() {
let user = axess_identity::testing::user("u1");
let tenant = axess_identity::testing::tenant("t1");
assert!(AuthState::Guest.user_id().is_none());
assert_eq!(
AuthState::Identifying {
user_id: user,
tenant_id: tenant,
}
.user_id(),
Some(&user)
);
assert_eq!(
AuthState::Authenticated {
user_id: user,
tenant_id: tenant,
authn_time: chrono::Utc::now(),
factors_completed: vec![],
}
.user_id(),
Some(&user)
);
}
#[test]
fn tenant_id_returns_none_only_for_guest() {
let user = axess_identity::testing::user("u1");
let tenant = axess_identity::testing::tenant("t1");
assert!(AuthState::Guest.tenant_id().is_none());
assert_eq!(
AuthState::Authenticated {
user_id: user,
tenant_id: tenant,
authn_time: chrono::Utc::now(),
factors_completed: vec![],
}
.tenant_id(),
Some(&tenant)
);
}
#[test]
fn is_authenticated_only_for_authenticated_variant() {
let user = axess_identity::testing::user("u1");
let tenant = axess_identity::testing::tenant("t1");
assert!(!AuthState::Guest.is_authenticated());
assert!(
AuthState::Authenticated {
user_id: user,
tenant_id: tenant,
authn_time: chrono::Utc::now(),
factors_completed: vec![],
}
.is_authenticated()
);
}
#[test]
fn is_guest_only_for_guest_variant() {
let user = axess_identity::testing::user("u1");
let tenant = axess_identity::testing::tenant("t1");
assert!(AuthState::Guest.is_guest());
assert!(
!AuthState::Authenticated {
user_id: user,
tenant_id: tenant,
authn_time: chrono::Utc::now(),
factors_completed: vec![],
}
.is_guest()
);
}
#[test]
fn workflow_state_new_starts_at_step_zero() {
let now = chrono::Utc::now();
let ws = WorkflowState::new(WorkflowKind::Signup, 3, now);
assert_eq!(ws.current_step, 0);
assert_eq!(ws.total_steps, 3);
assert_eq!(ws.initiated_at, now);
assert_eq!(ws.kind, WorkflowKind::Signup);
}
#[test]
fn session_data_json_round_trip() {
let data = SessionData {
version: SESSION_DATA_VERSION,
auth_state: AuthState::Authenticated {
user_id: axess_identity::testing::user("alice"),
tenant_id: axess_identity::testing::tenant("acme"),
authn_time: chrono::Utc::now(),
factors_completed: vec![FactorKind::Password, FactorKind::Totp],
},
fingerprint: Some("fp-abc".to_string()),
device_id: Some(axess_identity::testing::device("dev-1")),
custom: serde_json::json!({"key": "value"}),
};
let json = serde_json::to_value(&data).unwrap();
let restored: SessionData = serde_json::from_value(json).unwrap();
assert_eq!(restored.version, data.version);
assert_eq!(restored.fingerprint, data.fingerprint);
assert_eq!(restored.device_id, data.device_id);
assert_eq!(restored.auth_state, data.auth_state);
}
#[test]
fn is_authenticating_only_true_for_authenticating_variant() {
let user = axess_identity::testing::user("u");
let tenant = axess_identity::testing::tenant("t");
assert!(!AuthState::Guest.is_authenticating());
assert!(
!AuthState::Identifying {
user_id: user,
tenant_id: tenant,
}
.is_authenticating()
);
assert!(
AuthState::Authenticating {
user_id: user,
tenant_id: tenant,
method_name: Arc::from("password"),
remaining: vec![],
completed: vec![],
attempt_count: 0,
last_attempt: None,
}
.is_authenticating()
);
assert!(
!AuthState::Authenticated {
user_id: user,
tenant_id: tenant,
authn_time: chrono::Utc::now(),
factors_completed: vec![],
}
.is_authenticating()
);
assert!(
!AuthState::PendingWorkflow {
user_id: user,
tenant_id: tenant,
workflow: WorkflowState::new(WorkflowKind::Signup, 1, chrono::Utc::now()),
}
.is_authenticating()
);
}
fn now() -> DateTime<Utc> {
chrono::Utc::now()
}
#[test]
fn set_identifying_from_guest() {
let user = axess_identity::testing::user("u1");
let tenant = axess_identity::testing::tenant("t1");
let mut state = AuthState::Guest;
state.set_identifying(user, tenant);
assert_eq!(
state,
AuthState::Identifying {
user_id: user,
tenant_id: tenant
}
);
}
#[test]
fn begin_authenticating_constructs_authenticating_with_factors_in_order() {
let user = axess_identity::testing::user("u1");
let tenant = axess_identity::testing::tenant("t1");
let mut state = AuthState::Guest;
state.begin_authenticating(
user,
tenant,
Arc::from("password+totp"),
vec![FactorKind::Password, FactorKind::Totp],
);
match state {
AuthState::Authenticating {
user_id,
tenant_id,
method_name,
remaining,
completed,
attempt_count,
last_attempt,
} => {
assert_eq!(user_id, user);
assert_eq!(tenant_id, tenant);
assert_eq!(&*method_name, "password+totp");
assert_eq!(remaining, vec![FactorKind::Password, FactorKind::Totp]);
assert!(completed.is_empty());
assert_eq!(attempt_count, 0);
assert!(last_attempt.is_none());
}
other => panic!("expected Authenticating, got {other:?}"),
}
}
#[test]
fn set_authenticated_constructs_authenticated_with_empty_factors() {
let user = axess_identity::testing::user("u1");
let tenant = axess_identity::testing::tenant("t1");
let t = now();
let mut state = AuthState::Guest;
state.set_authenticated(user, tenant, t);
match state {
AuthState::Authenticated {
user_id,
tenant_id,
authn_time,
factors_completed,
} => {
assert_eq!(user_id, user);
assert_eq!(tenant_id, tenant);
assert_eq!(authn_time, t);
assert!(
factors_completed.is_empty(),
"direct transition: no factor sequence"
);
}
other => panic!("expected Authenticated, got {other:?}"),
}
}
#[test]
fn advance_factor_still_authenticating_when_factors_remain() {
let user = axess_identity::testing::user("u1");
let tenant = axess_identity::testing::tenant("t1");
let mut state = AuthState::Authenticating {
user_id: user,
tenant_id: tenant,
method_name: Arc::from("password+totp"),
remaining: vec![FactorKind::Password, FactorKind::Totp],
completed: vec![],
attempt_count: 0,
last_attempt: None,
};
let outcome = state.advance_factor(&FactorKind::Password, now());
assert_eq!(outcome, AdvanceOutcome::StillAuthenticating);
match state {
AuthState::Authenticating {
remaining,
completed,
..
} => {
assert_eq!(remaining, vec![FactorKind::Totp]);
assert_eq!(completed, vec![FactorKind::Password]);
}
other => panic!("expected still Authenticating, got {other:?}"),
}
}
#[test]
fn advance_factor_completes_when_last_factor_verified() {
let user = axess_identity::testing::user("u1");
let tenant = axess_identity::testing::tenant("t1");
let t = now();
let mut state = AuthState::Authenticating {
user_id: user,
tenant_id: tenant,
method_name: Arc::from("password"),
remaining: vec![FactorKind::Password],
completed: vec![],
attempt_count: 0,
last_attempt: None,
};
let outcome = state.advance_factor(&FactorKind::Password, t);
assert_eq!(outcome, AdvanceOutcome::Completed);
match state {
AuthState::Authenticated {
user_id,
tenant_id,
authn_time,
factors_completed,
} => {
assert_eq!(user_id, user);
assert_eq!(tenant_id, tenant);
assert_eq!(authn_time, t);
assert_eq!(factors_completed, vec![FactorKind::Password]);
}
other => panic!("expected Authenticated, got {other:?}"),
}
}
#[test]
fn advance_factor_carries_completed_factors_into_authenticated() {
let user = axess_identity::testing::user("u1");
let tenant = axess_identity::testing::tenant("t1");
let t = now();
let mut state = AuthState::Authenticating {
user_id: user,
tenant_id: tenant,
method_name: Arc::from("password+totp"),
remaining: vec![FactorKind::Password, FactorKind::Totp],
completed: vec![],
attempt_count: 0,
last_attempt: None,
};
assert_eq!(
state.advance_factor(&FactorKind::Password, t),
AdvanceOutcome::StillAuthenticating
);
assert_eq!(
state.advance_factor(&FactorKind::Totp, t),
AdvanceOutcome::Completed
);
match state {
AuthState::Authenticated {
factors_completed, ..
} => {
assert_eq!(
factors_completed,
vec![FactorKind::Password, FactorKind::Totp],
"completion order must be preserved"
);
}
other => panic!("expected Authenticated, got {other:?}"),
}
}
#[test]
fn advance_factor_no_op_when_kind_not_in_remaining() {
let user = axess_identity::testing::user("u1");
let tenant = axess_identity::testing::tenant("t1");
let mut state = AuthState::Authenticating {
user_id: user,
tenant_id: tenant,
method_name: Arc::from("password"),
remaining: vec![FactorKind::Password],
completed: vec![],
attempt_count: 0,
last_attempt: None,
};
let outcome = state.advance_factor(&FactorKind::Totp, now());
assert_eq!(outcome, AdvanceOutcome::StillAuthenticating);
match state {
AuthState::Authenticating {
remaining,
completed,
..
} => {
assert_eq!(remaining, vec![FactorKind::Password]);
assert!(completed.is_empty());
}
other => panic!("expected Authenticating, got {other:?}"),
}
}
#[test]
fn advance_factor_not_applicable_outside_authenticating() {
let mut state = AuthState::Guest;
assert_eq!(
state.advance_factor(&FactorKind::Password, now()),
AdvanceOutcome::NotApplicable
);
assert_eq!(state, AuthState::Guest);
}
#[test]
fn record_attempt_at_increments_counter_and_captures_timestamp() {
let user = axess_identity::testing::user("u1");
let tenant = axess_identity::testing::tenant("t1");
let t = now();
let mut state = AuthState::Authenticating {
user_id: user,
tenant_id: tenant,
method_name: Arc::from("password"),
remaining: vec![FactorKind::Password],
completed: vec![],
attempt_count: 0,
last_attempt: None,
};
state.record_attempt_at(t);
state.record_attempt_at(t);
match state {
AuthState::Authenticating {
attempt_count,
last_attempt,
..
} => {
assert_eq!(attempt_count, 2);
assert_eq!(last_attempt, Some(t));
}
other => panic!("expected Authenticating, got {other:?}"),
}
}
#[test]
fn record_attempt_at_no_op_outside_authenticating() {
let mut state = AuthState::Guest;
state.record_attempt_at(now());
assert_eq!(state, AuthState::Guest);
}
#[test]
fn set_pending_workflow_replaces_state() {
let user = axess_identity::testing::user("u1");
let tenant = axess_identity::testing::tenant("t1");
let workflow = WorkflowState::new(WorkflowKind::Signup, 3, now());
let mut state = AuthState::Guest;
state.set_pending_workflow(user, tenant, workflow.clone());
match state {
AuthState::PendingWorkflow {
user_id,
tenant_id,
workflow: w,
} => {
assert_eq!(user_id, user);
assert_eq!(tenant_id, tenant);
assert_eq!(w, workflow);
}
other => panic!("expected PendingWorkflow, got {other:?}"),
}
}