//! Video upload and playback integration tests. use crate::harness::TestHarness; use serde_json::{json, Value}; /// Helper: create a trusted creator with a video item. async fn setup_creator_with_video( h: &mut TestHarness, price_cents: i64, ) -> (String, String, String) { let setup = h .create_creator_with_item("creator", "video", price_cents) .await; h.trust_user(setup.user_id).await; h.grant_tier(setup.user_id, "big_files").await; ( setup.user_id.to_string(), setup.project_id, setup.item_id, ) } #[tokio::test] async fn video_presign_upload() { let mut h = TestHarness::with_storage().await; let (_, _, item_id) = setup_creator_with_video(&mut h, 0).await; let body = json!({ "item_id": item_id, "file_type": "video", "file_name": "demo.mp4", "content_type": "video/mp4", }); 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(); assert!(data["upload_url"] .as_str() .unwrap() .starts_with("http://test-storage/")); assert!(data["s3_key"] .as_str() .unwrap() .contains("/video/demo.mp4")); assert_eq!(data["expires_in"], 3600); } #[tokio::test] async fn video_confirm_upload_updates_db() { let mut h = TestHarness::with_storage().await; let (_, _, item_id) = setup_creator_with_video(&mut h, 0).await; // Presign let body = json!({ "item_id": item_id, "file_type": "video", "file_name": "clip.mp4", "content_type": "video/mp4", }); 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 client uploading to S3 h.storage .as_ref() .unwrap() .put(&s3_key, b"fake mp4 bytes".to_vec()); // Confirm let body = json!({ "item_id": item_id, "file_type": "video", "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); // Verify database let db_key: Option = sqlx::query_scalar("SELECT video_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())); } #[tokio::test] async fn video_stream_free_item() { let mut h = TestHarness::with_storage().await; let (_, project_id, item_id) = setup_creator_with_video(&mut h, 0).await; // Set up video key directly in DB let s3_key = format!("test/{}/video/clip.mp4", item_id); sqlx::query("UPDATE items SET video_s3_key = $1, scan_status = 'clean' WHERE id = $2::uuid") .bind(&s3_key) .bind(&item_id) .execute(&h.db) .await .unwrap(); h.storage .as_ref() .unwrap() .put(&s3_key, b"video data".to_vec()); // Publish h.client .put_form(&format!("/api/items/{}", item_id), "is_public=true") .await; h.client .put_json( &format!("/api/projects/{}", project_id), r#"{"is_public": true}"#, ) .await; // Stream let resp = h .client .get(&format!("/api/stream/{}", item_id)) .await; assert!(resp.status.is_success(), "Stream failed: {}", resp.text); let data: Value = resp.json(); assert!(data["stream_url"] .as_str() .unwrap() .starts_with("http://test-storage/")); } #[tokio::test] async fn video_stream_requires_purchase() { let mut h = TestHarness::with_storage().await; let (_, project_id, item_id) = setup_creator_with_video(&mut h, 500).await; // Set up video key let s3_key = format!("test/{}/video/clip.mp4", item_id); sqlx::query("UPDATE items SET video_s3_key = $1, scan_status = 'clean' WHERE id = $2::uuid") .bind(&s3_key) .bind(&item_id) .execute(&h.db) .await .unwrap(); h.storage .as_ref() .unwrap() .put(&s3_key, b"video data".to_vec()); // Publish h.client .put_form(&format!("/api/items/{}", item_id), "is_public=true") .await; h.client .put_json( &format!("/api/projects/{}", project_id), r#"{"is_public": true}"#, ) .await; // Log out and sign up buyer h.client.post_form("/logout", "").await; h.signup("buyer", "buyer@test.com", "password123").await; h.login("buyer", "password123").await; // Stream should be forbidden let resp = h .client .get(&format!("/api/stream/{}", item_id)) .await; assert_eq!( resp.status.as_u16(), 403, "Expected 403, got: {}", resp.text ); } #[tokio::test] async fn video_storage_tracking() { let mut h = TestHarness::with_storage().await; let (user_id, _, item_id) = setup_creator_with_video(&mut h, 0).await; // Presign + upload + confirm let body = json!({ "item_id": item_id, "file_type": "video", "file_name": "large.mp4", "content_type": "video/mp4", }); 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(); // Put a 1KB "video file" let video_data = vec![0u8; 1024]; h.storage .as_ref() .unwrap() .put(&s3_key, video_data); let body = json!({ "item_id": item_id, "file_type": "video", "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); // Verify video_file_size_bytes is set let size: Option = sqlx::query_scalar("SELECT video_file_size_bytes FROM items WHERE id = $1::uuid") .bind(&item_id) .fetch_one(&h.db) .await .unwrap(); assert!(size.unwrap() > 0, "video_file_size_bytes should be set"); // Verify storage_used_bytes is updated let storage: i64 = sqlx::query_scalar( "SELECT storage_used_bytes FROM users WHERE id = $1::uuid", ) .bind(&user_id) .fetch_one(&h.db) .await .unwrap(); assert!(storage > 0, "storage_used_bytes should include video"); } #[tokio::test] async fn video_wizard_group() { // Verify Video item type gets "video" wizard group (unit-level, but validates the integration) use makenotwork::db::ItemType; assert_eq!(ItemType::Video.wizard_group(), "video"); assert_ne!(ItemType::Video.wizard_group(), "file"); }