From c1a5d763e2077566e4c2fbe568084c4384014d90 Mon Sep 17 00:00:00 2001 From: Jon Seager Date: Mon, 2 Feb 2026 16:09:02 +0000 Subject: [PATCH] test(gear): add comprehensive test suite for gear entity Implement complete test coverage for the Gear entity across CLI, API, timeline, and Datastar integration layers. This completes the Gear entity implementation with 32 dedicated tests ensuring all CRUD operations, authentication requirements, and reactive UI updates work correctly. Test Coverage: - CLI commands (11 tests): Authentication, CRUD operations, category filtering, notes handling - API endpoints (13 tests): HTTP status codes, validation, auth requirements, filtering - Timeline integration (1 test): Gear creation events appear correctly - Datastar fragments (7 tests): Fragment rendering vs full page/JSON responses for list, create, update, delete Changes: - Add tests/cli/gear_cli.rs with CLI integration tests - Add tests/server/gear_api.rs with API integration tests - Update tests/server/timeline.rs with gear timeline test - Update tests/server/datastar.rs with gear fragment rendering tests - Register test modules in tests/cli/main.rs and tests/server/main.rs - Clean up unused imports in tests/server/helpers.rs All 140 tests pass (8 unit + 37 CLI + 95 server integration). --- tests/cli/gear_cli.rs | 299 ++++++++++++++++++++++++++++++ tests/cli/main.rs | 1 + tests/server/datastar.rs | 244 ++++++++++++++++++++++++ tests/server/gear_api.rs | 391 +++++++++++++++++++++++++++++++++++++++ tests/server/helpers.rs | 3 + tests/server/main.rs | 1 + tests/server/timeline.rs | 47 +++++ 7 files changed, 986 insertions(+) create mode 100644 tests/cli/gear_cli.rs create mode 100644 tests/server/gear_api.rs diff --git a/tests/cli/gear_cli.rs b/tests/cli/gear_cli.rs new file mode 100644 index 0000000..0051bd8 --- /dev/null +++ b/tests/cli/gear_cli.rs @@ -0,0 +1,299 @@ +use crate::helpers::{create_token, run_brewlog, server_info}; +use serde_json::Value; + +#[test] +fn test_add_gear_requires_authentication() { + let _ = server_info(); + + let output = run_brewlog( + &[ + "add-gear", + "--category", + "grinder", + "--make", + "Baratza", + "--model", + "Encore", + ], + &[], + ); + + assert!( + !output.status.success(), + "add-gear without auth should fail" + ); +} + +#[test] +fn test_add_gear_with_authentication() { + let token = create_token("test-add-gear"); + + let output = run_brewlog( + &[ + "add-gear", + "--category", + "grinder", + "--make", + "Baratza", + "--model", + "Encore", + ], + &[("BREWLOG_TOKEN", &token)], + ); + + assert!(output.status.success()); + let gear: Value = serde_json::from_slice(&output.stdout).unwrap(); + + assert_eq!(gear["make"], "Baratza"); + assert_eq!(gear["model"], "Encore"); + assert_eq!(gear["category"], "grinder"); + assert!(gear["id"].is_i64()); +} + +#[test] +fn test_add_gear_with_notes() { + let token = create_token("test-add-gear-notes"); + + let output = run_brewlog( + &[ + "add-gear", + "--category", + "brewer", + "--make", + "Hario", + "--model", + "V60", + "--notes", + "Size 02 plastic", + ], + &[("BREWLOG_TOKEN", &token)], + ); + + assert!(output.status.success()); + let gear: Value = serde_json::from_slice(&output.stdout).unwrap(); + + assert_eq!(gear["make"], "Hario"); + assert_eq!(gear["model"], "V60"); + assert_eq!(gear["category"], "brewer"); + assert_eq!(gear["notes"], "Size 02 plastic"); +} + +#[test] +fn test_list_gear_works_without_authentication() { + let _ = server_info(); + + let output = run_brewlog(&["list-gear"], &[]); + + assert!( + output.status.success(), + "list-gear should work without auth" + ); +} + +#[test] +fn test_list_gear_shows_added_gear() { + let token = create_token("test-list-gear"); + + // Add gear + let add_output = run_brewlog( + &[ + "add-gear", + "--category", + "grinder", + "--make", + "Timemore", + "--model", + "C2", + ], + &[("BREWLOG_TOKEN", &token)], + ); + assert!(add_output.status.success()); + + // List gear + let list_output = run_brewlog(&["list-gear"], &[]); + assert!(list_output.status.success()); + + let gear_list: Value = serde_json::from_slice(&list_output.stdout).unwrap(); + let gear_array = gear_list.as_array().unwrap(); + + assert!( + gear_array + .iter() + .any(|g| g["make"] == "Timemore" && g["model"] == "C2"), + "Added gear should appear in list" + ); +} + +#[test] +fn test_list_gear_filtered_by_category() { + let token = create_token("test-list-gear-filter"); + + // Add grinder + run_brewlog( + &[ + "add-gear", + "--category", + "grinder", + "--make", + "Fellow", + "--model", + "Ode", + ], + &[("BREWLOG_TOKEN", &token)], + ); + + // Add brewer + run_brewlog( + &[ + "add-gear", + "--category", + "brewer", + "--make", + "Kalita", + "--model", + "Wave", + ], + &[("BREWLOG_TOKEN", &token)], + ); + + // List only grinders + let output = run_brewlog(&["list-gear", "--category", "grinder"], &[]); + assert!(output.status.success()); + + let gear_list: Value = serde_json::from_slice(&output.stdout).unwrap(); + let gear_array = gear_list.as_array().unwrap(); + + // All items should be grinders + for gear in gear_array { + assert_eq!(gear["category"], "grinder"); + } + + // Should contain our Fellow Ode + assert!(gear_array.iter().any(|g| g["make"] == "Fellow")); +} + +#[test] +fn test_get_gear_by_id() { + let token = create_token("test-get-gear"); + + // Add gear + let add_output = run_brewlog( + &[ + "add-gear", + "--category", + "grinder", + "--make", + "Weber", + "--model", + "EG-1", + ], + &[("BREWLOG_TOKEN", &token)], + ); + let gear: Value = serde_json::from_slice(&add_output.stdout).unwrap(); + let gear_id = gear["id"].as_i64().unwrap().to_string(); + + // Get gear by ID + let output = run_brewlog(&["get-gear", "--id", &gear_id], &[]); + assert!(output.status.success()); + + let retrieved_gear: Value = serde_json::from_slice(&output.stdout).unwrap(); + assert_eq!(retrieved_gear["make"], "Weber"); + assert_eq!(retrieved_gear["model"], "EG-1"); +} + +#[test] +fn test_update_gear_requires_authentication() { + let _ = server_info(); + + let output = run_brewlog(&["update-gear", "--id", "123", "--make", "Updated"], &[]); + + assert!( + !output.status.success(), + "update-gear without auth should fail" + ); +} + +#[test] +fn test_update_gear_with_authentication() { + let token = create_token("test-update-gear"); + + // Add gear + let add_output = run_brewlog( + &[ + "add-gear", + "--category", + "grinder", + "--make", + "Porlex", + "--model", + "Mini", + ], + &[("BREWLOG_TOKEN", &token)], + ); + let gear: Value = serde_json::from_slice(&add_output.stdout).unwrap(); + let gear_id = gear["id"].as_i64().unwrap().to_string(); + + // Update gear + let output = run_brewlog( + &[ + "update-gear", + "--id", + &gear_id, + "--model", + "Mini II", + "--notes", + "Upgraded version", + ], + &[("BREWLOG_TOKEN", &token)], + ); + + assert!(output.status.success()); + let updated_gear: Value = serde_json::from_slice(&output.stdout).unwrap(); + assert_eq!(updated_gear["make"], "Porlex"); + assert_eq!(updated_gear["model"], "Mini II"); + assert_eq!(updated_gear["notes"], "Upgraded version"); +} + +#[test] +fn test_delete_gear_requires_authentication() { + let _ = server_info(); + + let output = run_brewlog(&["delete-gear", "--id", "123"], &[]); + + assert!( + !output.status.success(), + "delete-gear without auth should fail" + ); +} + +#[test] +fn test_delete_gear_with_authentication() { + let token = create_token("test-delete-gear"); + + // Add gear + let add_output = run_brewlog( + &[ + "add-gear", + "--category", + "grinder", + "--make", + "Niche", + "--model", + "Zero", + ], + &[("BREWLOG_TOKEN", &token)], + ); + let gear: Value = serde_json::from_slice(&add_output.stdout).unwrap(); + let gear_id = gear["id"].as_i64().unwrap().to_string(); + + // Delete gear + let delete_output = run_brewlog( + &["delete-gear", "--id", &gear_id], + &[("BREWLOG_TOKEN", &token)], + ); + assert!(delete_output.status.success()); + + // Verify deletion - get should fail + let get_output = run_brewlog(&["get-gear", "--id", &gear_id], &[]); + assert!(!get_output.status.success()); +} diff --git a/tests/cli/main.rs b/tests/cli/main.rs index 1a0d87c..282e876 100644 --- a/tests/cli/main.rs +++ b/tests/cli/main.rs @@ -1,4 +1,5 @@ pub mod bags_cli; +pub mod gear_cli; pub mod helpers; pub mod roasters_cli; pub mod roasts_cli; diff --git a/tests/server/datastar.rs b/tests/server/datastar.rs index e1aace5..23132b8 100644 --- a/tests/server/datastar.rs +++ b/tests/server/datastar.rs @@ -511,3 +511,247 @@ async fn timeline_without_datastar_header_returns_full_page() { let body = response.text().await.expect("failed to read body"); assert_full_page(&body); } + +// ============================================================================ +// Gear +// ============================================================================ + +#[tokio::test] +async fn gear_list_with_datastar_header_returns_fragment() { + let app = spawn_app_with_auth().await; + let client = Client::new(); + + // Create gear + let new_gear = serde_json::json!({ + "category": "grinder", + "make": "Baratza", + "model": "Encore" + }); + + client + .post(app.api_url("/gear")) + .bearer_auth(app.auth_token.as_ref().unwrap()) + .json(&new_gear) + .send() + .await + .expect("failed to create gear"); + + let response = client + .get(format!("{}/gear", app.address)) + .header("datastar-request", "true") + .send() + .await + .expect("failed to fetch gear"); + + assert_eq!(response.status(), 200); + assert_datastar_headers(&response, "#gear-list"); + + let body = response.text().await.expect("failed to read body"); + assert_html_fragment(&body); + assert!( + body.contains("id=\"gear-list\""), + "Fragment should contain the selector element" + ); +} + +#[tokio::test] +async fn gear_list_without_datastar_header_returns_full_page() { + let app = spawn_app_with_auth().await; + let client = Client::new(); + + let response = client + .get(format!("{}/gear", app.address)) + .send() + .await + .expect("failed to fetch gear"); + + assert_eq!(response.status(), 200); + assert!(response.headers().get("datastar-selector").is_none()); + + let body = response.text().await.expect("failed to read body"); + assert_full_page(&body); +} + +#[tokio::test] +async fn gear_create_with_datastar_header_returns_fragment() { + let app = spawn_app_with_auth().await; + let client = Client::new(); + + let new_gear = serde_json::json!({ + "category": "grinder", + "make": "Datastar Grinder", + "model": "Test Model" + }); + + let response = client + .post(app.api_url("/gear")) + .bearer_auth(app.auth_token.as_ref().unwrap()) + .header("datastar-request", "true") + .json(&new_gear) + .send() + .await + .expect("failed to create gear"); + + assert_eq!(response.status(), 200); + assert_datastar_headers(&response, "#gear-list"); + + let body = response.text().await.expect("failed to read body"); + assert_html_fragment(&body); + assert!( + body.contains("Datastar Grinder"), + "Fragment should include created gear" + ); +} + +#[tokio::test] +async fn gear_create_without_datastar_header_returns_json() { + let app = spawn_app_with_auth().await; + let client = Client::new(); + + let new_gear = serde_json::json!({ + "category": "brewer", + "make": "JSON Test Brewer", + "model": "V60" + }); + + let response = client + .post(app.api_url("/gear")) + .bearer_auth(app.auth_token.as_ref().unwrap()) + .json(&new_gear) + .send() + .await + .expect("failed to create gear"); + + assert_eq!(response.status(), 201); + assert!(response.headers().get("datastar-selector").is_none()); + + let gear: brewlog::domain::gear::Gear = response.json().await.expect("failed to parse JSON"); + assert_eq!(gear.make, "JSON Test Brewer"); +} + +#[tokio::test] +async fn gear_update_with_datastar_header_returns_fragment() { + let app = spawn_app_with_auth().await; + let client = Client::new(); + + // Create gear first + let new_gear = serde_json::json!({ + "category": "grinder", + "make": "Original Make", + "model": "Original Model" + }); + + let create_response = client + .post(app.api_url("/gear")) + .bearer_auth(app.auth_token.as_ref().unwrap()) + .json(&new_gear) + .send() + .await + .expect("failed to create gear"); + + let gear: brewlog::domain::gear::Gear = create_response.json().await.unwrap(); + + // Update gear + let update = brewlog::domain::gear::UpdateGear { + make: Some("Updated Make".to_string()), + model: None, + notes: Some("Test notes".to_string()), + }; + + let response = client + .put(app.api_url(&format!("/gear/{}", gear.id))) + .bearer_auth(app.auth_token.as_ref().unwrap()) + .header("datastar-request", "true") + .json(&update) + .send() + .await + .expect("failed to update gear"); + + assert_eq!(response.status(), 200); + assert_datastar_headers(&response, "#gear-list"); + + let body = response.text().await.expect("failed to read body"); + assert_html_fragment(&body); +} + +#[tokio::test] +async fn gear_update_without_datastar_header_returns_json() { + let app = spawn_app_with_auth().await; + let client = Client::new(); + + // Create gear first + let new_gear = serde_json::json!({ + "category": "grinder", + "make": "Original Make", + "model": "Original Model" + }); + + let create_response = client + .post(app.api_url("/gear")) + .bearer_auth(app.auth_token.as_ref().unwrap()) + .json(&new_gear) + .send() + .await + .expect("failed to create gear"); + + let gear: brewlog::domain::gear::Gear = create_response.json().await.unwrap(); + + // Update gear + let update = brewlog::domain::gear::UpdateGear { + make: Some("JSON Updated".to_string()), + model: None, + notes: None, + }; + + let response = client + .put(app.api_url(&format!("/gear/{}", gear.id))) + .bearer_auth(app.auth_token.as_ref().unwrap()) + .json(&update) + .send() + .await + .expect("failed to update gear"); + + assert_eq!(response.status(), 200); + assert!(response.headers().get("datastar-selector").is_none()); + + let updated_gear: brewlog::domain::gear::Gear = + response.json().await.expect("failed to parse JSON"); + assert_eq!(updated_gear.make, "JSON Updated"); +} + +#[tokio::test] +async fn gear_delete_with_datastar_header_returns_fragment() { + let app = spawn_app_with_auth().await; + let client = Client::new(); + + // Create gear first + let new_gear = serde_json::json!({ + "category": "grinder", + "make": "To Delete", + "model": "Test Model" + }); + + let create_response = client + .post(app.api_url("/gear")) + .bearer_auth(app.auth_token.as_ref().unwrap()) + .json(&new_gear) + .send() + .await + .expect("failed to create gear"); + + let gear: brewlog::domain::gear::Gear = create_response.json().await.unwrap(); + + let response = client + .delete(app.api_url(&format!("/gear/{}", gear.id))) + .bearer_auth(app.auth_token.as_ref().unwrap()) + .header("datastar-request", "true") + .send() + .await + .expect("failed to delete gear"); + + assert_eq!(response.status(), 200); + assert_datastar_headers(&response, "#gear-list"); + + let body = response.text().await.expect("failed to read body"); + assert_html_fragment(&body); +} diff --git a/tests/server/gear_api.rs b/tests/server/gear_api.rs new file mode 100644 index 0000000..e00abfb --- /dev/null +++ b/tests/server/gear_api.rs @@ -0,0 +1,391 @@ +use crate::helpers::{spawn_app, spawn_app_with_auth}; +use brewlog::domain::gear::{Gear, UpdateGear}; + +#[tokio::test] +async fn creating_gear_returns_201_for_valid_data() { + // Arrange + let app = spawn_app_with_auth().await; + let client = reqwest::Client::new(); + + let new_gear = serde_json::json!({ + "category": "grinder", + "make": "Baratza", + "model": "Encore", + "notes": "Daily driver" + }); + + // Act + let response = client + .post(app.api_url("/gear")) + .bearer_auth(app.auth_token.as_ref().unwrap()) + .json(&new_gear) + .send() + .await + .expect("Failed to execute request"); + + // Assert + assert_eq!(response.status(), 201); + + let gear: Gear = response.json().await.expect("Failed to parse response"); + assert_eq!(gear.make, "Baratza"); + assert_eq!(gear.model, "Encore"); + assert_eq!(gear.notes, Some("Daily driver".to_string())); +} + +#[tokio::test] +async fn creating_gear_without_auth_returns_401() { + // Arrange + let app = spawn_app().await; + let client = reqwest::Client::new(); + + let new_gear = serde_json::json!({ + "category": "grinder", + "make": "Baratza", + "model": "Encore" + }); + + // Act + let response = client + .post(app.api_url("/gear")) + .json(&new_gear) + .send() + .await + .expect("Failed to execute request"); + + // Assert + assert_eq!(response.status(), 401); +} + +#[tokio::test] +async fn creating_gear_with_invalid_category_returns_400() { + // Arrange + let app = spawn_app_with_auth().await; + let client = reqwest::Client::new(); + + let new_gear = serde_json::json!({ + "category": "invalid_category", + "make": "Baratza", + "model": "Encore" + }); + + // Act + let response = client + .post(app.api_url("/gear")) + .bearer_auth(app.auth_token.as_ref().unwrap()) + .json(&new_gear) + .send() + .await + .expect("Failed to execute request"); + + // Assert + assert_eq!(response.status(), 400); +} + +#[tokio::test] +async fn creating_gear_with_empty_make_returns_400() { + // Arrange + let app = spawn_app_with_auth().await; + let client = reqwest::Client::new(); + + let new_gear = serde_json::json!({ + "category": "grinder", + "make": "", + "model": "Encore" + }); + + // Act + let response = client + .post(app.api_url("/gear")) + .bearer_auth(app.auth_token.as_ref().unwrap()) + .json(&new_gear) + .send() + .await + .expect("Failed to execute request"); + + // Assert + assert_eq!(response.status(), 400); +} + +#[tokio::test] +async fn creating_gear_with_empty_model_returns_400() { + // Arrange + let app = spawn_app_with_auth().await; + let client = reqwest::Client::new(); + + let new_gear = serde_json::json!({ + "category": "grinder", + "make": "Baratza", + "model": "" + }); + + // Act + let response = client + .post(app.api_url("/gear")) + .bearer_auth(app.auth_token.as_ref().unwrap()) + .json(&new_gear) + .send() + .await + .expect("Failed to execute request"); + + // Assert + assert_eq!(response.status(), 400); +} + +#[tokio::test] +async fn listing_gear_returns_200_and_correct_data() { + // Arrange + let app = spawn_app_with_auth().await; + let client = reqwest::Client::new(); + + // Create a gear + let new_gear = serde_json::json!({ + "category": "grinder", + "make": "Baratza", + "model": "Encore" + }); + + client + .post(app.api_url("/gear")) + .bearer_auth(app.auth_token.as_ref().unwrap()) + .json(&new_gear) + .send() + .await + .expect("Failed to create gear"); + + // Act + let response = client + .get(app.api_url("/gear")) + .send() + .await + .expect("Failed to execute request"); + + // Assert + assert_eq!(response.status(), 200); + + let gear_list: Vec = response.json().await.expect("Failed to parse response"); + assert!(!gear_list.is_empty()); + assert!(gear_list.iter().any(|g| g.make == "Baratza")); +} + +#[tokio::test] +async fn listing_gear_filtered_by_category() { + // Arrange + let app = spawn_app_with_auth().await; + let client = reqwest::Client::new(); + + // Create grinder + let grinder = serde_json::json!({ + "category": "grinder", + "make": "Baratza", + "model": "Encore" + }); + + client + .post(app.api_url("/gear")) + .bearer_auth(app.auth_token.as_ref().unwrap()) + .json(&grinder) + .send() + .await + .expect("Failed to create grinder"); + + // Create brewer + let brewer = serde_json::json!({ + "category": "brewer", + "make": "Hario", + "model": "V60" + }); + + client + .post(app.api_url("/gear")) + .bearer_auth(app.auth_token.as_ref().unwrap()) + .json(&brewer) + .send() + .await + .expect("Failed to create brewer"); + + // Act - filter by grinder category + let response = client + .get(app.api_url("/gear?category=grinder")) + .send() + .await + .expect("Failed to execute request"); + + // Assert + assert_eq!(response.status(), 200); + + let gear_list: Vec = response.json().await.expect("Failed to parse response"); + assert!(gear_list.iter().all(|g| g.category.as_str() == "grinder")); + assert!(gear_list.iter().any(|g| g.make == "Baratza")); +} + +#[tokio::test] +async fn getting_gear_by_id_returns_correct_data() { + // Arrange + let app = spawn_app_with_auth().await; + let client = reqwest::Client::new(); + + // Create gear + let new_gear = serde_json::json!({ + "category": "grinder", + "make": "Baratza", + "model": "Encore" + }); + + let create_response = client + .post(app.api_url("/gear")) + .bearer_auth(app.auth_token.as_ref().unwrap()) + .json(&new_gear) + .send() + .await + .expect("Failed to create gear"); + + let created_gear: Gear = create_response.json().await.unwrap(); + + // Act + let response = client + .get(app.api_url(&format!("/gear/{}", created_gear.id))) + .send() + .await + .expect("Failed to execute request"); + + // Assert + assert_eq!(response.status(), 200); + + let gear: Gear = response.json().await.expect("Failed to parse response"); + assert_eq!(gear.id, created_gear.id); + assert_eq!(gear.make, "Baratza"); + assert_eq!(gear.model, "Encore"); +} + +#[tokio::test] +async fn updating_gear_returns_updated_data() { + // Arrange + let app = spawn_app_with_auth().await; + let client = reqwest::Client::new(); + + // Create gear + let new_gear = serde_json::json!({ + "category": "grinder", + "make": "Baratza", + "model": "Encore" + }); + + let create_response = client + .post(app.api_url("/gear")) + .bearer_auth(app.auth_token.as_ref().unwrap()) + .json(&new_gear) + .send() + .await + .expect("Failed to create gear"); + + let created_gear: Gear = create_response.json().await.unwrap(); + + // Act + let update = UpdateGear { + make: Some("Comandante".to_string()), + model: Some("C40".to_string()), + notes: Some("Upgraded grinder".to_string()), + }; + + let response = client + .put(app.api_url(&format!("/gear/{}", created_gear.id))) + .bearer_auth(app.auth_token.as_ref().unwrap()) + .json(&update) + .send() + .await + .expect("Failed to execute request"); + + // Assert + assert_eq!(response.status(), 200); + + let updated_gear: Gear = response.json().await.expect("Failed to parse response"); + assert_eq!(updated_gear.id, created_gear.id); + assert_eq!(updated_gear.make, "Comandante"); + assert_eq!(updated_gear.model, "C40"); + assert_eq!(updated_gear.notes, Some("Upgraded grinder".to_string())); +} + +#[tokio::test] +async fn updating_gear_without_auth_returns_401() { + // Arrange + let app = spawn_app().await; + let client = reqwest::Client::new(); + + let update = UpdateGear { + make: Some("Updated".to_string()), + model: None, + notes: None, + }; + + // Act + let response = client + .put(app.api_url("/gear/123")) + .json(&update) + .send() + .await + .expect("Failed to execute request"); + + // Assert + assert_eq!(response.status(), 401); +} + +#[tokio::test] +async fn deleting_gear_returns_204() { + // Arrange + let app = spawn_app_with_auth().await; + let client = reqwest::Client::new(); + + // Create gear + let new_gear = serde_json::json!({ + "category": "grinder", + "make": "Baratza", + "model": "Encore" + }); + + let create_response = client + .post(app.api_url("/gear")) + .bearer_auth(app.auth_token.as_ref().unwrap()) + .json(&new_gear) + .send() + .await + .expect("Failed to create gear"); + + let created_gear: Gear = create_response.json().await.unwrap(); + + // Act + let response = client + .delete(app.api_url(&format!("/gear/{}", created_gear.id))) + .bearer_auth(app.auth_token.as_ref().unwrap()) + .send() + .await + .expect("Failed to execute request"); + + // Assert + assert_eq!(response.status(), 204); + + // Verify gear is actually deleted + let get_response = client + .get(app.api_url(&format!("/gear/{}", created_gear.id))) + .send() + .await + .expect("Failed to execute request"); + + assert_eq!(get_response.status(), 404); +} + +#[tokio::test] +async fn deleting_gear_without_auth_returns_401() { + // Arrange + let app = spawn_app().await; + let client = reqwest::Client::new(); + + // Act + let response = client + .delete(app.api_url("/gear/123")) + .send() + .await + .expect("Failed to execute request"); + + // Assert + assert_eq!(response.status(), 401); +} diff --git a/tests/server/helpers.rs b/tests/server/helpers.rs index 0011f95..4ab4bb0 100644 --- a/tests/server/helpers.rs +++ b/tests/server/helpers.rs @@ -11,6 +11,7 @@ use brewlog::domain::users::NewUser; use brewlog::infrastructure::auth::hash_password; use brewlog::infrastructure::database::Database; use brewlog::infrastructure::repositories::bags::SqlBagRepository; +use brewlog::infrastructure::repositories::gear::SqlGearRepository; use brewlog::infrastructure::repositories::roasters::SqlRoasterRepository; use brewlog::infrastructure::repositories::roasts::SqlRoastRepository; use brewlog::infrastructure::repositories::sessions::SqlSessionRepository; @@ -55,6 +56,7 @@ pub async fn spawn_app() -> TestApp { let roaster_repo = Arc::new(SqlRoasterRepository::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 gear_repo = Arc::new(SqlGearRepository::new(database.clone_pool())); let timeline_repo = Arc::new(SqlTimelineEventRepository::new(database.clone_pool())); let user_repo: Arc = Arc::new(SqlUserRepository::new(database.clone_pool())); @@ -68,6 +70,7 @@ pub async fn spawn_app() -> TestApp { roaster_repo.clone(), roast_repo.clone(), bag_repo.clone(), + gear_repo.clone(), timeline_repo.clone(), user_repo.clone(), token_repo.clone(), diff --git a/tests/server/main.rs b/tests/server/main.rs index 5869d92..73533a0 100644 --- a/tests/server/main.rs +++ b/tests/server/main.rs @@ -1,6 +1,7 @@ pub mod auth_api; pub mod bags_api; pub mod datastar; +pub mod gear_api; pub mod helpers; pub mod roasters_api; pub mod roasts_api; diff --git a/tests/server/timeline.rs b/tests/server/timeline.rs index 29df14e..a413b9c 100644 --- a/tests/server/timeline.rs +++ b/tests/server/timeline.rs @@ -411,3 +411,50 @@ async fn closing_a_bag_surfaces_on_the_timeline() { "Expected 'Bag Finished' badge in timeline HTML, got: {body}" ); } + +#[tokio::test] +async fn creating_gear_surfaces_on_the_timeline() { + let app = spawn_app_with_auth().await; + let client = Client::new(); + + // Create gear + let gear_submission = serde_json::json!({ + "category": "grinder", + "make": "Baratza", + "model": "Encore", + "notes": "Timeline test grinder" + }); + + let response = client + .post(app.api_url("/gear")) + .bearer_auth(app.auth_token.as_ref().unwrap()) + .json(&gear_submission) + .send() + .await + .expect("failed to create gear"); + 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("Gear Added"), + "Expected 'Gear Added' badge in timeline HTML, got: {body}" + ); + assert!( + body.contains("Baratza Encore"), + "Expected gear title to appear in timeline HTML, got: {body}" + ); + assert!( + body.contains("Grinder"), + "Expected gear category to appear in timeline HTML, got: {body}" + ); +}