test(auth): add CLI test for revoked tokens and server tests for session auth

- 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>
This commit is contained in:
copilot-swe-agent[bot] 2025-11-25 15:30:13 +00:00 committed by Jon Seager
parent b46295d0cf
commit c91dd5d78d
No known key found for this signature in database
4 changed files with 256 additions and 1 deletions

51
Cargo.lock generated
View file

@ -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",

View file

@ -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"

View file

@ -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
);
}

View file

@ -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;