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).
This commit is contained in:
Jon Seager 2026-02-02 16:09:02 +00:00
parent d1ebe835f9
commit c1a5d763e2
No known key found for this signature in database
7 changed files with 986 additions and 0 deletions

299
tests/cli/gear_cli.rs Normal file
View file

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

View file

@ -1,4 +1,5 @@
pub mod bags_cli; pub mod bags_cli;
pub mod gear_cli;
pub mod helpers; pub mod helpers;
pub mod roasters_cli; pub mod roasters_cli;
pub mod roasts_cli; pub mod roasts_cli;

View file

@ -511,3 +511,247 @@ async fn timeline_without_datastar_header_returns_full_page() {
let body = response.text().await.expect("failed to read body"); let body = response.text().await.expect("failed to read body");
assert_full_page(&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);
}

391
tests/server/gear_api.rs Normal file
View file

@ -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<Gear> = 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<Gear> = 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);
}

View file

@ -11,6 +11,7 @@ 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::bags::SqlBagRepository;
use brewlog::infrastructure::repositories::gear::SqlGearRepository;
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;
@ -55,6 +56,7 @@ pub async fn spawn_app() -> TestApp {
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 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 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()));
@ -68,6 +70,7 @@ pub async fn spawn_app() -> TestApp {
roaster_repo.clone(), roaster_repo.clone(),
roast_repo.clone(), roast_repo.clone(),
bag_repo.clone(), bag_repo.clone(),
gear_repo.clone(),
timeline_repo.clone(), timeline_repo.clone(),
user_repo.clone(), user_repo.clone(),
token_repo.clone(), token_repo.clone(),

View file

@ -1,6 +1,7 @@
pub mod auth_api; pub mod auth_api;
pub mod bags_api; pub mod bags_api;
pub mod datastar; pub mod datastar;
pub mod gear_api;
pub mod helpers; pub mod helpers;
pub mod roasters_api; pub mod roasters_api;
pub mod roasts_api; pub mod roasts_api;

View file

@ -411,3 +411,50 @@ async fn closing_a_bag_surfaces_on_the_timeline() {
"Expected 'Bag Finished' badge in timeline HTML, got: {body}" "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}"
);
}