Skip to main content

max / makenotwork

16.9 KB · 512 lines History Blame Raw
1 //! Storage workflow tests — presign, confirm, stream, download, access control.
2
3 use crate::harness::TestHarness;
4 use serde_json::{json, Value};
5
6 /// Helper: create a trusted creator with a project and audio item. Returns (user_id, project_id, item_id).
7 async fn setup_creator_with_item(
8 h: &mut TestHarness,
9 price_cents: i64,
10 ) -> (String, String, String) {
11 let setup = h.create_creator_with_item("creator", "audio", price_cents).await;
12 h.trust_user(setup.user_id).await;
13 h.grant_tier(setup.user_id, "small_files").await;
14 (setup.user_id.to_string(), setup.project_id, setup.item_id)
15 }
16
17 // ---------------------------------------------------------------------------
18 // Presign
19 // ---------------------------------------------------------------------------
20
21 #[tokio::test]
22 async fn presign_upload_audio() {
23 let mut h = TestHarness::with_storage().await;
24 let (_, _, item_id) = setup_creator_with_item(&mut h, 0).await;
25
26 let body = json!({
27 "item_id": item_id,
28 "file_type": "audio",
29 "file_name": "episode.mp3",
30 "content_type": "audio/mpeg",
31 });
32 let resp = h.client.post_json("/api/upload/presign", &body.to_string()).await;
33 assert!(resp.status.is_success(), "Presign failed: {}", resp.text);
34
35 let data: Value = resp.json();
36 assert!(data["upload_url"].as_str().unwrap().starts_with("http://test-storage/"));
37 assert!(data["s3_key"].as_str().unwrap().contains("/audio/episode.mp3"));
38 assert_eq!(data["expires_in"], 3600);
39 }
40
41 // ---------------------------------------------------------------------------
42 // Confirm
43 // ---------------------------------------------------------------------------
44
45 #[tokio::test]
46 async fn confirm_upload_audio_updates_db() {
47 let mut h = TestHarness::with_storage().await;
48 let (_, _, item_id) = setup_creator_with_item(&mut h, 0).await;
49
50 // Presign
51 let body = json!({
52 "item_id": item_id,
53 "file_type": "audio",
54 "file_name": "song.mp3",
55 "content_type": "audio/mpeg",
56 });
57 let resp = h.client.post_json("/api/upload/presign", &body.to_string()).await;
58 assert!(resp.status.is_success());
59 let data: Value = resp.json();
60 let s3_key = data["s3_key"].as_str().unwrap().to_string();
61
62 // Simulate the client uploading to S3
63 h.storage.as_ref().unwrap().put(&s3_key, b"fake mp3 bytes".to_vec());
64
65 // Confirm
66 let body = json!({
67 "item_id": item_id,
68 "file_type": "audio",
69 "s3_key": s3_key,
70 });
71 let resp = h.client.post_json("/api/upload/confirm", &body.to_string()).await;
72 assert!(resp.status.is_success(), "Confirm failed: {}", resp.text);
73 let data: Value = resp.json();
74 assert_eq!(data["success"], true);
75
76 // Verify database
77 let db_key: Option<String> = sqlx::query_scalar(
78 "SELECT audio_s3_key FROM items WHERE id = $1::uuid",
79 )
80 .bind(&item_id)
81 .fetch_one(&h.db)
82 .await
83 .unwrap();
84 assert_eq!(db_key.as_deref(), Some(s3_key.as_str()));
85 }
86
87 #[tokio::test]
88 async fn confirm_upload_cover_updates_db() {
89 let mut h = TestHarness::with_storage().await;
90 let (_, _, item_id) = setup_creator_with_item(&mut h, 0).await;
91
92 // Presign
93 let body = json!({
94 "item_id": item_id,
95 "file_type": "cover",
96 "file_name": "art.png",
97 "content_type": "image/png",
98 });
99 let resp = h.client.post_json("/api/upload/presign", &body.to_string()).await;
100 assert!(resp.status.is_success());
101 let data: Value = resp.json();
102 let s3_key = data["s3_key"].as_str().unwrap().to_string();
103
104 // Simulate upload
105 h.storage.as_ref().unwrap().put(&s3_key, b"fake png bytes".to_vec());
106
107 // Confirm
108 let body = json!({
109 "item_id": item_id,
110 "file_type": "cover",
111 "s3_key": s3_key,
112 });
113 let resp = h.client.post_json("/api/upload/confirm", &body.to_string()).await;
114 assert!(resp.status.is_success(), "Confirm failed: {}", resp.text);
115
116 // Verify database
117 let db_key: Option<String> = sqlx::query_scalar(
118 "SELECT cover_s3_key FROM items WHERE id = $1::uuid",
119 )
120 .bind(&item_id)
121 .fetch_one(&h.db)
122 .await
123 .unwrap();
124 assert_eq!(db_key.as_deref(), Some(s3_key.as_str()));
125 }
126
127 // ---------------------------------------------------------------------------
128 // Versions
129 // ---------------------------------------------------------------------------
130
131 #[tokio::test]
132 async fn version_upload_and_download() {
133 let mut h = TestHarness::with_storage().await;
134 let (_, _, item_id) = setup_creator_with_item(&mut h, 0).await;
135
136 // Create a version (digital item needs a version for downloads)
137 let resp = h
138 .client
139 .post_json(
140 &format!("/api/items/{}/versions", item_id),
141 &json!({"version_number": "1.0.0"}).to_string(),
142 )
143 .await;
144 assert!(resp.status.is_success(), "Create version failed: {}", resp.text);
145 let version: Value = resp.json();
146 let version_id = version["id"].as_str().unwrap().to_string();
147
148 // Presign version upload
149 let resp = h
150 .client
151 .post_json(
152 &format!("/api/versions/{}/upload/presign", version_id),
153 &json!({
154 "file_name": "plugin.zip",
155 "content_type": "application/zip",
156 })
157 .to_string(),
158 )
159 .await;
160 assert!(resp.status.is_success(), "Version presign failed: {}", resp.text);
161 let data: Value = resp.json();
162 let s3_key = data["s3_key"].as_str().unwrap().to_string();
163
164 // Simulate upload
165 h.storage.as_ref().unwrap().put(&s3_key, b"fake zip data".to_vec());
166
167 // Confirm version upload
168 let resp = h
169 .client
170 .post_json(
171 &format!("/api/versions/{}/upload/confirm", version_id),
172 &json!({"s3_key": s3_key}).to_string(),
173 )
174 .await;
175 assert!(resp.status.is_success(), "Version confirm failed: {}", resp.text);
176
177 // Publish item + project so download works
178 h.client
179 .put_form(&format!("/api/items/{}", item_id), "is_public=true")
180 .await;
181 let project_id: String = sqlx::query_scalar(
182 "SELECT project_id::text FROM items WHERE id = $1::uuid",
183 )
184 .bind(&item_id)
185 .fetch_one(&h.db)
186 .await
187 .unwrap();
188 h.client
189 .put_json(
190 &format!("/api/projects/{}", project_id),
191 r#"{"is_public": true}"#,
192 )
193 .await;
194
195 // Download version
196 let resp = h
197 .client
198 .get(&format!("/api/versions/{}/download", version_id))
199 .await;
200 assert!(resp.status.is_success(), "Version download failed: {}", resp.text);
201 let data: Value = resp.json();
202 assert!(data["download_url"].as_str().unwrap().starts_with("http://test-storage/"));
203 }
204
205 // ---------------------------------------------------------------------------
206 // Audio Streaming
207 // ---------------------------------------------------------------------------
208
209 #[tokio::test]
210 async fn stream_url_free_item() {
211 let mut h = TestHarness::with_storage().await;
212 let (_, project_id, item_id) = setup_creator_with_item(&mut h, 0).await;
213
214 // Set up audio key directly in DB (simulates a completed upload)
215 let s3_key = format!("test/{}/audio/track.mp3", item_id);
216 sqlx::query("UPDATE items SET audio_s3_key = $1, scan_status = 'clean' WHERE id = $2::uuid")
217 .bind(&s3_key)
218 .bind(&item_id)
219 .execute(&h.db)
220 .await
221 .unwrap();
222
223 // Pre-populate storage
224 h.storage.as_ref().unwrap().put(&s3_key, b"audio data".to_vec());
225
226 // Publish
227 h.client
228 .put_form(&format!("/api/items/{}", item_id), "is_public=true")
229 .await;
230 h.client
231 .put_json(
232 &format!("/api/projects/{}", project_id),
233 r#"{"is_public": true}"#,
234 )
235 .await;
236
237 // Stream — free item, any user can access
238 let resp = h.client.get(&format!("/api/stream/{}", item_id)).await;
239 assert!(resp.status.is_success(), "Stream failed: {}", resp.text);
240 let data: Value = resp.json();
241 assert!(data["stream_url"].as_str().unwrap().starts_with("http://test-storage/"));
242 }
243
244 #[tokio::test]
245 async fn stream_url_paid_requires_purchase() {
246 let mut h = TestHarness::with_storage().await;
247 let (_, project_id, item_id) = setup_creator_with_item(&mut h, 500).await;
248
249 // Set up audio key
250 let s3_key = format!("test/{}/audio/track.mp3", item_id);
251 sqlx::query("UPDATE items SET audio_s3_key = $1, scan_status = 'clean' WHERE id = $2::uuid")
252 .bind(&s3_key)
253 .bind(&item_id)
254 .execute(&h.db)
255 .await
256 .unwrap();
257
258 h.storage.as_ref().unwrap().put(&s3_key, b"audio data".to_vec());
259
260 // Publish
261 h.client
262 .put_form(&format!("/api/items/{}", item_id), "is_public=true")
263 .await;
264 h.client
265 .put_json(
266 &format!("/api/projects/{}", project_id),
267 r#"{"is_public": true}"#,
268 )
269 .await;
270
271 // Log out the creator and sign up a buyer with no purchase
272 h.client.post_form("/logout", "").await;
273 h.signup("buyer", "buyer@test.com", "password123").await;
274 h.login("buyer", "password123").await;
275
276 // Stream should be forbidden (paid, no purchase)
277 let resp = h.client.get(&format!("/api/stream/{}", item_id)).await;
278 assert_eq!(resp.status.as_u16(), 403, "Expected 403, got: {}", resp.text);
279 }
280
281 /// Helper: set up a paid item with audio, publish it, return (creator_user_id, project_id, item_id, s3_key).
282 /// Leaves the creator logged in.
283 async fn setup_published_paid_audio(
284 h: &mut TestHarness,
285 username: &str,
286 price_cents: i64,
287 ) -> (String, String, String, String) {
288 let setup = h.create_creator_with_item(username, "audio", price_cents).await;
289 h.trust_user(setup.user_id).await;
290 h.grant_tier(setup.user_id, "small_files").await;
291 let user_id = setup.user_id.to_string();
292
293 let s3_key = format!("test/{}/audio/track.mp3", setup.item_id);
294 sqlx::query("UPDATE items SET audio_s3_key = $1, scan_status = 'clean' WHERE id = $2::uuid")
295 .bind(&s3_key)
296 .bind(&setup.item_id)
297 .execute(&h.db)
298 .await
299 .unwrap();
300 h.storage.as_ref().unwrap().put(&s3_key, b"audio data".to_vec());
301
302 h.publish_project_and_item(&setup.project_id, &setup.item_id)
303 .await;
304
305 (user_id, setup.project_id, setup.item_id, s3_key)
306 }
307
308 // ---------------------------------------------------------------------------
309 // Stream access control (test-fuzz)
310 // ---------------------------------------------------------------------------
311
312 /// Unauthenticated user gets 401 on paid item stream.
313 #[tokio::test]
314 async fn stream_url_paid_unauthenticated_returns_401() {
315 let mut h = TestHarness::with_storage().await;
316 let (_, _, item_id, _) = setup_published_paid_audio(&mut h, "seller401", 500).await;
317
318 // Log out — no session
319 h.client.post_form("/logout", "").await;
320
321 let resp = h.client.get(&format!("/api/stream/{}", item_id)).await;
322 assert_eq!(
323 resp.status.as_u16(),
324 401,
325 "Unauthenticated stream of paid item should be 401, got: {}",
326 resp.status
327 );
328 }
329
330 /// After a direct DB purchase record, buyer can stream paid content.
331 #[tokio::test]
332 async fn stream_url_paid_purchaser_gets_access() {
333 let mut h = TestHarness::with_storage().await;
334 let (creator_id, _, item_id, _) = setup_published_paid_audio(&mut h, "sellaccess", 999).await;
335
336 // Create buyer and insert a completed transaction (simulates webhook completion)
337 h.client.post_form("/logout", "").await;
338 let buyer_id = h.signup("buyaccess", "buyaccess@test.com", "password123").await;
339
340 sqlx::query(
341 r#"INSERT INTO transactions
342 (buyer_id, seller_id, item_id, amount_cents, status,
343 stripe_checkout_session_id, item_title, seller_username,
344 completed_at)
345 VALUES ($1, $2::uuid, $3::uuid, 999, 'completed',
346 'cs_access_test', 'Track', 'sellaccess', NOW())"#,
347 )
348 .bind(buyer_id)
349 .bind(&creator_id)
350 .bind(&item_id)
351 .execute(&h.db)
352 .await
353 .unwrap();
354
355 h.login("buyaccess", "password123").await;
356
357 let resp = h.client.get(&format!("/api/stream/{}", item_id)).await;
358 assert!(
359 resp.status.is_success(),
360 "Purchaser should be able to stream paid item, got: {} {}",
361 resp.status, resp.text
362 );
363 let data: Value = resp.json();
364 assert!(
365 data["stream_url"].as_str().is_some(),
366 "Response should contain stream_url"
367 );
368 }
369
370 /// Creator can always stream their own paid content.
371 #[tokio::test]
372 async fn stream_url_creator_always_has_access() {
373 let mut h = TestHarness::with_storage().await;
374 let (_, _, item_id, _) = setup_published_paid_audio(&mut h, "selfstream", 999).await;
375
376 // Creator is still logged in
377 let resp = h.client.get(&format!("/api/stream/{}", item_id)).await;
378 assert!(
379 resp.status.is_success(),
380 "Creator should stream their own paid item, got: {} {}",
381 resp.status, resp.text
382 );
383 }
384
385 /// Unpublished (draft) item returns 404 for non-owner.
386 #[tokio::test]
387 async fn stream_url_draft_item_404_for_non_owner() {
388 let mut h = TestHarness::with_storage().await;
389 let setup = h.create_creator_with_item("draftowner", "audio", 0).await;
390 h.trust_user(setup.user_id).await;
391 h.grant_tier(setup.user_id, "small_files").await;
392
393 let s3_key = format!("test/{}/audio/draft.mp3", setup.item_id);
394 sqlx::query("UPDATE items SET audio_s3_key = $1, scan_status = 'clean' WHERE id = $2::uuid")
395 .bind(&s3_key)
396 .bind(&setup.item_id)
397 .execute(&h.db)
398 .await
399 .unwrap();
400 h.storage.as_ref().unwrap().put(&s3_key, b"audio data".to_vec());
401
402 // Explicitly unpublish the item (items default to is_public=true)
403 h.client
404 .put_form(&format!("/api/items/{}", setup.item_id), "is_public=false")
405 .await;
406
407 h.client.post_form("/logout", "").await;
408 h.signup("snooper", "snooper@test.com", "password123").await;
409 h.login("snooper", "password123").await;
410
411 let resp = h
412 .client
413 .get(&format!("/api/stream/{}", setup.item_id))
414 .await;
415 assert_eq!(
416 resp.status.as_u16(),
417 404,
418 "Draft item should be 404 for non-owner, got: {}",
419 resp.status
420 );
421 }
422
423 /// Version download for paid item: non-purchaser gets 403.
424 #[tokio::test]
425 async fn version_download_paid_non_purchaser_forbidden() {
426 let mut h = TestHarness::with_storage().await;
427 let setup = h
428 .create_creator_with_item("verseller", "digital", 500)
429 .await;
430 h.trust_user(setup.user_id).await;
431 h.grant_tier(setup.user_id, "small_files").await;
432
433 // Create version with file
434 let resp = h
435 .client
436 .post_json(
437 &format!("/api/items/{}/versions", setup.item_id),
438 &json!({"version_number": "1.0.0"}).to_string(),
439 )
440 .await;
441 assert!(resp.status.is_success(), "Create version failed: {}", resp.text);
442 let version: Value = resp.json();
443 let version_id = version["id"].as_str().unwrap().to_string();
444
445 let resp = h
446 .client
447 .post_json(
448 &format!("/api/versions/{}/upload/presign", version_id),
449 &json!({"file_name": "app.zip", "content_type": "application/zip"}).to_string(),
450 )
451 .await;
452 assert!(resp.status.is_success());
453 let data: Value = resp.json();
454 let s3_key = data["s3_key"].as_str().unwrap().to_string();
455 h.storage
456 .as_ref()
457 .unwrap()
458 .put(&s3_key, b"zip data".to_vec());
459
460 h.client
461 .post_json(
462 &format!("/api/versions/{}/upload/confirm", version_id),
463 &json!({"s3_key": s3_key}).to_string(),
464 )
465 .await;
466
467 // Publish
468 h.publish_project_and_item(&setup.project_id, &setup.item_id)
469 .await;
470
471 // Non-purchaser tries to download
472 h.client.post_form("/logout", "").await;
473 h.signup("verbuyer", "verbuyer@test.com", "password123").await;
474 h.login("verbuyer", "password123").await;
475
476 let resp = h
477 .client
478 .get(&format!("/api/versions/{}/download", version_id))
479 .await;
480 assert_eq!(
481 resp.status.as_u16(),
482 403,
483 "Non-purchaser version download should be 403, got: {}",
484 resp.status
485 );
486 }
487
488 // ---------------------------------------------------------------------------
489 // Access control
490 // ---------------------------------------------------------------------------
491
492 #[tokio::test]
493 async fn upload_non_owner_forbidden() {
494 let mut h = TestHarness::with_storage().await;
495 let (_, _, item_id) = setup_creator_with_item(&mut h, 0).await;
496
497 // Log out creator, sign up a different user
498 h.client.post_form("/logout", "").await;
499 h.signup("intruder", "intruder@test.com", "password123").await;
500 h.login("intruder", "password123").await;
501
502 // Attempt to presign to creator's item
503 let body = json!({
504 "item_id": item_id,
505 "file_type": "audio",
506 "file_name": "evil.mp3",
507 "content_type": "audio/mpeg",
508 });
509 let resp = h.client.post_json("/api/upload/presign", &body.to_string()).await;
510 assert_eq!(resp.status.as_u16(), 403, "Expected 403, got: {}", resp.text);
511 }
512