test: add wiremock dependency and test infrastructure
Co-authored-by: jnsgruk <668505+jnsgruk@users.noreply.github.com>
This commit is contained in:
parent
9c4734172f
commit
52301e64b9
6 changed files with 976 additions and 0 deletions
108
Cargo.lock
generated
108
Cargo.lock
generated
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
5
tests/server.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
mod server {
|
||||
pub mod helpers;
|
||||
pub mod roasters;
|
||||
pub mod roasts;
|
||||
}
|
||||
71
tests/server/helpers.rs
Normal file
71
tests/server/helpers.rs
Normal 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
375
tests/server/roasters.rs
Normal 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
413
tests/server/roasts.rs
Normal 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);
|
||||
}
|
||||
Loading…
Reference in a new issue