test: add server tests for bags
This commit is contained in:
parent
5252cacad0
commit
ec8dbea7ea
4 changed files with 368 additions and 4 deletions
246
tests/server/bags_api.rs
Normal file
246
tests/server/bags_api.rs
Normal file
|
|
@ -0,0 +1,246 @@
|
||||||
|
use crate::helpers::{
|
||||||
|
create_default_roast, create_default_roaster, spawn_app, spawn_app_with_auth,
|
||||||
|
};
|
||||||
|
use brewlog::domain::bags::{Bag, BagWithRoast, NewBag, UpdateBag};
|
||||||
|
use chrono::NaiveDate;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn creating_a_bag_returns_a_201_for_valid_data() {
|
||||||
|
// Arrange
|
||||||
|
let app = spawn_app_with_auth().await;
|
||||||
|
let roaster = create_default_roaster(&app).await;
|
||||||
|
let roast = create_default_roast(&app, roaster.id).await;
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
|
let new_bag = NewBag {
|
||||||
|
roast_id: roast.id,
|
||||||
|
roast_date: Some(NaiveDate::from_ymd_opt(2023, 1, 1).unwrap()),
|
||||||
|
amount: 250.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
let response = client
|
||||||
|
.post(app.api_url("/bags"))
|
||||||
|
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||||
|
.json(&new_bag)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to execute request");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assert_eq!(response.status(), 201);
|
||||||
|
|
||||||
|
let bag: Bag = response.json().await.expect("Failed to parse response");
|
||||||
|
assert_eq!(bag.roast_id, roast.id);
|
||||||
|
assert_eq!(bag.amount, 250.0);
|
||||||
|
assert_eq!(bag.remaining, 250.0);
|
||||||
|
assert_eq!(
|
||||||
|
bag.roast_date,
|
||||||
|
Some(NaiveDate::from_ymd_opt(2023, 1, 1).unwrap())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn creating_a_bag_without_auth_returns_401() {
|
||||||
|
// Arrange
|
||||||
|
let app = spawn_app().await; // No auth
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
|
// We can't easily create a roast without auth, so we'll just try to create a bag
|
||||||
|
// with a dummy ID. The auth check should happen before validation.
|
||||||
|
let new_bag = serde_json::json!({
|
||||||
|
"roast_id": 123,
|
||||||
|
"amount": 250.0
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
let response = client
|
||||||
|
.post(app.api_url("/bags"))
|
||||||
|
.json(&new_bag)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to execute request");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assert_eq!(response.status(), 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn listing_bags_returns_200_and_correct_data() {
|
||||||
|
// Arrange
|
||||||
|
let app = spawn_app_with_auth().await;
|
||||||
|
let roaster = create_default_roaster(&app).await;
|
||||||
|
let roast = create_default_roast(&app, roaster.id).await;
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
|
// Create a bag
|
||||||
|
let new_bag = NewBag {
|
||||||
|
roast_id: roast.id,
|
||||||
|
roast_date: None,
|
||||||
|
amount: 500.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
client
|
||||||
|
.post(app.api_url("/bags"))
|
||||||
|
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||||
|
.json(&new_bag)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to create bag");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
let response = client
|
||||||
|
.get(app.api_url(&format!("/bags?roast_id={}", roast.id)))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to execute request");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assert_eq!(response.status(), 200);
|
||||||
|
|
||||||
|
let bags: Vec<BagWithRoast> = response.json().await.expect("Failed to parse response");
|
||||||
|
assert_eq!(bags.len(), 1);
|
||||||
|
assert_eq!(bags[0].bag.amount, 500.0);
|
||||||
|
assert_eq!(bags[0].roast_name, roast.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn getting_a_bag_returns_200_for_valid_id() {
|
||||||
|
// Arrange
|
||||||
|
let app = spawn_app_with_auth().await;
|
||||||
|
let roaster = create_default_roaster(&app).await;
|
||||||
|
let roast = create_default_roast(&app, roaster.id).await;
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
|
let new_bag = NewBag {
|
||||||
|
roast_id: roast.id,
|
||||||
|
roast_date: None,
|
||||||
|
amount: 250.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let create_response = client
|
||||||
|
.post(app.api_url("/bags"))
|
||||||
|
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||||
|
.json(&new_bag)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to create bag");
|
||||||
|
|
||||||
|
let created_bag: Bag = create_response
|
||||||
|
.json()
|
||||||
|
.await
|
||||||
|
.expect("Failed to parse response");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
let response = client
|
||||||
|
.get(app.api_url(&format!("/bags/{}", created_bag.id)))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to execute request");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assert_eq!(response.status(), 200);
|
||||||
|
let bag: Bag = response.json().await.expect("Failed to parse response");
|
||||||
|
assert_eq!(bag.id, created_bag.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn updating_a_bag_returns_200_and_updates_data() {
|
||||||
|
// Arrange
|
||||||
|
let app = spawn_app_with_auth().await;
|
||||||
|
let roaster = create_default_roaster(&app).await;
|
||||||
|
let roast = create_default_roast(&app, roaster.id).await;
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
|
let new_bag = NewBag {
|
||||||
|
roast_id: roast.id,
|
||||||
|
roast_date: None,
|
||||||
|
amount: 250.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let create_response = client
|
||||||
|
.post(app.api_url("/bags"))
|
||||||
|
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||||
|
.json(&new_bag)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to create bag");
|
||||||
|
|
||||||
|
let created_bag: Bag = create_response
|
||||||
|
.json()
|
||||||
|
.await
|
||||||
|
.expect("Failed to parse response");
|
||||||
|
|
||||||
|
let update_payload = UpdateBag {
|
||||||
|
remaining: Some(100.0),
|
||||||
|
closed: Some(true),
|
||||||
|
finished_at: Some(NaiveDate::from_ymd_opt(2023, 2, 1).unwrap()),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
let response = client
|
||||||
|
.put(app.api_url(&format!("/bags/{}", created_bag.id)))
|
||||||
|
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||||
|
.json(&update_payload)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to execute request");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assert_eq!(response.status(), 200);
|
||||||
|
let updated_bag: Bag = response.json().await.expect("Failed to parse response");
|
||||||
|
assert_eq!(updated_bag.remaining, 100.0);
|
||||||
|
assert!(updated_bag.closed);
|
||||||
|
assert_eq!(
|
||||||
|
updated_bag.finished_at,
|
||||||
|
Some(NaiveDate::from_ymd_opt(2023, 2, 1).unwrap())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn deleting_a_bag_returns_204() {
|
||||||
|
// Arrange
|
||||||
|
let app = spawn_app_with_auth().await;
|
||||||
|
let roaster = create_default_roaster(&app).await;
|
||||||
|
let roast = create_default_roast(&app, roaster.id).await;
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
|
let new_bag = NewBag {
|
||||||
|
roast_id: roast.id,
|
||||||
|
roast_date: None,
|
||||||
|
amount: 250.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let create_response = client
|
||||||
|
.post(app.api_url("/bags"))
|
||||||
|
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||||
|
.json(&new_bag)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to create bag");
|
||||||
|
|
||||||
|
let created_bag: Bag = create_response
|
||||||
|
.json()
|
||||||
|
.await
|
||||||
|
.expect("Failed to parse response");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
let response = client
|
||||||
|
.delete(app.api_url(&format!("/bags/{}", created_bag.id)))
|
||||||
|
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to execute request");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assert_eq!(response.status(), 204);
|
||||||
|
|
||||||
|
// Verify deletion
|
||||||
|
let get_response = client
|
||||||
|
.get(app.api_url(&format!("/bags/{}", created_bag.id)))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to execute request");
|
||||||
|
|
||||||
|
assert_eq!(get_response.status(), 404);
|
||||||
|
}
|
||||||
|
|
@ -10,6 +10,7 @@ use brewlog::domain::roasters::{NewRoaster, Roaster};
|
||||||
use brewlog::domain::users::NewUser;
|
use brewlog::domain::users::NewUser;
|
||||||
use brewlog::infrastructure::auth::hash_password;
|
use brewlog::infrastructure::auth::hash_password;
|
||||||
use brewlog::infrastructure::database::Database;
|
use brewlog::infrastructure::database::Database;
|
||||||
|
use brewlog::infrastructure::repositories::bags::SqlBagRepository;
|
||||||
use brewlog::infrastructure::repositories::roasters::SqlRoasterRepository;
|
use brewlog::infrastructure::repositories::roasters::SqlRoasterRepository;
|
||||||
use brewlog::infrastructure::repositories::roasts::SqlRoastRepository;
|
use brewlog::infrastructure::repositories::roasts::SqlRoastRepository;
|
||||||
use brewlog::infrastructure::repositories::sessions::SqlSessionRepository;
|
use brewlog::infrastructure::repositories::sessions::SqlSessionRepository;
|
||||||
|
|
@ -53,6 +54,7 @@ pub async fn spawn_app() -> TestApp {
|
||||||
// Create repositories
|
// Create repositories
|
||||||
let roaster_repo = Arc::new(SqlRoasterRepository::new(database.clone_pool()));
|
let roaster_repo = Arc::new(SqlRoasterRepository::new(database.clone_pool()));
|
||||||
let roast_repo = Arc::new(SqlRoastRepository::new(database.clone_pool()));
|
let roast_repo = Arc::new(SqlRoastRepository::new(database.clone_pool()));
|
||||||
|
let bag_repo = Arc::new(SqlBagRepository::new(database.clone_pool()));
|
||||||
let timeline_repo = Arc::new(SqlTimelineEventRepository::new(database.clone_pool()));
|
let timeline_repo = Arc::new(SqlTimelineEventRepository::new(database.clone_pool()));
|
||||||
let user_repo: Arc<dyn UserRepository> =
|
let user_repo: Arc<dyn UserRepository> =
|
||||||
Arc::new(SqlUserRepository::new(database.clone_pool()));
|
Arc::new(SqlUserRepository::new(database.clone_pool()));
|
||||||
|
|
@ -65,6 +67,7 @@ pub async fn spawn_app() -> TestApp {
|
||||||
let state = AppState::new(
|
let state = AppState::new(
|
||||||
roaster_repo.clone(),
|
roaster_repo.clone(),
|
||||||
roast_repo.clone(),
|
roast_repo.clone(),
|
||||||
|
bag_repo.clone(),
|
||||||
timeline_repo.clone(),
|
timeline_repo.clone(),
|
||||||
user_repo.clone(),
|
user_repo.clone(),
|
||||||
token_repo.clone(),
|
token_repo.clone(),
|
||||||
|
|
@ -154,6 +157,51 @@ pub async fn create_roaster_with_payload(app: &TestApp, payload: NewRoaster) ->
|
||||||
.expect("failed to deserialize roaster from response")
|
.expect("failed to deserialize roaster from response")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn create_roast_with_payload(
|
||||||
|
app: &TestApp,
|
||||||
|
payload: brewlog::domain::roasts::NewRoast,
|
||||||
|
) -> brewlog::domain::roasts::Roast {
|
||||||
|
let client = Client::new();
|
||||||
|
let mut request = client.post(app.api_url("/roasts")).json(&payload);
|
||||||
|
|
||||||
|
if let Some(token) = &app.auth_token {
|
||||||
|
request = request.bearer_auth(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = request
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("failed to create roast via API");
|
||||||
|
|
||||||
|
response
|
||||||
|
.json()
|
||||||
|
.await
|
||||||
|
.expect("failed to deserialize roast from response")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_default_roaster(app: &TestApp) -> Roaster {
|
||||||
|
create_roaster_with_name(app, "Test Roasters").await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_default_roast(
|
||||||
|
app: &TestApp,
|
||||||
|
roaster_id: brewlog::domain::ids::RoasterId,
|
||||||
|
) -> brewlog::domain::roasts::Roast {
|
||||||
|
create_roast_with_payload(
|
||||||
|
app,
|
||||||
|
brewlog::domain::roasts::NewRoast {
|
||||||
|
roaster_id,
|
||||||
|
name: "Test Roast".to_string(),
|
||||||
|
origin: "Ethiopia".to_string(),
|
||||||
|
region: "Yirgacheffe".to_string(),
|
||||||
|
producer: "Coop".to_string(),
|
||||||
|
tasting_notes: vec!["Blueberry".to_string()],
|
||||||
|
process: "Washed".to_string(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn create_roaster_with_name(app: &TestApp, name: &str) -> Roaster {
|
pub async fn create_roaster_with_name(app: &TestApp, name: &str) -> Roaster {
|
||||||
create_roaster_with_payload(
|
create_roaster_with_payload(
|
||||||
app,
|
app,
|
||||||
|
|
@ -167,7 +215,3 @@ pub async fn create_roaster_with_name(app: &TestApp, name: &str) -> Roaster {
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn create_default_roaster(app: &TestApp) -> Roaster {
|
|
||||||
create_roaster_with_name(app, "Test Roasters").await
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
pub mod auth_api;
|
pub mod auth_api;
|
||||||
|
pub mod bags_api;
|
||||||
pub mod helpers;
|
pub mod helpers;
|
||||||
pub mod roasters_api;
|
pub mod roasters_api;
|
||||||
pub mod roasts_api;
|
pub mod roasts_api;
|
||||||
|
|
|
||||||
|
|
@ -168,6 +168,79 @@ async fn creating_a_roast_surfaces_on_the_timeline() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn creating_a_bag_surfaces_on_the_timeline() {
|
||||||
|
let app = spawn_app_with_auth().await;
|
||||||
|
let client = Client::new();
|
||||||
|
|
||||||
|
let roaster_id = create_roaster_with_payload(
|
||||||
|
&app,
|
||||||
|
NewRoaster {
|
||||||
|
name: "Bag Timeline Roasters".to_string(),
|
||||||
|
country: "UK".to_string(),
|
||||||
|
city: Some("Bristol".to_string()),
|
||||||
|
homepage: Some("https://example.com".to_string()),
|
||||||
|
notes: None,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.id;
|
||||||
|
|
||||||
|
sleep(Duration::from_millis(5)).await;
|
||||||
|
let roast_name = "Bag Timeline Roast";
|
||||||
|
create_roast(&app, roaster_id, roast_name).await;
|
||||||
|
|
||||||
|
// Fetch the roast to get its ID
|
||||||
|
let roasts_response = client
|
||||||
|
.get(app.api_url("/roasts"))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("failed to fetch roasts");
|
||||||
|
|
||||||
|
assert_eq!(roasts_response.status(), 200);
|
||||||
|
let roasts: Vec<brewlog::domain::roasts::RoastWithRoaster> = roasts_response
|
||||||
|
.json()
|
||||||
|
.await
|
||||||
|
.expect("failed to parse roasts");
|
||||||
|
let roast_id = roasts.first().unwrap().roast.id;
|
||||||
|
|
||||||
|
// Create a bag
|
||||||
|
let bag_submission = serde_json::json!({
|
||||||
|
"roast_id": roast_id,
|
||||||
|
"roast_date": "2023-01-01",
|
||||||
|
"amount": 250.0
|
||||||
|
});
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.post(app.api_url("/bags"))
|
||||||
|
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||||
|
.json(&bag_submission)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("failed to create bag");
|
||||||
|
assert_eq!(response.status(), 201);
|
||||||
|
|
||||||
|
sleep(Duration::from_millis(10)).await;
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.get(format!("{}/timeline", app.address))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("failed to fetch timeline");
|
||||||
|
|
||||||
|
assert_eq!(response.status(), 200);
|
||||||
|
|
||||||
|
let body = response.text().await.expect("failed to read response body");
|
||||||
|
assert!(
|
||||||
|
body.contains("Bag Added"),
|
||||||
|
"Expected 'Bag Added' badge in timeline HTML, got: {body}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
body.contains(&format!("{}", roast_name)),
|
||||||
|
"Expected bag title to appear in timeline HTML, got: {body}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn timeline_page_signals_more_results_when_over_page_size() {
|
async fn timeline_page_signals_more_results_when_over_page_size() {
|
||||||
let app = spawn_app_with_auth().await;
|
let app = spawn_app_with_auth().await;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue