Skip to main content

max / makenotwork

7.3 KB · 251 lines History Blame Raw
1 //! Video upload and playback integration tests.
2
3 use crate::harness::TestHarness;
4 use serde_json::{Value, json};
5
6 /// Helper: create a trusted creator with a video item.
7 async fn setup_creator_with_video(
8 h: &mut TestHarness,
9 price_cents: i64,
10 ) -> (String, String, String) {
11 let setup = h
12 .create_creator_with_item("creator", "video", price_cents)
13 .await;
14 h.trust_user(setup.user_id).await;
15 h.grant_tier(setup.user_id, "big_files").await;
16 (setup.user_id.to_string(), setup.project_id, setup.item_id)
17 }
18
19 #[tokio::test]
20 async fn video_presign_upload() {
21 let mut h = TestHarness::with_storage().await;
22 let (_, _, item_id) = setup_creator_with_video(&mut h, 0).await;
23
24 let body = json!({
25 "item_id": item_id,
26 "file_type": "video",
27 "file_name": "demo.mp4",
28 "content_type": "video/mp4",
29 });
30 let resp = h
31 .client
32 .post_json("/api/upload/presign", &body.to_string())
33 .await;
34 assert_eq!(resp.status, 200, "Presign failed: {}", resp.text);
35
36 let data: Value = resp.json();
37 assert!(
38 data["upload_url"]
39 .as_str()
40 .unwrap()
41 .starts_with("http://test-storage/")
42 );
43 // Scan-then-promote: presign returns an unserved staging key that preserves
44 // the filename; the served key is minted by the scan worker on a Clean verdict.
45 let key = data["s3_key"].as_str().unwrap();
46 assert!(
47 key.starts_with("staging/"),
48 "presign must return a staging key: {key}"
49 );
50 assert!(
51 key.ends_with("/demo.mp4"),
52 "staging key preserves the filename: {key}"
53 );
54 assert_eq!(data["expires_in"], 3600);
55 }
56
57 #[tokio::test]
58 async fn video_confirm_upload_updates_db() {
59 let mut h = TestHarness::with_storage().await;
60 let (_, _, item_id) = setup_creator_with_video(&mut h, 0).await;
61
62 // Presign
63 let body = json!({
64 "item_id": item_id,
65 "file_type": "video",
66 "file_name": "clip.mp4",
67 "content_type": "video/mp4",
68 });
69 let resp = h
70 .client
71 .post_json("/api/upload/presign", &body.to_string())
72 .await;
73 assert_eq!(resp.status, 200, "{}", resp.text);
74 let data: Value = resp.json();
75 let s3_key = data["s3_key"].as_str().unwrap().to_string();
76
77 // Simulate client uploading to S3
78 h.storage
79 .as_ref()
80 .unwrap()
81 .put(&s3_key, b"fake mp4 bytes".to_vec());
82
83 // Confirm
84 let body = json!({
85 "item_id": item_id,
86 "file_type": "video",
87 "s3_key": s3_key,
88 });
89 let resp = h
90 .client
91 .post_json("/api/upload/confirm", &body.to_string())
92 .await;
93 assert_eq!(resp.status, 200, "Confirm failed: {}", resp.text);
94
95 // Verify database
96 let db_key: Option<String> =
97 sqlx::query_scalar("SELECT video_s3_key FROM items WHERE id = $1::uuid")
98 .bind(&item_id)
99 .fetch_one(&h.db)
100 .await
101 .unwrap();
102 assert_eq!(db_key.as_deref(), Some(s3_key.as_str()));
103 }
104
105 #[tokio::test]
106 async fn video_stream_free_item() {
107 let mut h = TestHarness::with_storage().await;
108 let (_, project_id, item_id) = setup_creator_with_video(&mut h, 0).await;
109
110 // Set up video key directly in DB
111 let s3_key = format!("test/{item_id}/video/clip.mp4");
112 sqlx::query("UPDATE items SET video_s3_key = $1, scan_status = 'clean' WHERE id = $2::uuid")
113 .bind(&s3_key)
114 .bind(&item_id)
115 .execute(&h.db)
116 .await
117 .unwrap();
118
119 h.storage
120 .as_ref()
121 .unwrap()
122 .put(&s3_key, b"video data".to_vec());
123
124 // Publish
125 h.client
126 .put_form(&format!("/api/items/{item_id}"), "is_public=true")
127 .await;
128 h.client
129 .put_json(
130 &format!("/api/projects/{project_id}"),
131 r#"{"is_public": true}"#,
132 )
133 .await;
134
135 let resp = h.client.get(&format!("/api/stream/{item_id}")).await;
136 assert_eq!(resp.status, 200, "Stream failed: {}", resp.text);
137 let data: Value = resp.json();
138 assert!(
139 data["stream_url"]
140 .as_str()
141 .unwrap()
142 .starts_with("http://test-storage/")
143 );
144 }
145
146 #[tokio::test]
147 async fn video_stream_requires_purchase() {
148 let mut h = TestHarness::with_storage().await;
149 let (_, project_id, item_id) = setup_creator_with_video(&mut h, 500).await;
150
151 // Set up video key
152 let s3_key = format!("test/{item_id}/video/clip.mp4");
153 sqlx::query("UPDATE items SET video_s3_key = $1, scan_status = 'clean' WHERE id = $2::uuid")
154 .bind(&s3_key)
155 .bind(&item_id)
156 .execute(&h.db)
157 .await
158 .unwrap();
159 h.storage
160 .as_ref()
161 .unwrap()
162 .put(&s3_key, b"video data".to_vec());
163
164 // Publish
165 h.client
166 .put_form(&format!("/api/items/{item_id}"), "is_public=true")
167 .await;
168 h.client
169 .put_json(
170 &format!("/api/projects/{project_id}"),
171 r#"{"is_public": true}"#,
172 )
173 .await;
174
175 // Log out and sign up buyer
176 h.client.post_form("/logout", "").await;
177 h.signup("buyer", "buyer@test.com", "password123").await;
178 h.login("buyer", "password123").await;
179
180 // Stream should be forbidden
181 let resp = h.client.get(&format!("/api/stream/{item_id}")).await;
182 assert_eq!(
183 resp.status.as_u16(),
184 403,
185 "Expected 403, got: {}",
186 resp.text
187 );
188 }
189
190 #[tokio::test]
191 async fn video_storage_tracking() {
192 let mut h = TestHarness::with_storage().await;
193 let (user_id, _, item_id) = setup_creator_with_video(&mut h, 0).await;
194
195 // Presign + upload + confirm
196 let body = json!({
197 "item_id": item_id,
198 "file_type": "video",
199 "file_name": "large.mp4",
200 "content_type": "video/mp4",
201 });
202 let resp = h
203 .client
204 .post_json("/api/upload/presign", &body.to_string())
205 .await;
206 assert_eq!(resp.status, 200, "{}", resp.text);
207 let data: Value = resp.json();
208 let s3_key = data["s3_key"].as_str().unwrap().to_string();
209
210 // Put a 1KB "video file"
211 let video_data = vec![0u8; 1024];
212 h.storage.as_ref().unwrap().put(&s3_key, video_data);
213
214 let body = json!({
215 "item_id": item_id,
216 "file_type": "video",
217 "s3_key": s3_key,
218 });
219 let resp = h
220 .client
221 .post_json("/api/upload/confirm", &body.to_string())
222 .await;
223 assert_eq!(resp.status, 200, "Confirm failed: {}", resp.text);
224
225 // Verify video_file_size_bytes is set
226 let size: Option<i64> =
227 sqlx::query_scalar("SELECT video_file_size_bytes FROM items WHERE id = $1::uuid")
228 .bind(&item_id)
229 .fetch_one(&h.db)
230 .await
231 .unwrap();
232 assert!(size.unwrap() > 0, "video_file_size_bytes should be set");
233
234 // Verify storage_used_bytes is updated
235 let storage: i64 =
236 sqlx::query_scalar("SELECT storage_used_bytes FROM users WHERE id = $1::uuid")
237 .bind(&user_id)
238 .fetch_one(&h.db)
239 .await
240 .unwrap();
241 assert!(storage > 0, "storage_used_bytes should include video");
242 }
243
244 #[tokio::test]
245 async fn video_wizard_group() {
246 // Verify Video item type gets "video" wizard group (unit-level, but validates the integration)
247 use makenotwork::db::ItemType;
248 assert_eq!(ItemType::Video.wizard_group(), "video");
249 assert_ne!(ItemType::Video.wizard_group(), "file");
250 }
251