From c91dd5d78de5d8283122082e0d8d64f06b63a66f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 25 Nov 2025 15:30:13 +0000 Subject: [PATCH] test(auth): add CLI test for revoked tokens and server tests for session auth MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add test_revoked_token_cannot_be_used to CLI tests - Add test_session_authentication_via_login to verify session cookies work - Add test_invalid_session_cookie_fails to verify unauthenticated requests fail - Add test_logout_invalidates_session to verify logout clears sessions - Add test_fake_session_cookie_fails to verify forged cookies don't work - Enable cookies feature for reqwest in dev-dependencies All 70 tests pass (8 unit + 46 server + 16 CLI) ✅ Co-authored-by: jnsgruk <668505+jnsgruk@users.noreply.github.com> --- Cargo.lock | 51 +++++++++++++ Cargo.toml | 2 +- tests/cli/tokens_cli.rs | 54 ++++++++++++++ tests/server/auth_api.rs | 150 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 256 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 44992a5..b099bbf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -474,6 +474,24 @@ dependencies = [ "version_check", ] +[[package]] +name = "cookie_store" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eac901828f88a5241ee0600950ab981148a18f2f756900ffba1b125ca6a3ef9" +dependencies = [ + "cookie", + "document-features", + "idna", + "log", + "publicsuffix", + "serde", + "serde_derive", + "serde_json", + "time", + "url", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -600,6 +618,15 @@ dependencies = [ "syn 2.0.110", ] +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + [[package]] name = "dotenvy" version = "0.15.7" @@ -1311,6 +1338,12 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + [[package]] name = "lock_api" version = "0.4.14" @@ -1686,6 +1719,22 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "psl-types" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" + +[[package]] +name = "publicsuffix" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42ea446cab60335f76979ec15e12619a2165b5ae2c12166bef27d283a9fadf" +dependencies = [ + "idna", + "psl-types", +] + [[package]] name = "quinn" version = "0.11.9" @@ -1870,6 +1919,8 @@ checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" dependencies = [ "base64 0.22.1", "bytes", + "cookie", + "cookie_store", "encoding_rs", "futures-channel", "futures-core", diff --git a/Cargo.toml b/Cargo.toml index 28b75c1..3e4cada 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,7 +41,7 @@ tower-cookies = "0.10" [dev-dependencies] portpicker = "0.1" -reqwest = { version = "0.12", features = ["blocking"] } +reqwest = { version = "0.12", features = ["blocking", "cookies"] } tempfile = "3.8" wiremock = "0.6" diff --git a/tests/cli/tokens_cli.rs b/tests/cli/tokens_cli.rs index b8d8524..b08c348 100644 --- a/tests/cli/tokens_cli.rs +++ b/tests/cli/tokens_cli.rs @@ -73,3 +73,57 @@ fn test_revoke_token_with_authentication() { ); } } + +#[test] +fn test_revoked_token_cannot_be_used() { + // Create a token that we will revoke + let token_to_revoke = create_token("test-revoked-token"); + + // Create a second token that we'll use to revoke the first and verify + let admin_token = create_token("test-admin-token"); + + // List tokens to get the ID of the token we want to revoke + let list_output = run_brewlog(&["list-tokens"], &[("BREWLOG_TOKEN", &admin_token)]); + assert!(list_output.status.success()); + + let list_stdout = String::from_utf8_lossy(&list_output.stdout); + let tokens: serde_json::Value = + serde_json::from_str(&list_stdout).expect("Should parse token list as JSON"); + + // Find the token to revoke by name + let tokens_array = tokens.as_array().expect("Should be an array"); + let token_to_revoke_entry = tokens_array + .iter() + .find(|t| t["name"].as_str() == Some("test-revoked-token")) + .expect("Should find token to revoke"); + + let token_id = token_to_revoke_entry["id"] + .as_str() + .expect("Token should have ID"); + + // Revoke the token + let revoke_output = run_brewlog( + &["revoke-token", "--id", token_id], + &[("BREWLOG_TOKEN", &admin_token)], + ); + assert!( + revoke_output.status.success(), + "Should successfully revoke token" + ); + + // Try to use the revoked token - it should fail + let list_with_revoked_output = + run_brewlog(&["list-tokens"], &[("BREWLOG_TOKEN", &token_to_revoke)]); + + assert!( + !list_with_revoked_output.status.success(), + "Revoked token should not be able to authenticate" + ); + + let stderr = String::from_utf8_lossy(&list_with_revoked_output.stderr); + assert!( + stderr.contains("401") || stderr.contains("Unauthorized") || stderr.contains("failed"), + "Error should indicate authentication failure, got: {}", + stderr + ); +} diff --git a/tests/server/auth_api.rs b/tests/server/auth_api.rs index c4208af..be174d3 100644 --- a/tests/server/auth_api.rs +++ b/tests/server/auth_api.rs @@ -249,6 +249,156 @@ async fn test_protected_endpoints_work_with_authentication() { assert_eq!(response.status(), StatusCode::CREATED); } +#[tokio::test] +async fn test_session_authentication_via_login() { + let app = spawn_app_with_auth().await; + let client = reqwest::Client::builder() + .cookie_store(true) + .build() + .unwrap(); + + // Login to create a session + let login_response = client + .post(&format!("{}/login", app.address)) + .form(&[("username", "admin"), ("password", "test_password")]) + .send() + .await + .expect("Failed to send login request"); + + assert!( + login_response.status().is_redirection() || login_response.status().is_success(), + "Login should succeed" + ); + + // Use the session cookie to create a roaster (no Bearer token needed) + let response = client + .post(&app.api_url("/roasters")) + .json(&json!({ + "name": "Session Test Roaster", + "country": "US" + })) + .send() + .await + .expect("Failed to send request"); + + assert_eq!( + response.status(), + StatusCode::CREATED, + "Session cookie should authenticate API request" + ); +} + +#[tokio::test] +async fn test_invalid_session_cookie_fails() { + let app = spawn_app_with_auth().await; + let client = reqwest::Client::builder() + .cookie_store(true) + .build() + .unwrap(); + + // Try to create a roaster with no session (unauthenticated) + let response = client + .post(&app.api_url("/roasters")) + .json(&json!({ + "name": "Invalid Session Roaster", + "country": "UK" + })) + .send() + .await + .expect("Failed to send request"); + + assert_eq!( + response.status(), + StatusCode::UNAUTHORIZED, + "Request without valid session should fail" + ); +} + +#[tokio::test] +async fn test_logout_invalidates_session() { + let app = spawn_app_with_auth().await; + let client = reqwest::Client::builder() + .cookie_store(true) + .build() + .unwrap(); + + // Login to create a session + let login_response = client + .post(&format!("{}/login", app.address)) + .form(&[("username", "admin"), ("password", "test_password")]) + .send() + .await + .expect("Failed to send login request"); + + assert!(login_response.status().is_redirection() || login_response.status().is_success()); + + // Verify the session works + let auth_response = client + .post(&app.api_url("/roasters")) + .json(&json!({ + "name": "Pre-Logout Roaster", + "country": "FR" + })) + .send() + .await + .expect("Failed to send request"); + + assert_eq!(auth_response.status(), StatusCode::CREATED); + + // Logout + let logout_response = client + .post(&format!("{}/logout", app.address)) + .send() + .await + .expect("Failed to send logout request"); + + assert!(logout_response.status().is_redirection() || logout_response.status().is_success()); + + // Try to use the session after logout + let post_logout_response = client + .post(&app.api_url("/roasters")) + .json(&json!({ + "name": "Post-Logout Roaster", + "country": "DE" + })) + .send() + .await + .expect("Failed to send request"); + + assert_eq!( + post_logout_response.status(), + StatusCode::UNAUTHORIZED, + "Session should be invalidated after logout" + ); +} + +#[tokio::test] +async fn test_fake_session_cookie_fails() { + let app = spawn_app_with_auth().await; + let client = reqwest::Client::builder() + .cookie_store(true) + .build() + .unwrap(); + + // Try to use a fake/forged session cookie + let response = client + .post(&app.api_url("/roasters")) + .header("Cookie", "brewlog_session=fake_session_token_12345") + .json(&json!({ + "name": "Fake Session Roaster", + "country": "IT" + })) + .send() + .await + .expect("Failed to send request"); + + assert_eq!( + response.status(), + StatusCode::UNAUTHORIZED, + "Fake session cookie should not authenticate" + ); +} + #[tokio::test] async fn test_read_endpoints_dont_require_authentication() { let app = spawn_app_with_auth().await;