brewlog/tests/server/auth_api.rs
Jon Seager 590868e51f
test: add integration tests for webauthn, passkeys, and input validation
Add webauthn API tests (register, auth, passkey add flows), passkey
endpoint auth tests, bag validation (zero/negative amount, invalid date),
brew validation (zero weight, negative grind, zero water, temp > 100),
and fix flaky date assertion in bag close test.
2026-02-13 13:06:27 +00:00

338 lines
9.5 KiB
Rust

use reqwest::{Client, StatusCode};
use serde_json::json;
use crate::helpers::{spawn_app, spawn_app_with_auth};
#[tokio::test]
async fn test_create_token_requires_authentication() {
let app = spawn_app_with_auth().await;
let client = Client::new();
// Token creation now requires session or bearer token auth
let response = client
.post(&app.api_url("/tokens"))
.json(&json!({ "name": "test-token" }))
.send()
.await
.expect("Failed to send request");
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn test_create_token_with_bearer_auth() {
let app = spawn_app_with_auth().await;
let client = Client::new();
let auth_token = app.auth_token.as_ref().unwrap();
// Create a token using bearer auth
let response = client
.post(&app.api_url("/tokens"))
.bearer_auth(auth_token)
.json(&json!({ "name": "new-token" }))
.send()
.await
.expect("Failed to send request");
assert_eq!(response.status(), StatusCode::OK);
let body: serde_json::Value = response.json().await.expect("Failed to parse response");
assert!(body.get("id").is_some());
assert_eq!(body.get("name").unwrap(), "new-token");
assert!(body.get("token").is_some());
// Token should be a non-empty string
let token = body.get("token").unwrap().as_str().unwrap();
assert!(!token.is_empty());
}
#[tokio::test]
async fn test_list_tokens_requires_authentication() {
let app = spawn_app_with_auth().await;
let client = Client::new();
// Try to list tokens without authentication
let response = client
.get(&app.api_url("/tokens"))
.send()
.await
.expect("Failed to send request");
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn test_list_tokens_with_authentication() {
let app = spawn_app_with_auth().await;
let client = Client::new();
let auth_token = app.auth_token.as_ref().unwrap();
// List tokens with authentication
let response = client
.get(&app.api_url("/tokens"))
.bearer_auth(auth_token)
.send()
.await
.expect("Failed to send request");
assert_eq!(response.status(), StatusCode::OK);
let tokens: Vec<serde_json::Value> = response.json().await.expect("Failed to parse response");
// We expect at least 1 token (the one created by spawn_app_with_auth)
assert!(
!tokens.is_empty(),
"Should have at least one token from test setup"
);
let test_token = tokens
.iter()
.find(|t| t.get("name").unwrap() == "test-token")
.expect("Could not find test-token");
assert_eq!(test_token.get("name").unwrap(), "test-token");
}
#[tokio::test]
async fn test_revoke_token() {
let app = spawn_app_with_auth().await;
let client = Client::new();
let auth_token = app.auth_token.as_ref().unwrap();
// Create a new token to revoke
let create_response = client
.post(&app.api_url("/tokens"))
.bearer_auth(auth_token)
.json(&json!({ "name": "token-to-revoke" }))
.send()
.await
.expect("Failed to send request");
let create_body: serde_json::Value = create_response
.json()
.await
.expect("Failed to parse response");
let token_id = create_body.get("id").unwrap().as_i64().unwrap();
// Revoke the token
let response = client
.post(&app.api_url(&format!("/tokens/{}/revoke", token_id)))
.bearer_auth(auth_token)
.send()
.await
.expect("Failed to send request");
assert_eq!(response.status(), StatusCode::OK);
let body: serde_json::Value = response.json().await.expect("Failed to parse response");
assert!(body.get("revoked_at").is_some());
}
#[tokio::test]
async fn test_revoked_token_cannot_be_used() {
let app = spawn_app_with_auth().await;
let client = Client::new();
let auth_token = app.auth_token.as_ref().unwrap();
// Create a new token
let create_response = client
.post(&app.api_url("/tokens"))
.bearer_auth(auth_token)
.json(&json!({ "name": "will-be-revoked" }))
.send()
.await
.expect("Failed to send request");
let create_body: serde_json::Value = create_response
.json()
.await
.expect("Failed to parse response");
let new_token = create_body.get("token").unwrap().as_str().unwrap();
let token_id = create_body.get("id").unwrap().as_i64().unwrap();
// Revoke it using the original auth token
client
.post(&app.api_url(&format!("/tokens/{}/revoke", token_id)))
.bearer_auth(auth_token)
.send()
.await
.expect("Failed to send request");
// Try to use the revoked token
let response = client
.get(&app.api_url("/tokens"))
.header("Authorization", format!("Bearer {}", new_token))
.send()
.await
.expect("Failed to send request");
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn test_protected_endpoints_require_authentication() {
let app = spawn_app_with_auth().await;
let client = Client::new();
// Try to create a roaster without authentication
let response = client
.post(&app.api_url("/roasters"))
.json(&json!({
"name": "Test Roaster",
"country": "UK"
}))
.send()
.await
.expect("Failed to send request");
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn test_protected_endpoints_work_with_authentication() {
let app = spawn_app_with_auth().await;
let client = Client::new();
let auth_token = app.auth_token.as_ref().unwrap();
// Create a roaster with authentication
let response = client
.post(&app.api_url("/roasters"))
.bearer_auth(auth_token)
.json(&json!({
"name": "Test Roaster",
"country": "UK"
}))
.send()
.await
.expect("Failed to send request");
assert_eq!(response.status(), StatusCode::CREATED);
}
#[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_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;
let client = Client::new();
// List roasters without authentication should work
let response = client
.get(&app.api_url("/roasters"))
.send()
.await
.expect("Failed to send request");
assert_eq!(response.status(), StatusCode::OK);
}
// --- Admin passkey endpoint tests ---
#[tokio::test]
async fn list_passkeys_requires_auth() {
let app = spawn_app().await;
let client = Client::new();
let response = client
.get(&app.api_url("/passkeys"))
.send()
.await
.expect("Failed to send request");
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn list_passkeys_returns_empty_for_user_without_passkeys() {
let app = spawn_app_with_auth().await;
let client = Client::new();
let response = client
.get(&app.api_url("/passkeys"))
.bearer_auth(app.auth_token.as_ref().unwrap())
.send()
.await
.expect("Failed to send request");
assert_eq!(response.status(), StatusCode::OK);
let passkeys: Vec<serde_json::Value> = response.json().await.expect("Failed to parse response");
assert!(passkeys.is_empty(), "expected empty passkeys list");
}
#[tokio::test]
async fn delete_passkey_nonexistent_returns_404() {
let app = spawn_app_with_auth().await;
let client = Client::new();
let response = client
.delete(&app.api_url("/passkeys/99999"))
.bearer_auth(app.auth_token.as_ref().unwrap())
.send()
.await
.expect("Failed to send request");
assert_eq!(response.status(), StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn delete_passkey_requires_auth() {
let app = spawn_app().await;
let client = Client::new();
let response = client
.delete(&app.api_url("/passkeys/1"))
.send()
.await
.expect("Failed to send request");
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}