Skip to main content

max / makenotwork

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