test: add coverage for homepage, check-in, scan, and AI extraction
- 12 page smoke tests: homepage (empty/with data/stats), login, auth-gated redirects (check-in, add, account), authenticated page loads, logout, and scan redirect - 5 check-in API tests: existing cafe, new cafe creation, auth, and validation - 7 scan API tests: roaster+roast creation, slug-based reuse, bag creation with timeline event, auth, and field validation - 9 AI extraction tests: all 3 extract endpoints with wiremock-mocked OpenRouter, both JSON and Datastar signal responses, auth and empty input validation
This commit is contained in:
parent
a9d97d40bd
commit
35ea246ba2
5 changed files with 936 additions and 0 deletions
142
tests/server/checkin_api.rs
Normal file
142
tests/server/checkin_api.rs
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
use brewlog::domain::cups::Cup;
|
||||
|
||||
use crate::helpers::{
|
||||
create_default_cafe, create_default_roast, create_default_roaster, spawn_app_with_auth,
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn checkin_with_existing_cafe_creates_cup() {
|
||||
let app = spawn_app_with_auth().await;
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let roaster = create_default_roaster(&app).await;
|
||||
let roast = create_default_roast(&app, roaster.id).await;
|
||||
let cafe = create_default_cafe(&app).await;
|
||||
|
||||
let payload = serde_json::json!({
|
||||
"cafe_id": cafe.id.to_string(),
|
||||
"roast_id": roast.id.to_string(),
|
||||
});
|
||||
|
||||
let response = client
|
||||
.post(app.api_url("/check-in"))
|
||||
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to execute request");
|
||||
|
||||
assert_eq!(response.status(), 201);
|
||||
|
||||
let cup: Cup = response.json().await.expect("Failed to parse response");
|
||||
assert_eq!(cup.roast_id, roast.id);
|
||||
assert_eq!(cup.cafe_id, cafe.id);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn checkin_with_new_cafe_creates_cafe_and_cup() {
|
||||
let app = spawn_app_with_auth().await;
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let roaster = create_default_roaster(&app).await;
|
||||
let roast = create_default_roast(&app, roaster.id).await;
|
||||
|
||||
let payload = serde_json::json!({
|
||||
"roast_id": roast.id.to_string(),
|
||||
"cafe_name": "New Test Cafe",
|
||||
"cafe_city": "London",
|
||||
"cafe_country": "UK",
|
||||
"cafe_lat": 51.5074,
|
||||
"cafe_lng": -0.1278,
|
||||
});
|
||||
|
||||
let response = client
|
||||
.post(app.api_url("/check-in"))
|
||||
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to execute request");
|
||||
|
||||
assert_eq!(response.status(), 201);
|
||||
|
||||
let cup: Cup = response.json().await.expect("Failed to parse response");
|
||||
assert_eq!(cup.roast_id, roast.id);
|
||||
|
||||
// Verify the cafe was also created
|
||||
let cafes_response = client
|
||||
.get(app.api_url("/cafes"))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to list cafes");
|
||||
|
||||
let body = cafes_response.text().await.expect("Failed to read body");
|
||||
assert!(
|
||||
body.contains("New Test Cafe"),
|
||||
"Created cafe should appear in cafe list"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn checkin_requires_authentication() {
|
||||
let app = spawn_app_with_auth().await;
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let payload = serde_json::json!({
|
||||
"cafe_id": "1",
|
||||
"roast_id": "1",
|
||||
});
|
||||
|
||||
let response = client
|
||||
.post(app.api_url("/check-in"))
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to execute request");
|
||||
|
||||
assert_eq!(response.status(), 401);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn checkin_rejects_invalid_roast_id() {
|
||||
let app = spawn_app_with_auth().await;
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let payload = serde_json::json!({
|
||||
"cafe_id": "1",
|
||||
"roast_id": "not-a-number",
|
||||
});
|
||||
|
||||
let response = client
|
||||
.post(app.api_url("/check-in"))
|
||||
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to execute request");
|
||||
|
||||
assert_eq!(response.status(), 400);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn checkin_requires_cafe_name_when_no_cafe_id() {
|
||||
let app = spawn_app_with_auth().await;
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let roaster = create_default_roaster(&app).await;
|
||||
let roast = create_default_roast(&app, roaster.id).await;
|
||||
|
||||
let payload = serde_json::json!({
|
||||
"roast_id": roast.id.to_string(),
|
||||
});
|
||||
|
||||
let response = client
|
||||
.post(app.api_url("/check-in"))
|
||||
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to execute request");
|
||||
|
||||
assert_eq!(response.status(), 400);
|
||||
}
|
||||
282
tests/server/extraction_api.rs
Normal file
282
tests/server/extraction_api.rs
Normal file
|
|
@ -0,0 +1,282 @@
|
|||
use brewlog::infrastructure::ai::{ExtractedBagScan, ExtractedRoast, ExtractedRoaster};
|
||||
use wiremock::matchers::{method, path};
|
||||
use wiremock::{Mock, ResponseTemplate};
|
||||
|
||||
use crate::helpers::spawn_app_with_openrouter_mock;
|
||||
|
||||
fn mock_openrouter_response(json_content: &str) -> ResponseTemplate {
|
||||
let body = serde_json::json!({
|
||||
"id": "gen-test",
|
||||
"model": "test-model",
|
||||
"choices": [{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": json_content
|
||||
},
|
||||
"finish_reason": "stop"
|
||||
}],
|
||||
"usage": {
|
||||
"prompt_tokens": 100,
|
||||
"completion_tokens": 50,
|
||||
"total_tokens": 150,
|
||||
"cost": 0.001
|
||||
}
|
||||
});
|
||||
ResponseTemplate::new(200).set_body_json(body)
|
||||
}
|
||||
|
||||
// --- extract-roaster ---
|
||||
|
||||
#[tokio::test]
|
||||
async fn extract_roaster_returns_json() {
|
||||
let app = spawn_app_with_openrouter_mock().await;
|
||||
let mock_server = app.mock_server.as_ref().unwrap();
|
||||
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/api/v1/chat/completions"))
|
||||
.respond_with(mock_openrouter_response(
|
||||
r#"{"name": "Square Mile", "country": "United Kingdom", "city": "London"}"#,
|
||||
))
|
||||
.mount(mock_server)
|
||||
.await;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let payload = serde_json::json!({ "prompt": "Square Mile Coffee" });
|
||||
|
||||
let response = client
|
||||
.post(app.api_url("/extract-roaster"))
|
||||
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to execute request");
|
||||
|
||||
assert_eq!(response.status(), 200);
|
||||
|
||||
let result: ExtractedRoaster = response.json().await.expect("Failed to parse response");
|
||||
assert_eq!(result.name.as_deref(), Some("Square Mile"));
|
||||
assert_eq!(result.country.as_deref(), Some("United Kingdom"));
|
||||
assert_eq!(result.city.as_deref(), Some("London"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn extract_roaster_returns_datastar_signals() {
|
||||
let app = spawn_app_with_openrouter_mock().await;
|
||||
let mock_server = app.mock_server.as_ref().unwrap();
|
||||
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/api/v1/chat/completions"))
|
||||
.respond_with(mock_openrouter_response(
|
||||
r#"{"name": "Has Bean", "country": "UK"}"#,
|
||||
))
|
||||
.mount(mock_server)
|
||||
.await;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let payload = serde_json::json!({ "prompt": "Has Bean" });
|
||||
|
||||
let response = client
|
||||
.post(app.api_url("/extract-roaster"))
|
||||
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||
.header("datastar-request", "true")
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to execute request");
|
||||
|
||||
assert_eq!(response.status(), 200);
|
||||
|
||||
let body: serde_json::Value = response.json().await.expect("Failed to parse response");
|
||||
assert_eq!(body["_roasterName"], "Has Bean");
|
||||
assert_eq!(body["_roasterCountry"], "UK");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn extract_roaster_requires_auth() {
|
||||
let app = spawn_app_with_openrouter_mock().await;
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let payload = serde_json::json!({ "prompt": "test" });
|
||||
|
||||
let response = client
|
||||
.post(app.api_url("/extract-roaster"))
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to execute request");
|
||||
|
||||
assert_eq!(response.status(), 401);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn extract_roaster_rejects_empty_input() {
|
||||
let app = spawn_app_with_openrouter_mock().await;
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let payload = serde_json::json!({});
|
||||
|
||||
let response = client
|
||||
.post(app.api_url("/extract-roaster"))
|
||||
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to execute request");
|
||||
|
||||
assert_eq!(response.status(), 400);
|
||||
}
|
||||
|
||||
// --- extract-roast ---
|
||||
|
||||
#[tokio::test]
|
||||
async fn extract_roast_returns_json() {
|
||||
let app = spawn_app_with_openrouter_mock().await;
|
||||
let mock_server = app.mock_server.as_ref().unwrap();
|
||||
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/api/v1/chat/completions"))
|
||||
.respond_with(mock_openrouter_response(
|
||||
r#"{"name": "Red Brick", "origin": "Brazil", "tasting_notes": ["Chocolate", "Hazelnut"]}"#,
|
||||
))
|
||||
.mount(mock_server)
|
||||
.await;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let payload = serde_json::json!({ "prompt": "Red Brick coffee" });
|
||||
|
||||
let response = client
|
||||
.post(app.api_url("/extract-roast"))
|
||||
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to execute request");
|
||||
|
||||
assert_eq!(response.status(), 200);
|
||||
|
||||
let result: ExtractedRoast = response.json().await.expect("Failed to parse response");
|
||||
assert_eq!(result.name.as_deref(), Some("Red Brick"));
|
||||
assert_eq!(result.origin.as_deref(), Some("Brazil"));
|
||||
assert_eq!(
|
||||
result.tasting_notes.as_deref(),
|
||||
Some(&["Chocolate".to_string(), "Hazelnut".to_string()][..])
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn extract_roast_returns_datastar_signals() {
|
||||
let app = spawn_app_with_openrouter_mock().await;
|
||||
let mock_server = app.mock_server.as_ref().unwrap();
|
||||
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/api/v1/chat/completions"))
|
||||
.respond_with(mock_openrouter_response(
|
||||
r#"{"name": "Ethiopia Natural", "origin": "Ethiopia", "process": "Natural"}"#,
|
||||
))
|
||||
.mount(mock_server)
|
||||
.await;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let payload = serde_json::json!({ "prompt": "Ethiopia Natural" });
|
||||
|
||||
let response = client
|
||||
.post(app.api_url("/extract-roast"))
|
||||
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||
.header("datastar-request", "true")
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to execute request");
|
||||
|
||||
assert_eq!(response.status(), 200);
|
||||
|
||||
let body: serde_json::Value = response.json().await.expect("Failed to parse response");
|
||||
assert_eq!(body["_roastName"], "Ethiopia Natural");
|
||||
assert_eq!(body["_origin"], "Ethiopia");
|
||||
assert_eq!(body["_process"], "Natural");
|
||||
}
|
||||
|
||||
// --- extract-bag-scan ---
|
||||
|
||||
#[tokio::test]
|
||||
async fn extract_bag_scan_returns_json() {
|
||||
let app = spawn_app_with_openrouter_mock().await;
|
||||
let mock_server = app.mock_server.as_ref().unwrap();
|
||||
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/api/v1/chat/completions"))
|
||||
.respond_with(mock_openrouter_response(
|
||||
r#"{"roaster": {"name": "Origin", "country": "UK"}, "roast": {"name": "Blend One", "origin": "Colombia"}}"#,
|
||||
))
|
||||
.mount(mock_server)
|
||||
.await;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let payload = serde_json::json!({ "prompt": "Origin Blend One" });
|
||||
|
||||
let response = client
|
||||
.post(app.api_url("/extract-bag-scan"))
|
||||
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to execute request");
|
||||
|
||||
assert_eq!(response.status(), 200);
|
||||
|
||||
let result: ExtractedBagScan = response.json().await.expect("Failed to parse response");
|
||||
assert_eq!(result.roaster.name.as_deref(), Some("Origin"));
|
||||
assert_eq!(result.roast.name.as_deref(), Some("Blend One"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn extract_bag_scan_returns_datastar_signals() {
|
||||
let app = spawn_app_with_openrouter_mock().await;
|
||||
let mock_server = app.mock_server.as_ref().unwrap();
|
||||
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/api/v1/chat/completions"))
|
||||
.respond_with(mock_openrouter_response(
|
||||
r#"{"roaster": {"name": "Allpress"}, "roast": {"name": "Redchurch", "tasting_notes": ["Chocolate", "Caramel"]}}"#,
|
||||
))
|
||||
.mount(mock_server)
|
||||
.await;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let payload = serde_json::json!({ "prompt": "Allpress Redchurch" });
|
||||
|
||||
let response = client
|
||||
.post(app.api_url("/extract-bag-scan"))
|
||||
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||
.header("datastar-request", "true")
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to execute request");
|
||||
|
||||
assert_eq!(response.status(), 200);
|
||||
|
||||
let body: serde_json::Value = response.json().await.expect("Failed to parse response");
|
||||
assert_eq!(body["_roasterName"], "Allpress");
|
||||
assert_eq!(body["_roastName"], "Redchurch");
|
||||
assert_eq!(body["_tastingNotes"], "Chocolate, Caramel");
|
||||
assert_eq!(body["_scanExtracted"], true);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn extract_bag_scan_requires_auth() {
|
||||
let app = spawn_app_with_openrouter_mock().await;
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let payload = serde_json::json!({ "prompt": "test" });
|
||||
|
||||
let response = client
|
||||
.post(app.api_url("/extract-bag-scan"))
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to execute request");
|
||||
|
||||
assert_eq!(response.status(), 401);
|
||||
}
|
||||
|
|
@ -3,11 +3,15 @@ pub mod backup;
|
|||
pub mod bags_api;
|
||||
pub mod brews_api;
|
||||
pub mod cafes_api;
|
||||
pub mod checkin_api;
|
||||
pub mod cups_api;
|
||||
pub mod datastar;
|
||||
pub mod extraction_api;
|
||||
pub mod gear_api;
|
||||
pub mod helpers;
|
||||
pub mod nearby_api;
|
||||
pub mod pages;
|
||||
pub mod roasters_api;
|
||||
pub mod roasts_api;
|
||||
pub mod scan_api;
|
||||
pub mod timeline;
|
||||
|
|
|
|||
286
tests/server/pages.rs
Normal file
286
tests/server/pages.rs
Normal file
|
|
@ -0,0 +1,286 @@
|
|||
use reqwest::redirect::Policy;
|
||||
|
||||
use crate::helpers::{
|
||||
assert_full_page, create_default_bag, create_default_roast, create_default_roaster,
|
||||
create_session, spawn_app, spawn_app_with_auth,
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn homepage_returns_200_with_empty_database() {
|
||||
let app = spawn_app().await;
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let response = client
|
||||
.get(app.page_url("/"))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to execute request");
|
||||
|
||||
assert_eq!(response.status(), 200);
|
||||
|
||||
let body = response.text().await.expect("Failed to read body");
|
||||
assert_full_page(&body);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn homepage_returns_200_with_data() {
|
||||
let app = spawn_app_with_auth().await;
|
||||
|
||||
let roaster = create_default_roaster(&app).await;
|
||||
let roast = create_default_roast(&app, roaster.id).await;
|
||||
let _bag = create_default_bag(&app, roast.id).await;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let response = client
|
||||
.get(app.page_url("/"))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to execute request");
|
||||
|
||||
assert_eq!(response.status(), 200);
|
||||
|
||||
let body = response.text().await.expect("Failed to read body");
|
||||
assert_full_page(&body);
|
||||
assert!(
|
||||
body.contains("Test Roasters"),
|
||||
"Homepage should contain roaster name"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn homepage_shows_stats_counts() {
|
||||
let app = spawn_app_with_auth().await;
|
||||
|
||||
let roaster1 = create_default_roaster(&app).await;
|
||||
let _roaster2 = crate::helpers::create_roaster_with_name(&app, "Second Roasters").await;
|
||||
let _roast = create_default_roast(&app, roaster1.id).await;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let response = client
|
||||
.get(app.page_url("/"))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to execute request");
|
||||
|
||||
assert_eq!(response.status(), 200);
|
||||
|
||||
let body = response.text().await.expect("Failed to read body");
|
||||
// Stats section should show counts
|
||||
assert!(body.contains(">2<"), "Should show 2 roasters in stats");
|
||||
assert!(body.contains(">1<"), "Should show 1 roast in stats");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn login_page_returns_200() {
|
||||
let app = spawn_app().await;
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let response = client
|
||||
.get(app.page_url("/login"))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to execute request");
|
||||
|
||||
assert_eq!(response.status(), 200);
|
||||
|
||||
let body = response.text().await.expect("Failed to read body");
|
||||
assert_full_page(&body);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn checkin_page_redirects_unauthenticated_to_login() {
|
||||
let app = spawn_app().await;
|
||||
let client = reqwest::Client::builder()
|
||||
.redirect(Policy::none())
|
||||
.build()
|
||||
.expect("Failed to build client");
|
||||
|
||||
let response = client
|
||||
.get(app.page_url("/check-in"))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to execute request");
|
||||
|
||||
assert!(
|
||||
response.status().is_redirection(),
|
||||
"Expected redirect, got {}",
|
||||
response.status()
|
||||
);
|
||||
let location = response
|
||||
.headers()
|
||||
.get("location")
|
||||
.and_then(|v| v.to_str().ok());
|
||||
assert_eq!(location, Some("/login"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn checkin_page_returns_200_when_authenticated() {
|
||||
let app = spawn_app_with_auth().await;
|
||||
let session_token = create_session(&app).await;
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.redirect(Policy::none())
|
||||
.build()
|
||||
.expect("Failed to build client");
|
||||
|
||||
let response = client
|
||||
.get(app.page_url("/check-in"))
|
||||
.header("Cookie", format!("brewlog_session={session_token}"))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to execute request");
|
||||
|
||||
assert_eq!(response.status(), 200);
|
||||
|
||||
let body = response.text().await.expect("Failed to read body");
|
||||
assert_full_page(&body);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn add_page_redirects_unauthenticated_to_login() {
|
||||
let app = spawn_app().await;
|
||||
let client = reqwest::Client::builder()
|
||||
.redirect(Policy::none())
|
||||
.build()
|
||||
.expect("Failed to build client");
|
||||
|
||||
let response = client
|
||||
.get(app.page_url("/add"))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to execute request");
|
||||
|
||||
assert!(
|
||||
response.status().is_redirection(),
|
||||
"Expected redirect, got {}",
|
||||
response.status()
|
||||
);
|
||||
let location = response
|
||||
.headers()
|
||||
.get("location")
|
||||
.and_then(|v| v.to_str().ok());
|
||||
assert_eq!(location, Some("/login"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn add_page_returns_200_when_authenticated() {
|
||||
let app = spawn_app_with_auth().await;
|
||||
let session_token = create_session(&app).await;
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.redirect(Policy::none())
|
||||
.build()
|
||||
.expect("Failed to build client");
|
||||
|
||||
let response = client
|
||||
.get(app.page_url("/add"))
|
||||
.header("Cookie", format!("brewlog_session={session_token}"))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to execute request");
|
||||
|
||||
assert_eq!(response.status(), 200);
|
||||
|
||||
let body = response.text().await.expect("Failed to read body");
|
||||
assert_full_page(&body);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn account_page_redirects_unauthenticated_to_login() {
|
||||
let app = spawn_app().await;
|
||||
let client = reqwest::Client::builder()
|
||||
.redirect(Policy::none())
|
||||
.build()
|
||||
.expect("Failed to build client");
|
||||
|
||||
let response = client
|
||||
.get(app.page_url("/account"))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to execute request");
|
||||
|
||||
assert!(
|
||||
response.status().is_redirection(),
|
||||
"Expected redirect, got {}",
|
||||
response.status()
|
||||
);
|
||||
let location = response
|
||||
.headers()
|
||||
.get("location")
|
||||
.and_then(|v| v.to_str().ok());
|
||||
assert_eq!(location, Some("/login"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn account_page_returns_200_when_authenticated() {
|
||||
let app = spawn_app_with_auth().await;
|
||||
let session_token = create_session(&app).await;
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.redirect(Policy::none())
|
||||
.build()
|
||||
.expect("Failed to build client");
|
||||
|
||||
let response = client
|
||||
.get(app.page_url("/account"))
|
||||
.header("Cookie", format!("brewlog_session={session_token}"))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to execute request");
|
||||
|
||||
assert_eq!(response.status(), 200);
|
||||
|
||||
let body = response.text().await.expect("Failed to read body");
|
||||
assert_full_page(&body);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn logout_redirects_to_homepage() {
|
||||
let app = spawn_app_with_auth().await;
|
||||
let session_token = create_session(&app).await;
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.redirect(Policy::none())
|
||||
.build()
|
||||
.expect("Failed to build client");
|
||||
|
||||
let response = client
|
||||
.post(app.page_url("/logout"))
|
||||
.header("Cookie", format!("brewlog_session={session_token}"))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to execute request");
|
||||
|
||||
assert!(
|
||||
response.status().is_redirection(),
|
||||
"Expected redirect, got {}",
|
||||
response.status()
|
||||
);
|
||||
let location = response
|
||||
.headers()
|
||||
.get("location")
|
||||
.and_then(|v| v.to_str().ok());
|
||||
assert_eq!(location, Some("/"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn scan_redirect_returns_permanent_redirect() {
|
||||
let app = spawn_app().await;
|
||||
let client = reqwest::Client::builder()
|
||||
.redirect(Policy::none())
|
||||
.build()
|
||||
.expect("Failed to build client");
|
||||
|
||||
let response = client
|
||||
.get(app.page_url("/scan"))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to execute request");
|
||||
|
||||
assert_eq!(response.status(), 308);
|
||||
let location = response
|
||||
.headers()
|
||||
.get("location")
|
||||
.and_then(|v| v.to_str().ok());
|
||||
assert_eq!(location, Some("/"));
|
||||
}
|
||||
222
tests/server/scan_api.rs
Normal file
222
tests/server/scan_api.rs
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
use serde::Deserialize;
|
||||
|
||||
use crate::helpers::{create_default_roaster, spawn_app_with_auth};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ScanResult {
|
||||
redirect: String,
|
||||
roast_id: i64,
|
||||
}
|
||||
|
||||
fn scan_payload(roaster_name: &str, roast_name: &str, tasting_notes: &str) -> serde_json::Value {
|
||||
serde_json::json!({
|
||||
"roaster_name": roaster_name,
|
||||
"roaster_country": "UK",
|
||||
"roast_name": roast_name,
|
||||
"origin": "Ethiopia",
|
||||
"region": "Yirgacheffe",
|
||||
"producer": "Test Farm",
|
||||
"process": "Washed",
|
||||
"tasting_notes": tasting_notes,
|
||||
})
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn scan_creates_roaster_and_roast() {
|
||||
let app = spawn_app_with_auth().await;
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let payload = scan_payload("Scan Roasters", "Scan Roast", "Blueberry, Chocolate");
|
||||
|
||||
let response = client
|
||||
.post(app.api_url("/scan"))
|
||||
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to execute request");
|
||||
|
||||
assert_eq!(response.status(), 201);
|
||||
|
||||
let result: ScanResult = response.json().await.expect("Failed to parse response");
|
||||
assert!(result.roast_id > 0);
|
||||
assert!(
|
||||
result.redirect.contains("scan-roasters"),
|
||||
"Redirect should contain roaster slug, got: {}",
|
||||
result.redirect
|
||||
);
|
||||
assert!(
|
||||
result.redirect.contains("scan-roast"),
|
||||
"Redirect should contain roast slug, got: {}",
|
||||
result.redirect
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn scan_reuses_existing_roaster_by_slug() {
|
||||
let app = spawn_app_with_auth().await;
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
// Create roaster first via the normal API
|
||||
let _existing = create_default_roaster(&app).await;
|
||||
|
||||
// Submit scan with the same roaster name
|
||||
let payload = scan_payload("Test Roasters", "Scan Roast", "Caramel, Nutty");
|
||||
|
||||
let response = client
|
||||
.post(app.api_url("/scan"))
|
||||
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to execute request");
|
||||
|
||||
assert_eq!(response.status(), 201);
|
||||
|
||||
// Verify only one roaster exists (the pre-existing one was reused)
|
||||
let roasters_response = client
|
||||
.get(app.api_url("/roasters"))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to list roasters");
|
||||
|
||||
let roasters: Vec<serde_json::Value> = roasters_response
|
||||
.json()
|
||||
.await
|
||||
.expect("Failed to parse roasters");
|
||||
assert_eq!(
|
||||
roasters.len(),
|
||||
1,
|
||||
"Should reuse existing roaster, not create a new one"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn scan_creates_bag_when_open_bag_is_true() {
|
||||
let app = spawn_app_with_auth().await;
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let mut payload = scan_payload("Bag Roasters", "Bag Roast", "Cherry");
|
||||
payload["open_bag"] = serde_json::Value::String("true".to_string());
|
||||
payload["bag_amount"] = serde_json::json!(250.0);
|
||||
|
||||
let response = client
|
||||
.post(app.api_url("/scan"))
|
||||
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to execute request");
|
||||
|
||||
assert_eq!(response.status(), 201);
|
||||
|
||||
// Verify bag was created
|
||||
let bags_response = client
|
||||
.get(app.api_url("/bags"))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to list bags");
|
||||
|
||||
let bags: Vec<serde_json::Value> = bags_response.json().await.expect("Failed to parse bags");
|
||||
assert_eq!(bags.len(), 1, "A bag should have been created");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn scan_skips_bag_when_open_bag_absent() {
|
||||
let app = spawn_app_with_auth().await;
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let payload = scan_payload("No Bag Roasters", "No Bag Roast", "Floral");
|
||||
|
||||
let response = client
|
||||
.post(app.api_url("/scan"))
|
||||
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to execute request");
|
||||
|
||||
assert_eq!(response.status(), 201);
|
||||
|
||||
// Verify no bag was created
|
||||
let bags_response = client
|
||||
.get(app.api_url("/bags"))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to list bags");
|
||||
|
||||
let bags: Vec<serde_json::Value> = bags_response.json().await.expect("Failed to parse bags");
|
||||
assert_eq!(bags.len(), 0, "No bag should have been created");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn scan_requires_authentication() {
|
||||
let app = spawn_app_with_auth().await;
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let payload = scan_payload("Auth Roasters", "Auth Roast", "Berry");
|
||||
|
||||
let response = client
|
||||
.post(app.api_url("/scan"))
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to execute request");
|
||||
|
||||
assert_eq!(response.status(), 401);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn scan_validates_required_fields() {
|
||||
let app = spawn_app_with_auth().await;
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
// Empty roast_name should fail
|
||||
let payload = serde_json::json!({
|
||||
"roaster_name": "Valid Roasters",
|
||||
"roaster_country": "UK",
|
||||
"roast_name": "",
|
||||
"origin": "Ethiopia",
|
||||
"region": "Yirgacheffe",
|
||||
"producer": "Test Farm",
|
||||
"process": "Washed",
|
||||
"tasting_notes": "Blueberry",
|
||||
});
|
||||
|
||||
let response = client
|
||||
.post(app.api_url("/scan"))
|
||||
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to execute request");
|
||||
|
||||
assert_eq!(response.status(), 400);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn scan_requires_tasting_notes_for_manual_submission() {
|
||||
let app = spawn_app_with_auth().await;
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
// No tasting_notes and no image/prompt → should fail validation
|
||||
let payload = serde_json::json!({
|
||||
"roaster_name": "Notes Roasters",
|
||||
"roaster_country": "UK",
|
||||
"roast_name": "Notes Roast",
|
||||
"origin": "Ethiopia",
|
||||
"region": "Yirgacheffe",
|
||||
"producer": "Test Farm",
|
||||
"process": "Washed",
|
||||
});
|
||||
|
||||
let response = client
|
||||
.post(app.api_url("/scan"))
|
||||
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to execute request");
|
||||
|
||||
assert_eq!(response.status(), 400);
|
||||
}
|
||||
Loading…
Reference in a new issue