//! File scanning workflow tests — clean files pass, malicious magic bytes quarantined. use crate::harness::TestHarness; use serde_json::{json, Value}; use makenotwork::db::UserId; /// Helper: set up a trusted creator with a project and audio item. async fn setup_creator_with_item(h: &mut TestHarness) -> (String, String) { let setup = h.create_creator_with_item("scancreator", "audio", 0).await; h.trust_user(setup.user_id).await; h.grant_tier(setup.user_id, "small_files").await; (setup.project_id, setup.item_id) } #[tokio::test] async fn confirm_upload_clean_file_passes() { let mut h = TestHarness::with_storage_and_scanner().await; let (_project_id, item_id) = setup_creator_with_item(&mut h).await; // Presign let body = json!({ "item_id": item_id, "file_type": "audio", "file_name": "clean.mp3", "content_type": "audio/mpeg", }); let resp = h.client.post_json("/api/upload/presign", &body.to_string()).await; assert!(resp.status.is_success(), "Presign failed: {}", resp.text); let data: Value = resp.json(); let s3_key = data["s3_key"].as_str().unwrap().to_string(); // Simulate upload: ID3v2 header (valid MP3 magic bytes) let mut mp3_data = b"ID3".to_vec(); mp3_data.extend_from_slice(&[0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]); // ID3v2.4 header mp3_data.extend_from_slice(&[0u8; 100]); // padding h.storage.as_ref().unwrap().put(&s3_key, mp3_data); // Confirm — scanner should see MP3/ID3 magic and pass let body = json!({ "item_id": item_id, "file_type": "audio", "s3_key": s3_key, }); let resp = h.client.post_json("/api/upload/confirm", &body.to_string()).await; assert!(resp.status.is_success(), "Confirm should pass for clean file: {}", resp.text); // Scanning is async (Phase 1 worker pipeline). Drive the worker to // completion before asserting final state. h.drain_scan_jobs().await; // Verify audio_s3_key was set in DB let db_key: Option = sqlx::query_scalar( "SELECT audio_s3_key FROM items WHERE id = $1::uuid", ) .bind(&item_id) .fetch_one(&h.db) .await .unwrap(); assert_eq!(db_key.as_deref(), Some(s3_key.as_str())); // Verify scan_status is clean let scan_status: String = sqlx::query_scalar( "SELECT scan_status FROM items WHERE id = $1::uuid", ) .bind(&item_id) .fetch_one(&h.db) .await .unwrap(); assert_eq!(scan_status, "clean"); } #[tokio::test] async fn confirm_upload_bad_magic_quarantined() { let mut h = TestHarness::with_storage_and_scanner().await; let (_project_id, item_id) = setup_creator_with_item(&mut h).await; // Presign let body = json!({ "item_id": item_id, "file_type": "audio", "file_name": "sneaky.mp3", "content_type": "audio/mpeg", }); let resp = h.client.post_json("/api/upload/presign", &body.to_string()).await; assert!(resp.status.is_success()); let data: Value = resp.json(); let s3_key = data["s3_key"].as_str().unwrap().to_string(); // Simulate upload: ELF binary magic disguised as audio let mut elf_data = vec![0x7f, b'E', b'L', b'F']; elf_data.extend_from_slice(&[0x02, 0x01, 0x01, 0x00]); // 64-bit, LE, current elf_data.extend_from_slice(&[0u8; 100]); // padding h.storage.as_ref().unwrap().put(&s3_key, elf_data); // Confirm — scanner enqueues async; the worker decides quarantine. let body = json!({ "item_id": item_id, "file_type": "audio", "s3_key": s3_key, }); let resp = h.client.post_json("/api/upload/confirm", &body.to_string()).await; assert!( resp.status.is_success(), "Confirm enqueues async; the worker decides scan verdict. Got {}: {}", resp.status, resp.text ); h.drain_scan_jobs().await; // Verify scan_status is quarantined let scan_status: String = sqlx::query_scalar( "SELECT scan_status FROM items WHERE id = $1::uuid", ) .bind(&item_id) .fetch_one(&h.db) .await .unwrap(); assert_eq!(scan_status, "quarantined"); } // ============================================================================ // Upload Trust Tier Tests // ============================================================================ /// Helper: set up an untrusted creator with a project and audio item. /// Returns (user_id, project_id, item_id). async fn setup_untrusted_creator_with_item( h: &mut TestHarness, username: &str, email: &str, ) -> (UserId, String, String) { let user_id = h.signup(username, email, "password123").await; h.grant_creator(user_id).await; h.grant_tier(user_id, "small_files").await; // NOTE: deliberately NOT calling h.trust_user(user_id) h.client.post_form("/logout", "").await; h.login(username, "password123").await; let resp = h .client .post_form("/api/projects", &format!("slug={username}proj&title={username}+Project")) .await; assert!(resp.status.is_success(), "Create project: {}", resp.text); let project: Value = resp.json(); let project_id = project["id"].as_str().unwrap().to_string(); let resp = h .client .post_form( &format!("/api/projects/{}/items", project_id), "title=Trust+Track&price_cents=0&item_type=audio", ) .await; assert!(resp.status.is_success(), "Create item: {}", resp.text); let item: Value = resp.json(); let item_id = item["id"].as_str().unwrap().to_string(); (user_id, project_id, item_id) } /// Helper: presign, simulate upload, and confirm for clean MP3 data. /// Returns the s3_key. async fn upload_clean_mp3(h: &mut TestHarness, item_id: &str) -> String { let body = json!({ "item_id": item_id, "file_type": "audio", "file_name": "trust_test.mp3", "content_type": "audio/mpeg", }); let resp = h.client.post_json("/api/upload/presign", &body.to_string()).await; assert!(resp.status.is_success(), "Presign failed: {}", resp.text); let data: Value = resp.json(); let s3_key = data["s3_key"].as_str().unwrap().to_string(); // Valid MP3 magic bytes let mut mp3_data = b"ID3".to_vec(); mp3_data.extend_from_slice(&[0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]); mp3_data.extend_from_slice(&[0u8; 100]); h.storage.as_ref().unwrap().put(&s3_key, mp3_data); let body = json!({ "item_id": item_id, "file_type": "audio", "s3_key": s3_key, }); let resp = h.client.post_json("/api/upload/confirm", &body.to_string()).await; assert!(resp.status.is_success(), "Confirm failed: {}", resp.text); // Drive the async worker so callers can immediately assert final state. h.drain_scan_jobs().await; s3_key } #[tokio::test] async fn untrusted_creator_upload_held_for_review() { let mut h = TestHarness::with_storage_and_scanner().await; let (_user_id, _project_id, item_id) = setup_untrusted_creator_with_item(&mut h, "untrusted", "untrusted@test.com").await; let _s3_key = upload_clean_mp3(&mut h, &item_id).await; // Verify scan_status is held_for_review (not clean) let scan_status: String = sqlx::query_scalar( "SELECT scan_status FROM items WHERE id = $1::uuid", ) .bind(&item_id) .fetch_one(&h.db) .await .unwrap(); assert_eq!(scan_status, "held_for_review"); // Creator can preview their own held content (200) let resp = h.client.get(&format!("/api/stream/{}", item_id)).await; assert_eq!(resp.status.as_u16(), 200, "Creators should be able to preview held uploads"); // But a different user should not be able to stream it (log in as buyer) h.client.post_form("/logout", "").await; h.signup("buyer", "buyer@test.com", "password123").await; let resp = h.client.get(&format!("/api/stream/{}", item_id)).await; assert_eq!(resp.status.as_u16(), 404, "Non-creators should not stream held uploads"); } #[tokio::test] async fn trusted_creator_upload_auto_publishes() { let mut h = TestHarness::with_storage_and_scanner().await; let (user_id, _project_id, item_id) = setup_untrusted_creator_with_item(&mut h, "trusted", "trusted@test.com").await; // Trust the user, then upload h.trust_user(user_id).await; let _s3_key = upload_clean_mp3(&mut h, &item_id).await; // Verify scan_status is clean (auto-published) let scan_status: String = sqlx::query_scalar( "SELECT scan_status FROM items WHERE id = $1::uuid", ) .bind(&item_id) .fetch_one(&h.db) .await .unwrap(); assert_eq!(scan_status, "clean"); } #[tokio::test] async fn admin_approve_held_upload() { let (mut h, _admin_id) = TestHarness::with_admin_storage_and_scanner().await; let (_user_id, _project_id, item_id) = setup_untrusted_creator_with_item(&mut h, "heldcreator", "held@test.com").await; let _s3_key = upload_clean_mp3(&mut h, &item_id).await; // Verify it's held let scan_status: String = sqlx::query_scalar( "SELECT scan_status FROM items WHERE id = $1::uuid", ) .bind(&item_id) .fetch_one(&h.db) .await .unwrap(); assert_eq!(scan_status, "held_for_review"); // Log in as admin and approve h.client.post_form("/logout", "").await; h.login("admin", "password123").await; let resp = h .client .post_form(&format!("/api/admin/uploads/items/{}/promote", item_id), "") .await; assert!( resp.status.is_success(), "Admin approve failed: {} {}", resp.status, resp.text ); // Verify scan_status is now clean let scan_status: String = sqlx::query_scalar( "SELECT scan_status FROM items WHERE id = $1::uuid", ) .bind(&item_id) .fetch_one(&h.db) .await .unwrap(); assert_eq!(scan_status, "clean"); // Verify streaming now works (item is public and free by default) let resp = h.client.get(&format!("/api/stream/{}", item_id)).await; assert!( resp.status.is_success(), "Stream should work after approval: {}", resp.status ); }