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:
parent
b46295d0cf
commit
c91dd5d78d
4 changed files with 256 additions and 1 deletions
51
Cargo.lock
generated
51
Cargo.lock
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue