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",
|
"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]]
|
[[package]]
|
||||||
name = "core-foundation"
|
name = "core-foundation"
|
||||||
version = "0.9.4"
|
version = "0.9.4"
|
||||||
|
|
@ -600,6 +618,15 @@ dependencies = [
|
||||||
"syn 2.0.110",
|
"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]]
|
[[package]]
|
||||||
name = "dotenvy"
|
name = "dotenvy"
|
||||||
version = "0.15.7"
|
version = "0.15.7"
|
||||||
|
|
@ -1311,6 +1338,12 @@ version = "0.8.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"
|
checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "litrs"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lock_api"
|
name = "lock_api"
|
||||||
version = "0.4.14"
|
version = "0.4.14"
|
||||||
|
|
@ -1686,6 +1719,22 @@ dependencies = [
|
||||||
"unicode-ident",
|
"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]]
|
[[package]]
|
||||||
name = "quinn"
|
name = "quinn"
|
||||||
version = "0.11.9"
|
version = "0.11.9"
|
||||||
|
|
@ -1870,6 +1919,8 @@ checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
"cookie",
|
||||||
|
"cookie_store",
|
||||||
"encoding_rs",
|
"encoding_rs",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ tower-cookies = "0.10"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
portpicker = "0.1"
|
portpicker = "0.1"
|
||||||
reqwest = { version = "0.12", features = ["blocking"] }
|
reqwest = { version = "0.12", features = ["blocking", "cookies"] }
|
||||||
tempfile = "3.8"
|
tempfile = "3.8"
|
||||||
wiremock = "0.6"
|
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);
|
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]
|
#[tokio::test]
|
||||||
async fn test_read_endpoints_dont_require_authentication() {
|
async fn test_read_endpoints_dont_require_authentication() {
|
||||||
let app = spawn_app_with_auth().await;
|
let app = spawn_app_with_auth().await;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue