test: add wiremock dependency and test infrastructure

Co-authored-by: jnsgruk <668505+jnsgruk@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2025-11-24 16:57:16 +00:00 committed by Jon Seager
parent 9c4734172f
commit 52301e64b9
No known key found for this signature in database
6 changed files with 976 additions and 0 deletions

108
Cargo.lock generated
View file

@ -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"

View file

@ -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"

5
tests/server.rs Normal file
View file

@ -0,0 +1,5 @@
mod server {
pub mod helpers;
pub mod roasters;
pub mod roasts;
}

71
tests/server/helpers.rs Normal file
View file

@ -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<dyn RoasterRepository>,
pub roast_repo: Arc<dyn RoastRepository>,
#[allow(dead_code)]
pub timeline_repo: Arc<dyn TimelineEventRepository>,
}
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,
}
}

375
tests/server/roasters.rs Normal file
View file

@ -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<Roaster> = 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<Roaster> = 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);
}

413
tests/server/roasts.rs Normal file
View file

@ -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<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;
// 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<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: "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);
}