//! Import system integration tests. //! //! Tests CSV upload, progress tracking, deduplication, error handling, //! and ownership validation. use base64::Engine; use serde_json::Value; use crate::harness::TestHarness; /// Encode a CSV string as base64 for the import API. fn csv_to_base64(csv: &str) -> String { base64::engine::general_purpose::STANDARD.encode(csv.as_bytes()) } #[tokio::test] async fn import_csv_subscribers() { let mut h = TestHarness::new().await; let setup = h.create_creator_with_item("importer", "digital", 0).await; let csv = "email,name\nalice@test.com,Alice\nbob@test.com,Bob\ncharlie@test.com,Charlie\n"; let body = serde_json::json!({ "project_id": setup.project_id, "source": "generic_csv", "csv_data": csv_to_base64(csv), "column_mapping": { "email": 0, "name": 1 } }); let resp = h.client.post_json("/api/users/me/import", &body.to_string()).await; assert!(resp.status.is_success(), "Start import failed: {} {}", resp.status, resp.text); let data: Value = resp.json(); let job_id = data["job_id"].as_str().expect("should have job_id"); // Wait for background task to complete tokio::time::sleep(std::time::Duration::from_millis(500)).await; let resp = h.client.get(&format!("/api/users/me/import/{}", job_id)).await; assert!(resp.status.is_success()); let status: Value = resp.json(); assert_eq!(status["status"].as_str().unwrap(), "completed"); assert_eq!(status["total_rows"].as_i64().unwrap(), 3); assert_eq!(status["created_rows"].as_i64().unwrap(), 3); // Verify mailing_list_subscribers were created let count: i64 = sqlx::query_scalar( "SELECT COUNT(*) FROM mailing_list_subscribers WHERE email IS NOT NULL", ) .fetch_one(&h.db) .await .unwrap(); assert_eq!(count, 3); } #[tokio::test] async fn import_csv_with_transactions() { let mut h = TestHarness::new().await; let setup = h.create_creator_with_item("importer2", "digital", 0).await; let csv = "email,amount,date\nbuyer@test.com,$25.00,2024-01-15\nseller@test.com,$50.00,2024-06-01\n"; let body = serde_json::json!({ "project_id": setup.project_id, "source": "generic_csv", "csv_data": csv_to_base64(csv), "column_mapping": { "email": 0, "amount": 1, "date": 2 } }); let resp = h.client.post_json("/api/users/me/import", &body.to_string()).await; assert!(resp.status.is_success()); let data: Value = resp.json(); let job_id = data["job_id"].as_str().unwrap(); tokio::time::sleep(std::time::Duration::from_millis(500)).await; let resp = h.client.get(&format!("/api/users/me/import/{}", job_id)).await; let status: Value = resp.json(); assert_eq!(status["status"].as_str().unwrap(), "completed"); // 2 subscribers + 2 transactions = 4 total rows assert_eq!(status["total_rows"].as_i64().unwrap(), 4); // Subscribers are created, transactions are skipped (no buyer accounts) assert_eq!(status["created_rows"].as_i64().unwrap(), 2); } #[tokio::test] async fn import_duplicate_emails_deduped() { let mut h = TestHarness::new().await; let setup = h.create_creator_with_item("importer3", "digital", 0).await; let csv = "email\nalice@test.com\nalice@test.com\nbob@test.com\n"; let body = serde_json::json!({ "project_id": setup.project_id, "source": "generic_csv", "csv_data": csv_to_base64(csv), "column_mapping": { "email": 0 } }); let resp = h.client.post_json("/api/users/me/import", &body.to_string()).await; assert!(resp.status.is_success()); let data: Value = resp.json(); let job_id = data["job_id"].as_str().unwrap(); tokio::time::sleep(std::time::Duration::from_millis(500)).await; let resp = h.client.get(&format!("/api/users/me/import/{}", job_id)).await; let status: Value = resp.json(); assert_eq!(status["status"].as_str().unwrap(), "completed"); assert_eq!(status["total_rows"].as_i64().unwrap(), 3); // First alice@test.com creates, second is deduped (skipped) assert_eq!(status["created_rows"].as_i64().unwrap(), 2); assert_eq!(status["skipped_rows"].as_i64().unwrap(), 1); } #[tokio::test] async fn import_invalid_csv_returns_error() { let mut h = TestHarness::new().await; let setup = h.create_creator_with_item("importer4", "digital", 0).await; // CSV with no valid email rows let csv = "name\nAlice\nBob\n"; let body = serde_json::json!({ "project_id": setup.project_id, "source": "generic_csv", "csv_data": csv_to_base64(csv), "column_mapping": { "name": 0 } }); let resp = h.client.post_json("/api/users/me/import", &body.to_string()).await; // Validation error: no email or amount column mapped assert_eq!(resp.status.as_u16(), 422, "Should be validation error: {}", resp.text); } #[tokio::test] async fn import_wrong_project_returns_forbidden() { let mut h = TestHarness::new().await; let setup = h.create_creator_with_item("importer5", "digital", 0).await; // Sign in as a different user let _ = h.signup("otheruser", "otheruser@test.com", "password123").await; let csv = "email\nalice@test.com\n"; let body = serde_json::json!({ "project_id": setup.project_id, "source": "generic_csv", "csv_data": csv_to_base64(csv), "column_mapping": { "email": 0 } }); let resp = h.client.post_json("/api/users/me/import", &body.to_string()).await; assert_eq!(resp.status.as_u16(), 403, "Should be forbidden: {}", resp.text); } #[tokio::test] async fn import_list_jobs() { let mut h = TestHarness::new().await; let setup = h.create_creator_with_item("importer6", "digital", 0).await; let csv = "email\na@test.com\n"; let body = serde_json::json!({ "project_id": setup.project_id, "source": "generic_csv", "csv_data": csv_to_base64(csv), "column_mapping": { "email": 0 } }); let resp = h.client.post_json("/api/users/me/import", &body.to_string()).await; assert!(resp.status.is_success()); tokio::time::sleep(std::time::Duration::from_millis(300)).await; let resp = h.client.get("/api/users/me/imports").await; assert!(resp.status.is_success()); let data: Value = resp.json(); let jobs = data["data"].as_array().unwrap(); assert_eq!(jobs.len(), 1); assert_eq!(jobs[0]["source"].as_str().unwrap(), "generic_csv"); } #[tokio::test] async fn import_status_not_found_for_other_user() { let mut h = TestHarness::new().await; let setup = h.create_creator_with_item("importer7", "digital", 0).await; let csv = "email\na@test.com\n"; let body = serde_json::json!({ "project_id": setup.project_id, "source": "generic_csv", "csv_data": csv_to_base64(csv), "column_mapping": { "email": 0 } }); let resp = h.client.post_json("/api/users/me/import", &body.to_string()).await; let data: Value = resp.json(); let job_id = data["job_id"].as_str().unwrap().to_string(); // Sign in as different user let _ = h.signup("otheruser7", "otheruser7@test.com", "password123").await; let resp = h.client.get(&format!("/api/users/me/import/{}", job_id)).await; assert_eq!(resp.status.as_u16(), 404, "Other user should not see this job"); } #[tokio::test] async fn import_unsupported_source_returns_error() { let mut h = TestHarness::new().await; let setup = h.create_creator_with_item("importer8", "digital", 0).await; let csv = "email\na@test.com\n"; let body = serde_json::json!({ "project_id": setup.project_id, "source": "substack", "csv_data": csv_to_base64(csv), "column_mapping": { "email": 0 } }); let resp = h.client.post_json("/api/users/me/import", &body.to_string()).await; assert_eq!(resp.status.as_u16(), 422, "Unsupported source: {}", resp.text); }