Add {contentType: 'form'} to all edit template @put() calls so form
inputs are submitted by name rather than as Datastar signals. Replace
the brew edit quick notes text input with toggle pill buttons. Recompute
bag remaining when amount is updated to account for brew deductions.
Add CLI tests for brew and cup update commands.
360 lines
10 KiB
Rust
360 lines
10 KiB
Rust
use crate::helpers::{
|
|
create_default_bag, create_default_gear, 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,
|
|
created_at: None,
|
|
};
|
|
|
|
// 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,
|
|
created_at: None,
|
|
};
|
|
|
|
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,
|
|
created_at: None,
|
|
};
|
|
|
|
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,
|
|
created_at: None,
|
|
};
|
|
|
|
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()),
|
|
..Default::default()
|
|
};
|
|
|
|
// 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,
|
|
created_at: None,
|
|
};
|
|
|
|
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);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn closing_a_bag_automatically_sets_finished_at() {
|
|
// 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,
|
|
created_at: None,
|
|
};
|
|
|
|
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 = serde_json::json!({
|
|
"closed": true
|
|
});
|
|
|
|
// 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!(updated_bag.closed);
|
|
assert!(updated_bag.finished_at.is_some());
|
|
assert_eq!(
|
|
updated_bag.finished_at.unwrap(),
|
|
chrono::Utc::now().date_naive()
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn updating_bag_amount_recomputes_remaining() {
|
|
// Arrange: create a bag and brew from it to consume some coffee
|
|
let app = spawn_app_with_auth().await;
|
|
let roaster = create_default_roaster(&app).await;
|
|
let roast = create_default_roast(&app, roaster.id).await;
|
|
let bag = create_default_bag(&app, roast.id).await;
|
|
let grinder = create_default_gear(&app, "grinder", "Comandante", "C40 MK4").await;
|
|
let brewer = create_default_gear(&app, "brewer", "Hario", "V60 02").await;
|
|
let client = reqwest::Client::new();
|
|
|
|
// Brew 15g from the 250g bag → remaining should be 235g
|
|
let new_brew = brewlog::domain::brews::NewBrew {
|
|
bag_id: bag.id,
|
|
coffee_weight: 15.0,
|
|
grinder_id: grinder.id,
|
|
grind_setting: 24.0,
|
|
brewer_id: brewer.id,
|
|
filter_paper_id: None,
|
|
water_volume: 250,
|
|
water_temp: 92.0,
|
|
quick_notes: Vec::new(),
|
|
brew_time: None,
|
|
created_at: None,
|
|
};
|
|
|
|
client
|
|
.post(app.api_url("/brews"))
|
|
.bearer_auth(app.auth_token.as_ref().unwrap())
|
|
.json(&new_brew)
|
|
.send()
|
|
.await
|
|
.expect("Failed to create brew");
|
|
|
|
// Act: update the bag amount from 250g to 500g
|
|
let update_payload = UpdateBag {
|
|
amount: Some(500.0),
|
|
..Default::default()
|
|
};
|
|
|
|
let response = client
|
|
.put(app.api_url(&format!("/bags/{}", bag.id)))
|
|
.bearer_auth(app.auth_token.as_ref().unwrap())
|
|
.json(&update_payload)
|
|
.send()
|
|
.await
|
|
.expect("Failed to execute request");
|
|
|
|
// Assert: remaining should be 500 - 15 = 485 (recomputed from consumed)
|
|
assert_eq!(response.status(), 200);
|
|
let updated_bag: Bag = response.json().await.expect("Failed to parse response");
|
|
assert_eq!(updated_bag.amount, 500.0);
|
|
assert_eq!(updated_bag.remaining, 485.0);
|
|
}
|