Skip to main content

max / makenotwork

10.0 KB · 296 lines History Blame Raw
1 //! File scanning workflow tests — clean files pass, malicious magic bytes quarantined.
2
3 use crate::harness::TestHarness;
4 use serde_json::{json, Value};
5
6 use makenotwork::db::UserId;
7
8 /// Helper: set up a trusted creator with a project and audio item.
9 async fn setup_creator_with_item(h: &mut TestHarness) -> (String, String) {
10 let setup = h.create_creator_with_item("scancreator", "audio", 0).await;
11 h.trust_user(setup.user_id).await;
12 h.grant_tier(setup.user_id, "small_files").await;
13 (setup.project_id, setup.item_id)
14 }
15
16 #[tokio::test]
17 async fn confirm_upload_clean_file_passes() {
18 let mut h = TestHarness::with_storage_and_scanner().await;
19 let (_project_id, item_id) = setup_creator_with_item(&mut h).await;
20
21 // Presign
22 let body = json!({
23 "item_id": item_id,
24 "file_type": "audio",
25 "file_name": "clean.mp3",
26 "content_type": "audio/mpeg",
27 });
28 let resp = h.client.post_json("/api/upload/presign", &body.to_string()).await;
29 assert!(resp.status.is_success(), "Presign failed: {}", resp.text);
30 let data: Value = resp.json();
31 let s3_key = data["s3_key"].as_str().unwrap().to_string();
32
33 // Simulate upload: ID3v2 header (valid MP3 magic bytes)
34 let mut mp3_data = b"ID3".to_vec();
35 mp3_data.extend_from_slice(&[0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]); // ID3v2.4 header
36 mp3_data.extend_from_slice(&[0u8; 100]); // padding
37 h.storage.as_ref().unwrap().put(&s3_key, mp3_data);
38
39 // Confirm — scanner should see MP3/ID3 magic and pass
40 let body = json!({
41 "item_id": item_id,
42 "file_type": "audio",
43 "s3_key": s3_key,
44 });
45 let resp = h.client.post_json("/api/upload/confirm", &body.to_string()).await;
46 assert!(resp.status.is_success(), "Confirm should pass for clean file: {}", resp.text);
47
48 // Scanning is async (Phase 1 worker pipeline). Drive the worker to
49 // completion before asserting final state.
50 h.drain_scan_jobs().await;
51
52 // Verify audio_s3_key was set in DB
53 let db_key: Option<String> = sqlx::query_scalar(
54 "SELECT audio_s3_key FROM items WHERE id = $1::uuid",
55 )
56 .bind(&item_id)
57 .fetch_one(&h.db)
58 .await
59 .unwrap();
60 assert_eq!(db_key.as_deref(), Some(s3_key.as_str()));
61
62 // Verify scan_status is clean
63 let scan_status: String = sqlx::query_scalar(
64 "SELECT scan_status FROM items WHERE id = $1::uuid",
65 )
66 .bind(&item_id)
67 .fetch_one(&h.db)
68 .await
69 .unwrap();
70 assert_eq!(scan_status, "clean");
71 }
72
73 #[tokio::test]
74 async fn confirm_upload_bad_magic_quarantined() {
75 let mut h = TestHarness::with_storage_and_scanner().await;
76 let (_project_id, item_id) = setup_creator_with_item(&mut h).await;
77
78 // Presign
79 let body = json!({
80 "item_id": item_id,
81 "file_type": "audio",
82 "file_name": "sneaky.mp3",
83 "content_type": "audio/mpeg",
84 });
85 let resp = h.client.post_json("/api/upload/presign", &body.to_string()).await;
86 assert!(resp.status.is_success());
87 let data: Value = resp.json();
88 let s3_key = data["s3_key"].as_str().unwrap().to_string();
89
90 // Simulate upload: ELF binary magic disguised as audio
91 let mut elf_data = vec![0x7f, b'E', b'L', b'F'];
92 elf_data.extend_from_slice(&[0x02, 0x01, 0x01, 0x00]); // 64-bit, LE, current
93 elf_data.extend_from_slice(&[0u8; 100]); // padding
94 h.storage.as_ref().unwrap().put(&s3_key, elf_data);
95
96 // Confirm — scanner enqueues async; the worker decides quarantine.
97 let body = json!({
98 "item_id": item_id,
99 "file_type": "audio",
100 "s3_key": s3_key,
101 });
102 let resp = h.client.post_json("/api/upload/confirm", &body.to_string()).await;
103 assert!(
104 resp.status.is_success(),
105 "Confirm enqueues async; the worker decides scan verdict. Got {}: {}",
106 resp.status, resp.text
107 );
108 h.drain_scan_jobs().await;
109
110 // Verify scan_status is quarantined
111 let scan_status: String = sqlx::query_scalar(
112 "SELECT scan_status FROM items WHERE id = $1::uuid",
113 )
114 .bind(&item_id)
115 .fetch_one(&h.db)
116 .await
117 .unwrap();
118 assert_eq!(scan_status, "quarantined");
119 }
120
121 // ============================================================================
122 // Upload Trust Tier Tests
123 // ============================================================================
124
125 /// Helper: set up an untrusted creator with a project and audio item.
126 /// Returns (user_id, project_id, item_id).
127 async fn setup_untrusted_creator_with_item(
128 h: &mut TestHarness,
129 username: &str,
130 email: &str,
131 ) -> (UserId, String, String) {
132 let user_id = h.signup(username, email, "password123").await;
133 h.grant_creator(user_id).await;
134 h.grant_tier(user_id, "small_files").await;
135 // NOTE: deliberately NOT calling h.trust_user(user_id)
136 h.client.post_form("/logout", "").await;
137 h.login(username, "password123").await;
138
139 let resp = h
140 .client
141 .post_form("/api/projects", &format!("slug={username}proj&title={username}+Project"))
142 .await;
143 assert!(resp.status.is_success(), "Create project: {}", resp.text);
144 let project: Value = resp.json();
145 let project_id = project["id"].as_str().unwrap().to_string();
146
147 let resp = h
148 .client
149 .post_form(
150 &format!("/api/projects/{}/items", project_id),
151 "title=Trust+Track&price_cents=0&item_type=audio",
152 )
153 .await;
154 assert!(resp.status.is_success(), "Create item: {}", resp.text);
155 let item: Value = resp.json();
156 let item_id = item["id"].as_str().unwrap().to_string();
157
158 (user_id, project_id, item_id)
159 }
160
161 /// Helper: presign, simulate upload, and confirm for clean MP3 data.
162 /// Returns the s3_key.
163 async fn upload_clean_mp3(h: &mut TestHarness, item_id: &str) -> String {
164 let body = json!({
165 "item_id": item_id,
166 "file_type": "audio",
167 "file_name": "trust_test.mp3",
168 "content_type": "audio/mpeg",
169 });
170 let resp = h.client.post_json("/api/upload/presign", &body.to_string()).await;
171 assert!(resp.status.is_success(), "Presign failed: {}", resp.text);
172 let data: Value = resp.json();
173 let s3_key = data["s3_key"].as_str().unwrap().to_string();
174
175 // Valid MP3 magic bytes
176 let mut mp3_data = b"ID3".to_vec();
177 mp3_data.extend_from_slice(&[0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
178 mp3_data.extend_from_slice(&[0u8; 100]);
179 h.storage.as_ref().unwrap().put(&s3_key, mp3_data);
180
181 let body = json!({
182 "item_id": item_id,
183 "file_type": "audio",
184 "s3_key": s3_key,
185 });
186 let resp = h.client.post_json("/api/upload/confirm", &body.to_string()).await;
187 assert!(resp.status.is_success(), "Confirm failed: {}", resp.text);
188
189 // Drive the async worker so callers can immediately assert final state.
190 h.drain_scan_jobs().await;
191
192 s3_key
193 }
194
195 #[tokio::test]
196 async fn untrusted_creator_upload_held_for_review() {
197 let mut h = TestHarness::with_storage_and_scanner().await;
198 let (_user_id, _project_id, item_id) =
199 setup_untrusted_creator_with_item(&mut h, "untrusted", "untrusted@test.com").await;
200
201 let _s3_key = upload_clean_mp3(&mut h, &item_id).await;
202
203 // Verify scan_status is held_for_review (not clean)
204 let scan_status: String = sqlx::query_scalar(
205 "SELECT scan_status FROM items WHERE id = $1::uuid",
206 )
207 .bind(&item_id)
208 .fetch_one(&h.db)
209 .await
210 .unwrap();
211 assert_eq!(scan_status, "held_for_review");
212
213 // Creator can preview their own held content (200)
214 let resp = h.client.get(&format!("/api/stream/{}", item_id)).await;
215 assert_eq!(resp.status.as_u16(), 200, "Creators should be able to preview held uploads");
216
217 // But a different user should not be able to stream it (log in as buyer)
218 h.client.post_form("/logout", "").await;
219 h.signup("buyer", "buyer@test.com", "password123").await;
220 let resp = h.client.get(&format!("/api/stream/{}", item_id)).await;
221 assert_eq!(resp.status.as_u16(), 404, "Non-creators should not stream held uploads");
222 }
223
224 #[tokio::test]
225 async fn trusted_creator_upload_auto_publishes() {
226 let mut h = TestHarness::with_storage_and_scanner().await;
227 let (user_id, _project_id, item_id) =
228 setup_untrusted_creator_with_item(&mut h, "trusted", "trusted@test.com").await;
229
230 // Trust the user, then upload
231 h.trust_user(user_id).await;
232 let _s3_key = upload_clean_mp3(&mut h, &item_id).await;
233
234 // Verify scan_status is clean (auto-published)
235 let scan_status: String = sqlx::query_scalar(
236 "SELECT scan_status FROM items WHERE id = $1::uuid",
237 )
238 .bind(&item_id)
239 .fetch_one(&h.db)
240 .await
241 .unwrap();
242 assert_eq!(scan_status, "clean");
243 }
244
245 #[tokio::test]
246 async fn admin_approve_held_upload() {
247 let (mut h, _admin_id) = TestHarness::with_admin_storage_and_scanner().await;
248 let (_user_id, _project_id, item_id) =
249 setup_untrusted_creator_with_item(&mut h, "heldcreator", "held@test.com").await;
250
251 let _s3_key = upload_clean_mp3(&mut h, &item_id).await;
252
253 // Verify it's held
254 let scan_status: String = sqlx::query_scalar(
255 "SELECT scan_status FROM items WHERE id = $1::uuid",
256 )
257 .bind(&item_id)
258 .fetch_one(&h.db)
259 .await
260 .unwrap();
261 assert_eq!(scan_status, "held_for_review");
262
263 // Log in as admin and approve
264 h.client.post_form("/logout", "").await;
265 h.login("admin", "password123").await;
266
267 let resp = h
268 .client
269 .post_form(&format!("/api/admin/uploads/items/{}/promote", item_id), "")
270 .await;
271 assert!(
272 resp.status.is_success(),
273 "Admin approve failed: {} {}",
274 resp.status,
275 resp.text
276 );
277
278 // Verify scan_status is now clean
279 let scan_status: String = sqlx::query_scalar(
280 "SELECT scan_status FROM items WHERE id = $1::uuid",
281 )
282 .bind(&item_id)
283 .fetch_one(&h.db)
284 .await
285 .unwrap();
286 assert_eq!(scan_status, "clean");
287
288 // Verify streaming now works (item is public and free by default)
289 let resp = h.client.get(&format!("/api/stream/{}", item_id)).await;
290 assert!(
291 resp.status.is_success(),
292 "Stream should work after approval: {}",
293 resp.status
294 );
295 }
296