Skip to main content

max / makenotwork

7.8 KB · 225 lines History Blame Raw
1 //! Import system integration tests.
2 //!
3 //! Tests CSV upload, progress tracking, deduplication, error handling,
4 //! and ownership validation.
5
6 use base64::Engine;
7 use serde_json::Value;
8
9 use crate::harness::TestHarness;
10
11 /// Encode a CSV string as base64 for the import API.
12 fn csv_to_base64(csv: &str) -> String {
13 base64::engine::general_purpose::STANDARD.encode(csv.as_bytes())
14 }
15
16 #[tokio::test]
17 async fn import_csv_subscribers() {
18 let mut h = TestHarness::new().await;
19 let setup = h.create_creator_with_item("importer", "digital", 0).await;
20
21 let csv = "email,name\nalice@test.com,Alice\nbob@test.com,Bob\ncharlie@test.com,Charlie\n";
22
23 let body = serde_json::json!({
24 "project_id": setup.project_id,
25 "source": "generic_csv",
26 "csv_data": csv_to_base64(csv),
27 "column_mapping": { "email": 0, "name": 1 }
28 });
29
30 let resp = h.client.post_json("/api/users/me/import", &body.to_string()).await;
31 assert!(resp.status.is_success(), "Start import failed: {} {}", resp.status, resp.text);
32
33 let data: Value = resp.json();
34 let job_id = data["job_id"].as_str().expect("should have job_id");
35
36 // Wait for background task to complete
37 tokio::time::sleep(std::time::Duration::from_millis(500)).await;
38
39 let resp = h.client.get(&format!("/api/users/me/import/{}", job_id)).await;
40 assert!(resp.status.is_success());
41 let status: Value = resp.json();
42 assert_eq!(status["status"].as_str().unwrap(), "completed");
43 assert_eq!(status["total_rows"].as_i64().unwrap(), 3);
44 assert_eq!(status["created_rows"].as_i64().unwrap(), 3);
45
46 // Verify mailing_list_subscribers were created
47 let count: i64 = sqlx::query_scalar(
48 "SELECT COUNT(*) FROM mailing_list_subscribers WHERE email IS NOT NULL",
49 )
50 .fetch_one(&h.db)
51 .await
52 .unwrap();
53 assert_eq!(count, 3);
54 }
55
56 #[tokio::test]
57 async fn import_csv_with_transactions() {
58 let mut h = TestHarness::new().await;
59 let setup = h.create_creator_with_item("importer2", "digital", 0).await;
60
61 let csv = "email,amount,date\nbuyer@test.com,$25.00,2024-01-15\nseller@test.com,$50.00,2024-06-01\n";
62
63 let body = serde_json::json!({
64 "project_id": setup.project_id,
65 "source": "generic_csv",
66 "csv_data": csv_to_base64(csv),
67 "column_mapping": { "email": 0, "amount": 1, "date": 2 }
68 });
69
70 let resp = h.client.post_json("/api/users/me/import", &body.to_string()).await;
71 assert!(resp.status.is_success());
72
73 let data: Value = resp.json();
74 let job_id = data["job_id"].as_str().unwrap();
75
76 tokio::time::sleep(std::time::Duration::from_millis(500)).await;
77
78 let resp = h.client.get(&format!("/api/users/me/import/{}", job_id)).await;
79 let status: Value = resp.json();
80 assert_eq!(status["status"].as_str().unwrap(), "completed");
81 // 2 subscribers + 2 transactions = 4 total rows
82 assert_eq!(status["total_rows"].as_i64().unwrap(), 4);
83 // Subscribers are created, transactions are skipped (no buyer accounts)
84 assert_eq!(status["created_rows"].as_i64().unwrap(), 2);
85 }
86
87 #[tokio::test]
88 async fn import_duplicate_emails_deduped() {
89 let mut h = TestHarness::new().await;
90 let setup = h.create_creator_with_item("importer3", "digital", 0).await;
91
92 let csv = "email\nalice@test.com\nalice@test.com\nbob@test.com\n";
93
94 let body = serde_json::json!({
95 "project_id": setup.project_id,
96 "source": "generic_csv",
97 "csv_data": csv_to_base64(csv),
98 "column_mapping": { "email": 0 }
99 });
100
101 let resp = h.client.post_json("/api/users/me/import", &body.to_string()).await;
102 assert!(resp.status.is_success());
103
104 let data: Value = resp.json();
105 let job_id = data["job_id"].as_str().unwrap();
106
107 tokio::time::sleep(std::time::Duration::from_millis(500)).await;
108
109 let resp = h.client.get(&format!("/api/users/me/import/{}", job_id)).await;
110 let status: Value = resp.json();
111 assert_eq!(status["status"].as_str().unwrap(), "completed");
112 assert_eq!(status["total_rows"].as_i64().unwrap(), 3);
113 // First alice@test.com creates, second is deduped (skipped)
114 assert_eq!(status["created_rows"].as_i64().unwrap(), 2);
115 assert_eq!(status["skipped_rows"].as_i64().unwrap(), 1);
116 }
117
118 #[tokio::test]
119 async fn import_invalid_csv_returns_error() {
120 let mut h = TestHarness::new().await;
121 let setup = h.create_creator_with_item("importer4", "digital", 0).await;
122
123 // CSV with no valid email rows
124 let csv = "name\nAlice\nBob\n";
125
126 let body = serde_json::json!({
127 "project_id": setup.project_id,
128 "source": "generic_csv",
129 "csv_data": csv_to_base64(csv),
130 "column_mapping": { "name": 0 }
131 });
132
133 let resp = h.client.post_json("/api/users/me/import", &body.to_string()).await;
134 // Validation error: no email or amount column mapped
135 assert_eq!(resp.status.as_u16(), 422, "Should be validation error: {}", resp.text);
136 }
137
138 #[tokio::test]
139 async fn import_wrong_project_returns_forbidden() {
140 let mut h = TestHarness::new().await;
141 let setup = h.create_creator_with_item("importer5", "digital", 0).await;
142
143 // Sign in as a different user
144 let _ = h.signup("otheruser", "otheruser@test.com", "password123").await;
145
146 let csv = "email\nalice@test.com\n";
147
148 let body = serde_json::json!({
149 "project_id": setup.project_id,
150 "source": "generic_csv",
151 "csv_data": csv_to_base64(csv),
152 "column_mapping": { "email": 0 }
153 });
154
155 let resp = h.client.post_json("/api/users/me/import", &body.to_string()).await;
156 assert_eq!(resp.status.as_u16(), 403, "Should be forbidden: {}", resp.text);
157 }
158
159 #[tokio::test]
160 async fn import_list_jobs() {
161 let mut h = TestHarness::new().await;
162 let setup = h.create_creator_with_item("importer6", "digital", 0).await;
163
164 let csv = "email\na@test.com\n";
165 let body = serde_json::json!({
166 "project_id": setup.project_id,
167 "source": "generic_csv",
168 "csv_data": csv_to_base64(csv),
169 "column_mapping": { "email": 0 }
170 });
171
172 let resp = h.client.post_json("/api/users/me/import", &body.to_string()).await;
173 assert!(resp.status.is_success());
174
175 tokio::time::sleep(std::time::Duration::from_millis(300)).await;
176
177 let resp = h.client.get("/api/users/me/imports").await;
178 assert!(resp.status.is_success());
179 let data: Value = resp.json();
180 let jobs = data["data"].as_array().unwrap();
181 assert_eq!(jobs.len(), 1);
182 assert_eq!(jobs[0]["source"].as_str().unwrap(), "generic_csv");
183 }
184
185 #[tokio::test]
186 async fn import_status_not_found_for_other_user() {
187 let mut h = TestHarness::new().await;
188 let setup = h.create_creator_with_item("importer7", "digital", 0).await;
189
190 let csv = "email\na@test.com\n";
191 let body = serde_json::json!({
192 "project_id": setup.project_id,
193 "source": "generic_csv",
194 "csv_data": csv_to_base64(csv),
195 "column_mapping": { "email": 0 }
196 });
197
198 let resp = h.client.post_json("/api/users/me/import", &body.to_string()).await;
199 let data: Value = resp.json();
200 let job_id = data["job_id"].as_str().unwrap().to_string();
201
202 // Sign in as different user
203 let _ = h.signup("otheruser7", "otheruser7@test.com", "password123").await;
204
205 let resp = h.client.get(&format!("/api/users/me/import/{}", job_id)).await;
206 assert_eq!(resp.status.as_u16(), 404, "Other user should not see this job");
207 }
208
209 #[tokio::test]
210 async fn import_unsupported_source_returns_error() {
211 let mut h = TestHarness::new().await;
212 let setup = h.create_creator_with_item("importer8", "digital", 0).await;
213
214 let csv = "email\na@test.com\n";
215 let body = serde_json::json!({
216 "project_id": setup.project_id,
217 "source": "substack",
218 "csv_data": csv_to_base64(csv),
219 "column_mapping": { "email": 0 }
220 });
221
222 let resp = h.client.post_json("/api/users/me/import", &body.to_string()).await;
223 assert_eq!(resp.status.as_u16(), 422, "Unsupported source: {}", resp.text);
224 }
225