Skip to main content

max / makenotwork

8.4 KB · 288 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
31 .client
32 .post_json("/api/users/me/import", &body.to_string())
33 .await;
34 assert_eq!(
35 resp.status, 200,
36 "Start import failed: {} {}",
37 resp.status, resp.text
38 );
39
40 let data: Value = resp.json();
41 let job_id = data["job_id"].as_str().expect("should have job_id");
42
43 // Wait for background task to complete
44 tokio::time::sleep(std::time::Duration::from_millis(500)).await;
45
46 let resp = h
47 .client
48 .get(&format!("/api/users/me/import/{job_id}"))
49 .await;
50 assert_eq!(resp.status, 200, "{}", resp.text);
51 let status: Value = resp.json();
52 assert_eq!(status["status"].as_str().unwrap(), "completed");
53 assert_eq!(status["total_rows"].as_i64().unwrap(), 3);
54 assert_eq!(status["created_rows"].as_i64().unwrap(), 3);
55
56 // Verify mailing_list_subscribers were created
57 let count: i64 =
58 sqlx::query_scalar("SELECT COUNT(*) FROM mailing_list_subscribers WHERE email IS NOT NULL")
59 .fetch_one(&h.db)
60 .await
61 .unwrap();
62 assert_eq!(count, 3);
63 }
64
65 #[tokio::test]
66 async fn import_csv_with_transactions() {
67 let mut h = TestHarness::new().await;
68 let setup = h.create_creator_with_item("importer2", "digital", 0).await;
69
70 let csv =
71 "email,amount,date\nbuyer@test.com,$25.00,2024-01-15\nseller@test.com,$50.00,2024-06-01\n";
72
73 let body = serde_json::json!({
74 "project_id": setup.project_id,
75 "source": "generic_csv",
76 "csv_data": csv_to_base64(csv),
77 "column_mapping": { "email": 0, "amount": 1, "date": 2 }
78 });
79
80 let resp = h
81 .client
82 .post_json("/api/users/me/import", &body.to_string())
83 .await;
84 assert_eq!(resp.status, 200, "{}", resp.text);
85
86 let data: Value = resp.json();
87 let job_id = data["job_id"].as_str().unwrap();
88
89 tokio::time::sleep(std::time::Duration::from_millis(500)).await;
90
91 let resp = h
92 .client
93 .get(&format!("/api/users/me/import/{job_id}"))
94 .await;
95 let status: Value = resp.json();
96 assert_eq!(status["status"].as_str().unwrap(), "completed");
97 // 2 subscribers + 2 transactions = 4 total rows
98 assert_eq!(status["total_rows"].as_i64().unwrap(), 4);
99 // Subscribers are created, transactions are skipped (no buyer accounts)
100 assert_eq!(status["created_rows"].as_i64().unwrap(), 2);
101 }
102
103 #[tokio::test]
104 async fn import_duplicate_emails_deduped() {
105 let mut h = TestHarness::new().await;
106 let setup = h.create_creator_with_item("importer3", "digital", 0).await;
107
108 let csv = "email\nalice@test.com\nalice@test.com\nbob@test.com\n";
109
110 let body = serde_json::json!({
111 "project_id": setup.project_id,
112 "source": "generic_csv",
113 "csv_data": csv_to_base64(csv),
114 "column_mapping": { "email": 0 }
115 });
116
117 let resp = h
118 .client
119 .post_json("/api/users/me/import", &body.to_string())
120 .await;
121 assert_eq!(resp.status, 200, "{}", resp.text);
122
123 let data: Value = resp.json();
124 let job_id = data["job_id"].as_str().unwrap();
125
126 tokio::time::sleep(std::time::Duration::from_millis(500)).await;
127
128 let resp = h
129 .client
130 .get(&format!("/api/users/me/import/{job_id}"))
131 .await;
132 let status: Value = resp.json();
133 assert_eq!(status["status"].as_str().unwrap(), "completed");
134 assert_eq!(status["total_rows"].as_i64().unwrap(), 3);
135 // First alice@test.com creates, second is deduped (skipped)
136 assert_eq!(status["created_rows"].as_i64().unwrap(), 2);
137 assert_eq!(status["skipped_rows"].as_i64().unwrap(), 1);
138 }
139
140 #[tokio::test]
141 async fn import_invalid_csv_returns_error() {
142 let mut h = TestHarness::new().await;
143 let setup = h.create_creator_with_item("importer4", "digital", 0).await;
144
145 // CSV with no valid email rows
146 let csv = "name\nAlice\nBob\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": { "name": 0 }
153 });
154
155 let resp = h
156 .client
157 .post_json("/api/users/me/import", &body.to_string())
158 .await;
159 // Validation error: no email or amount column mapped
160 assert_eq!(
161 resp.status.as_u16(),
162 422,
163 "Should be validation error: {}",
164 resp.text
165 );
166 }
167
168 #[tokio::test]
169 async fn import_wrong_project_returns_forbidden() {
170 let mut h = TestHarness::new().await;
171 let setup = h.create_creator_with_item("importer5", "digital", 0).await;
172
173 // Sign in as a different user
174 let _ = h
175 .signup("otheruser", "otheruser@test.com", "password123")
176 .await;
177
178 let csv = "email\nalice@test.com\n";
179
180 let body = serde_json::json!({
181 "project_id": setup.project_id,
182 "source": "generic_csv",
183 "csv_data": csv_to_base64(csv),
184 "column_mapping": { "email": 0 }
185 });
186
187 let resp = h
188 .client
189 .post_json("/api/users/me/import", &body.to_string())
190 .await;
191 assert_eq!(
192 resp.status.as_u16(),
193 403,
194 "Should be forbidden: {}",
195 resp.text
196 );
197 }
198
199 #[tokio::test]
200 async fn import_list_jobs() {
201 let mut h = TestHarness::new().await;
202 let setup = h.create_creator_with_item("importer6", "digital", 0).await;
203
204 let csv = "email\na@test.com\n";
205 let body = serde_json::json!({
206 "project_id": setup.project_id,
207 "source": "generic_csv",
208 "csv_data": csv_to_base64(csv),
209 "column_mapping": { "email": 0 }
210 });
211
212 let resp = h
213 .client
214 .post_json("/api/users/me/import", &body.to_string())
215 .await;
216 assert_eq!(resp.status, 200, "{}", resp.text);
217
218 tokio::time::sleep(std::time::Duration::from_millis(300)).await;
219
220 let resp = h.client.get("/api/users/me/imports").await;
221 assert_eq!(resp.status, 200, "{}", resp.text);
222 let data: Value = resp.json();
223 let jobs = data["data"].as_array().unwrap();
224 assert_eq!(jobs.len(), 1);
225 assert_eq!(jobs[0]["source"].as_str().unwrap(), "generic_csv");
226 }
227
228 #[tokio::test]
229 async fn import_status_not_found_for_other_user() {
230 let mut h = TestHarness::new().await;
231 let setup = h.create_creator_with_item("importer7", "digital", 0).await;
232
233 let csv = "email\na@test.com\n";
234 let body = serde_json::json!({
235 "project_id": setup.project_id,
236 "source": "generic_csv",
237 "csv_data": csv_to_base64(csv),
238 "column_mapping": { "email": 0 }
239 });
240
241 let resp = h
242 .client
243 .post_json("/api/users/me/import", &body.to_string())
244 .await;
245 let data: Value = resp.json();
246 let job_id = data["job_id"].as_str().unwrap().to_string();
247
248 // Sign in as different user
249 let _ = h
250 .signup("otheruser7", "otheruser7@test.com", "password123")
251 .await;
252
253 let resp = h
254 .client
255 .get(&format!("/api/users/me/import/{job_id}"))
256 .await;
257 assert_eq!(
258 resp.status.as_u16(),
259 404,
260 "Other user should not see this job"
261 );
262 }
263
264 #[tokio::test]
265 async fn import_unsupported_source_returns_error() {
266 let mut h = TestHarness::new().await;
267 let setup = h.create_creator_with_item("importer8", "digital", 0).await;
268
269 let csv = "email\na@test.com\n";
270 let body = serde_json::json!({
271 "project_id": setup.project_id,
272 "source": "substack",
273 "csv_data": csv_to_base64(csv),
274 "column_mapping": { "email": 0 }
275 });
276
277 let resp = h
278 .client
279 .post_json("/api/users/me/import", &body.to_string())
280 .await;
281 assert_eq!(
282 resp.status.as_u16(),
283 422,
284 "Unsupported source: {}",
285 resp.text
286 );
287 }
288