- 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
222 lines
6.2 KiB
Rust
222 lines
6.2 KiB
Rust
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);
|
|
}
|