brewlog/tests/server/bags_api.rs
Jon Seager 9650735057
fix: wire edit forms for Datastar submission and recompute bag remaining
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.
2026-02-10 19:43:00 +00:00

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);
}