From 52301e64b9341d9d3e25b52b1ab33432559e078d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 24 Nov 2025 16:57:16 +0000 Subject: [PATCH] test: add wiremock dependency and test infrastructure Co-authored-by: jnsgruk <668505+jnsgruk@users.noreply.github.com> --- Cargo.lock | 108 ++++++++++ Cargo.toml | 4 + tests/server.rs | 5 + tests/server/helpers.rs | 71 +++++++ tests/server/roasters.rs | 375 +++++++++++++++++++++++++++++++++++ tests/server/roasts.rs | 413 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 976 insertions(+) create mode 100644 tests/server.rs create mode 100644 tests/server/helpers.rs create mode 100644 tests/server/roasters.rs create mode 100644 tests/server/roasts.rs diff --git a/Cargo.lock b/Cargo.lock index 6528914..f55c62f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -139,6 +139,16 @@ dependencies = [ "nom", ] +[[package]] +name = "assert-json-diff" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "async-trait" version = "0.1.89" @@ -315,6 +325,7 @@ dependencies = [ "tokio", "tracing", "tracing-subscriber", + "wiremock", ] [[package]] @@ -488,6 +499,24 @@ dependencies = [ "typenum", ] +[[package]] +name = "deadpool" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0be2b1d1d6ec8d846f05e137292d0b89133caf95ef33695424c09568bdd39b1b" +dependencies = [ + "deadpool-runtime", + "lazy_static", + "num_cpus", + "tokio", +] + +[[package]] +name = "deadpool-runtime" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" + [[package]] name = "der" version = "0.7.10" @@ -632,6 +661,21 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.31" @@ -676,6 +720,17 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + [[package]] name = "futures-sink" version = "0.3.31" @@ -694,8 +749,10 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ + "futures-channel", "futures-core", "futures-io", + "futures-macro", "futures-sink", "futures-task", "memchr", @@ -800,6 +857,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "hex" version = "0.4.3" @@ -1366,6 +1429,16 @@ dependencies = [ "libm", ] +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -1679,6 +1752,18 @@ dependencies = [ "bitflags", ] +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + [[package]] name = "regex-automata" version = "0.4.13" @@ -3137,6 +3222,29 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" +[[package]] +name = "wiremock" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08db1edfb05d9b3c1542e521aea074442088292f00b5f28e435c714a98f85031" +dependencies = [ + "assert-json-diff", + "base64 0.22.1", + "deadpool", + "futures", + "http", + "http-body-util", + "hyper", + "hyper-util", + "log", + "once_cell", + "regex", + "serde", + "serde_json", + "tokio", + "url", +] + [[package]] name = "wit-bindgen" version = "0.46.0" diff --git a/Cargo.toml b/Cargo.toml index cce23e3..70e4be4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,3 +32,7 @@ thiserror = "1.0" tokio = { version = "1.38", features = ["rt-multi-thread", "macros", "signal"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } + +[dev-dependencies] +tokio = { version = "1.38", features = ["rt-multi-thread", "macros"] } +wiremock = "0.6" diff --git a/tests/server.rs b/tests/server.rs new file mode 100644 index 0000000..2c04a24 --- /dev/null +++ b/tests/server.rs @@ -0,0 +1,5 @@ +mod server { + pub mod helpers; + pub mod roasters; + pub mod roasts; +} diff --git a/tests/server/helpers.rs b/tests/server/helpers.rs new file mode 100644 index 0000000..be080fb --- /dev/null +++ b/tests/server/helpers.rs @@ -0,0 +1,71 @@ +use std::sync::Arc; + +use brewlog::domain::repositories::{RoastRepository, RoasterRepository, TimelineEventRepository}; +use brewlog::infrastructure::database::Database; +use brewlog::infrastructure::repositories::roasters::SqlRoasterRepository; +use brewlog::infrastructure::repositories::roasts::SqlRoastRepository; +use brewlog::infrastructure::repositories::timeline_events::SqlTimelineEventRepository; +use brewlog::server::routes::app_router; +use brewlog::server::server::AppState; +use tokio::net::TcpListener; + +pub struct TestApp { + pub address: String, + pub roaster_repo: Arc, + pub roast_repo: Arc, + #[allow(dead_code)] + pub timeline_repo: Arc, +} + +impl TestApp { + pub fn api_url(&self, path: &str) -> String { + format!("{}/api/v1{}", self.address, path) + } +} + +pub async fn spawn_app() -> TestApp { + // Use in-memory SQLite database for testing + let database = Database::connect("sqlite::memory:") + .await + .expect("Failed to connect to in-memory database"); + + // Run migrations + database.migrate().await.expect("Failed to migrate database"); + + // Create repositories + let roaster_repo = Arc::new(SqlRoasterRepository::new(database.clone_pool())); + let roast_repo = Arc::new(SqlRoastRepository::new(database.clone_pool())); + let timeline_repo = Arc::new(SqlTimelineEventRepository::new(database.clone_pool())); + + // Create application state + let state = AppState::new( + roaster_repo.clone(), + roast_repo.clone(), + timeline_repo.clone(), + ); + + // Create router + let app = app_router(state); + + // Bind to a random port + let listener = TcpListener::bind("127.0.0.1:0") + .await + .expect("Failed to bind to random port"); + + let local_addr = listener.local_addr().expect("Failed to get local address"); + let address = format!("http://{}", local_addr); + + // Spawn the server in a background task + tokio::spawn(async move { + axum::serve(listener, app) + .await + .expect("Server failed to start"); + }); + + TestApp { + address, + roaster_repo, + roast_repo, + timeline_repo, + } +} diff --git a/tests/server/roasters.rs b/tests/server/roasters.rs new file mode 100644 index 0000000..3c6724b --- /dev/null +++ b/tests/server/roasters.rs @@ -0,0 +1,375 @@ +use crate::server::helpers::spawn_app; +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 client = reqwest::Client::new(); + + let new_roaster = NewRoaster { + name: "Test Roasters".to_string(), + country: "United Kingdom".to_string(), + city: Some("London".to_string()), + homepage: Some("https://example.com".to_string()), + notes: Some("Great coffee".to_string()), + }; + + // Act + let response = client + .post(app.api_url("/roasters")) + .json(&new_roaster) + .send() + .await + .expect("Failed to execute request"); + + // Assert + assert_eq!(response.status(), 201); + + let roaster: Roaster = response.json().await.expect("Failed to parse response"); + assert_eq!(roaster.name, "Test Roasters"); + assert_eq!(roaster.country, "United Kingdom"); + assert_eq!(roaster.city, Some("London".to_string())); + assert_eq!(roaster.homepage, Some("https://example.com".to_string())); + assert_eq!(roaster.notes, Some("Great coffee".to_string())); +} + +#[tokio::test] +async fn creating_a_roaster_persists_the_data() { + // Arrange + let app = spawn_app().await; + let client = reqwest::Client::new(); + + let new_roaster = NewRoaster { + name: "Persistent Roasters".to_string(), + country: "France".to_string(), + city: Some("Paris".to_string()), + homepage: None, + notes: None, + }; + + // Act + let response = client + .post(app.api_url("/roasters")) + .json(&new_roaster) + .send() + .await + .expect("Failed to execute request"); + + let roaster: Roaster = response.json().await.expect("Failed to parse response"); + + // Assert - Fetch the roaster to verify it was persisted + let fetched_roaster = app + .roaster_repo + .get(roaster.id.clone()) + .await + .expect("Failed to fetch roaster"); + + assert_eq!(fetched_roaster.name, "Persistent Roasters"); + assert_eq!(fetched_roaster.country, "France"); + assert_eq!(fetched_roaster.city, Some("Paris".to_string())); +} + +#[tokio::test] +async fn getting_a_roaster_returns_a_200_for_valid_id() { + // Arrange + let app = spawn_app().await; + let client = reqwest::Client::new(); + + let new_roaster = NewRoaster { + name: "Fetchable Roasters".to_string(), + country: "Germany".to_string(), + city: Some("Berlin".to_string()), + homepage: None, + notes: None, + }; + + let create_response = client + .post(app.api_url("/roasters")) + .json(&new_roaster) + .send() + .await + .expect("Failed to create roaster"); + + let created_roaster: Roaster = create_response.json().await.expect("Failed to parse response"); + + // Act + let response = client + .get(app.api_url(&format!("/roasters/{}", created_roaster.id))) + .send() + .await + .expect("Failed to execute request"); + + // Assert + assert_eq!(response.status(), 200); + + let roaster: Roaster = response.json().await.expect("Failed to parse response"); + assert_eq!(roaster.id, created_roaster.id); + assert_eq!(roaster.name, "Fetchable Roasters"); +} + +#[tokio::test] +async fn getting_a_nonexistent_roaster_returns_a_404() { + // Arrange + let app = spawn_app().await; + let client = reqwest::Client::new(); + + // Act + let response = client + .get(app.api_url("/roasters/nonexistent-id")) + .send() + .await + .expect("Failed to execute request"); + + // Assert + assert_eq!(response.status(), 404); +} + +#[tokio::test] +async fn listing_roasters_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("/roasters")) + .send() + .await + .expect("Failed to execute request"); + + // Assert + assert_eq!(response.status(), 200); + + let roasters: Vec = response.json().await.expect("Failed to parse response"); + assert_eq!(roasters.len(), 0); +} + +#[tokio::test] +async fn listing_roasters_returns_a_200_with_multiple_roasters() { + // Arrange + let app = spawn_app().await; + let client = reqwest::Client::new(); + + // Create multiple roasters + let roaster1 = NewRoaster { + name: "First Roasters".to_string(), + country: "UK".to_string(), + city: None, + homepage: None, + notes: None, + }; + + let roaster2 = NewRoaster { + name: "Second Roasters".to_string(), + country: "USA".to_string(), + city: Some("New York".to_string()), + homepage: None, + notes: None, + }; + + client + .post(app.api_url("/roasters")) + .json(&roaster1) + .send() + .await + .expect("Failed to create first roaster"); + + client + .post(app.api_url("/roasters")) + .json(&roaster2) + .send() + .await + .expect("Failed to create second roaster"); + + // Act + let response = client + .get(app.api_url("/roasters")) + .send() + .await + .expect("Failed to execute request"); + + // Assert + assert_eq!(response.status(), 200); + + let roasters: Vec = response.json().await.expect("Failed to parse response"); + assert_eq!(roasters.len(), 2); +} + +#[tokio::test] +async fn updating_a_roaster_returns_a_200_for_valid_data() { + // Arrange + let app = spawn_app().await; + let client = reqwest::Client::new(); + + let new_roaster = NewRoaster { + name: "Original Name".to_string(), + country: "UK".to_string(), + city: Some("Manchester".to_string()), + homepage: None, + notes: None, + }; + + let create_response = client + .post(app.api_url("/roasters")) + .json(&new_roaster) + .send() + .await + .expect("Failed to create roaster"); + + let created_roaster: Roaster = create_response.json().await.expect("Failed to parse response"); + + let update = UpdateRoaster { + name: Some("Updated Name".to_string()), + country: None, + city: Some("Liverpool".to_string()), + homepage: Some("https://updated.com".to_string()), + notes: None, + }; + + // Act + let response = client + .put(app.api_url(&format!("/roasters/{}", created_roaster.id))) + .json(&update) + .send() + .await + .expect("Failed to execute request"); + + // Assert + assert_eq!(response.status(), 200); + + let updated_roaster: Roaster = response.json().await.expect("Failed to parse response"); + assert_eq!(updated_roaster.name, "Updated Name"); + assert_eq!(updated_roaster.country, "UK"); // Should remain unchanged + assert_eq!(updated_roaster.city, Some("Liverpool".to_string())); + assert_eq!(updated_roaster.homepage, Some("https://updated.com".to_string())); +} + +#[tokio::test] +async fn updating_a_roaster_with_no_changes_returns_a_400() { + // Arrange + let app = spawn_app().await; + let client = reqwest::Client::new(); + + let new_roaster = NewRoaster { + name: "Test Roaster".to_string(), + country: "UK".to_string(), + city: None, + homepage: None, + notes: None, + }; + + let create_response = client + .post(app.api_url("/roasters")) + .json(&new_roaster) + .send() + .await + .expect("Failed to create roaster"); + + let created_roaster: Roaster = create_response.json().await.expect("Failed to parse response"); + + let update = UpdateRoaster { + name: None, + country: None, + city: None, + homepage: None, + notes: None, + }; + + // Act + let response = client + .put(app.api_url(&format!("/roasters/{}", created_roaster.id))) + .json(&update) + .send() + .await + .expect("Failed to execute request"); + + // Assert + assert_eq!(response.status(), 400); +} + +#[tokio::test] +async fn updating_a_nonexistent_roaster_returns_a_404() { + // Arrange + let app = spawn_app().await; + let client = reqwest::Client::new(); + + let update = UpdateRoaster { + name: Some("New Name".to_string()), + country: None, + city: None, + homepage: None, + notes: None, + }; + + // Act + let response = client + .put(app.api_url("/roasters/nonexistent-id")) + .json(&update) + .send() + .await + .expect("Failed to execute request"); + + // Assert + assert_eq!(response.status(), 404); +} + +#[tokio::test] +async fn deleting_a_roaster_returns_a_204_for_valid_id() { + // Arrange + let app = spawn_app().await; + let client = reqwest::Client::new(); + + let new_roaster = NewRoaster { + name: "To Be Deleted".to_string(), + country: "UK".to_string(), + city: None, + homepage: None, + notes: None, + }; + + let create_response = client + .post(app.api_url("/roasters")) + .json(&new_roaster) + .send() + .await + .expect("Failed to create roaster"); + + let created_roaster: Roaster = create_response.json().await.expect("Failed to parse response"); + + // Act + let response = client + .delete(app.api_url(&format!("/roasters/{}", created_roaster.id))) + .send() + .await + .expect("Failed to execute request"); + + // Assert + assert_eq!(response.status(), 204); + + // Verify roaster was deleted + let get_response = client + .get(app.api_url(&format!("/roasters/{}", created_roaster.id))) + .send() + .await + .expect("Failed to execute request"); + + assert_eq!(get_response.status(), 404); +} + +#[tokio::test] +async fn deleting_a_nonexistent_roaster_returns_a_404() { + // Arrange + let app = spawn_app().await; + let client = reqwest::Client::new(); + + // Act + let response = client + .delete(app.api_url("/roasters/nonexistent-id")) + .send() + .await + .expect("Failed to execute request"); + + // Assert + assert_eq!(response.status(), 404); +} diff --git a/tests/server/roasts.rs b/tests/server/roasts.rs new file mode 100644 index 0000000..a062e7d --- /dev/null +++ b/tests/server/roasts.rs @@ -0,0 +1,413 @@ +use crate::server::helpers::spawn_app; +use brewlog::domain::roasters::NewRoaster; +use brewlog::domain::roasts::{NewRoast, Roast, RoastWithRoaster}; + +async fn create_test_roaster(app: &crate::server::helpers::TestApp) -> String { + let new_roaster = NewRoaster { + name: "Test Roasters".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: "Test Roast".to_string(), + origin: "Test".to_string(), + region: "Test".to_string(), + producer: "Test".to_string(), + tasting_notes: vec!["Test".to_string()], + process: "Test".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 = 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 = 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; + + // Create a second roaster + let new_roaster2 = NewRoaster { + name: "Second Roasters".to_string(), + country: "USA".to_string(), + city: None, + homepage: None, + notes: None, + }; + + let client = reqwest::Client::new(); + let response = client + .post(app.api_url("/roasters")) + .json(&new_roaster2) + .send() + .await + .expect("Failed to create second roaster"); + + let roaster2: brewlog::domain::roasters::Roaster = response + .json() + .await + .expect("Failed to parse roaster"); + let roaster2_id = roaster2.id; + + // 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 = 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: "To Be Deleted".to_string(), + origin: "Test".to_string(), + region: "Test".to_string(), + producer: "Test".to_string(), + tasting_notes: vec!["Test".to_string()], + process: "Test".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); +}