Skip to main content

max / makenotwork

11.1 KB · 350 lines History Blame Raw
1 //! Content insertion workflow tests: presign, confirm, rename, delete.
2 //!
3 //! These tests use the in-memory storage backend.
4
5 use crate::harness::TestHarness;
6 use serde_json::Value;
7
8 /// Setup: creator user logged in with small_files tier. Returns user_id.
9 async fn setup_creator(h: &mut TestHarness) -> String {
10 let user_id = h.create_creator("insuser").await;
11 h.grant_tier(user_id, "small_files").await;
12 user_id.to_string()
13 }
14
15 /// Record a pending-upload row for a test-forged key. Under scan-then-promote the
16 /// confirm proves ownership via `pending_uploads` (a `staging/{uuid}` key has no
17 /// user in its path), so a test that fabricates a key + object must also register
18 /// it as if presign had, otherwise the ownership gate correctly rejects it.
19 async fn record_pending(h: &TestHarness, user_id: &str, s3_key: &str) {
20 sqlx::query("INSERT INTO pending_uploads (user_id, s3_key, bucket) VALUES ($1::uuid, $2, 'main') ON CONFLICT DO NOTHING")
21 .bind(user_id)
22 .bind(s3_key)
23 .execute(&h.db)
24 .await
25 .expect("record pending upload");
26 }
27
28 #[tokio::test]
29 async fn presign_requires_auth() {
30 let mut h = TestHarness::with_storage().await;
31
32 let resp = h
33 .client
34 .post_json(
35 "/api/users/me/insertions/presign",
36 r#"{"file_name": "test.mp3", "content_type": "audio/mpeg"}"#,
37 )
38 .await;
39 assert!(
40 resp.status.is_client_error() || resp.status.is_redirection(),
41 "Unauthenticated presign should be rejected: {} {}",
42 resp.status,
43 resp.text
44 );
45 }
46
47 #[tokio::test]
48 async fn presign_invalid_content_type_rejected() {
49 let mut h = TestHarness::with_storage().await;
50 let _user_id = setup_creator(&mut h).await;
51
52 let resp = h
53 .client
54 .post_json(
55 "/api/users/me/insertions/presign",
56 r#"{"file_name": "evil.exe", "content_type": "application/octet-stream"}"#,
57 )
58 .await;
59 assert!(
60 resp.status.is_client_error(),
61 "Invalid content type should be rejected: {} {}",
62 resp.status,
63 resp.text
64 );
65 }
66
67 #[tokio::test]
68 async fn presign_valid_audio_succeeds() {
69 let mut h = TestHarness::with_storage().await;
70 let _user_id = setup_creator(&mut h).await;
71
72 let resp = h
73 .client
74 .post_json(
75 "/api/users/me/insertions/presign",
76 r#"{"file_name": "intro.mp3", "content_type": "audio/mpeg"}"#,
77 )
78 .await;
79 assert!(
80 resp.status.is_success(),
81 "Valid presign should succeed: {} {}",
82 resp.status,
83 resp.text
84 );
85 let body: Value = resp.json();
86 assert!(body["upload_url"].is_string(), "Should return upload_url");
87 assert!(body["s3_key"].is_string(), "Should return s3_key");
88 }
89
90 #[tokio::test]
91 async fn confirm_nonexistent_object_rejected() {
92 let mut h = TestHarness::with_storage().await;
93 let _user_id = setup_creator(&mut h).await;
94
95 let resp = h
96 .client
97 .post_json(
98 "/api/users/me/insertions/confirm",
99 r#"{"s3_key": "nonexistent/key.mp3", "title": "Test", "duration_ms": 5000, "file_size": 1024, "mime_type": "audio/mpeg"}"#,
100 )
101 .await;
102 assert!(
103 resp.status.is_client_error() || resp.status.is_server_error(),
104 "Confirming nonexistent object should fail: {} {}",
105 resp.status,
106 resp.text
107 );
108 }
109
110 #[tokio::test]
111 async fn confirm_with_object_succeeds() {
112 let mut h = TestHarness::with_storage().await;
113 let user_id = setup_creator(&mut h).await;
114
115 // Pre-populate storage with a fake object and record it as a pending upload
116 // (the confirm now proves ownership via pending_uploads, not a key prefix).
117 let s3_key = format!("{user_id}/insertions/intro.mp3");
118 h.storage.as_ref().unwrap().put(&s3_key, vec![0u8; 1024]);
119 record_pending(&h, &user_id, &s3_key).await;
120
121 let resp = h
122 .client
123 .post_json(
124 "/api/users/me/insertions/confirm",
125 &format!(
126 r#"{{"s3_key": "{s3_key}", "title": "Intro Music", "duration_ms": 5000, "file_size": 1024, "mime_type": "audio/mpeg"}}"#
127 ),
128 )
129 .await;
130 assert!(
131 resp.status.is_success(),
132 "Confirm with existing object should succeed: {} {}",
133 resp.status,
134 resp.text
135 );
136 let body: Value = resp.json();
137 assert_eq!(body["title"].as_str().unwrap(), "Intro Music");
138 }
139
140 #[tokio::test]
141 async fn confirm_empty_title_rejected() {
142 let mut h = TestHarness::with_storage().await;
143 let user_id = setup_creator(&mut h).await;
144
145 let s3_key = format!("{user_id}/insertions/empty.mp3");
146 h.storage.as_ref().unwrap().put(&s3_key, vec![0u8; 1024]);
147
148 let resp = h
149 .client
150 .post_json(
151 "/api/users/me/insertions/confirm",
152 &format!(
153 r#"{{"s3_key": "{s3_key}", "title": "", "duration_ms": 5000, "file_size": 1024, "mime_type": "audio/mpeg"}}"#
154 ),
155 )
156 .await;
157 assert!(
158 resp.status.is_client_error(),
159 "Empty title should be rejected: {} {}",
160 resp.status,
161 resp.text
162 );
163 }
164
165 #[tokio::test]
166 async fn delete_nonexistent_insertion_returns_404() {
167 let mut h = TestHarness::with_storage().await;
168 let _user_id = setup_creator(&mut h).await;
169
170 let fake_id = uuid::Uuid::new_v4();
171 let resp = h.client.delete(&format!("/api/insertions/{fake_id}")).await;
172 assert_eq!(
173 resp.status, 404,
174 "Deleting nonexistent insertion should return 404: {} {}",
175 resp.status, resp.text
176 );
177 }
178
179 #[tokio::test]
180 async fn rename_nonexistent_insertion_returns_404() {
181 let mut h = TestHarness::with_storage().await;
182 let _user_id = setup_creator(&mut h).await;
183
184 let fake_id = uuid::Uuid::new_v4();
185 let resp = h
186 .client
187 .put_json(
188 &format!("/api/insertions/{fake_id}"),
189 r#"{"title": "New Name"}"#,
190 )
191 .await;
192 assert_eq!(
193 resp.status, 404,
194 "Renaming nonexistent insertion should return 404: {} {}",
195 resp.status, resp.text
196 );
197 }
198
199 // ── Video clips ──
200
201 #[tokio::test]
202 async fn presign_valid_video_succeeds() {
203 let mut h = TestHarness::with_storage().await;
204 let _user_id = setup_creator(&mut h).await;
205
206 let resp = h
207 .client
208 .post_json(
209 "/api/users/me/insertions/presign",
210 r#"{"file_name": "bumper.mp4", "content_type": "video/mp4"}"#,
211 )
212 .await;
213 assert!(
214 resp.status.is_success(),
215 "Valid video presign should succeed: {} {}",
216 resp.status,
217 resp.text
218 );
219 }
220
221 /// Confirm an insertion clip of the given kind and return its JSON body.
222 /// Uses placeholder bytes: confirm validates size/mime/title synchronously and
223 /// only sniffs magic bytes later in the async scan worker.
224 async fn confirm_clip(h: &mut TestHarness, user_id: &str, file: &str, mime: &str) -> Value {
225 let s3_key = format!("{user_id}/insertions/{file}");
226 h.storage.as_ref().unwrap().put(&s3_key, vec![0u8; 1024]);
227 record_pending(h, user_id, &s3_key).await;
228 let resp = h
229 .client
230 .post_json(
231 "/api/users/me/insertions/confirm",
232 &format!(
233 r#"{{"s3_key": "{s3_key}", "title": "Clip", "duration_ms": 3000, "file_size": 1024, "mime_type": "{mime}"}}"#
234 ),
235 )
236 .await;
237 assert!(
238 resp.status.is_success(),
239 "Confirm {} should succeed: {} {}",
240 file,
241 resp.status,
242 resp.text
243 );
244 resp.json()
245 }
246
247 #[tokio::test]
248 async fn confirm_video_persists_video_media_type() {
249 let mut h = TestHarness::with_storage().await;
250 let user_id = setup_creator(&mut h).await;
251
252 let body = confirm_clip(&mut h, &user_id, "bumper.mp4", "video/mp4").await;
253 assert_eq!(
254 body["media_type"].as_str().unwrap(),
255 "video",
256 "A video/mp4 clip must persist media_type=video, not the legacy audio default"
257 );
258 }
259
260 #[tokio::test]
261 async fn confirm_audio_persists_audio_media_type() {
262 let mut h = TestHarness::with_storage().await;
263 let user_id = setup_creator(&mut h).await;
264
265 let body = confirm_clip(&mut h, &user_id, "intro.mp3", "audio/mpeg").await;
266 assert_eq!(body["media_type"].as_str().unwrap(), "audio");
267 }
268
269 /// POST a placement of `insertion_id` on `item_id` as a pre-roll.
270 async fn place_pre_roll(h: &mut TestHarness, item_id: &str, insertion_id: &str) -> u16 {
271 let resp = h
272 .client
273 .post_json(
274 &format!("/api/items/{item_id}/insertions"),
275 &format!(
276 r#"{{"insertion_id": "{insertion_id}", "position": "pre_roll", "sort_order": 0}}"#
277 ),
278 )
279 .await;
280 resp.status.as_u16()
281 }
282
283 #[tokio::test]
284 async fn video_clip_rejected_on_audio_item() {
285 let mut h = TestHarness::with_storage().await;
286 let setup = h.create_creator_with_item("vidonaud", "audio", 0).await;
287 h.grant_tier(setup.user_id, "small_files").await;
288 let user_id = setup.user_id.to_string();
289
290 let clip = confirm_clip(&mut h, &user_id, "bumper.mp4", "video/mp4").await;
291 let insertion_id = clip["id"].as_str().unwrap().to_string();
292
293 let status = place_pre_roll(&mut h, &setup.item_id, &insertion_id).await;
294 assert_eq!(
295 status, 400,
296 "A video clip must not be placeable on an audio item"
297 );
298 }
299
300 #[tokio::test]
301 async fn video_clip_allowed_on_video_item() {
302 let mut h = TestHarness::with_storage().await;
303 let setup = h.create_creator_with_item("vidonvid", "video", 0).await;
304 h.grant_tier(setup.user_id, "small_files").await;
305 let user_id = setup.user_id.to_string();
306
307 let clip = confirm_clip(&mut h, &user_id, "bumper.mp4", "video/mp4").await;
308 let insertion_id = clip["id"].as_str().unwrap().to_string();
309
310 let status = place_pre_roll(&mut h, &setup.item_id, &insertion_id).await;
311 assert!(
312 (200..300).contains(&status),
313 "A video clip must be placeable on a video item, got {status}"
314 );
315 }
316
317 #[tokio::test]
318 async fn audio_clip_allowed_on_video_item() {
319 let mut h = TestHarness::with_storage().await;
320 let setup = h.create_creator_with_item("audonvid", "video", 0).await;
321 h.grant_tier(setup.user_id, "small_files").await;
322 let user_id = setup.user_id.to_string();
323
324 let clip = confirm_clip(&mut h, &user_id, "intro.mp3", "audio/mpeg").await;
325 let insertion_id = clip["id"].as_str().unwrap().to_string();
326
327 let status = place_pre_roll(&mut h, &setup.item_id, &insertion_id).await;
328 assert!(
329 (200..300).contains(&status),
330 "An audio clip must be placeable on a video item, got {status}"
331 );
332 }
333
334 #[tokio::test]
335 async fn clip_rejected_on_non_media_item() {
336 let mut h = TestHarness::with_storage().await;
337 let setup = h.create_creator_with_item("cliponwtext", "text", 0).await;
338 h.grant_tier(setup.user_id, "small_files").await;
339 let user_id = setup.user_id.to_string();
340
341 let clip = confirm_clip(&mut h, &user_id, "intro.mp3", "audio/mpeg").await;
342 let insertion_id = clip["id"].as_str().unwrap().to_string();
343
344 let status = place_pre_roll(&mut h, &setup.item_id, &insertion_id).await;
345 assert_eq!(
346 status, 400,
347 "Clips must not be placeable on a non-media (text) item"
348 );
349 }
350