Skip to main content

max / makenotwork

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