Skip to main content

max / makenotwork

14.7 KB · 517 lines History Blame Raw
1 //! Creator media workflow tests, real audio files through the full upload pipeline.
2 //!
3 //! Uses ffmpeg-generated test fixtures from `tests/fixtures/`.
4
5 use crate::harness::TestHarness;
6 use serde_json::{Value, json};
7
8 const TEST_MP3: &[u8] = include_bytes!("../fixtures/test.mp3");
9 const TEST_FLAC: &[u8] = include_bytes!("../fixtures/test.flac");
10 const TEST_WAV: &[u8] = include_bytes!("../fixtures/test.wav");
11 const TEST_OGG: &[u8] = include_bytes!("../fixtures/test.ogg");
12 const TEST_M4A: &[u8] = include_bytes!("../fixtures/test.m4a");
13
14 /// Helper: set up a trusted creator with a project and audio item.
15 async fn setup_creator(h: &mut TestHarness, username: &str) -> (String, String, String) {
16 let setup = h.create_creator_with_item(username, "audio", 0).await;
17 h.trust_user(setup.user_id).await;
18 h.grant_tier(setup.user_id, "small_files").await;
19 (setup.user_id.to_string(), setup.project_id, setup.item_id)
20 }
21
22 /// Helper: presign + upload real bytes + confirm for an audio file.
23 async fn upload_audio(
24 h: &mut TestHarness,
25 item_id: &str,
26 file_name: &str,
27 content_type: &str,
28 data: &[u8],
29 ) -> String {
30 let body = json!({
31 "item_id": item_id,
32 "file_type": "audio",
33 "file_name": file_name,
34 "content_type": content_type,
35 });
36 let resp = h
37 .client
38 .post_json("/api/upload/presign", &body.to_string())
39 .await;
40 assert!(
41 resp.status.is_success(),
42 "Presign {} failed: {}",
43 file_name,
44 resp.text
45 );
46 let data_resp: Value = resp.json();
47 let s3_key = data_resp["s3_key"].as_str().unwrap().to_string();
48
49 // Put real file bytes into in-memory storage
50 h.storage.as_ref().unwrap().put(&s3_key, data.to_vec());
51
52 // Confirm
53 let body = json!({
54 "item_id": item_id,
55 "file_type": "audio",
56 "s3_key": s3_key,
57 });
58 let resp = h
59 .client
60 .post_json("/api/upload/confirm", &body.to_string())
61 .await;
62 assert!(
63 resp.status.is_success(),
64 "Confirm {} failed: {}",
65 file_name,
66 resp.text
67 );
68
69 // Async scan pipeline (Phase 1), drive the worker so the caller can
70 // assert final scan_status without sleeping.
71 h.drain_scan_jobs().await;
72
73 s3_key
74 }
75
76 // Multi-format upload
77
78 #[tokio::test]
79 async fn upload_real_mp3() {
80 let mut h = TestHarness::with_storage().await;
81 let (_, _, item_id) = setup_creator(&mut h, "mp3user").await;
82 let s3_key = upload_audio(&mut h, &item_id, "track.mp3", "audio/mpeg", TEST_MP3).await;
83
84 let db_key: Option<String> =
85 sqlx::query_scalar("SELECT audio_s3_key FROM items WHERE id = $1::uuid")
86 .bind(&item_id)
87 .fetch_one(&h.db)
88 .await
89 .unwrap();
90 assert_eq!(db_key.as_deref(), Some(s3_key.as_str()));
91 }
92
93 #[tokio::test]
94 async fn upload_real_flac() {
95 let mut h = TestHarness::with_storage().await;
96 let (_, _, item_id) = setup_creator(&mut h, "flacuser").await;
97 upload_audio(&mut h, &item_id, "track.flac", "audio/flac", TEST_FLAC).await;
98 }
99
100 #[tokio::test]
101 async fn upload_real_wav() {
102 let mut h = TestHarness::with_storage().await;
103 let (_, _, item_id) = setup_creator(&mut h, "wavuser").await;
104 upload_audio(&mut h, &item_id, "track.wav", "audio/wav", TEST_WAV).await;
105 }
106
107 #[tokio::test]
108 async fn upload_real_ogg() {
109 let mut h = TestHarness::with_storage().await;
110 let (_, _, item_id) = setup_creator(&mut h, "ogguser").await;
111 upload_audio(&mut h, &item_id, "track.ogg", "audio/ogg", TEST_OGG).await;
112 }
113
114 #[tokio::test]
115 async fn upload_real_m4a() {
116 let mut h = TestHarness::with_storage().await;
117 let (_, _, item_id) = setup_creator(&mut h, "m4auser").await;
118 upload_audio(&mut h, &item_id, "track.m4a", "audio/mp4", TEST_M4A).await;
119 }
120
121 // Scanning with real audio
122
123 #[tokio::test]
124 async fn scan_real_mp3_passes() {
125 let mut h = TestHarness::with_storage_and_scanner().await;
126 let (_, _, item_id) = setup_creator(&mut h, "scanmp3").await;
127 upload_audio(&mut h, &item_id, "clean.mp3", "audio/mpeg", TEST_MP3).await;
128
129 let scan_status: String =
130 sqlx::query_scalar("SELECT scan_status FROM items WHERE id = $1::uuid")
131 .bind(&item_id)
132 .fetch_one(&h.db)
133 .await
134 .unwrap();
135 assert_eq!(scan_status, "clean", "Real MP3 should pass scanning");
136 }
137
138 #[tokio::test]
139 async fn scan_real_flac_passes() {
140 let mut h = TestHarness::with_storage_and_scanner().await;
141 let (_, _, item_id) = setup_creator(&mut h, "scanflac").await;
142 upload_audio(&mut h, &item_id, "clean.flac", "audio/flac", TEST_FLAC).await;
143
144 let scan_status: String =
145 sqlx::query_scalar("SELECT scan_status FROM items WHERE id = $1::uuid")
146 .bind(&item_id)
147 .fetch_one(&h.db)
148 .await
149 .unwrap();
150 assert_eq!(scan_status, "clean", "Real FLAC should pass scanning");
151 }
152
153 #[tokio::test]
154 async fn scan_real_wav_passes() {
155 let mut h = TestHarness::with_storage_and_scanner().await;
156 let (_, _, item_id) = setup_creator(&mut h, "scanwav").await;
157 upload_audio(&mut h, &item_id, "clean.wav", "audio/wav", TEST_WAV).await;
158
159 let scan_status: String =
160 sqlx::query_scalar("SELECT scan_status FROM items WHERE id = $1::uuid")
161 .bind(&item_id)
162 .fetch_one(&h.db)
163 .await
164 .unwrap();
165 assert_eq!(scan_status, "clean", "Real WAV should pass scanning");
166 }
167
168 // Full lifecycle: upload → chapters → publish → stream
169
170 #[tokio::test]
171 async fn full_audio_lifecycle() {
172 let mut h = TestHarness::with_storage().await;
173 let (_, project_id, item_id) = setup_creator(&mut h, "lifecycle").await;
174
175 // Upload real MP3
176 let s3_key = upload_audio(&mut h, &item_id, "episode.mp3", "audio/mpeg", TEST_MP3).await;
177
178 // Add chapters
179 let resp = h
180 .client
181 .post_json(
182 &format!("/api/items/{item_id}/chapters"),
183 r#"{"title": "Intro", "start_seconds": 0.0, "sort_order": 0}"#,
184 )
185 .await;
186 assert!(
187 resp.status.is_success(),
188 "Create chapter failed: {}",
189 resp.text
190 );
191
192 let resp = h
193 .client
194 .post_json(
195 &format!("/api/items/{item_id}/chapters"),
196 r#"{"title": "Main Content", "start_seconds": 15.0, "sort_order": 1}"#,
197 )
198 .await;
199 assert!(
200 resp.status.is_success(),
201 "Create chapter 2 failed: {}",
202 resp.text
203 );
204
205 // Publish item + project
206 let resp = h
207 .client
208 .put_form(&format!("/api/items/{item_id}"), "is_public=true")
209 .await;
210 assert!(
211 resp.status.is_success(),
212 "Publish item failed: {}",
213 resp.text
214 );
215
216 let resp = h
217 .client
218 .put_json(
219 &format!("/api/projects/{project_id}"),
220 r#"{"is_public": true}"#,
221 )
222 .await;
223 assert!(
224 resp.status.is_success(),
225 "Publish project failed: {}",
226 resp.text
227 );
228
229 // Get stream URL
230 let resp = h.client.get(&format!("/api/stream/{item_id}")).await;
231 assert!(resp.status.is_success(), "Stream failed: {}", resp.text);
232 let data: Value = resp.json();
233 assert!(data["stream_url"].is_string(), "Should return stream_url");
234
235 // Verify the audio bytes are actually in storage
236 let stored = h.storage.as_ref().unwrap().get(&s3_key);
237 assert_eq!(
238 stored.len(),
239 TEST_MP3.len(),
240 "Stored bytes should match uploaded MP3"
241 );
242
243 // Verify chapters exist
244 let resp = h
245 .client
246 .get(&format!("/api/items/{item_id}/chapters"))
247 .await;
248 assert!(
249 resp.status.is_success(),
250 "List chapters failed: {}",
251 resp.text
252 );
253 let chapters: Value = resp.json();
254 let data = chapters["data"].as_array().unwrap();
255 assert_eq!(data.len(), 2);
256 }
257
258 // Version upload with real file
259
260 #[tokio::test]
261 async fn version_upload_real_audio() {
262 let mut h = TestHarness::with_storage().await;
263 let (_, project_id, item_id) = setup_creator(&mut h, "versmedia").await;
264
265 // Create a version
266 let resp = h
267 .client
268 .post_json(
269 &format!("/api/items/{item_id}/versions"),
270 r#"{"version_number": "1.0.0", "changelog": "Initial release"}"#,
271 )
272 .await;
273 assert!(
274 resp.status.is_success(),
275 "Create version failed: {}",
276 resp.text
277 );
278 let version: Value = resp.json();
279 let version_id = version["id"].as_str().unwrap().to_string();
280
281 // Presign version upload (downloads accept application/octet-stream)
282 let resp = h
283 .client
284 .post_json(
285 &format!("/api/versions/{version_id}/upload/presign"),
286 r#"{"file_name": "track-v1.zip", "content_type": "application/zip"}"#,
287 )
288 .await;
289 assert!(
290 resp.status.is_success(),
291 "Version presign failed: {}",
292 resp.text
293 );
294 let data: Value = resp.json();
295 let s3_key = data["s3_key"].as_str().unwrap().to_string();
296
297 // Upload real bytes (using MP3 data as a stand-in for zip, content doesn't matter for storage)
298 h.storage.as_ref().unwrap().put(&s3_key, TEST_MP3.to_vec());
299
300 // Confirm
301 let resp = h
302 .client
303 .post_json(
304 &format!("/api/versions/{version_id}/upload/confirm"),
305 &json!({"s3_key": s3_key}).to_string(),
306 )
307 .await;
308 assert!(
309 resp.status.is_success(),
310 "Version confirm failed: {}",
311 resp.text
312 );
313
314 // Publish and download
315 h.client
316 .put_form(&format!("/api/items/{item_id}"), "is_public=true")
317 .await;
318 h.client
319 .put_json(
320 &format!("/api/projects/{project_id}"),
321 r#"{"is_public": true}"#,
322 )
323 .await;
324
325 let resp = h
326 .client
327 .get(&format!("/api/versions/{version_id}/download"))
328 .await;
329 assert!(resp.status.is_success(), "Download failed: {}", resp.text);
330 let data: Value = resp.json();
331 assert!(
332 data["download_url"].is_string(),
333 "Should return download_url"
334 );
335 }
336
337 // Content insertion with real audio
338
339 #[tokio::test]
340 async fn insertion_lifecycle_with_real_audio() {
341 let mut h = TestHarness::with_storage().await;
342 let (_, _, item_id) = setup_creator(&mut h, "inslife").await;
343
344 // Presign insertion
345 let resp = h
346 .client
347 .post_json(
348 "/api/users/me/insertions/presign",
349 r#"{"file_name": "intro.mp3", "content_type": "audio/mpeg"}"#,
350 )
351 .await;
352 assert!(
353 resp.status.is_success(),
354 "Presign insertion failed: {}",
355 resp.text
356 );
357 let data: Value = resp.json();
358 let s3_key = data["s3_key"].as_str().unwrap().to_string();
359
360 // Upload real MP3
361 h.storage.as_ref().unwrap().put(&s3_key, TEST_MP3.to_vec());
362
363 // Confirm insertion
364 let resp = h
365 .client
366 .post_json(
367 "/api/users/me/insertions/confirm",
368 &json!({
369 "s3_key": s3_key,
370 "title": "Intro Jingle",
371 "duration_ms": 1000,
372 "file_size": TEST_MP3.len(),
373 "mime_type": "audio/mpeg",
374 })
375 .to_string(),
376 )
377 .await;
378 assert!(
379 resp.status.is_success(),
380 "Confirm insertion failed: {}",
381 resp.text
382 );
383 let insertion: Value = resp.json();
384 let insertion_id = insertion["id"].as_str().unwrap().to_string();
385 assert_eq!(insertion["title"].as_str().unwrap(), "Intro Jingle");
386
387 // Attach as pre-roll to item
388 let resp = h
389 .client
390 .post_json(
391 &format!("/api/items/{item_id}/insertions"),
392 &json!({
393 "insertion_id": insertion_id,
394 "position": "pre_roll",
395 "sort_order": 0,
396 })
397 .to_string(),
398 )
399 .await;
400 assert!(
401 resp.status.is_success(),
402 "Create placement failed: {}",
403 resp.text
404 );
405
406 // Rename insertion
407 let resp = h
408 .client
409 .put_json(
410 &format!("/api/insertions/{insertion_id}"),
411 r#"{"title": "Updated Intro"}"#,
412 )
413 .await;
414 assert!(
415 resp.status.is_success(),
416 "Rename insertion failed: {}",
417 resp.text
418 );
419
420 // Delete insertion (cascades to placements)
421 let resp = h
422 .client
423 .delete(&format!("/api/insertions/{insertion_id}"))
424 .await;
425 assert!(
426 resp.status.is_success(),
427 "Delete insertion failed: {}",
428 resp.text
429 );
430 }
431
432 // Mid-roll placement requires offset
433
434 #[tokio::test]
435 async fn midroll_placement_requires_offset() {
436 let mut h = TestHarness::with_storage().await;
437 let (_, _, item_id) = setup_creator(&mut h, "midroll").await;
438
439 // Create a quick insertion via DB shortcut
440 let s3_key = "midroll/insertions/clip.mp3";
441 h.storage.as_ref().unwrap().put(s3_key, TEST_OGG.to_vec());
442
443 let resp = h
444 .client
445 .post_json(
446 "/api/users/me/insertions/presign",
447 r#"{"file_name": "clip.ogg", "content_type": "audio/ogg"}"#,
448 )
449 .await;
450 assert!(resp.status.is_success());
451 let data: Value = resp.json();
452 let real_key = data["s3_key"].as_str().unwrap().to_string();
453 h.storage
454 .as_ref()
455 .unwrap()
456 .put(&real_key, TEST_OGG.to_vec());
457
458 let resp = h
459 .client
460 .post_json(
461 "/api/users/me/insertions/confirm",
462 &json!({
463 "s3_key": real_key,
464 "title": "Mid Ad",
465 "duration_ms": 1000,
466 "file_size": TEST_OGG.len(),
467 "mime_type": "audio/ogg",
468 })
469 .to_string(),
470 )
471 .await;
472 assert!(resp.status.is_success());
473 let insertion: Value = resp.json();
474 let insertion_id = insertion["id"].as_str().unwrap();
475
476 // Mid-roll without offset should fail
477 let resp = h
478 .client
479 .post_json(
480 &format!("/api/items/{item_id}/insertions"),
481 &json!({
482 "insertion_id": insertion_id,
483 "position": "mid_roll",
484 "sort_order": 0,
485 })
486 .to_string(),
487 )
488 .await;
489 assert!(
490 resp.status.is_client_error(),
491 "Mid-roll without offset should fail: {} {}",
492 resp.status,
493 resp.text
494 );
495
496 // Mid-roll with offset should succeed
497 let resp = h
498 .client
499 .post_json(
500 &format!("/api/items/{item_id}/insertions"),
501 &json!({
502 "insertion_id": insertion_id,
503 "position": "mid_roll",
504 "offset_ms": 30000,
505 "sort_order": 0,
506 })
507 .to_string(),
508 )
509 .await;
510 assert!(
511 resp.status.is_success(),
512 "Mid-roll with offset should succeed: {} {}",
513 resp.status,
514 resp.text
515 );
516 }
517