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