test: add simple tests for timeline view
This commit is contained in:
parent
9ca4b4930d
commit
f60e759935
4 changed files with 206 additions and 48 deletions
|
|
@ -1,12 +1,14 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use brewlog::domain::repositories::{RoastRepository, RoasterRepository, TimelineEventRepository};
|
||||
use brewlog::domain::roasters::{NewRoaster, Roaster};
|
||||
use brewlog::infrastructure::database::Database;
|
||||
use brewlog::infrastructure::repositories::roasters::SqlRoasterRepository;
|
||||
use brewlog::infrastructure::repositories::roasts::SqlRoastRepository;
|
||||
use brewlog::infrastructure::repositories::timeline_events::SqlTimelineEventRepository;
|
||||
use brewlog::server::routes::app_router;
|
||||
use brewlog::server::server::AppState;
|
||||
use reqwest::Client;
|
||||
use tokio::net::TcpListener;
|
||||
|
||||
pub struct TestApp {
|
||||
|
|
@ -30,7 +32,10 @@ pub async fn spawn_app() -> TestApp {
|
|||
.expect("Failed to connect to in-memory database");
|
||||
|
||||
// Run migrations
|
||||
database.migrate().await.expect("Failed to migrate database");
|
||||
database
|
||||
.migrate()
|
||||
.await
|
||||
.expect("Failed to migrate database");
|
||||
|
||||
// Create repositories
|
||||
let roaster_repo = Arc::new(SqlRoasterRepository::new(database.clone_pool()));
|
||||
|
|
@ -69,3 +74,36 @@ pub async fn spawn_app() -> TestApp {
|
|||
timeline_repo,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create_roaster_with_payload(app: &TestApp, payload: NewRoaster) -> Roaster {
|
||||
let client = Client::new();
|
||||
let response = client
|
||||
.post(app.api_url("/roasters"))
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await
|
||||
.expect("failed to create roaster via API");
|
||||
|
||||
response
|
||||
.json()
|
||||
.await
|
||||
.expect("failed to deserialize roaster from response")
|
||||
}
|
||||
|
||||
pub async fn create_roaster_with_name(app: &TestApp, name: &str) -> Roaster {
|
||||
create_roaster_with_payload(
|
||||
app,
|
||||
NewRoaster {
|
||||
name: name.to_string(),
|
||||
country: "UK".to_string(),
|
||||
city: None,
|
||||
homepage: None,
|
||||
notes: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn create_default_roaster(app: &TestApp) -> Roaster {
|
||||
create_roaster_with_name(app, "Test Roasters").await
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
pub mod helpers;
|
||||
pub mod roasters_api;
|
||||
pub mod roasts_api;
|
||||
pub mod timeline;
|
||||
|
|
|
|||
|
|
@ -1,44 +1,11 @@
|
|||
use crate::helpers::spawn_app;
|
||||
use brewlog::domain::roasters::NewRoaster;
|
||||
use crate::helpers::{create_default_roaster, create_roaster_with_name, spawn_app};
|
||||
use brewlog::domain::roasts::{NewRoast, Roast, RoastWithRoaster};
|
||||
|
||||
async fn create_test_roaster(app: &crate::helpers::TestApp) -> String {
|
||||
create_test_roaster_with_name(app, "Test Roasters").await
|
||||
}
|
||||
|
||||
async fn create_test_roaster_with_name(
|
||||
app: &crate::helpers::TestApp,
|
||||
name: &str,
|
||||
) -> String {
|
||||
let new_roaster = NewRoaster {
|
||||
name: name.to_string(),
|
||||
country: "UK".to_string(),
|
||||
city: None,
|
||||
homepage: None,
|
||||
notes: None,
|
||||
};
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let response = client
|
||||
.post(app.api_url("/roasters"))
|
||||
.json(&new_roaster)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to create roaster");
|
||||
|
||||
let roaster: brewlog::domain::roasters::Roaster = response
|
||||
.json()
|
||||
.await
|
||||
.expect("Failed to parse roaster");
|
||||
|
||||
roaster.id
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn creating_a_roast_returns_a_201_for_valid_data() {
|
||||
// Arrange
|
||||
let app = spawn_app().await;
|
||||
let roaster_id = create_test_roaster(&app).await;
|
||||
let roaster_id = create_default_roaster(&app).await.id;
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let new_roast = NewRoast {
|
||||
|
|
@ -80,7 +47,7 @@ async fn creating_a_roast_returns_a_201_for_valid_data() {
|
|||
async fn creating_a_roast_persists_the_data() {
|
||||
// Arrange
|
||||
let app = spawn_app().await;
|
||||
let roaster_id = create_test_roaster(&app).await;
|
||||
let roaster_id = create_default_roaster(&app).await.id;
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let new_roast = NewRoast {
|
||||
|
|
@ -147,7 +114,7 @@ async fn creating_a_roast_with_nonexistent_roaster_returns_a_404() {
|
|||
async fn getting_a_roast_returns_a_200_for_valid_id() {
|
||||
// Arrange
|
||||
let app = spawn_app().await;
|
||||
let roaster_id = create_test_roaster(&app).await;
|
||||
let roaster_id = create_default_roaster(&app).await.id;
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let new_roast = NewRoast {
|
||||
|
|
@ -167,7 +134,10 @@ async fn getting_a_roast_returns_a_200_for_valid_id() {
|
|||
.await
|
||||
.expect("Failed to create roast");
|
||||
|
||||
let created_roast: Roast = create_response.json().await.expect("Failed to parse response");
|
||||
let created_roast: Roast = create_response
|
||||
.json()
|
||||
.await
|
||||
.expect("Failed to parse response");
|
||||
|
||||
// Act
|
||||
let response = client
|
||||
|
|
@ -225,7 +195,7 @@ async fn listing_roasts_returns_a_200_with_empty_list() {
|
|||
async fn listing_roasts_returns_a_200_with_multiple_roasts() {
|
||||
// Arrange
|
||||
let app = spawn_app().await;
|
||||
let roaster_id = create_test_roaster(&app).await;
|
||||
let roaster_id = create_default_roaster(&app).await.id;
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
// Create multiple roasts
|
||||
|
|
@ -281,9 +251,9 @@ async fn listing_roasts_returns_a_200_with_multiple_roasts() {
|
|||
async fn listing_roasts_by_roaster_returns_a_200_with_filtered_list() {
|
||||
// Arrange
|
||||
let app = spawn_app().await;
|
||||
let roaster1_id = create_test_roaster(&app).await;
|
||||
let roaster2_id = create_test_roaster_with_name(&app, "Second Roasters").await;
|
||||
|
||||
let roaster1_id = create_default_roaster(&app).await.id;
|
||||
let roaster2_id = create_roaster_with_name(&app, "Second Roasters").await.id;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
// Create roasts for both roasters
|
||||
|
|
@ -340,7 +310,7 @@ async fn listing_roasts_by_roaster_returns_a_200_with_filtered_list() {
|
|||
async fn deleting_a_roast_returns_a_204_for_valid_id() {
|
||||
// Arrange
|
||||
let app = spawn_app().await;
|
||||
let roaster_id = create_test_roaster(&app).await;
|
||||
let roaster_id = create_default_roaster(&app).await.id;
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let new_roast = NewRoast {
|
||||
|
|
@ -360,7 +330,10 @@ async fn deleting_a_roast_returns_a_204_for_valid_id() {
|
|||
.await
|
||||
.expect("Failed to create roast");
|
||||
|
||||
let created_roast: Roast = create_response.json().await.expect("Failed to parse response");
|
||||
let created_roast: Roast = create_response
|
||||
.json()
|
||||
.await
|
||||
.expect("Failed to parse response");
|
||||
|
||||
// Act
|
||||
let response = client
|
||||
|
|
@ -403,7 +376,7 @@ async fn deleting_a_nonexistent_roast_returns_a_404() {
|
|||
async fn creating_a_roast_with_empty_name_returns_a_400() {
|
||||
// Arrange
|
||||
let app = spawn_app().await;
|
||||
let roaster_id = create_test_roaster(&app).await;
|
||||
let roaster_id = create_default_roaster(&app).await.id;
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
// Act - Create roast with empty name (after trim)
|
||||
|
|
@ -434,7 +407,7 @@ async fn creating_a_roast_with_empty_name_returns_a_400() {
|
|||
async fn creating_a_roast_with_missing_required_fields_returns_a_400() {
|
||||
// Arrange
|
||||
let app = spawn_app().await;
|
||||
let roaster_id = create_test_roaster(&app).await;
|
||||
let roaster_id = create_default_roaster(&app).await.id;
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
// Act - Missing 'origin' field
|
||||
|
|
@ -464,7 +437,7 @@ async fn creating_a_roast_with_missing_required_fields_returns_a_400() {
|
|||
async fn creating_a_roast_with_empty_tasting_notes_returns_a_400() {
|
||||
// Arrange
|
||||
let app = spawn_app().await;
|
||||
let roaster_id = create_test_roaster(&app).await;
|
||||
let roaster_id = create_default_roaster(&app).await.id;
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
// Act - Empty tasting notes
|
||||
|
|
|
|||
146
tests/server/timeline.rs
Normal file
146
tests/server/timeline.rs
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
use crate::helpers::{create_roaster_with_payload, spawn_app};
|
||||
use brewlog::domain::roasters::NewRoaster;
|
||||
use brewlog::domain::roasts::NewRoast;
|
||||
use reqwest::Client;
|
||||
use tokio::time::{Duration, sleep};
|
||||
|
||||
async fn create_roast(app: &crate::helpers::TestApp, roaster_id: &str, name: &str) {
|
||||
let client = Client::new();
|
||||
let roast = NewRoast {
|
||||
roaster_id: roaster_id.to_string(),
|
||||
name: name.to_string(),
|
||||
origin: "Ethiopia".to_string(),
|
||||
region: "Yirgacheffe".to_string(),
|
||||
producer: "Chelbesa Cooperative".to_string(),
|
||||
tasting_notes: vec!["Blueberry".to_string(), "Jasmine".to_string()],
|
||||
process: "Washed".to_string(),
|
||||
};
|
||||
|
||||
let response = client
|
||||
.post(app.api_url("/roasts"))
|
||||
.json(&roast)
|
||||
.send()
|
||||
.await
|
||||
.expect("failed to create roast");
|
||||
|
||||
assert_eq!(response.status(), 201);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn timeline_page_returns_a_200_with_empty_state() {
|
||||
// Arrange
|
||||
let app = spawn_app().await;
|
||||
let client = Client::new();
|
||||
|
||||
// Act
|
||||
let response = client
|
||||
.get(format!("{}/timeline", app.address))
|
||||
.send()
|
||||
.await
|
||||
.expect("failed to fetch timeline");
|
||||
|
||||
// Assert
|
||||
assert_eq!(response.status(), 200);
|
||||
|
||||
let body = response.text().await.expect("failed to read response body");
|
||||
assert!(
|
||||
body.contains("No events yet"),
|
||||
"Expected empty timeline state message, got: {body}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn creating_a_roaster_surfaces_on_the_timeline() {
|
||||
// Arrange
|
||||
let app = spawn_app().await;
|
||||
let client = Client::new();
|
||||
|
||||
let roaster_name = "Timeline Roasters";
|
||||
let roaster = create_roaster_with_payload(
|
||||
&app,
|
||||
NewRoaster {
|
||||
name: roaster_name.to_string(),
|
||||
country: "UK".to_string(),
|
||||
city: Some("Bristol".to_string()),
|
||||
homepage: Some("https://example.com".to_string()),
|
||||
notes: None,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
let roaster_id = roaster.id.clone();
|
||||
|
||||
// Give the database a brief moment to commit timestamps
|
||||
sleep(Duration::from_millis(10)).await;
|
||||
|
||||
// Act
|
||||
let response = client
|
||||
.get(format!("{}/timeline", app.address))
|
||||
.send()
|
||||
.await
|
||||
.expect("failed to fetch timeline");
|
||||
|
||||
// Assert
|
||||
assert_eq!(response.status(), 200);
|
||||
|
||||
let body = response.text().await.expect("failed to read response body");
|
||||
assert!(
|
||||
body.contains("Roaster Added"),
|
||||
"Expected 'Roaster Added' badge in timeline HTML, got: {body}"
|
||||
);
|
||||
assert!(
|
||||
body.contains(roaster_name),
|
||||
"Expected roaster name to appear in timeline HTML, got: {body}"
|
||||
);
|
||||
assert!(
|
||||
body.contains(&format!("/roasters/{}", roaster_id)),
|
||||
"Expected roaster detail link in timeline HTML, got: {body}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn creating_a_roast_surfaces_on_the_timeline() {
|
||||
// Arrange
|
||||
let app = spawn_app().await;
|
||||
let client = Client::new();
|
||||
|
||||
let roaster_id = create_roaster_with_payload(
|
||||
&app,
|
||||
NewRoaster {
|
||||
name: "Timeline Roast Roasters".to_string(),
|
||||
country: "UK".to_string(),
|
||||
city: Some("Bristol".to_string()),
|
||||
homepage: Some("https://example.com".to_string()),
|
||||
notes: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.id;
|
||||
// Ensure the roast event occurs after the roaster event to make ordering deterministic
|
||||
sleep(Duration::from_millis(5)).await;
|
||||
let roast_name = "Timeline Natural";
|
||||
create_roast(&app, &roaster_id, roast_name).await;
|
||||
|
||||
// Act
|
||||
let response = client
|
||||
.get(format!("{}/timeline", app.address))
|
||||
.send()
|
||||
.await
|
||||
.expect("failed to fetch timeline");
|
||||
|
||||
// Assert
|
||||
assert_eq!(response.status(), 200);
|
||||
|
||||
let body = response.text().await.expect("failed to read response body");
|
||||
assert!(
|
||||
body.contains("Roast Added"),
|
||||
"Expected 'Roast Added' badge in timeline HTML, got: {body}"
|
||||
);
|
||||
assert!(
|
||||
body.contains(roast_name),
|
||||
"Expected roast name to appear in timeline HTML, got: {body}"
|
||||
);
|
||||
assert!(
|
||||
body.contains("Blueberry"),
|
||||
"Expected tasting notes to appear in timeline HTML, got: {body}"
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue