test(server): fix broken tests by adding authentication support

- Update all server tests to use spawn_app_with_auth() helper
- spawn_app_with_auth() creates admin user and test token automatically
- Add helper methods (post/put/delete/get) to TestApp for authenticated requests
- Update create_roaster_with_payload to include auth token when available
- Add bearer_auth() to all write operation test requests
- Fix auth_api test to expect 2 tokens (test helper + test-created)
- All 42 server tests now pass

Co-authored-by: jnsgruk <668505+jnsgruk@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2025-11-25 11:13:04 +00:00 committed by Jon Seager
parent 0e35d18a21
commit ac355b1b8e
No known key found for this signature in database
5 changed files with 145 additions and 42 deletions

View file

@ -101,8 +101,14 @@ async fn test_list_tokens_with_authentication() {
assert_eq!(response.status(), StatusCode::OK);
let tokens: Vec<serde_json::Value> = response.json().await.expect("Failed to parse response");
assert_eq!(tokens.len(), 1);
assert_eq!(tokens[0].get("name").unwrap(), "test-token");
// We expect 2 tokens: the one created by spawn_app_with_auth() and the one we just created
assert_eq!(tokens.len(), 2);
// Find the token we created
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]

View file

@ -28,12 +28,49 @@ pub struct TestApp {
pub user_repo: Option<Arc<dyn UserRepository>>,
#[allow(dead_code)]
pub token_repo: Option<Arc<dyn TokenRepository>>,
pub auth_token: Option<String>,
}
impl TestApp {
pub fn api_url(&self, path: &str) -> String {
format!("{}/api/v1{}", self.address, path)
}
/// Create an authenticated POST request
pub fn post(&self, path: &str) -> reqwest::RequestBuilder {
let client = Client::new();
let mut req = client.post(self.api_url(path));
if let Some(token) = &self.auth_token {
req = req.bearer_auth(token);
}
req
}
/// Create an authenticated PUT request
pub fn put(&self, path: &str) -> reqwest::RequestBuilder {
let client = Client::new();
let mut req = client.put(self.api_url(path));
if let Some(token) = &self.auth_token {
req = req.bearer_auth(token);
}
req
}
/// Create an authenticated DELETE request
pub fn delete(&self, path: &str) -> reqwest::RequestBuilder {
let client = Client::new();
let mut req = client.delete(self.api_url(path));
if let Some(token) = &self.auth_token {
req = req.bearer_auth(token);
}
req
}
/// Create a GET request (doesn't need auth for reads)
pub fn get(&self, path: &str) -> reqwest::RequestBuilder {
let client = Client::new();
client.get(self.api_url(path))
}
}
pub async fn spawn_app() -> TestApp {
@ -91,11 +128,12 @@ pub async fn spawn_app() -> TestApp {
timeline_repo,
user_repo: Some(user_repo),
token_repo: Some(token_repo),
auth_token: None,
}
}
pub async fn spawn_app_with_auth() -> TestApp {
let app = spawn_app().await;
let mut app = spawn_app().await;
// Create admin user with known password
let password_hash = hash_password("test_password").expect("Failed to hash password");
@ -113,14 +151,41 @@ pub async fn spawn_app_with_auth() -> TestApp {
.await
.expect("Failed to create admin user");
// Create a token for testing
use brewlog::domain::tokens::Token;
use brewlog::infrastructure::auth::{generate_token, hash_token};
let token_value = generate_token().expect("Failed to generate token");
let token_hash = hash_token(&token_value);
let token = Token::new(
"test_token_id".to_string(),
"test_admin_id".to_string(),
token_hash,
"test-token".to_string(),
Utc::now(),
);
app.token_repo
.as_ref()
.unwrap()
.insert(token)
.await
.expect("Failed to insert token");
app.auth_token = Some(token_value);
app
}
pub async fn create_roaster_with_payload(app: &TestApp, payload: NewRoaster) -> Roaster {
let client = Client::new();
let response = client
.post(app.api_url("/roasters"))
.json(&payload)
let mut request = client.post(app.api_url("/roasters")).json(&payload);
// Add auth token if available
if let Some(token) = &app.auth_token {
request = request.bearer_auth(token);
}
let response = request
.send()
.await
.expect("failed to create roaster via API");

View file

@ -1,10 +1,10 @@
use crate::helpers::spawn_app;
use crate::helpers::spawn_app_with_auth;
use brewlog::domain::roasters::{NewRoaster, Roaster, UpdateRoaster};
#[tokio::test]
async fn creating_a_roaster_returns_a_201_for_valid_data() {
// Arrange
let app = spawn_app().await;
let app = spawn_app_with_auth().await;
let client = reqwest::Client::new();
let new_roaster = NewRoaster {
@ -18,6 +18,7 @@ async fn creating_a_roaster_returns_a_201_for_valid_data() {
// Act
let response = client
.post(app.api_url("/roasters"))
.bearer_auth(app.auth_token.as_ref().unwrap())
.json(&new_roaster)
.send()
.await
@ -37,7 +38,7 @@ async fn creating_a_roaster_returns_a_201_for_valid_data() {
#[tokio::test]
async fn creating_a_roaster_persists_the_data() {
// Arrange
let app = spawn_app().await;
let app = spawn_app_with_auth().await;
let client = reqwest::Client::new();
let new_roaster = NewRoaster {
@ -51,6 +52,7 @@ async fn creating_a_roaster_persists_the_data() {
// Act
let response = client
.post(app.api_url("/roasters"))
.bearer_auth(app.auth_token.as_ref().unwrap())
.json(&new_roaster)
.send()
.await
@ -73,7 +75,7 @@ async fn creating_a_roaster_persists_the_data() {
#[tokio::test]
async fn getting_a_roaster_returns_a_200_for_valid_id() {
// Arrange
let app = spawn_app().await;
let app = spawn_app_with_auth().await;
let client = reqwest::Client::new();
let new_roaster = NewRoaster {
@ -86,6 +88,7 @@ async fn getting_a_roaster_returns_a_200_for_valid_id() {
let create_response = client
.post(app.api_url("/roasters"))
.bearer_auth(app.auth_token.as_ref().unwrap())
.json(&new_roaster)
.send()
.await
@ -114,7 +117,7 @@ async fn getting_a_roaster_returns_a_200_for_valid_id() {
#[tokio::test]
async fn getting_a_nonexistent_roaster_returns_a_404() {
// Arrange
let app = spawn_app().await;
let app = spawn_app_with_auth().await;
let client = reqwest::Client::new();
// Act
@ -131,7 +134,7 @@ async fn getting_a_nonexistent_roaster_returns_a_404() {
#[tokio::test]
async fn listing_roasters_returns_a_200_with_empty_list() {
// Arrange
let app = spawn_app().await;
let app = spawn_app_with_auth().await;
let client = reqwest::Client::new();
// Act
@ -151,7 +154,7 @@ async fn listing_roasters_returns_a_200_with_empty_list() {
#[tokio::test]
async fn listing_roasters_returns_a_200_with_multiple_roasters() {
// Arrange
let app = spawn_app().await;
let app = spawn_app_with_auth().await;
let client = reqwest::Client::new();
// Create multiple roasters
@ -173,6 +176,7 @@ async fn listing_roasters_returns_a_200_with_multiple_roasters() {
client
.post(app.api_url("/roasters"))
.bearer_auth(app.auth_token.as_ref().unwrap())
.json(&roaster1)
.send()
.await
@ -180,6 +184,7 @@ async fn listing_roasters_returns_a_200_with_multiple_roasters() {
client
.post(app.api_url("/roasters"))
.bearer_auth(app.auth_token.as_ref().unwrap())
.json(&roaster2)
.send()
.await
@ -202,7 +207,7 @@ async fn listing_roasters_returns_a_200_with_multiple_roasters() {
#[tokio::test]
async fn updating_a_roaster_returns_a_200_for_valid_data() {
// Arrange
let app = spawn_app().await;
let app = spawn_app_with_auth().await;
let client = reqwest::Client::new();
let new_roaster = NewRoaster {
@ -215,6 +220,7 @@ async fn updating_a_roaster_returns_a_200_for_valid_data() {
let create_response = client
.post(app.api_url("/roasters"))
.bearer_auth(app.auth_token.as_ref().unwrap())
.json(&new_roaster)
.send()
.await
@ -236,6 +242,7 @@ async fn updating_a_roaster_returns_a_200_for_valid_data() {
// Act
let response = client
.put(app.api_url(&format!("/roasters/{}", created_roaster.id)))
.bearer_auth(app.auth_token.as_ref().unwrap())
.json(&update)
.send()
.await
@ -257,7 +264,7 @@ async fn updating_a_roaster_returns_a_200_for_valid_data() {
#[tokio::test]
async fn updating_a_roaster_with_no_changes_returns_a_400() {
// Arrange
let app = spawn_app().await;
let app = spawn_app_with_auth().await;
let client = reqwest::Client::new();
let new_roaster = NewRoaster {
@ -270,6 +277,7 @@ async fn updating_a_roaster_with_no_changes_returns_a_400() {
let create_response = client
.post(app.api_url("/roasters"))
.bearer_auth(app.auth_token.as_ref().unwrap())
.json(&new_roaster)
.send()
.await
@ -291,6 +299,7 @@ async fn updating_a_roaster_with_no_changes_returns_a_400() {
// Act
let response = client
.put(app.api_url(&format!("/roasters/{}", created_roaster.id)))
.bearer_auth(app.auth_token.as_ref().unwrap())
.json(&update)
.send()
.await
@ -303,7 +312,7 @@ async fn updating_a_roaster_with_no_changes_returns_a_400() {
#[tokio::test]
async fn updating_a_nonexistent_roaster_returns_a_404() {
// Arrange
let app = spawn_app().await;
let app = spawn_app_with_auth().await;
let client = reqwest::Client::new();
let update = UpdateRoaster {
@ -317,6 +326,7 @@ async fn updating_a_nonexistent_roaster_returns_a_404() {
// Act
let response = client
.put(app.api_url("/roasters/nonexistent-id"))
.bearer_auth(app.auth_token.as_ref().unwrap())
.json(&update)
.send()
.await
@ -329,7 +339,7 @@ async fn updating_a_nonexistent_roaster_returns_a_404() {
#[tokio::test]
async fn deleting_a_roaster_returns_a_204_for_valid_id() {
// Arrange
let app = spawn_app().await;
let app = spawn_app_with_auth().await;
let client = reqwest::Client::new();
let new_roaster = NewRoaster {
@ -342,6 +352,7 @@ async fn deleting_a_roaster_returns_a_204_for_valid_id() {
let create_response = client
.post(app.api_url("/roasters"))
.bearer_auth(app.auth_token.as_ref().unwrap())
.json(&new_roaster)
.send()
.await
@ -355,6 +366,7 @@ async fn deleting_a_roaster_returns_a_204_for_valid_id() {
// Act
let response = client
.delete(app.api_url(&format!("/roasters/{}", created_roaster.id)))
.bearer_auth(app.auth_token.as_ref().unwrap())
.send()
.await
.expect("Failed to execute request");
@ -375,12 +387,13 @@ async fn deleting_a_roaster_returns_a_204_for_valid_id() {
#[tokio::test]
async fn deleting_a_nonexistent_roaster_returns_a_404() {
// Arrange
let app = spawn_app().await;
let app = spawn_app_with_auth().await;
let client = reqwest::Client::new();
// Act
let response = client
.delete(app.api_url("/roasters/nonexistent-id"))
.bearer_auth(app.auth_token.as_ref().unwrap())
.send()
.await
.expect("Failed to execute request");
@ -392,7 +405,7 @@ async fn deleting_a_nonexistent_roaster_returns_a_404() {
#[tokio::test]
async fn creating_a_roaster_with_empty_name_returns_a_201_after_normalization() {
// Arrange
let app = spawn_app().await;
let app = spawn_app_with_auth().await;
let client = reqwest::Client::new();
// The normalize function trims whitespace, so empty/whitespace names become empty
@ -407,6 +420,7 @@ async fn creating_a_roaster_with_empty_name_returns_a_201_after_normalization()
// Act
let response = client
.post(app.api_url("/roasters"))
.bearer_auth(app.auth_token.as_ref().unwrap())
.json(&new_roaster)
.send()
.await
@ -419,12 +433,13 @@ async fn creating_a_roaster_with_empty_name_returns_a_201_after_normalization()
#[tokio::test]
async fn creating_a_roaster_with_malformed_json_returns_a_400() {
// Arrange
let app = spawn_app().await;
let app = spawn_app_with_auth().await;
let client = reqwest::Client::new();
// Act
let response = client
.post(app.api_url("/roasters"))
.bearer_auth(app.auth_token.as_ref().unwrap())
.header("content-type", "application/json")
.body(r#"{"name": "Test", "country": }"#) // Invalid JSON
.send()
@ -438,12 +453,13 @@ async fn creating_a_roaster_with_malformed_json_returns_a_400() {
#[tokio::test]
async fn creating_a_roaster_with_missing_required_fields_returns_a_400() {
// Arrange
let app = spawn_app().await;
let app = spawn_app_with_auth().await;
let client = reqwest::Client::new();
// Act - Missing 'country' field
let response = client
.post(app.api_url("/roasters"))
.bearer_auth(app.auth_token.as_ref().unwrap())
.header("content-type", "application/json")
.body(r#"{"name": "Test Roasters"}"#)
.send()

View file

@ -1,10 +1,10 @@
use crate::helpers::{create_default_roaster, create_roaster_with_name, spawn_app};
use crate::helpers::{create_default_roaster, create_roaster_with_name, spawn_app_with_auth};
use brewlog::domain::roasts::{NewRoast, Roast, RoastWithRoaster};
#[tokio::test]
async fn creating_a_roast_returns_a_201_for_valid_data() {
// Arrange
let app = spawn_app().await;
let app = spawn_app_with_auth().await;
let roaster_id = create_default_roaster(&app).await.id;
let client = reqwest::Client::new();
@ -25,6 +25,7 @@ async fn creating_a_roast_returns_a_201_for_valid_data() {
// Act
let response = client
.post(app.api_url("/roasts"))
.bearer_auth(app.auth_token.as_ref().unwrap())
.json(&new_roast)
.send()
.await
@ -46,7 +47,7 @@ async fn creating_a_roast_returns_a_201_for_valid_data() {
#[tokio::test]
async fn creating_a_roast_persists_the_data() {
// Arrange
let app = spawn_app().await;
let app = spawn_app_with_auth().await;
let roaster_id = create_default_roaster(&app).await.id;
let client = reqwest::Client::new();
@ -63,6 +64,7 @@ async fn creating_a_roast_persists_the_data() {
// Act
let response = client
.post(app.api_url("/roasts"))
.bearer_auth(app.auth_token.as_ref().unwrap())
.json(&new_roast)
.send()
.await
@ -85,7 +87,7 @@ async fn creating_a_roast_persists_the_data() {
#[tokio::test]
async fn creating_a_roast_with_nonexistent_roaster_returns_a_404() {
// Arrange
let app = spawn_app().await;
let app = spawn_app_with_auth().await;
let client = reqwest::Client::new();
let new_roast = NewRoast {
@ -101,6 +103,7 @@ async fn creating_a_roast_with_nonexistent_roaster_returns_a_404() {
// Act
let response = client
.post(app.api_url("/roasts"))
.bearer_auth(app.auth_token.as_ref().unwrap())
.json(&new_roast)
.send()
.await
@ -113,7 +116,7 @@ async fn creating_a_roast_with_nonexistent_roaster_returns_a_404() {
#[tokio::test]
async fn getting_a_roast_returns_a_200_for_valid_id() {
// Arrange
let app = spawn_app().await;
let app = spawn_app_with_auth().await;
let roaster_id = create_default_roaster(&app).await.id;
let client = reqwest::Client::new();
@ -129,6 +132,7 @@ async fn getting_a_roast_returns_a_200_for_valid_id() {
let create_response = client
.post(app.api_url("/roasts"))
.bearer_auth(app.auth_token.as_ref().unwrap())
.json(&new_roast)
.send()
.await
@ -157,7 +161,7 @@ async fn getting_a_roast_returns_a_200_for_valid_id() {
#[tokio::test]
async fn getting_a_nonexistent_roast_returns_a_404() {
// Arrange
let app = spawn_app().await;
let app = spawn_app_with_auth().await;
let client = reqwest::Client::new();
// Act
@ -174,7 +178,7 @@ async fn getting_a_nonexistent_roast_returns_a_404() {
#[tokio::test]
async fn listing_roasts_returns_a_200_with_empty_list() {
// Arrange
let app = spawn_app().await;
let app = spawn_app_with_auth().await;
let client = reqwest::Client::new();
// Act
@ -194,7 +198,7 @@ async fn listing_roasts_returns_a_200_with_empty_list() {
#[tokio::test]
async fn listing_roasts_returns_a_200_with_multiple_roasts() {
// Arrange
let app = spawn_app().await;
let app = spawn_app_with_auth().await;
let roaster_id = create_default_roaster(&app).await.id;
let client = reqwest::Client::new();
@ -221,6 +225,7 @@ async fn listing_roasts_returns_a_200_with_multiple_roasts() {
client
.post(app.api_url("/roasts"))
.bearer_auth(app.auth_token.as_ref().unwrap())
.json(&roast1)
.send()
.await
@ -228,6 +233,7 @@ async fn listing_roasts_returns_a_200_with_multiple_roasts() {
client
.post(app.api_url("/roasts"))
.bearer_auth(app.auth_token.as_ref().unwrap())
.json(&roast2)
.send()
.await
@ -250,7 +256,7 @@ async fn listing_roasts_returns_a_200_with_multiple_roasts() {
#[tokio::test]
async fn listing_roasts_by_roaster_returns_a_200_with_filtered_list() {
// Arrange
let app = spawn_app().await;
let app = spawn_app_with_auth().await;
let roaster1_id = create_default_roaster(&app).await.id;
let roaster2_id = create_roaster_with_name(&app, "Second Roasters").await.id;
@ -279,6 +285,7 @@ async fn listing_roasts_by_roaster_returns_a_200_with_filtered_list() {
client
.post(app.api_url("/roasts"))
.bearer_auth(app.auth_token.as_ref().unwrap())
.json(&roast1)
.send()
.await
@ -286,6 +293,7 @@ async fn listing_roasts_by_roaster_returns_a_200_with_filtered_list() {
client
.post(app.api_url("/roasts"))
.bearer_auth(app.auth_token.as_ref().unwrap())
.json(&roast2)
.send()
.await
@ -309,7 +317,7 @@ async fn listing_roasts_by_roaster_returns_a_200_with_filtered_list() {
#[tokio::test]
async fn deleting_a_roast_returns_a_204_for_valid_id() {
// Arrange
let app = spawn_app().await;
let app = spawn_app_with_auth().await;
let roaster_id = create_default_roaster(&app).await.id;
let client = reqwest::Client::new();
@ -325,6 +333,7 @@ async fn deleting_a_roast_returns_a_204_for_valid_id() {
let create_response = client
.post(app.api_url("/roasts"))
.bearer_auth(app.auth_token.as_ref().unwrap())
.json(&new_roast)
.send()
.await
@ -338,6 +347,7 @@ async fn deleting_a_roast_returns_a_204_for_valid_id() {
// Act
let response = client
.delete(app.api_url(&format!("/roasts/{}", created_roast.id)))
.bearer_auth(app.auth_token.as_ref().unwrap())
.send()
.await
.expect("Failed to execute request");
@ -358,12 +368,13 @@ async fn deleting_a_roast_returns_a_204_for_valid_id() {
#[tokio::test]
async fn deleting_a_nonexistent_roast_returns_a_404() {
// Arrange
let app = spawn_app().await;
let app = spawn_app_with_auth().await;
let client = reqwest::Client::new();
// Act
let response = client
.delete(app.api_url("/roasts/nonexistent-id"))
.bearer_auth(app.auth_token.as_ref().unwrap())
.send()
.await
.expect("Failed to execute request");
@ -375,13 +386,14 @@ async fn deleting_a_nonexistent_roast_returns_a_404() {
#[tokio::test]
async fn creating_a_roast_with_empty_name_returns_a_400() {
// Arrange
let app = spawn_app().await;
let app = spawn_app_with_auth().await;
let roaster_id = create_default_roaster(&app).await.id;
let client = reqwest::Client::new();
// Act - Create roast with empty name (after trim)
let response = client
.post(app.api_url("/roasts"))
.bearer_auth(app.auth_token.as_ref().unwrap())
.header("content-type", "application/json")
.body(format!(
r#"{{
@ -406,13 +418,14 @@ async fn creating_a_roast_with_empty_name_returns_a_400() {
#[tokio::test]
async fn creating_a_roast_with_missing_required_fields_returns_a_400() {
// Arrange
let app = spawn_app().await;
let app = spawn_app_with_auth().await;
let roaster_id = create_default_roaster(&app).await.id;
let client = reqwest::Client::new();
// Act - Missing 'origin' field
let response = client
.post(app.api_url("/roasts"))
.bearer_auth(app.auth_token.as_ref().unwrap())
.header("content-type", "application/json")
.body(format!(
r#"{{
@ -436,13 +449,14 @@ async fn creating_a_roast_with_missing_required_fields_returns_a_400() {
#[tokio::test]
async fn creating_a_roast_with_empty_tasting_notes_returns_a_400() {
// Arrange
let app = spawn_app().await;
let app = spawn_app_with_auth().await;
let roaster_id = create_default_roaster(&app).await.id;
let client = reqwest::Client::new();
// Act - Empty tasting notes
let response = client
.post(app.api_url("/roasts"))
.bearer_auth(app.auth_token.as_ref().unwrap())
.header("content-type", "application/json")
.body(format!(
r#"{{
@ -467,12 +481,13 @@ async fn creating_a_roast_with_empty_tasting_notes_returns_a_400() {
#[tokio::test]
async fn creating_a_roast_with_malformed_json_returns_a_400() {
// Arrange
let app = spawn_app().await;
let app = spawn_app_with_auth().await;
let client = reqwest::Client::new();
// Act
let response = client
.post(app.api_url("/roasts"))
.bearer_auth(app.auth_token.as_ref().unwrap())
.header("content-type", "application/json")
.body(r#"{"name": "Test", "roaster_id": }"#) // Invalid JSON
.send()

View file

@ -1,4 +1,4 @@
use crate::helpers::{create_roaster_with_payload, spawn_app};
use crate::helpers::{create_roaster_with_payload, spawn_app_with_auth};
use brewlog::domain::roasters::NewRoaster;
use brewlog::domain::roasts::NewRoast;
use reqwest::Client;
@ -18,6 +18,7 @@ async fn create_roast(app: &crate::helpers::TestApp, roaster_id: &str, name: &st
let response = client
.post(app.api_url("/roasts"))
.bearer_auth(app.auth_token.as_ref().unwrap())
.json(&roast)
.send()
.await
@ -60,7 +61,7 @@ async fn seed_timeline_with_roasts(
#[tokio::test]
async fn timeline_page_returns_a_200_with_empty_state() {
let app = spawn_app().await;
let app = spawn_app_with_auth().await;
let client = Client::new();
let response = client
@ -80,7 +81,7 @@ async fn timeline_page_returns_a_200_with_empty_state() {
#[tokio::test]
async fn creating_a_roaster_surfaces_on_the_timeline() {
let app = spawn_app().await;
let app = spawn_app_with_auth().await;
let client = Client::new();
let roaster_name = "Timeline Roasters";
@ -124,7 +125,7 @@ async fn creating_a_roaster_surfaces_on_the_timeline() {
#[tokio::test]
async fn creating_a_roast_surfaces_on_the_timeline() {
let app = spawn_app().await;
let app = spawn_app_with_auth().await;
let client = Client::new();
let roaster_id = create_roaster_with_payload(
@ -169,7 +170,7 @@ async fn creating_a_roast_surfaces_on_the_timeline() {
#[tokio::test]
async fn timeline_page_signals_more_results_when_over_page_size() {
let app = spawn_app().await;
let app = spawn_app_with_auth().await;
let (_, roast_names) = seed_timeline_with_roasts(&app, 6).await;
assert_eq!(roast_names.len(), 6);
@ -211,7 +212,7 @@ async fn timeline_page_signals_more_results_when_over_page_size() {
#[tokio::test]
async fn timeline_chunk_endpoint_serves_remaining_events() {
let app = spawn_app().await;
let app = spawn_app_with_auth().await;
let (roaster_name, roast_names) = seed_timeline_with_roasts(&app, 6).await;
let oldest_roast = roast_names
.first()