test(cafes): add API, CLI, Datastar, timeline, and backup tests
Add 14 server API tests, 6 CLI tests, 4 Datastar fragment tests, 1 timeline event test, and backup round-trip verification. Wire cafe_repo into test helpers.
This commit is contained in:
parent
3f83d74e20
commit
069e861255
9 changed files with 805 additions and 8 deletions
148
tests/cli/cafes_cli.rs
Normal file
148
tests/cli/cafes_cli.rs
Normal file
|
|
@ -0,0 +1,148 @@
|
||||||
|
use crate::helpers::{create_cafe, create_token, run_brewlog, server_info};
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_add_cafe_requires_authentication() {
|
||||||
|
let _ = server_info();
|
||||||
|
|
||||||
|
let output = run_brewlog(
|
||||||
|
&[
|
||||||
|
"cafe",
|
||||||
|
"add",
|
||||||
|
"--name",
|
||||||
|
"Test Cafe",
|
||||||
|
"--city",
|
||||||
|
"London",
|
||||||
|
"--country",
|
||||||
|
"UK",
|
||||||
|
"--latitude",
|
||||||
|
"51.5074",
|
||||||
|
"--longitude",
|
||||||
|
"-0.1278",
|
||||||
|
],
|
||||||
|
&[],
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
!output.status.success(),
|
||||||
|
"cafe add without auth should fail"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_add_cafe_with_authentication() {
|
||||||
|
let token = create_token("test-add-cafe");
|
||||||
|
|
||||||
|
let output = run_brewlog(
|
||||||
|
&[
|
||||||
|
"cafe",
|
||||||
|
"add",
|
||||||
|
"--name",
|
||||||
|
"Test Cafe",
|
||||||
|
"--city",
|
||||||
|
"London",
|
||||||
|
"--country",
|
||||||
|
"UK",
|
||||||
|
"--latitude",
|
||||||
|
"51.5074",
|
||||||
|
"--longitude",
|
||||||
|
"-0.1278",
|
||||||
|
],
|
||||||
|
&[("BREWLOG_TOKEN", &token)],
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
output.status.success(),
|
||||||
|
"cafe add with auth should succeed: {}",
|
||||||
|
String::from_utf8_lossy(&output.stderr)
|
||||||
|
);
|
||||||
|
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
let cafe: Value = serde_json::from_str(&stdout)
|
||||||
|
.unwrap_or_else(|_| panic!("Should output valid JSON, got: {}", stdout));
|
||||||
|
|
||||||
|
assert_eq!(cafe["name"], "Test Cafe");
|
||||||
|
assert_eq!(cafe["city"], "London");
|
||||||
|
assert_eq!(cafe["country"], "UK");
|
||||||
|
assert!(cafe["id"].is_i64(), "Should have an ID");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_list_cafes_works_without_authentication() {
|
||||||
|
let _ = server_info();
|
||||||
|
|
||||||
|
let output = run_brewlog(&["cafe", "list"], &[]);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
output.status.success(),
|
||||||
|
"cafe list should work without auth"
|
||||||
|
);
|
||||||
|
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
let cafes: Value = serde_json::from_str(&stdout).expect("Should output valid JSON array");
|
||||||
|
|
||||||
|
assert!(cafes.is_array(), "Should return an array");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_list_cafes_shows_added_cafe() {
|
||||||
|
let token = create_token("test-list-cafes");
|
||||||
|
|
||||||
|
let cafe_id = create_cafe(
|
||||||
|
"List Test Cafe",
|
||||||
|
"Bristol",
|
||||||
|
"UK",
|
||||||
|
"51.4545",
|
||||||
|
"-2.5879",
|
||||||
|
&token,
|
||||||
|
);
|
||||||
|
|
||||||
|
let list_output = run_brewlog(&["cafe", "list"], &[]);
|
||||||
|
|
||||||
|
assert!(list_output.status.success());
|
||||||
|
|
||||||
|
let list_stdout = String::from_utf8_lossy(&list_output.stdout);
|
||||||
|
let cafes: Value = serde_json::from_str(&list_stdout).expect("Should output valid JSON array");
|
||||||
|
|
||||||
|
assert!(cafes.is_array(), "Should return an array");
|
||||||
|
let cafes_array = cafes.as_array().unwrap();
|
||||||
|
|
||||||
|
let found = cafes_array
|
||||||
|
.iter()
|
||||||
|
.any(|c| c["id"].as_i64().unwrap().to_string() == cafe_id);
|
||||||
|
assert!(found, "Should find the added cafe in the list");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_delete_cafe_requires_authentication() {
|
||||||
|
let _ = server_info();
|
||||||
|
|
||||||
|
let output = run_brewlog(&["cafe", "delete", "--id", "some-id"], &[]);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
!output.status.success(),
|
||||||
|
"cafe delete without auth should fail"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_update_cafe_requires_authentication() {
|
||||||
|
let _ = server_info();
|
||||||
|
|
||||||
|
let output = run_brewlog(
|
||||||
|
&[
|
||||||
|
"cafe",
|
||||||
|
"update",
|
||||||
|
"--id",
|
||||||
|
"some-id",
|
||||||
|
"--name",
|
||||||
|
"Updated Name",
|
||||||
|
],
|
||||||
|
&[],
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
!output.status.success(),
|
||||||
|
"cafe update without auth should fail"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -224,3 +224,45 @@ pub fn create_roast(roaster_id: &str, name: &str, token: &str) -> String {
|
||||||
.expect("roast id should be numeric")
|
.expect("roast id should be numeric")
|
||||||
.to_string()
|
.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Helper to create a cafe and return its ID
|
||||||
|
pub fn create_cafe(
|
||||||
|
name: &str,
|
||||||
|
city: &str,
|
||||||
|
country: &str,
|
||||||
|
latitude: &str,
|
||||||
|
longitude: &str,
|
||||||
|
token: &str,
|
||||||
|
) -> String {
|
||||||
|
let output = run_brewlog(
|
||||||
|
&[
|
||||||
|
"cafe",
|
||||||
|
"add",
|
||||||
|
"--name",
|
||||||
|
name,
|
||||||
|
"--city",
|
||||||
|
city,
|
||||||
|
"--country",
|
||||||
|
country,
|
||||||
|
"--latitude",
|
||||||
|
latitude,
|
||||||
|
"--longitude",
|
||||||
|
longitude,
|
||||||
|
],
|
||||||
|
&[("BREWLOG_TOKEN", token)],
|
||||||
|
);
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
panic!(
|
||||||
|
"Failed to create cafe: {}",
|
||||||
|
String::from_utf8_lossy(&output.stderr)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
let cafe: serde_json::Value = serde_json::from_str(&stdout).expect("Should output valid JSON");
|
||||||
|
cafe["id"]
|
||||||
|
.as_i64()
|
||||||
|
.expect("cafe id should be numeric")
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
pub mod bags_cli;
|
pub mod bags_cli;
|
||||||
pub mod brews_cli;
|
pub mod brews_cli;
|
||||||
|
pub mod cafes_cli;
|
||||||
pub mod gear_cli;
|
pub mod gear_cli;
|
||||||
pub mod helpers;
|
pub mod helpers;
|
||||||
pub mod roasters_cli;
|
pub mod roasters_cli;
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,12 @@ use std::sync::Arc;
|
||||||
|
|
||||||
use brewlog::domain::bags::{Bag, BagFilter, BagSortKey, NewBag};
|
use brewlog::domain::bags::{Bag, BagFilter, BagSortKey, NewBag};
|
||||||
use brewlog::domain::brews::{Brew, BrewFilter, BrewSortKey, NewBrew};
|
use brewlog::domain::brews::{Brew, BrewFilter, BrewSortKey, NewBrew};
|
||||||
|
use brewlog::domain::cafes::{Cafe, CafeSortKey, NewCafe};
|
||||||
use brewlog::domain::gear::{Gear, GearCategory, GearFilter, GearSortKey, NewGear};
|
use brewlog::domain::gear::{Gear, GearCategory, GearFilter, GearSortKey, NewGear};
|
||||||
use brewlog::domain::listing::{ListRequest, PageSize};
|
use brewlog::domain::listing::{ListRequest, PageSize};
|
||||||
use brewlog::domain::repositories::{
|
use brewlog::domain::repositories::{
|
||||||
BagRepository, BrewRepository, GearRepository, RoastRepository, RoasterRepository,
|
BagRepository, BrewRepository, CafeRepository, GearRepository, RoastRepository,
|
||||||
TimelineEventRepository,
|
RoasterRepository, TimelineEventRepository,
|
||||||
};
|
};
|
||||||
use brewlog::domain::roasters::{NewRoaster, Roaster, RoasterSortKey};
|
use brewlog::domain::roasters::{NewRoaster, Roaster, RoasterSortKey};
|
||||||
use brewlog::domain::roasts::{NewRoast, Roast, RoastSortKey};
|
use brewlog::domain::roasts::{NewRoast, Roast, RoastSortKey};
|
||||||
|
|
@ -15,6 +16,7 @@ use brewlog::infrastructure::backup::{BackupData, BackupService};
|
||||||
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::brews::SqlBrewRepository;
|
use brewlog::infrastructure::repositories::brews::SqlBrewRepository;
|
||||||
|
use brewlog::infrastructure::repositories::cafes::SqlCafeRepository;
|
||||||
use brewlog::infrastructure::repositories::gear::SqlGearRepository;
|
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;
|
||||||
|
|
@ -26,6 +28,7 @@ struct TestDb {
|
||||||
bag_repo: Arc<dyn BagRepository>,
|
bag_repo: Arc<dyn BagRepository>,
|
||||||
gear_repo: Arc<dyn GearRepository>,
|
gear_repo: Arc<dyn GearRepository>,
|
||||||
brew_repo: Arc<dyn BrewRepository>,
|
brew_repo: Arc<dyn BrewRepository>,
|
||||||
|
cafe_repo: Arc<dyn CafeRepository>,
|
||||||
timeline_repo: Arc<dyn TimelineEventRepository>,
|
timeline_repo: Arc<dyn TimelineEventRepository>,
|
||||||
backup_service: BackupService,
|
backup_service: BackupService,
|
||||||
}
|
}
|
||||||
|
|
@ -43,6 +46,7 @@ async fn create_test_db() -> TestDb {
|
||||||
bag_repo: Arc::new(SqlBagRepository::new(pool.clone())),
|
bag_repo: Arc::new(SqlBagRepository::new(pool.clone())),
|
||||||
gear_repo: Arc::new(SqlGearRepository::new(pool.clone())),
|
gear_repo: Arc::new(SqlGearRepository::new(pool.clone())),
|
||||||
brew_repo: Arc::new(SqlBrewRepository::new(pool.clone())),
|
brew_repo: Arc::new(SqlBrewRepository::new(pool.clone())),
|
||||||
|
cafe_repo: Arc::new(SqlCafeRepository::new(pool.clone())),
|
||||||
timeline_repo: Arc::new(SqlTimelineEventRepository::new(pool.clone())),
|
timeline_repo: Arc::new(SqlTimelineEventRepository::new(pool.clone())),
|
||||||
backup_service: BackupService::new(pool),
|
backup_service: BackupService::new(pool),
|
||||||
}
|
}
|
||||||
|
|
@ -87,6 +91,13 @@ async fn list_all_gear(repo: &dyn GearRepository) -> Vec<Gear> {
|
||||||
.items
|
.items
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn list_all_cafes(repo: &dyn CafeRepository) -> Vec<Cafe> {
|
||||||
|
repo.list(&list_all_request::<CafeSortKey>(), None)
|
||||||
|
.await
|
||||||
|
.expect("failed to list cafes")
|
||||||
|
.items
|
||||||
|
}
|
||||||
|
|
||||||
async fn list_all_brews(repo: &dyn BrewRepository) -> Vec<Brew> {
|
async fn list_all_brews(repo: &dyn BrewRepository) -> Vec<Brew> {
|
||||||
let page = repo
|
let page = repo
|
||||||
.list(BrewFilter::all(), &list_all_request::<BrewSortKey>(), None)
|
.list(BrewFilter::all(), &list_all_request::<BrewSortKey>(), None)
|
||||||
|
|
@ -102,7 +113,7 @@ async fn list_all_timeline_events(repo: &dyn TimelineEventRepository) -> Vec<Tim
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Populate a database with representative test data and return the key entities.
|
/// Populate a database with representative test data and return the key entities.
|
||||||
async fn populate_test_data(db: &TestDb) -> (Roaster, Roast, Bag, Gear, Gear, Gear, Brew) {
|
async fn populate_test_data(db: &TestDb) -> (Roaster, Roast, Bag, Gear, Gear, Gear, Brew, Cafe) {
|
||||||
// Create roaster
|
// Create roaster
|
||||||
let roaster = db
|
let roaster = db
|
||||||
.roaster_repo
|
.roaster_repo
|
||||||
|
|
@ -205,14 +216,38 @@ async fn populate_test_data(db: &TestDb) -> (Roaster, Roast, Bag, Gear, Gear, Ge
|
||||||
"bag remaining should be 235 after brew"
|
"bag remaining should be 235 after brew"
|
||||||
);
|
);
|
||||||
|
|
||||||
(roaster, roast, bag, grinder, brewer, filter_paper, brew)
|
// Create cafe
|
||||||
|
let cafe = db
|
||||||
|
.cafe_repo
|
||||||
|
.insert(NewCafe {
|
||||||
|
name: "Prufrock".to_string(),
|
||||||
|
city: "London".to_string(),
|
||||||
|
country: "UK".to_string(),
|
||||||
|
latitude: 51.5246,
|
||||||
|
longitude: -0.1098,
|
||||||
|
website: Some("https://prufrockcoffee.com".to_string()),
|
||||||
|
notes: Some("Award-winning espresso bar".to_string()),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect("failed to create cafe");
|
||||||
|
|
||||||
|
(
|
||||||
|
roaster,
|
||||||
|
roast,
|
||||||
|
bag,
|
||||||
|
grinder,
|
||||||
|
brewer,
|
||||||
|
filter_paper,
|
||||||
|
brew,
|
||||||
|
cafe,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn backup_and_restore_round_trip() {
|
async fn backup_and_restore_round_trip() {
|
||||||
// 1. Create source database and populate with test data
|
// 1. Create source database and populate with test data
|
||||||
let source = create_test_db().await;
|
let source = create_test_db().await;
|
||||||
let (roaster, roast, bag, grinder, brewer, filter_paper, brew) =
|
let (roaster, roast, bag, grinder, brewer, filter_paper, brew, cafe) =
|
||||||
populate_test_data(&source).await;
|
populate_test_data(&source).await;
|
||||||
|
|
||||||
// Verify timeline events were created (roaster + roast inserts create them)
|
// Verify timeline events were created (roaster + roast inserts create them)
|
||||||
|
|
@ -235,6 +270,7 @@ async fn backup_and_restore_round_trip() {
|
||||||
assert_eq!(backup_data.bags.len(), 1);
|
assert_eq!(backup_data.bags.len(), 1);
|
||||||
assert_eq!(backup_data.gear.len(), 3);
|
assert_eq!(backup_data.gear.len(), 3);
|
||||||
assert_eq!(backup_data.brews.len(), 1);
|
assert_eq!(backup_data.brews.len(), 1);
|
||||||
|
assert_eq!(backup_data.cafes.len(), 1);
|
||||||
assert_eq!(backup_data.timeline_events.len(), source_timeline.len());
|
assert_eq!(backup_data.timeline_events.len(), source_timeline.len());
|
||||||
|
|
||||||
// 3. Serialize to JSON and deserialize back (verify serde round-trip)
|
// 3. Serialize to JSON and deserialize back (verify serde round-trip)
|
||||||
|
|
@ -248,6 +284,7 @@ async fn backup_and_restore_round_trip() {
|
||||||
assert_eq!(restored_data.bags.len(), 1);
|
assert_eq!(restored_data.bags.len(), 1);
|
||||||
assert_eq!(restored_data.gear.len(), 3);
|
assert_eq!(restored_data.gear.len(), 3);
|
||||||
assert_eq!(restored_data.brews.len(), 1);
|
assert_eq!(restored_data.brews.len(), 1);
|
||||||
|
assert_eq!(restored_data.cafes.len(), 1);
|
||||||
|
|
||||||
// 4. Restore to a fresh database
|
// 4. Restore to a fresh database
|
||||||
let target = create_test_db().await;
|
let target = create_test_db().await;
|
||||||
|
|
@ -329,6 +366,20 @@ async fn backup_and_restore_round_trip() {
|
||||||
assert_eq!(restored_brew.water_volume, 250);
|
assert_eq!(restored_brew.water_volume, 250);
|
||||||
assert_eq!(restored_brew.water_temp, 93.5);
|
assert_eq!(restored_brew.water_temp, 93.5);
|
||||||
|
|
||||||
|
// Cafes
|
||||||
|
let target_cafes = list_all_cafes(target.cafe_repo.as_ref()).await;
|
||||||
|
assert_eq!(target_cafes.len(), 1);
|
||||||
|
let restored_cafe = &target_cafes[0];
|
||||||
|
assert_eq!(restored_cafe.id, cafe.id);
|
||||||
|
assert_eq!(restored_cafe.name, cafe.name);
|
||||||
|
assert_eq!(restored_cafe.slug, cafe.slug);
|
||||||
|
assert_eq!(restored_cafe.city, cafe.city);
|
||||||
|
assert_eq!(restored_cafe.country, cafe.country);
|
||||||
|
assert_eq!(restored_cafe.latitude, cafe.latitude);
|
||||||
|
assert_eq!(restored_cafe.longitude, cafe.longitude);
|
||||||
|
assert_eq!(restored_cafe.website, cafe.website);
|
||||||
|
assert_eq!(restored_cafe.notes, cafe.notes);
|
||||||
|
|
||||||
// Timeline events
|
// Timeline events
|
||||||
let target_timeline = list_all_timeline_events(target.timeline_repo.as_ref()).await;
|
let target_timeline = list_all_timeline_events(target.timeline_repo.as_ref()).await;
|
||||||
assert_eq!(target_timeline.len(), source_timeline.len());
|
assert_eq!(target_timeline.len(), source_timeline.len());
|
||||||
|
|
@ -370,6 +421,7 @@ async fn restore_to_non_empty_database_fails() {
|
||||||
roasts: vec![],
|
roasts: vec![],
|
||||||
bags: vec![],
|
bags: vec![],
|
||||||
brews: vec![],
|
brews: vec![],
|
||||||
|
cafes: vec![],
|
||||||
timeline_events: vec![],
|
timeline_events: vec![],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -399,6 +451,7 @@ async fn backup_empty_database() {
|
||||||
assert!(backup_data.bags.is_empty());
|
assert!(backup_data.bags.is_empty());
|
||||||
assert!(backup_data.gear.is_empty());
|
assert!(backup_data.gear.is_empty());
|
||||||
assert!(backup_data.brews.is_empty());
|
assert!(backup_data.brews.is_empty());
|
||||||
|
assert!(backup_data.cafes.is_empty());
|
||||||
assert!(backup_data.timeline_events.is_empty());
|
assert!(backup_data.timeline_events.is_empty());
|
||||||
|
|
||||||
// Should serialize to valid JSON
|
// Should serialize to valid JSON
|
||||||
|
|
|
||||||
363
tests/server/cafes_api.rs
Normal file
363
tests/server/cafes_api.rs
Normal file
|
|
@ -0,0 +1,363 @@
|
||||||
|
use crate::helpers::{create_default_cafe, spawn_app_with_auth};
|
||||||
|
use brewlog::domain::cafes::{Cafe, NewCafe, UpdateCafe};
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn creating_a_cafe_returns_a_201_for_valid_data() {
|
||||||
|
let app = spawn_app_with_auth().await;
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
|
let new_cafe = NewCafe {
|
||||||
|
name: "Blue Bottle".to_string(),
|
||||||
|
city: "San Francisco".to_string(),
|
||||||
|
country: "United States".to_string(),
|
||||||
|
latitude: 37.7749,
|
||||||
|
longitude: -122.4194,
|
||||||
|
website: Some("https://bluebottlecoffee.com".to_string()),
|
||||||
|
notes: Some("Great pour-over".to_string()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.post(app.api_url("/cafes"))
|
||||||
|
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||||
|
.json(&new_cafe)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to execute request");
|
||||||
|
|
||||||
|
assert_eq!(response.status(), 201);
|
||||||
|
|
||||||
|
let cafe: Cafe = response.json().await.expect("Failed to parse response");
|
||||||
|
assert_eq!(cafe.name, "Blue Bottle");
|
||||||
|
assert_eq!(cafe.city, "San Francisco");
|
||||||
|
assert_eq!(cafe.country, "United States");
|
||||||
|
assert_eq!(cafe.latitude, 37.7749);
|
||||||
|
assert_eq!(cafe.longitude, -122.4194);
|
||||||
|
assert_eq!(
|
||||||
|
cafe.website,
|
||||||
|
Some("https://bluebottlecoffee.com".to_string())
|
||||||
|
);
|
||||||
|
assert_eq!(cafe.notes, Some("Great pour-over".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn creating_a_cafe_persists_the_data() {
|
||||||
|
let app = spawn_app_with_auth().await;
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
|
let new_cafe = NewCafe {
|
||||||
|
name: "Persistent Cafe".to_string(),
|
||||||
|
city: "Paris".to_string(),
|
||||||
|
country: "France".to_string(),
|
||||||
|
latitude: 48.8566,
|
||||||
|
longitude: 2.3522,
|
||||||
|
website: None,
|
||||||
|
notes: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.post(app.api_url("/cafes"))
|
||||||
|
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||||
|
.json(&new_cafe)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to execute request");
|
||||||
|
|
||||||
|
let cafe: Cafe = response.json().await.expect("Failed to parse response");
|
||||||
|
|
||||||
|
let fetched_cafe = app
|
||||||
|
.cafe_repo
|
||||||
|
.get(cafe.id)
|
||||||
|
.await
|
||||||
|
.expect("Failed to fetch cafe");
|
||||||
|
|
||||||
|
assert_eq!(fetched_cafe.name, "Persistent Cafe");
|
||||||
|
assert_eq!(fetched_cafe.city, "Paris");
|
||||||
|
assert_eq!(fetched_cafe.country, "France");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn creating_a_cafe_requires_authentication() {
|
||||||
|
let app = spawn_app_with_auth().await;
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
|
let new_cafe = NewCafe {
|
||||||
|
name: "Auth Test Cafe".to_string(),
|
||||||
|
city: "London".to_string(),
|
||||||
|
country: "UK".to_string(),
|
||||||
|
latitude: 51.5074,
|
||||||
|
longitude: -0.1278,
|
||||||
|
website: None,
|
||||||
|
notes: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.post(app.api_url("/cafes"))
|
||||||
|
.json(&new_cafe)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to execute request");
|
||||||
|
|
||||||
|
assert_eq!(response.status(), 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn creating_a_cafe_with_missing_required_fields_returns_a_400() {
|
||||||
|
let app = spawn_app_with_auth().await;
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.post(app.api_url("/cafes"))
|
||||||
|
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||||
|
.header("content-type", "application/json")
|
||||||
|
.body(r#"{"name": "Test Cafe"}"#)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to execute request");
|
||||||
|
|
||||||
|
assert_eq!(response.status(), 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn creating_a_cafe_with_malformed_json_returns_a_400() {
|
||||||
|
let app = spawn_app_with_auth().await;
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.post(app.api_url("/cafes"))
|
||||||
|
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||||
|
.header("content-type", "application/json")
|
||||||
|
.body(r#"{"name": "Test", "city": }"#)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to execute request");
|
||||||
|
|
||||||
|
assert_eq!(response.status(), 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn listing_cafes_returns_a_200_with_empty_list() {
|
||||||
|
let app = spawn_app_with_auth().await;
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.get(app.api_url("/cafes"))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to execute request");
|
||||||
|
|
||||||
|
assert_eq!(response.status(), 200);
|
||||||
|
|
||||||
|
let cafes: Vec<Cafe> = response.json().await.expect("Failed to parse response");
|
||||||
|
assert_eq!(cafes.len(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn listing_cafes_returns_a_200_with_multiple_cafes() {
|
||||||
|
let app = spawn_app_with_auth().await;
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
|
let cafe1 = NewCafe {
|
||||||
|
name: "First Cafe".to_string(),
|
||||||
|
city: "London".to_string(),
|
||||||
|
country: "UK".to_string(),
|
||||||
|
latitude: 51.5074,
|
||||||
|
longitude: -0.1278,
|
||||||
|
website: None,
|
||||||
|
notes: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let cafe2 = NewCafe {
|
||||||
|
name: "Second Cafe".to_string(),
|
||||||
|
city: "Berlin".to_string(),
|
||||||
|
country: "Germany".to_string(),
|
||||||
|
latitude: 52.52,
|
||||||
|
longitude: 13.405,
|
||||||
|
website: None,
|
||||||
|
notes: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
client
|
||||||
|
.post(app.api_url("/cafes"))
|
||||||
|
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||||
|
.json(&cafe1)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to create first cafe");
|
||||||
|
|
||||||
|
client
|
||||||
|
.post(app.api_url("/cafes"))
|
||||||
|
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||||
|
.json(&cafe2)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to create second cafe");
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.get(app.api_url("/cafes"))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to execute request");
|
||||||
|
|
||||||
|
assert_eq!(response.status(), 200);
|
||||||
|
|
||||||
|
let cafes: Vec<Cafe> = response.json().await.expect("Failed to parse response");
|
||||||
|
assert_eq!(cafes.len(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn getting_a_cafe_returns_a_200_for_valid_id() {
|
||||||
|
let app = spawn_app_with_auth().await;
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let cafe = create_default_cafe(&app).await;
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.get(app.api_url(&format!("/cafes/{}", cafe.id)))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to execute request");
|
||||||
|
|
||||||
|
assert_eq!(response.status(), 200);
|
||||||
|
|
||||||
|
let fetched: Cafe = response.json().await.expect("Failed to parse response");
|
||||||
|
assert_eq!(fetched.id, cafe.id);
|
||||||
|
assert_eq!(fetched.name, "Blue Bottle");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn getting_a_nonexistent_cafe_returns_a_404() {
|
||||||
|
let app = spawn_app_with_auth().await;
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.get(app.api_url("/cafes/999999"))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to execute request");
|
||||||
|
|
||||||
|
assert_eq!(response.status(), 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn updating_a_cafe_returns_a_200_for_valid_data() {
|
||||||
|
let app = spawn_app_with_auth().await;
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let cafe = create_default_cafe(&app).await;
|
||||||
|
|
||||||
|
let update = UpdateCafe {
|
||||||
|
name: Some("Updated Cafe".to_string()),
|
||||||
|
city: None,
|
||||||
|
country: None,
|
||||||
|
latitude: None,
|
||||||
|
longitude: None,
|
||||||
|
website: Some("https://updated.com".to_string()),
|
||||||
|
notes: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.put(app.api_url(&format!("/cafes/{}", cafe.id)))
|
||||||
|
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||||
|
.json(&update)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to execute request");
|
||||||
|
|
||||||
|
assert_eq!(response.status(), 200);
|
||||||
|
|
||||||
|
let updated: Cafe = response.json().await.expect("Failed to parse response");
|
||||||
|
assert_eq!(updated.name, "Updated Cafe");
|
||||||
|
assert_eq!(updated.city, "San Francisco"); // unchanged
|
||||||
|
assert_eq!(updated.website, Some("https://updated.com".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn updating_a_cafe_with_no_changes_returns_a_400() {
|
||||||
|
let app = spawn_app_with_auth().await;
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let cafe = create_default_cafe(&app).await;
|
||||||
|
|
||||||
|
let update = UpdateCafe {
|
||||||
|
name: None,
|
||||||
|
city: None,
|
||||||
|
country: None,
|
||||||
|
latitude: None,
|
||||||
|
longitude: None,
|
||||||
|
website: None,
|
||||||
|
notes: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.put(app.api_url(&format!("/cafes/{}", cafe.id)))
|
||||||
|
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||||
|
.json(&update)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to execute request");
|
||||||
|
|
||||||
|
assert_eq!(response.status(), 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn updating_a_nonexistent_cafe_returns_a_404() {
|
||||||
|
let app = spawn_app_with_auth().await;
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
|
let update = UpdateCafe {
|
||||||
|
name: Some("New Name".to_string()),
|
||||||
|
city: None,
|
||||||
|
country: None,
|
||||||
|
latitude: None,
|
||||||
|
longitude: None,
|
||||||
|
website: None,
|
||||||
|
notes: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.put(app.api_url("/cafes/999999"))
|
||||||
|
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||||
|
.json(&update)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to execute request");
|
||||||
|
|
||||||
|
assert_eq!(response.status(), 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn deleting_a_cafe_returns_a_204_for_valid_id() {
|
||||||
|
let app = spawn_app_with_auth().await;
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let cafe = create_default_cafe(&app).await;
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.delete(app.api_url(&format!("/cafes/{}", cafe.id)))
|
||||||
|
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to execute request");
|
||||||
|
|
||||||
|
assert_eq!(response.status(), 204);
|
||||||
|
|
||||||
|
// Verify cafe was deleted
|
||||||
|
let get_response = client
|
||||||
|
.get(app.api_url(&format!("/cafes/{}", cafe.id)))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to execute request");
|
||||||
|
|
||||||
|
assert_eq!(get_response.status(), 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn deleting_a_nonexistent_cafe_returns_a_404() {
|
||||||
|
let app = spawn_app_with_auth().await;
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.delete(app.api_url("/cafes/999999"))
|
||||||
|
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to execute request");
|
||||||
|
|
||||||
|
assert_eq!(response.status(), 404);
|
||||||
|
}
|
||||||
|
|
@ -5,9 +5,10 @@
|
||||||
|
|
||||||
use crate::helpers::{
|
use crate::helpers::{
|
||||||
assert_datastar_headers, assert_full_page, assert_html_fragment, create_default_bag,
|
assert_datastar_headers, assert_full_page, assert_html_fragment, create_default_bag,
|
||||||
create_default_roast, create_default_roaster, spawn_app_with_auth,
|
create_default_cafe, create_default_roast, create_default_roaster, spawn_app_with_auth,
|
||||||
};
|
};
|
||||||
use brewlog::domain::bags::UpdateBag;
|
use brewlog::domain::bags::UpdateBag;
|
||||||
|
use brewlog::domain::cafes::NewCafe;
|
||||||
use brewlog::domain::roasters::NewRoaster;
|
use brewlog::domain::roasters::NewRoaster;
|
||||||
use brewlog::domain::roasts::NewRoast;
|
use brewlog::domain::roasts::NewRoast;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
|
|
@ -753,3 +754,106 @@ async fn gear_delete_with_datastar_header_returns_fragment() {
|
||||||
let body = response.text().await.expect("failed to read body");
|
let body = response.text().await.expect("failed to read body");
|
||||||
assert_html_fragment(&body);
|
assert_html_fragment(&body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Cafes
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn cafes_list_with_datastar_header_returns_fragment() {
|
||||||
|
let app = spawn_app_with_auth().await;
|
||||||
|
create_default_cafe(&app).await;
|
||||||
|
let client = Client::new();
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.get(format!("{}/cafes", app.address))
|
||||||
|
.header("datastar-request", "true")
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("failed to fetch cafes");
|
||||||
|
|
||||||
|
assert_eq!(response.status(), 200);
|
||||||
|
assert_datastar_headers(&response, "#cafe-list");
|
||||||
|
|
||||||
|
let body = response.text().await.expect("failed to read body");
|
||||||
|
assert_html_fragment(&body);
|
||||||
|
assert!(
|
||||||
|
body.contains("id=\"cafe-list\""),
|
||||||
|
"Fragment should contain the selector element"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn cafes_list_without_datastar_header_returns_full_page() {
|
||||||
|
let app = spawn_app_with_auth().await;
|
||||||
|
create_default_cafe(&app).await;
|
||||||
|
let client = Client::new();
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.get(format!("{}/cafes", app.address))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("failed to fetch cafes");
|
||||||
|
|
||||||
|
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 cafes_create_with_datastar_header_returns_fragment() {
|
||||||
|
let app = spawn_app_with_auth().await;
|
||||||
|
let client = Client::new();
|
||||||
|
|
||||||
|
let new_cafe = NewCafe {
|
||||||
|
name: "Datastar Test Cafe".to_string(),
|
||||||
|
city: "London".to_string(),
|
||||||
|
country: "UK".to_string(),
|
||||||
|
latitude: 51.5074,
|
||||||
|
longitude: -0.1278,
|
||||||
|
website: None,
|
||||||
|
notes: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.post(app.api_url("/cafes"))
|
||||||
|
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||||
|
.header("datastar-request", "true")
|
||||||
|
.json(&new_cafe)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("failed to create cafe");
|
||||||
|
|
||||||
|
assert_eq!(response.status(), 200);
|
||||||
|
assert_datastar_headers(&response, "#cafe-list");
|
||||||
|
|
||||||
|
let body = response.text().await.expect("failed to read body");
|
||||||
|
assert_html_fragment(&body);
|
||||||
|
assert!(
|
||||||
|
body.contains("Datastar Test Cafe"),
|
||||||
|
"Fragment should include created cafe"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn cafes_delete_with_datastar_header_returns_fragment() {
|
||||||
|
let app = spawn_app_with_auth().await;
|
||||||
|
let cafe = create_default_cafe(&app).await;
|
||||||
|
let client = Client::new();
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.delete(app.api_url(&format!("/cafes/{}", cafe.id)))
|
||||||
|
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||||
|
.header("datastar-request", "true")
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("failed to delete cafe");
|
||||||
|
|
||||||
|
assert_eq!(response.status(), 200);
|
||||||
|
assert_datastar_headers(&response, "#cafe-list");
|
||||||
|
|
||||||
|
let body = response.text().await.expect("failed to read body");
|
||||||
|
assert_html_fragment(&body);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,9 @@ use std::sync::Arc;
|
||||||
|
|
||||||
use brewlog::application::routes::app_router;
|
use brewlog::application::routes::app_router;
|
||||||
use brewlog::application::server::AppState;
|
use brewlog::application::server::AppState;
|
||||||
|
use brewlog::domain::cafes::{Cafe, NewCafe};
|
||||||
use brewlog::domain::repositories::{
|
use brewlog::domain::repositories::{
|
||||||
RoastRepository, RoasterRepository, SessionRepository, TimelineEventRepository,
|
CafeRepository, RoastRepository, RoasterRepository, SessionRepository, TimelineEventRepository,
|
||||||
TokenRepository, UserRepository,
|
TokenRepository, UserRepository,
|
||||||
};
|
};
|
||||||
use brewlog::domain::roasters::{NewRoaster, Roaster};
|
use brewlog::domain::roasters::{NewRoaster, Roaster};
|
||||||
|
|
@ -12,6 +13,7 @@ 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::brews::SqlBrewRepository;
|
use brewlog::infrastructure::repositories::brews::SqlBrewRepository;
|
||||||
|
use brewlog::infrastructure::repositories::cafes::SqlCafeRepository;
|
||||||
use brewlog::infrastructure::repositories::gear::SqlGearRepository;
|
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;
|
||||||
|
|
@ -27,6 +29,8 @@ pub struct TestApp {
|
||||||
pub roaster_repo: Arc<dyn RoasterRepository>,
|
pub roaster_repo: Arc<dyn RoasterRepository>,
|
||||||
pub roast_repo: Arc<dyn RoastRepository>,
|
pub roast_repo: Arc<dyn RoastRepository>,
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
|
pub cafe_repo: Arc<dyn CafeRepository>,
|
||||||
|
#[allow(dead_code)]
|
||||||
pub timeline_repo: Arc<dyn TimelineEventRepository>,
|
pub timeline_repo: Arc<dyn TimelineEventRepository>,
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub user_repo: Option<Arc<dyn UserRepository>>,
|
pub user_repo: Option<Arc<dyn UserRepository>>,
|
||||||
|
|
@ -59,6 +63,7 @@ pub async fn spawn_app() -> TestApp {
|
||||||
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 gear_repo = Arc::new(SqlGearRepository::new(database.clone_pool()));
|
||||||
let brew_repo = Arc::new(SqlBrewRepository::new(database.clone_pool()));
|
let brew_repo = Arc::new(SqlBrewRepository::new(database.clone_pool()));
|
||||||
|
let cafe_repo = Arc::new(SqlCafeRepository::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()));
|
||||||
|
|
@ -74,6 +79,7 @@ pub async fn spawn_app() -> TestApp {
|
||||||
bag_repo.clone(),
|
bag_repo.clone(),
|
||||||
gear_repo.clone(),
|
gear_repo.clone(),
|
||||||
brew_repo.clone(),
|
brew_repo.clone(),
|
||||||
|
cafe_repo.clone(),
|
||||||
timeline_repo.clone(),
|
timeline_repo.clone(),
|
||||||
user_repo.clone(),
|
user_repo.clone(),
|
||||||
token_repo.clone(),
|
token_repo.clone(),
|
||||||
|
|
@ -102,6 +108,7 @@ pub async fn spawn_app() -> TestApp {
|
||||||
address,
|
address,
|
||||||
roaster_repo,
|
roaster_repo,
|
||||||
roast_repo,
|
roast_repo,
|
||||||
|
cafe_repo,
|
||||||
timeline_repo,
|
timeline_repo,
|
||||||
user_repo: Some(user_repo),
|
user_repo: Some(user_repo),
|
||||||
token_repo: Some(token_repo),
|
token_repo: Some(token_repo),
|
||||||
|
|
@ -325,3 +332,35 @@ pub async fn create_default_gear(
|
||||||
.await
|
.await
|
||||||
.expect("failed to deserialize gear from response")
|
.expect("failed to deserialize gear from response")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn create_default_cafe(app: &TestApp) -> Cafe {
|
||||||
|
create_cafe_with_payload(
|
||||||
|
app,
|
||||||
|
NewCafe {
|
||||||
|
name: "Blue Bottle".to_string(),
|
||||||
|
city: "San Francisco".to_string(),
|
||||||
|
country: "US".to_string(),
|
||||||
|
latitude: 37.7749,
|
||||||
|
longitude: -122.4194,
|
||||||
|
website: Some("https://bluebottlecoffee.com".to_string()),
|
||||||
|
notes: Some("Great pour-over".to_string()),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_cafe_with_payload(app: &TestApp, payload: NewCafe) -> Cafe {
|
||||||
|
let client = Client::new();
|
||||||
|
let mut request = client.post(app.api_url("/cafes")).json(&payload);
|
||||||
|
|
||||||
|
if let Some(token) = &app.auth_token {
|
||||||
|
request = request.bearer_auth(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = request.send().await.expect("failed to create cafe via API");
|
||||||
|
|
||||||
|
response
|
||||||
|
.json()
|
||||||
|
.await
|
||||||
|
.expect("failed to deserialize cafe from response")
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ pub mod auth_api;
|
||||||
pub mod backup;
|
pub mod backup;
|
||||||
pub mod bags_api;
|
pub mod bags_api;
|
||||||
pub mod brews_api;
|
pub mod brews_api;
|
||||||
|
pub mod cafes_api;
|
||||||
pub mod datastar;
|
pub mod datastar;
|
||||||
pub mod gear_api;
|
pub mod gear_api;
|
||||||
pub mod helpers;
|
pub mod helpers;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
use crate::helpers::{create_roaster_with_payload, spawn_app_with_auth};
|
use crate::helpers::{create_cafe_with_payload, create_roaster_with_payload, spawn_app_with_auth};
|
||||||
|
use brewlog::domain::cafes::NewCafe;
|
||||||
use brewlog::domain::ids::RoasterId;
|
use brewlog::domain::ids::RoasterId;
|
||||||
use brewlog::domain::roasters::NewRoaster;
|
use brewlog::domain::roasters::NewRoaster;
|
||||||
use brewlog::domain::roasts::NewRoast;
|
use brewlog::domain::roasts::NewRoast;
|
||||||
|
|
@ -459,3 +460,48 @@ async fn creating_gear_surfaces_on_the_timeline() {
|
||||||
"Expected gear category to appear in timeline HTML, got: {body}"
|
"Expected gear category to appear in timeline HTML, got: {body}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn creating_a_cafe_surfaces_on_the_timeline() {
|
||||||
|
let app = spawn_app_with_auth().await;
|
||||||
|
let client = Client::new();
|
||||||
|
|
||||||
|
let cafe_name = "Timeline Test Cafe";
|
||||||
|
let cafe = create_cafe_with_payload(
|
||||||
|
&app,
|
||||||
|
NewCafe {
|
||||||
|
name: cafe_name.to_string(),
|
||||||
|
city: "Bristol".to_string(),
|
||||||
|
country: "UK".to_string(),
|
||||||
|
latitude: 51.4545,
|
||||||
|
longitude: -2.5879,
|
||||||
|
website: Some("https://example.com".to_string()),
|
||||||
|
notes: None,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
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("Cafe Added"),
|
||||||
|
"Expected 'Cafe Added' badge in timeline HTML, got: {body}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
body.contains(cafe_name),
|
||||||
|
"Expected cafe name to appear in timeline HTML, got: {body}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
body.contains(&format!("/cafes/{}", cafe.slug)),
|
||||||
|
"Expected cafe detail link in timeline HTML, got: {body}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue