Skip to main content

max / makenotwork

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