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