From 615ce4f9399110cf396a6ec7dca6d2cd8adef66c Mon Sep 17 00:00:00 2001 From: Dorian Date: Tue, 10 Mar 2026 23:54:14 +0000 Subject: [PATCH] test: add auth and session unit tests (20 test cases) Co-Authored-By: Claude Opus 4.6 --- core/archipelago/src/auth.rs | 80 ++++++++++++++++++++++ core/archipelago/src/session.rs | 114 ++++++++++++++++++++++++++++++++ loop/plan.md | 2 +- 3 files changed, 195 insertions(+), 1 deletion(-) diff --git a/core/archipelago/src/auth.rs b/core/archipelago/src/auth.rs index b1abe575..b2f4faed 100644 --- a/core/archipelago/src/auth.rs +++ b/core/archipelago/src/auth.rs @@ -234,6 +234,86 @@ fn validate_password_strength(password: &str) -> Result<()> { Ok(()) } +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_setup_user_and_verify_password() { + let dir = tempfile::tempdir().unwrap(); + let auth = AuthManager::new(dir.path().to_path_buf()); + + assert!(!auth.is_setup().await.unwrap()); + + auth.setup_user("password123").await.unwrap(); + + assert!(auth.is_setup().await.unwrap()); + assert!(auth.verify_password("password123").await.unwrap()); + assert!(!auth.verify_password("wrong").await.unwrap()); + } + + #[tokio::test] + async fn test_verify_password_no_user() { + let dir = tempfile::tempdir().unwrap(); + let auth = AuthManager::new(dir.path().to_path_buf()); + + assert!(!auth.verify_password("anything").await.unwrap()); + } + + #[tokio::test] + async fn test_onboarding_lifecycle() { + let dir = tempfile::tempdir().unwrap(); + let auth = AuthManager::new(dir.path().to_path_buf()); + + assert!(!auth.is_onboarding_complete().await.unwrap()); + + auth.complete_onboarding().await.unwrap(); + assert!(auth.is_onboarding_complete().await.unwrap()); + + auth.reset_onboarding().await.unwrap(); + assert!(!auth.is_onboarding_complete().await.unwrap()); + } + + #[tokio::test] + async fn test_onboarding_persists_to_user() { + let dir = tempfile::tempdir().unwrap(); + let auth = AuthManager::new(dir.path().to_path_buf()); + + auth.setup_user("password123").await.unwrap(); + let user = auth.get_user().await.unwrap().unwrap(); + assert!(!user.onboarding_complete); + + auth.complete_onboarding().await.unwrap(); + let user = auth.get_user().await.unwrap().unwrap(); + assert!(user.onboarding_complete); + } + + #[test] + fn test_validate_password_strength_valid() { + assert!(validate_password_strength("MyP@ssw0rd!123").is_ok()); + } + + #[test] + fn test_validate_password_strength_too_short() { + assert!(validate_password_strength("Ab1!").is_err()); + } + + #[test] + fn test_validate_password_strength_no_uppercase() { + assert!(validate_password_strength("mypassword1!xx").is_err()); + } + + #[test] + fn test_validate_password_strength_no_digit() { + assert!(validate_password_strength("MyPassword!!xx").is_err()); + } + + #[test] + fn test_validate_password_strength_no_special() { + assert!(validate_password_strength("MyPassword1234").is_err()); + } +} + /// Change the archipelago user's SSH/login password. /// Uses usermod + openssl to bypass PAM (avoids "Authentication token manipulation" errors). /// Uses absolute paths (/usr/bin/openssl, /usr/sbin/usermod) for systemd's minimal PATH. diff --git a/core/archipelago/src/session.rs b/core/archipelago/src/session.rs index e141406a..77015222 100644 --- a/core/archipelago/src/session.rs +++ b/core/archipelago/src/session.rs @@ -179,3 +179,117 @@ impl LoginRateLimiter { entry.push(Instant::now()); } } + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_session_create_and_validate() { + let store = SessionStore::new(); + let token = store.create().await; + + assert!(store.validate(&token).await); + } + + #[tokio::test] + async fn test_session_invalid_token() { + let store = SessionStore::new(); + assert!(!store.validate("nonexistent_token").await); + } + + #[tokio::test] + async fn test_session_remove() { + let store = SessionStore::new(); + let token = store.create().await; + + assert!(store.validate(&token).await); + store.remove(&token).await; + assert!(!store.validate(&token).await); + } + + #[tokio::test] + async fn test_pending_session_upgrade() { + let store = SessionStore::new(); + let secret = vec![1, 2, 3, 4]; + let token = store.create_pending(secret.clone()).await; + + // Pending session should not validate as full + assert!(!store.validate(&token).await); + + // Can get the TOTP secret + let got = store.get_pending_secret(&token).await; + assert_eq!(got, Some(secret)); + + // Upgrade to full + store.upgrade_to_full(&token).await; + assert!(store.validate(&token).await); + } + + #[tokio::test] + async fn test_pending_session_max_attempts() { + let store = SessionStore::new(); + let secret = vec![1, 2, 3]; + let token = store.create_pending(secret).await; + + // Exhaust MAX_TOTP_ATTEMPTS (5) + 1 to trigger removal + for _ in 0..MAX_TOTP_ATTEMPTS { + assert!(store.get_pending_secret(&token).await.is_some()); + } + // 6th attempt should fail (session removed) + assert!(store.get_pending_secret(&token).await.is_none()); + } + + #[tokio::test] + async fn test_extract_session_cookie() { + let mut headers = hyper::HeaderMap::new(); + headers.insert("cookie", "session=abc123; other=xyz".parse().unwrap()); + + assert_eq!(extract_session_cookie(&headers), Some("abc123".to_string())); + } + + #[tokio::test] + async fn test_extract_session_cookie_missing() { + let headers = hyper::HeaderMap::new(); + assert_eq!(extract_session_cookie(&headers), None); + } + + #[tokio::test] + async fn test_rate_limiter_allows_under_limit() { + let limiter = LoginRateLimiter::new(); + let ip: IpAddr = "127.0.0.1".parse().unwrap(); + + for _ in 0..MAX_ATTEMPTS { + assert!(limiter.check(ip).await); + limiter.record_failure(ip).await; + } + } + + #[tokio::test] + async fn test_rate_limiter_blocks_over_limit() { + let limiter = LoginRateLimiter::new(); + let ip: IpAddr = "127.0.0.1".parse().unwrap(); + + for _ in 0..MAX_ATTEMPTS { + limiter.record_failure(ip).await; + } + + assert!(!limiter.check(ip).await); + } + + #[tokio::test] + async fn test_rate_limiter_different_ips() { + let limiter = LoginRateLimiter::new(); + let ip1: IpAddr = "127.0.0.1".parse().unwrap(); + let ip2: IpAddr = "192.168.1.1".parse().unwrap(); + + for _ in 0..MAX_ATTEMPTS { + limiter.record_failure(ip1).await; + } + + // ip1 should be blocked + assert!(!limiter.check(ip1).await); + // ip2 should still be allowed + assert!(limiter.check(ip2).await); + } +} diff --git a/loop/plan.md b/loop/plan.md index 7b352599..4c19fcf4 100644 --- a/loop/plan.md +++ b/loop/plan.md @@ -28,7 +28,7 @@ - [x] **TEST-06** — Create backend integration test scaffolding. On dev server, create `core/archipelago/tests/rpc_integration.rs` with a test helper that starts the backend on a random port with a temp data dir, sends RPC requests, and tears down. Verify with `cargo test --test rpc_integration`. **Acceptance**: one echo test passes on dev server. -- [ ] **TEST-07** — Create backend unit tests: auth module. Add `#[cfg(test)] mod tests` to `core/archipelago/src/auth.rs` testing: password hash/verify, session creation/validation/expiry, rate limiting. Target: 6+ test cases. Run on dev server with `cargo test -p archipelago`. **Acceptance**: all pass. +- [x] **TEST-07** — Create backend unit tests: auth module. Add `#[cfg(test)] mod tests` to `core/archipelago/src/auth.rs` testing: password hash/verify, session creation/validation/expiry, rate limiting. Target: 6+ test cases. Run on dev server with `cargo test -p archipelago`. **Acceptance**: all pass. - [ ] **TEST-08** — Create backend unit tests: identity module. Add tests to `core/archipelago/src/identity.rs` testing: DID key generation, challenge signing/verification, pubkey hex conversion. Target: 5+ test cases. **Acceptance**: all pass on dev server.