test(auth): update tests for passkey migration
- Replace password-based token creation with direct DB inserts in CLI helpers - Use INSERT OR IGNORE for concurrent test thread safety on shared user - Update server test helpers with passkey/registration_token repos and WebAuthn state - Remove password-based auth tests, keep bearer token validation tests - Set BREWLOG_RP_ID, RP_ORIGIN, OPENROUTER_API_KEY, FOURSQUARE_API_KEY in test env - Use localhost instead of 127.0.0.1 for WebAuthn RP ID compatibility
This commit is contained in:
parent
03e03d87d9
commit
4bc64dd12f
3 changed files with 154 additions and 246 deletions
|
|
@ -8,11 +8,9 @@ use tempfile::TempDir;
|
||||||
/// Fields prefixed with `_` are kept alive to prevent cleanup:
|
/// Fields prefixed with `_` are kept alive to prevent cleanup:
|
||||||
/// - `_temp_dir`: keeps temporary database file from being deleted
|
/// - `_temp_dir`: keeps temporary database file from being deleted
|
||||||
/// - `_process`: keeps server process running
|
/// - `_process`: keeps server process running
|
||||||
/// - `_db_url`: retained for consistency
|
|
||||||
struct SharedServer {
|
struct SharedServer {
|
||||||
address: String,
|
address: String,
|
||||||
admin_password: String,
|
db_url: String,
|
||||||
_db_url: String,
|
|
||||||
_temp_dir: TempDir,
|
_temp_dir: TempDir,
|
||||||
_process: std::process::Child,
|
_process: std::process::Child,
|
||||||
}
|
}
|
||||||
|
|
@ -40,11 +38,10 @@ fn ensure_server_started() -> Result<(String, String), String> {
|
||||||
let temp_dir = TempDir::new().map_err(|e| format!("Failed to create temp dir: {}", e))?;
|
let temp_dir = TempDir::new().map_err(|e| format!("Failed to create temp dir: {}", e))?;
|
||||||
let db_path = temp_dir.path().join("test.db");
|
let db_path = temp_dir.path().join("test.db");
|
||||||
let db_url = format!("sqlite:{}", db_path.display());
|
let db_url = format!("sqlite:{}", db_path.display());
|
||||||
let admin_password = "test_admin_password";
|
|
||||||
|
|
||||||
// Start server on a random port
|
// Start server on a random port
|
||||||
let port = portpicker::pick_unused_port().ok_or("No ports available")?;
|
let port = portpicker::pick_unused_port().ok_or("No ports available")?;
|
||||||
let address = format!("http://127.0.0.1:{}", port);
|
let address = format!("http://localhost:{}", port);
|
||||||
|
|
||||||
eprintln!("Starting server on {}", address);
|
eprintln!("Starting server on {}", address);
|
||||||
|
|
||||||
|
|
@ -57,8 +54,10 @@ fn ensure_server_started() -> Result<(String, String), String> {
|
||||||
"--database-url",
|
"--database-url",
|
||||||
&db_url,
|
&db_url,
|
||||||
])
|
])
|
||||||
.env("BREWLOG_ADMIN_PASSWORD", admin_password)
|
.env("BREWLOG_RP_ID", "localhost")
|
||||||
.env("BREWLOG_ADMIN_USERNAME", "admin")
|
.env("BREWLOG_RP_ORIGIN", &address)
|
||||||
|
.env("BREWLOG_OPENROUTER_API_KEY", "test-key")
|
||||||
|
.env("BREWLOG_FOURSQUARE_API_KEY", "test-key")
|
||||||
.env("RUST_LOG", "error")
|
.env("RUST_LOG", "error")
|
||||||
.stdout(Stdio::null())
|
.stdout(Stdio::null())
|
||||||
.stderr(Stdio::null())
|
.stderr(Stdio::null())
|
||||||
|
|
@ -97,55 +96,70 @@ fn ensure_server_started() -> Result<(String, String), String> {
|
||||||
|
|
||||||
*server = Some(SharedServer {
|
*server = Some(SharedServer {
|
||||||
address: address.clone(),
|
address: address.clone(),
|
||||||
admin_password: admin_password.to_string(),
|
db_url: db_url.clone(),
|
||||||
_db_url: db_url,
|
|
||||||
_temp_dir: temp_dir,
|
_temp_dir: temp_dir,
|
||||||
_process: process,
|
_process: process,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let srv = server.as_ref().unwrap();
|
let srv = server.as_ref().unwrap();
|
||||||
Ok((srv.address.clone(), srv.admin_password.clone()))
|
Ok((srv.address.clone(), srv.db_url.clone()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the shared server address and admin password
|
/// Get the shared server address and database URL
|
||||||
pub fn server_info() -> (String, String) {
|
pub fn server_info() -> (String, String) {
|
||||||
ensure_server_started().expect("Failed to start test server")
|
ensure_server_started().expect("Failed to start test server")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a token for testing using the CLI
|
/// Create a token for testing by directly inserting into the database
|
||||||
pub fn create_token(name: &str) -> String {
|
pub fn create_token(name: &str) -> String {
|
||||||
let (_, password) = ensure_server_started().expect("Failed to start test server");
|
let (_, db_url) = ensure_server_started().expect("Failed to start test server");
|
||||||
|
|
||||||
let output = run_brewlog(
|
let rt = tokio::runtime::Builder::new_current_thread()
|
||||||
&[
|
.enable_all()
|
||||||
"token",
|
.build()
|
||||||
"create",
|
.expect("Failed to create tokio runtime");
|
||||||
"--name",
|
|
||||||
name,
|
|
||||||
"--username",
|
|
||||||
"admin",
|
|
||||||
"--password",
|
|
||||||
&password,
|
|
||||||
],
|
|
||||||
&[],
|
|
||||||
);
|
|
||||||
|
|
||||||
if !output.status.success() {
|
rt.block_on(async {
|
||||||
panic!(
|
let pool = sqlx::SqlitePool::connect(&db_url)
|
||||||
"Failed to create token: {}",
|
.await
|
||||||
String::from_utf8_lossy(&output.stderr)
|
.expect("Failed to connect to test database");
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
// Ensure a user exists (INSERT OR IGNORE to handle concurrent test threads)
|
||||||
for line in stdout.lines() {
|
let uuid = uuid::Uuid::new_v4().to_string();
|
||||||
if let Some(token) = line.trim().strip_prefix("export BREWLOG_TOKEN=") {
|
sqlx::query(
|
||||||
return token.to_string();
|
"INSERT OR IGNORE INTO users (username, uuid, created_at) VALUES (?, ?, datetime('now'))",
|
||||||
}
|
)
|
||||||
}
|
.bind("admin")
|
||||||
|
.bind(&uuid)
|
||||||
|
.execute(&pool)
|
||||||
|
.await
|
||||||
|
.expect("Failed to ensure test user");
|
||||||
|
|
||||||
panic!("Could not find token in output: {}", stdout);
|
let user_id: i64 =
|
||||||
|
sqlx::query_scalar::<_, i64>("SELECT id FROM users WHERE username = ?")
|
||||||
|
.bind("admin")
|
||||||
|
.fetch_one(&pool)
|
||||||
|
.await
|
||||||
|
.expect("Failed to query test user");
|
||||||
|
|
||||||
|
// Generate and insert a bearer token
|
||||||
|
let token_value =
|
||||||
|
brewlog::infrastructure::auth::generate_token().expect("Failed to generate token");
|
||||||
|
let token_hash = brewlog::infrastructure::auth::hash_token(&token_value);
|
||||||
|
|
||||||
|
sqlx::query(
|
||||||
|
"INSERT INTO tokens (user_id, token_hash, name, created_at) VALUES (?, ?, ?, datetime('now'))",
|
||||||
|
)
|
||||||
|
.bind(user_id)
|
||||||
|
.bind(&token_hash)
|
||||||
|
.bind(name)
|
||||||
|
.execute(&pool)
|
||||||
|
.await
|
||||||
|
.expect("Failed to insert token");
|
||||||
|
|
||||||
|
token_value
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run a brewlog CLI command and return the output
|
/// Run a brewlog CLI command and return the output
|
||||||
|
|
|
||||||
|
|
@ -4,18 +4,32 @@ use serde_json::json;
|
||||||
use crate::helpers::spawn_app_with_auth;
|
use crate::helpers::spawn_app_with_auth;
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_create_token_with_valid_credentials() {
|
async fn test_create_token_requires_authentication() {
|
||||||
let app = spawn_app_with_auth().await;
|
let app = spawn_app_with_auth().await;
|
||||||
let client = Client::new();
|
let client = Client::new();
|
||||||
|
|
||||||
// Create a token
|
// Token creation now requires session or bearer token auth
|
||||||
let response = client
|
let response = client
|
||||||
.post(&app.api_url("/tokens"))
|
.post(&app.api_url("/tokens"))
|
||||||
.json(&json!({
|
.json(&json!({ "name": "test-token" }))
|
||||||
"username": "admin",
|
.send()
|
||||||
"password": "test_password",
|
.await
|
||||||
"name": "test-token"
|
.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()
|
.send()
|
||||||
.await
|
.await
|
||||||
.expect("Failed to send request");
|
.expect("Failed to send request");
|
||||||
|
|
@ -24,7 +38,7 @@ async fn test_create_token_with_valid_credentials() {
|
||||||
|
|
||||||
let body: serde_json::Value = response.json().await.expect("Failed to parse response");
|
let body: serde_json::Value = response.json().await.expect("Failed to parse response");
|
||||||
assert!(body.get("id").is_some());
|
assert!(body.get("id").is_some());
|
||||||
assert_eq!(body.get("name").unwrap(), "test-token");
|
assert_eq!(body.get("name").unwrap(), "new-token");
|
||||||
assert!(body.get("token").is_some());
|
assert!(body.get("token").is_some());
|
||||||
|
|
||||||
// Token should be a non-empty string
|
// Token should be a non-empty string
|
||||||
|
|
@ -32,26 +46,6 @@ async fn test_create_token_with_valid_credentials() {
|
||||||
assert!(!token.is_empty());
|
assert!(!token.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_create_token_with_invalid_credentials() {
|
|
||||||
let app = spawn_app_with_auth().await;
|
|
||||||
let client = Client::new();
|
|
||||||
|
|
||||||
// Try to create a token with wrong password
|
|
||||||
let response = client
|
|
||||||
.post(&app.api_url("/tokens"))
|
|
||||||
.json(&json!({
|
|
||||||
"username": "admin",
|
|
||||||
"password": "wrong_password",
|
|
||||||
"name": "test-token"
|
|
||||||
}))
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
.expect("Failed to send request");
|
|
||||||
|
|
||||||
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_list_tokens_requires_authentication() {
|
async fn test_list_tokens_requires_authentication() {
|
||||||
let app = spawn_app_with_auth().await;
|
let app = spawn_app_with_auth().await;
|
||||||
|
|
@ -71,29 +65,12 @@ async fn test_list_tokens_requires_authentication() {
|
||||||
async fn test_list_tokens_with_authentication() {
|
async fn test_list_tokens_with_authentication() {
|
||||||
let app = spawn_app_with_auth().await;
|
let app = spawn_app_with_auth().await;
|
||||||
let client = Client::new();
|
let client = Client::new();
|
||||||
|
let auth_token = app.auth_token.as_ref().unwrap();
|
||||||
|
|
||||||
// First, create a token
|
// List tokens with authentication
|
||||||
let create_response = client
|
|
||||||
.post(&app.api_url("/tokens"))
|
|
||||||
.json(&json!({
|
|
||||||
"username": "admin",
|
|
||||||
"password": "test_password",
|
|
||||||
"name": "test-token"
|
|
||||||
}))
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
.expect("Failed to send request");
|
|
||||||
|
|
||||||
let create_body: serde_json::Value = create_response
|
|
||||||
.json()
|
|
||||||
.await
|
|
||||||
.expect("Failed to parse response");
|
|
||||||
let token = create_body.get("token").unwrap().as_str().unwrap();
|
|
||||||
|
|
||||||
// Now list tokens with authentication
|
|
||||||
let response = client
|
let response = client
|
||||||
.get(&app.api_url("/tokens"))
|
.get(&app.api_url("/tokens"))
|
||||||
.header("Authorization", format!("Bearer {}", token))
|
.bearer_auth(auth_token)
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.expect("Failed to send request");
|
.expect("Failed to send request");
|
||||||
|
|
@ -101,9 +78,11 @@ async fn test_list_tokens_with_authentication() {
|
||||||
assert_eq!(response.status(), StatusCode::OK);
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
|
||||||
let tokens: Vec<serde_json::Value> = response.json().await.expect("Failed to parse response");
|
let tokens: Vec<serde_json::Value> = response.json().await.expect("Failed to parse response");
|
||||||
// We expect 2 tokens: the one created by spawn_app_with_auth() and the one we just created
|
// We expect at least 1 token (the one created by spawn_app_with_auth)
|
||||||
assert_eq!(tokens.len(), 2);
|
assert!(
|
||||||
// Find the token we created
|
!tokens.is_empty(),
|
||||||
|
"Should have at least one token from test setup"
|
||||||
|
);
|
||||||
let test_token = tokens
|
let test_token = tokens
|
||||||
.iter()
|
.iter()
|
||||||
.find(|t| t.get("name").unwrap() == "test-token")
|
.find(|t| t.get("name").unwrap() == "test-token")
|
||||||
|
|
@ -115,15 +94,13 @@ async fn test_list_tokens_with_authentication() {
|
||||||
async fn test_revoke_token() {
|
async fn test_revoke_token() {
|
||||||
let app = spawn_app_with_auth().await;
|
let app = spawn_app_with_auth().await;
|
||||||
let client = Client::new();
|
let client = Client::new();
|
||||||
|
let auth_token = app.auth_token.as_ref().unwrap();
|
||||||
|
|
||||||
// First, create a token
|
// Create a new token to revoke
|
||||||
let create_response = client
|
let create_response = client
|
||||||
.post(&app.api_url("/tokens"))
|
.post(&app.api_url("/tokens"))
|
||||||
.json(&json!({
|
.bearer_auth(auth_token)
|
||||||
"username": "admin",
|
.json(&json!({ "name": "token-to-revoke" }))
|
||||||
"password": "test_password",
|
|
||||||
"name": "test-token"
|
|
||||||
}))
|
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.expect("Failed to send request");
|
.expect("Failed to send request");
|
||||||
|
|
@ -132,13 +109,12 @@ async fn test_revoke_token() {
|
||||||
.json()
|
.json()
|
||||||
.await
|
.await
|
||||||
.expect("Failed to parse response");
|
.expect("Failed to parse response");
|
||||||
let token = create_body.get("token").unwrap().as_str().unwrap();
|
|
||||||
let token_id = create_body.get("id").unwrap().as_i64().unwrap();
|
let token_id = create_body.get("id").unwrap().as_i64().unwrap();
|
||||||
|
|
||||||
// Revoke the token
|
// Revoke the token
|
||||||
let response = client
|
let response = client
|
||||||
.post(&app.api_url(&format!("/tokens/{}/revoke", token_id)))
|
.post(&app.api_url(&format!("/tokens/{}/revoke", token_id)))
|
||||||
.header("Authorization", format!("Bearer {}", token))
|
.bearer_auth(auth_token)
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.expect("Failed to send request");
|
.expect("Failed to send request");
|
||||||
|
|
@ -153,15 +129,13 @@ async fn test_revoke_token() {
|
||||||
async fn test_revoked_token_cannot_be_used() {
|
async fn test_revoked_token_cannot_be_used() {
|
||||||
let app = spawn_app_with_auth().await;
|
let app = spawn_app_with_auth().await;
|
||||||
let client = Client::new();
|
let client = Client::new();
|
||||||
|
let auth_token = app.auth_token.as_ref().unwrap();
|
||||||
|
|
||||||
// Create a token
|
// Create a new token
|
||||||
let create_response = client
|
let create_response = client
|
||||||
.post(&app.api_url("/tokens"))
|
.post(&app.api_url("/tokens"))
|
||||||
.json(&json!({
|
.bearer_auth(auth_token)
|
||||||
"username": "admin",
|
.json(&json!({ "name": "will-be-revoked" }))
|
||||||
"password": "test_password",
|
|
||||||
"name": "test-token"
|
|
||||||
}))
|
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.expect("Failed to send request");
|
.expect("Failed to send request");
|
||||||
|
|
@ -170,13 +144,13 @@ async fn test_revoked_token_cannot_be_used() {
|
||||||
.json()
|
.json()
|
||||||
.await
|
.await
|
||||||
.expect("Failed to parse response");
|
.expect("Failed to parse response");
|
||||||
let token = create_body.get("token").unwrap().as_str().unwrap();
|
let new_token = create_body.get("token").unwrap().as_str().unwrap();
|
||||||
let token_id = create_body.get("id").unwrap().as_i64().unwrap();
|
let token_id = create_body.get("id").unwrap().as_i64().unwrap();
|
||||||
|
|
||||||
// Revoke the token
|
// Revoke it using the original auth token
|
||||||
client
|
client
|
||||||
.post(&app.api_url(&format!("/tokens/{}/revoke", token_id)))
|
.post(&app.api_url(&format!("/tokens/{}/revoke", token_id)))
|
||||||
.header("Authorization", format!("Bearer {}", token))
|
.bearer_auth(auth_token)
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.expect("Failed to send request");
|
.expect("Failed to send request");
|
||||||
|
|
@ -184,7 +158,7 @@ async fn test_revoked_token_cannot_be_used() {
|
||||||
// Try to use the revoked token
|
// Try to use the revoked token
|
||||||
let response = client
|
let response = client
|
||||||
.get(&app.api_url("/tokens"))
|
.get(&app.api_url("/tokens"))
|
||||||
.header("Authorization", format!("Bearer {}", token))
|
.header("Authorization", format!("Bearer {}", new_token))
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.expect("Failed to send request");
|
.expect("Failed to send request");
|
||||||
|
|
@ -215,29 +189,12 @@ async fn test_protected_endpoints_require_authentication() {
|
||||||
async fn test_protected_endpoints_work_with_authentication() {
|
async fn test_protected_endpoints_work_with_authentication() {
|
||||||
let app = spawn_app_with_auth().await;
|
let app = spawn_app_with_auth().await;
|
||||||
let client = Client::new();
|
let client = Client::new();
|
||||||
|
let auth_token = app.auth_token.as_ref().unwrap();
|
||||||
// Create a token
|
|
||||||
let create_response = client
|
|
||||||
.post(&app.api_url("/tokens"))
|
|
||||||
.json(&json!({
|
|
||||||
"username": "admin",
|
|
||||||
"password": "test_password",
|
|
||||||
"name": "test-token"
|
|
||||||
}))
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
.expect("Failed to send request");
|
|
||||||
|
|
||||||
let create_body: serde_json::Value = create_response
|
|
||||||
.json()
|
|
||||||
.await
|
|
||||||
.expect("Failed to parse response");
|
|
||||||
let token = create_body.get("token").unwrap().as_str().unwrap();
|
|
||||||
|
|
||||||
// Create a roaster with authentication
|
// Create a roaster with authentication
|
||||||
let response = client
|
let response = client
|
||||||
.post(&app.api_url("/roasters"))
|
.post(&app.api_url("/roasters"))
|
||||||
.header("Authorization", format!("Bearer {}", token))
|
.bearer_auth(auth_token)
|
||||||
.json(&json!({
|
.json(&json!({
|
||||||
"name": "Test Roaster",
|
"name": "Test Roaster",
|
||||||
"country": "UK"
|
"country": "UK"
|
||||||
|
|
@ -249,45 +206,6 @@ 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]
|
#[tokio::test]
|
||||||
async fn test_invalid_session_cookie_fails() {
|
async fn test_invalid_session_cookie_fails() {
|
||||||
let app = spawn_app_with_auth().await;
|
let app = spawn_app_with_auth().await;
|
||||||
|
|
@ -314,64 +232,6 @@ async fn test_invalid_session_cookie_fails() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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]
|
#[tokio::test]
|
||||||
async fn test_fake_session_cookie_fails() {
|
async fn test_fake_session_cookie_fails() {
|
||||||
let app = spawn_app_with_auth().await;
|
let app = spawn_app_with_auth().await;
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,11 @@ use brewlog::application::routes::app_router;
|
||||||
use brewlog::application::server::AppState;
|
use brewlog::application::server::AppState;
|
||||||
use brewlog::domain::cafes::{Cafe, NewCafe};
|
use brewlog::domain::cafes::{Cafe, NewCafe};
|
||||||
use brewlog::domain::repositories::{
|
use brewlog::domain::repositories::{
|
||||||
CafeRepository, RoastRepository, RoasterRepository, SessionRepository, TimelineEventRepository,
|
CafeRepository, PasskeyCredentialRepository, RegistrationTokenRepository, RoastRepository,
|
||||||
TokenRepository, UserRepository,
|
RoasterRepository, SessionRepository, TimelineEventRepository, TokenRepository, UserRepository,
|
||||||
};
|
};
|
||||||
use brewlog::domain::roasters::{NewRoaster, Roaster};
|
use brewlog::domain::roasters::{NewRoaster, Roaster};
|
||||||
use brewlog::domain::users::NewUser;
|
use brewlog::domain::users::NewUser;
|
||||||
use brewlog::infrastructure::auth::hash_password;
|
|
||||||
use brewlog::infrastructure::backup::BackupService;
|
use brewlog::infrastructure::backup::BackupService;
|
||||||
use brewlog::infrastructure::database::Database;
|
use brewlog::infrastructure::database::Database;
|
||||||
use brewlog::infrastructure::repositories::bags::SqlBagRepository;
|
use brewlog::infrastructure::repositories::bags::SqlBagRepository;
|
||||||
|
|
@ -17,15 +16,19 @@ use brewlog::infrastructure::repositories::brews::SqlBrewRepository;
|
||||||
use brewlog::infrastructure::repositories::cafes::SqlCafeRepository;
|
use brewlog::infrastructure::repositories::cafes::SqlCafeRepository;
|
||||||
use brewlog::infrastructure::repositories::cups::SqlCupRepository;
|
use brewlog::infrastructure::repositories::cups::SqlCupRepository;
|
||||||
use brewlog::infrastructure::repositories::gear::SqlGearRepository;
|
use brewlog::infrastructure::repositories::gear::SqlGearRepository;
|
||||||
|
use brewlog::infrastructure::repositories::passkey_credentials::SqlPasskeyCredentialRepository;
|
||||||
|
use brewlog::infrastructure::repositories::registration_tokens::SqlRegistrationTokenRepository;
|
||||||
use brewlog::infrastructure::repositories::roasters::SqlRoasterRepository;
|
use brewlog::infrastructure::repositories::roasters::SqlRoasterRepository;
|
||||||
use brewlog::infrastructure::repositories::roasts::SqlRoastRepository;
|
use brewlog::infrastructure::repositories::roasts::SqlRoastRepository;
|
||||||
use brewlog::infrastructure::repositories::sessions::SqlSessionRepository;
|
use brewlog::infrastructure::repositories::sessions::SqlSessionRepository;
|
||||||
use brewlog::infrastructure::repositories::timeline_events::SqlTimelineEventRepository;
|
use brewlog::infrastructure::repositories::timeline_events::SqlTimelineEventRepository;
|
||||||
use brewlog::infrastructure::repositories::tokens::SqlTokenRepository;
|
use brewlog::infrastructure::repositories::tokens::SqlTokenRepository;
|
||||||
use brewlog::infrastructure::repositories::users::SqlUserRepository;
|
use brewlog::infrastructure::repositories::users::SqlUserRepository;
|
||||||
|
use brewlog::infrastructure::webauthn::ChallengeStore;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
use tokio::task::AbortHandle;
|
use tokio::task::AbortHandle;
|
||||||
|
use webauthn_rs::prelude::*;
|
||||||
|
|
||||||
pub struct TestApp {
|
pub struct TestApp {
|
||||||
pub address: String,
|
pub address: String,
|
||||||
|
|
@ -57,6 +60,19 @@ impl Drop for TestApp {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn test_webauthn() -> Arc<Webauthn> {
|
||||||
|
#[allow(clippy::expect_used)]
|
||||||
|
let rp_origin = url::Url::parse("http://localhost:0").expect("valid URL");
|
||||||
|
#[allow(clippy::expect_used)]
|
||||||
|
Arc::new(
|
||||||
|
WebauthnBuilder::new("localhost", &rp_origin)
|
||||||
|
.expect("valid RP config")
|
||||||
|
.rp_name("Brewlog Test")
|
||||||
|
.build()
|
||||||
|
.expect("valid WebAuthn"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn spawn_app() -> TestApp {
|
pub async fn spawn_app() -> TestApp {
|
||||||
// Use in-memory SQLite database for testing
|
// Use in-memory SQLite database for testing
|
||||||
let database = Database::connect("sqlite::memory:")
|
let database = Database::connect("sqlite::memory:")
|
||||||
|
|
@ -84,6 +100,10 @@ pub async fn spawn_app() -> TestApp {
|
||||||
Arc::new(SqlTokenRepository::new(database.clone_pool()));
|
Arc::new(SqlTokenRepository::new(database.clone_pool()));
|
||||||
let session_repo: Arc<dyn SessionRepository> =
|
let session_repo: Arc<dyn SessionRepository> =
|
||||||
Arc::new(SqlSessionRepository::new(database.clone_pool()));
|
Arc::new(SqlSessionRepository::new(database.clone_pool()));
|
||||||
|
let passkey_repo: Arc<dyn PasskeyCredentialRepository> =
|
||||||
|
Arc::new(SqlPasskeyCredentialRepository::new(database.clone_pool()));
|
||||||
|
let registration_token_repo: Arc<dyn RegistrationTokenRepository> =
|
||||||
|
Arc::new(SqlRegistrationTokenRepository::new(database.clone_pool()));
|
||||||
|
|
||||||
spawn_app_inner(
|
spawn_app_inner(
|
||||||
database,
|
database,
|
||||||
|
|
@ -98,6 +118,8 @@ pub async fn spawn_app() -> TestApp {
|
||||||
user_repo,
|
user_repo,
|
||||||
token_repo,
|
token_repo,
|
||||||
session_repo,
|
session_repo,
|
||||||
|
passkey_repo,
|
||||||
|
registration_token_repo,
|
||||||
brewlog::infrastructure::foursquare::FOURSQUARE_SEARCH_URL.to_string(),
|
brewlog::infrastructure::foursquare::FOURSQUARE_SEARCH_URL.to_string(),
|
||||||
String::new(),
|
String::new(),
|
||||||
None,
|
None,
|
||||||
|
|
@ -119,6 +141,8 @@ async fn spawn_app_inner(
|
||||||
user_repo: Arc<dyn UserRepository>,
|
user_repo: Arc<dyn UserRepository>,
|
||||||
token_repo: Arc<dyn TokenRepository>,
|
token_repo: Arc<dyn TokenRepository>,
|
||||||
session_repo: Arc<dyn SessionRepository>,
|
session_repo: Arc<dyn SessionRepository>,
|
||||||
|
passkey_repo: Arc<dyn PasskeyCredentialRepository>,
|
||||||
|
registration_token_repo: Arc<dyn RegistrationTokenRepository>,
|
||||||
foursquare_url: String,
|
foursquare_url: String,
|
||||||
foursquare_api_key: String,
|
foursquare_api_key: String,
|
||||||
mock_server: Option<wiremock::MockServer>,
|
mock_server: Option<wiremock::MockServer>,
|
||||||
|
|
@ -126,25 +150,29 @@ async fn spawn_app_inner(
|
||||||
let backup_service = Arc::new(BackupService::new(_database.clone_pool()));
|
let backup_service = Arc::new(BackupService::new(_database.clone_pool()));
|
||||||
|
|
||||||
// Create application state
|
// Create application state
|
||||||
let state = AppState::new(
|
let state = AppState {
|
||||||
roaster_repo.clone(),
|
roaster_repo: roaster_repo.clone(),
|
||||||
roast_repo.clone(),
|
roast_repo: roast_repo.clone(),
|
||||||
bag_repo.clone(),
|
bag_repo: bag_repo.clone(),
|
||||||
gear_repo.clone(),
|
gear_repo: gear_repo.clone(),
|
||||||
brew_repo.clone(),
|
brew_repo: brew_repo.clone(),
|
||||||
cafe_repo.clone(),
|
cafe_repo: cafe_repo.clone(),
|
||||||
cup_repo.clone(),
|
cup_repo: cup_repo.clone(),
|
||||||
timeline_repo.clone(),
|
timeline_repo: timeline_repo.clone(),
|
||||||
user_repo.clone(),
|
user_repo: user_repo.clone(),
|
||||||
token_repo.clone(),
|
token_repo: token_repo.clone(),
|
||||||
session_repo,
|
session_repo,
|
||||||
reqwest::Client::new(),
|
passkey_repo,
|
||||||
|
registration_token_repo,
|
||||||
|
webauthn: test_webauthn(),
|
||||||
|
challenge_store: Arc::new(ChallengeStore::new()),
|
||||||
|
http_client: reqwest::Client::new(),
|
||||||
foursquare_url,
|
foursquare_url,
|
||||||
foursquare_api_key,
|
foursquare_api_key,
|
||||||
String::new(),
|
openrouter_api_key: String::new(),
|
||||||
"openrouter/free".to_string(),
|
openrouter_model: "openrouter/free".to_string(),
|
||||||
backup_service,
|
backup_service,
|
||||||
);
|
};
|
||||||
|
|
||||||
// Create router
|
// Create router
|
||||||
let app = app_router(state);
|
let app = app_router(state);
|
||||||
|
|
@ -211,6 +239,10 @@ pub async fn spawn_app_with_foursquare_mock() -> TestApp {
|
||||||
Arc::new(SqlTokenRepository::new(database.clone_pool()));
|
Arc::new(SqlTokenRepository::new(database.clone_pool()));
|
||||||
let session_repo: Arc<dyn SessionRepository> =
|
let session_repo: Arc<dyn SessionRepository> =
|
||||||
Arc::new(SqlSessionRepository::new(database.clone_pool()));
|
Arc::new(SqlSessionRepository::new(database.clone_pool()));
|
||||||
|
let passkey_repo: Arc<dyn PasskeyCredentialRepository> =
|
||||||
|
Arc::new(SqlPasskeyCredentialRepository::new(database.clone_pool()));
|
||||||
|
let registration_token_repo: Arc<dyn RegistrationTokenRepository> =
|
||||||
|
Arc::new(SqlRegistrationTokenRepository::new(database.clone_pool()));
|
||||||
|
|
||||||
let app = spawn_app_inner(
|
let app = spawn_app_inner(
|
||||||
database,
|
database,
|
||||||
|
|
@ -225,6 +257,8 @@ pub async fn spawn_app_with_foursquare_mock() -> TestApp {
|
||||||
user_repo,
|
user_repo,
|
||||||
token_repo,
|
token_repo,
|
||||||
session_repo,
|
session_repo,
|
||||||
|
passkey_repo,
|
||||||
|
registration_token_repo,
|
||||||
foursquare_url,
|
foursquare_url,
|
||||||
"test-api-key".to_string(),
|
"test-api-key".to_string(),
|
||||||
Some(mock_server),
|
Some(mock_server),
|
||||||
|
|
@ -235,9 +269,9 @@ pub async fn spawn_app_with_foursquare_mock() -> TestApp {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn add_auth_to_app(mut app: TestApp) -> TestApp {
|
async fn add_auth_to_app(mut app: TestApp) -> TestApp {
|
||||||
// Create admin user with known password
|
// Create user with UUID (no password)
|
||||||
let password_hash = hash_password("test_password").expect("Failed to hash password");
|
let user_uuid = uuid::Uuid::new_v4().to_string();
|
||||||
let admin_user = NewUser::new("admin".to_string(), password_hash);
|
let admin_user = NewUser::new("admin".to_string(), user_uuid);
|
||||||
|
|
||||||
let admin_user = app
|
let admin_user = app
|
||||||
.user_repo
|
.user_repo
|
||||||
|
|
@ -247,7 +281,7 @@ async fn add_auth_to_app(mut app: TestApp) -> TestApp {
|
||||||
.await
|
.await
|
||||||
.expect("Failed to create admin user");
|
.expect("Failed to create admin user");
|
||||||
|
|
||||||
// Create a token for testing
|
// Create a token for testing via direct DB insert
|
||||||
use brewlog::domain::tokens::NewToken;
|
use brewlog::domain::tokens::NewToken;
|
||||||
use brewlog::infrastructure::auth::{generate_token, hash_token};
|
use brewlog::infrastructure::auth::{generate_token, hash_token};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue