Skip to main content

max / makenotwork

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