brewlog/tests/server/roasts.rs
copilot-swe-agent[bot] 05d856a41d
refactor: move server tests to standalone module
Move tests/server.rs to tests/server/main.rs and update module
paths to make the server tests more self-contained. All test
imports now use crate::helpers instead of crate::server::helpers.

Co-authored-by: jnsgruk <668505+jnsgruk@users.noreply.github.com>
2025-11-24 17:58:24 +00:00

595 lines
17 KiB
Rust

use crate::helpers::spawn_app;
use brewlog::domain::roasters::NewRoaster;
use brewlog::domain::roasts::{NewRoast, Roast, RoastWithRoaster};
async fn create_test_roaster(app: &crate::helpers::TestApp) -> String {
create_test_roaster_with_name(app, "Test Roasters").await
}
async fn create_test_roaster_with_name(
app: &crate::helpers::TestApp,
name: &str,
) -> String {
let new_roaster = NewRoaster {
name: name.to_string(),
country: "UK".to_string(),
city: None,
homepage: None,
notes: None,
};
let client = reqwest::Client::new();
let response = client
.post(app.api_url("/roasters"))
.json(&new_roaster)
.send()
.await
.expect("Failed to create roaster");
let roaster: brewlog::domain::roasters::Roaster = response
.json()
.await
.expect("Failed to parse roaster");
roaster.id
}
#[tokio::test]
async fn creating_a_roast_returns_a_201_for_valid_data() {
// Arrange
let app = spawn_app().await;
let roaster_id = create_test_roaster(&app).await;
let client = reqwest::Client::new();
let new_roast = NewRoast {
roaster_id: roaster_id.clone(),
name: "Ethiopian Yirgacheffe".to_string(),
origin: "Ethiopia".to_string(),
region: "Yirgacheffe".to_string(),
producer: "Local Cooperative".to_string(),
tasting_notes: vec![
"Blueberry".to_string(),
"Chocolate".to_string(),
"Citrus".to_string(),
],
process: "Washed".to_string(),
};
// Act
let response = client
.post(app.api_url("/roasts"))
.json(&new_roast)
.send()
.await
.expect("Failed to execute request");
// Assert
assert_eq!(response.status(), 201);
let roast: Roast = response.json().await.expect("Failed to parse response");
assert_eq!(roast.name, "Ethiopian Yirgacheffe");
assert_eq!(roast.roaster_id, roaster_id);
assert_eq!(roast.origin, Some("Ethiopia".to_string()));
assert_eq!(roast.region, Some("Yirgacheffe".to_string()));
assert_eq!(roast.producer, Some("Local Cooperative".to_string()));
assert_eq!(roast.tasting_notes.len(), 3);
assert_eq!(roast.process, Some("Washed".to_string()));
}
#[tokio::test]
async fn creating_a_roast_persists_the_data() {
// Arrange
let app = spawn_app().await;
let roaster_id = create_test_roaster(&app).await;
let client = reqwest::Client::new();
let new_roast = NewRoast {
roaster_id: roaster_id.clone(),
name: "Colombian Supremo".to_string(),
origin: "Colombia".to_string(),
region: "Huila".to_string(),
producer: "Farm Co-op".to_string(),
tasting_notes: vec!["Caramel".to_string(), "Nuts".to_string()],
process: "Natural".to_string(),
};
// Act
let response = client
.post(app.api_url("/roasts"))
.json(&new_roast)
.send()
.await
.expect("Failed to execute request");
let roast: Roast = response.json().await.expect("Failed to parse response");
// Assert - Fetch the roast to verify it was persisted
let fetched_roast = app
.roast_repo
.get(roast.id.clone())
.await
.expect("Failed to fetch roast");
assert_eq!(fetched_roast.name, "Colombian Supremo");
assert_eq!(fetched_roast.roaster_id, roaster_id);
assert_eq!(fetched_roast.origin, Some("Colombia".to_string()));
}
#[tokio::test]
async fn creating_a_roast_with_nonexistent_roaster_returns_a_404() {
// Arrange
let app = spawn_app().await;
let client = reqwest::Client::new();
let new_roast = NewRoast {
roaster_id: "nonexistent-roaster-id".to_string(),
name: "Orphaned Roast".to_string(),
origin: "Unknown".to_string(),
region: "Unknown".to_string(),
producer: "Unknown Producer".to_string(),
tasting_notes: vec!["Bitter".to_string()],
process: "Unknown".to_string(),
};
// Act
let response = client
.post(app.api_url("/roasts"))
.json(&new_roast)
.send()
.await
.expect("Failed to execute request");
// Assert
assert_eq!(response.status(), 404);
}
#[tokio::test]
async fn getting_a_roast_returns_a_200_for_valid_id() {
// Arrange
let app = spawn_app().await;
let roaster_id = create_test_roaster(&app).await;
let client = reqwest::Client::new();
let new_roast = NewRoast {
roaster_id: roaster_id.clone(),
name: "Kenyan AA".to_string(),
origin: "Kenya".to_string(),
region: "Nyeri".to_string(),
producer: "Estate".to_string(),
tasting_notes: vec!["Blackcurrant".to_string()],
process: "Washed".to_string(),
};
let create_response = client
.post(app.api_url("/roasts"))
.json(&new_roast)
.send()
.await
.expect("Failed to create roast");
let created_roast: Roast = create_response.json().await.expect("Failed to parse response");
// Act
let response = client
.get(app.api_url(&format!("/roasts/{}", created_roast.id)))
.send()
.await
.expect("Failed to execute request");
// Assert
assert_eq!(response.status(), 200);
let roast: Roast = response.json().await.expect("Failed to parse response");
assert_eq!(roast.id, created_roast.id);
assert_eq!(roast.name, "Kenyan AA");
}
#[tokio::test]
async fn getting_a_nonexistent_roast_returns_a_404() {
// Arrange
let app = spawn_app().await;
let client = reqwest::Client::new();
// Act
let response = client
.get(app.api_url("/roasts/nonexistent-id"))
.send()
.await
.expect("Failed to execute request");
// Assert
assert_eq!(response.status(), 404);
}
#[tokio::test]
async fn listing_roasts_returns_a_200_with_empty_list() {
// Arrange
let app = spawn_app().await;
let client = reqwest::Client::new();
// Act
let response = client
.get(app.api_url("/roasts"))
.send()
.await
.expect("Failed to execute request");
// Assert
assert_eq!(response.status(), 200);
let roasts: Vec<RoastWithRoaster> = response.json().await.expect("Failed to parse response");
assert_eq!(roasts.len(), 0);
}
#[tokio::test]
async fn listing_roasts_returns_a_200_with_multiple_roasts() {
// Arrange
let app = spawn_app().await;
let roaster_id = create_test_roaster(&app).await;
let client = reqwest::Client::new();
// Create multiple roasts
let roast1 = NewRoast {
roaster_id: roaster_id.clone(),
name: "First Roast".to_string(),
origin: "Brazil".to_string(),
region: "Santos".to_string(),
producer: "Farm A".to_string(),
tasting_notes: vec!["Chocolate".to_string()],
process: "Natural".to_string(),
};
let roast2 = NewRoast {
roaster_id: roaster_id.clone(),
name: "Second Roast".to_string(),
origin: "Guatemala".to_string(),
region: "Antigua".to_string(),
producer: "Farm B".to_string(),
tasting_notes: vec!["Caramel".to_string()],
process: "Washed".to_string(),
};
client
.post(app.api_url("/roasts"))
.json(&roast1)
.send()
.await
.expect("Failed to create first roast");
client
.post(app.api_url("/roasts"))
.json(&roast2)
.send()
.await
.expect("Failed to create second roast");
// Act
let response = client
.get(app.api_url("/roasts"))
.send()
.await
.expect("Failed to execute request");
// Assert
assert_eq!(response.status(), 200);
let roasts: Vec<RoastWithRoaster> = response.json().await.expect("Failed to parse response");
assert_eq!(roasts.len(), 2);
}
#[tokio::test]
async fn listing_roasts_by_roaster_returns_a_200_with_filtered_list() {
// Arrange
let app = spawn_app().await;
let roaster1_id = create_test_roaster(&app).await;
let roaster2_id = create_test_roaster_with_name(&app, "Second Roasters").await;
let client = reqwest::Client::new();
// Create roasts for both roasters
let roast1 = NewRoast {
roaster_id: roaster1_id.clone(),
name: "Roaster 1 Roast".to_string(),
origin: "Brazil".to_string(),
region: "Santos".to_string(),
producer: "Farm A".to_string(),
tasting_notes: vec!["Chocolate".to_string()],
process: "Natural".to_string(),
};
let roast2 = NewRoast {
roaster_id: roaster2_id.clone(),
name: "Roaster 2 Roast".to_string(),
origin: "Guatemala".to_string(),
region: "Antigua".to_string(),
producer: "Farm B".to_string(),
tasting_notes: vec!["Caramel".to_string()],
process: "Washed".to_string(),
};
client
.post(app.api_url("/roasts"))
.json(&roast1)
.send()
.await
.expect("Failed to create first roast");
client
.post(app.api_url("/roasts"))
.json(&roast2)
.send()
.await
.expect("Failed to create second roast");
// Act
let response = client
.get(app.api_url(&format!("/roasts?roaster_id={}", roaster1_id)))
.send()
.await
.expect("Failed to execute request");
// Assert
assert_eq!(response.status(), 200);
let roasts: Vec<RoastWithRoaster> = response.json().await.expect("Failed to parse response");
assert_eq!(roasts.len(), 1);
assert_eq!(roasts[0].roast.roaster_id, roaster1_id);
}
#[tokio::test]
async fn deleting_a_roast_returns_a_204_for_valid_id() {
// Arrange
let app = spawn_app().await;
let roaster_id = create_test_roaster(&app).await;
let client = reqwest::Client::new();
let new_roast = NewRoast {
roaster_id: roaster_id.clone(),
name: "Temporary Roast".to_string(),
origin: "Peru".to_string(),
region: "Cusco".to_string(),
producer: "Temporary Co-op".to_string(),
tasting_notes: vec!["Fleeting".to_string()],
process: "Washed".to_string(),
};
let create_response = client
.post(app.api_url("/roasts"))
.json(&new_roast)
.send()
.await
.expect("Failed to create roast");
let created_roast: Roast = create_response.json().await.expect("Failed to parse response");
// Act
let response = client
.delete(app.api_url(&format!("/roasts/{}", created_roast.id)))
.send()
.await
.expect("Failed to execute request");
// Assert
assert_eq!(response.status(), 204);
// Verify roast was deleted
let get_response = client
.get(app.api_url(&format!("/roasts/{}", created_roast.id)))
.send()
.await
.expect("Failed to execute request");
assert_eq!(get_response.status(), 404);
}
#[tokio::test]
async fn deleting_a_nonexistent_roast_returns_a_404() {
// Arrange
let app = spawn_app().await;
let client = reqwest::Client::new();
// Act
let response = client
.delete(app.api_url("/roasts/nonexistent-id"))
.send()
.await
.expect("Failed to execute request");
// Assert
assert_eq!(response.status(), 404);
}
#[tokio::test]
async fn creating_a_roast_with_empty_name_returns_a_400() {
// Arrange
let app = spawn_app().await;
let roaster_id = create_test_roaster(&app).await;
let client = reqwest::Client::new();
// Act - Create roast with empty name (after trim)
let response = client
.post(app.api_url("/roasts"))
.header("content-type", "application/json")
.body(format!(
r#"{{
"roaster_id": "{}",
"name": " ",
"origin": "Ethiopia",
"region": "Yirgacheffe",
"producer": "Co-op",
"tasting_notes": "Blueberry",
"process": "Washed"
}}"#,
roaster_id
))
.send()
.await
.expect("Failed to execute request");
// Assert
assert_eq!(response.status(), 400);
}
#[tokio::test]
async fn creating_a_roast_with_missing_required_fields_returns_a_400() {
// Arrange
let app = spawn_app().await;
let roaster_id = create_test_roaster(&app).await;
let client = reqwest::Client::new();
// Act - Missing 'origin' field
let response = client
.post(app.api_url("/roasts"))
.header("content-type", "application/json")
.body(format!(
r#"{{
"roaster_id": "{}",
"name": "Test Roast",
"region": "Yirgacheffe",
"producer": "Co-op",
"tasting_notes": "Blueberry",
"process": "Washed"
}}"#,
roaster_id
))
.send()
.await
.expect("Failed to execute request");
// Assert
assert_eq!(response.status(), 400);
}
#[tokio::test]
async fn creating_a_roast_with_empty_tasting_notes_returns_a_400() {
// Arrange
let app = spawn_app().await;
let roaster_id = create_test_roaster(&app).await;
let client = reqwest::Client::new();
// Act - Empty tasting notes
let response = client
.post(app.api_url("/roasts"))
.header("content-type", "application/json")
.body(format!(
r#"{{
"roaster_id": "{}",
"name": "Test Roast",
"origin": "Ethiopia",
"region": "Yirgacheffe",
"producer": "Co-op",
"tasting_notes": "",
"process": "Washed"
}}"#,
roaster_id
))
.send()
.await
.expect("Failed to execute request");
// Assert
assert_eq!(response.status(), 400);
}
#[tokio::test]
async fn creating_a_roast_with_malformed_json_returns_a_400() {
// Arrange
let app = spawn_app().await;
let client = reqwest::Client::new();
// Act
let response = client
.post(app.api_url("/roasts"))
.header("content-type", "application/json")
.body(r#"{"name": "Test", "roaster_id": }"#) // Invalid JSON
.send()
.await
.expect("Failed to execute request");
// Assert
assert_eq!(response.status(), 400);
}
#[tokio::test]
async fn listing_roasts_with_pagination_returns_correct_page() {
// Arrange
let app = spawn_app().await;
let roaster_id = create_test_roaster(&app).await;
let client = reqwest::Client::new();
// Create 5 roasts
for i in 1..=5 {
let roast = NewRoast {
roaster_id: roaster_id.clone(),
name: format!("Roast {}", i),
origin: "Brazil".to_string(),
region: "Santos".to_string(),
producer: "Farm".to_string(),
tasting_notes: vec!["Chocolate".to_string()],
process: "Natural".to_string(),
};
client
.post(app.api_url("/roasts"))
.json(&roast)
.send()
.await
.expect("Failed to create roast");
}
// Act - Request page 1 with page_size=2
let response = client
.get(app.api_url("/roasts?page=1&page_size=2"))
.send()
.await
.expect("Failed to execute request");
// Assert
assert_eq!(response.status(), 200);
let roasts: Vec<RoastWithRoaster> = response.json().await.expect("Failed to parse response");
// Note: The list endpoint returns all roasts, not paginated
assert_eq!(roasts.len(), 5);
}
#[tokio::test]
async fn listing_roasts_returns_sorted_by_created_at_descending() {
// Arrange
let app = spawn_app().await;
let roaster_id = create_test_roaster(&app).await;
let client = reqwest::Client::new();
// Create roasts with different names (they'll be sorted by created_at descending by default)
let names = vec!["First Roast", "Second Roast", "Third Roast"];
for name in names {
let roast = NewRoast {
roaster_id: roaster_id.clone(),
name: name.to_string(),
origin: "Brazil".to_string(),
region: "Santos".to_string(),
producer: "Farm".to_string(),
tasting_notes: vec!["Chocolate".to_string()],
process: "Natural".to_string(),
};
client
.post(app.api_url("/roasts"))
.json(&roast)
.send()
.await
.expect("Failed to create roast");
}
// Act - The API returns roasts sorted by created_at descending by default
let response = client
.get(app.api_url("/roasts"))
.send()
.await
.expect("Failed to execute request");
// Assert
assert_eq!(response.status(), 200);
let roasts: Vec<RoastWithRoaster> = response.json().await.expect("Failed to parse response");
assert_eq!(roasts.len(), 3);
// Verify they're sorted by created_at descending (newest first)
assert_eq!(roasts[0].roast.name, "Third Roast");
assert_eq!(roasts[1].roast.name, "Second Roast");
assert_eq!(roasts[2].roast.name, "First Roast");
}