Skip to main content

max / makenotwork

22.1 KB · 601 lines History Blame Raw
1 //! Tests for [`super`].
2
3 use super::*;
4 use crate::db::{ItemId, ProjectId, UserId, VersionId};
5 use crate::error::AppError;
6 use std::str::FromStr;
7
8 #[tokio::test]
9 async fn capped_read_aborts_when_body_exceeds_cap() {
10 // A recorded-small object whose real body is larger must not aggregate
11 // past the cap (Run 22 Perf, the buffered scan-download ceiling).
12 let stream = s3_storage::ByteStream::from(vec![0u8; 100]);
13 let err = read_bytestream_capped(stream, "k", 50).await.unwrap_err();
14 assert!(
15 matches!(err, AppError::Storage(_)),
16 "expected Storage error, got {err:?}"
17 );
18 }
19
20 #[tokio::test]
21 async fn capped_read_allows_body_within_cap() {
22 let stream = s3_storage::ByteStream::from(vec![7u8; 40]);
23 let out = read_bytestream_capped(stream, "k", 50).await.unwrap();
24 assert_eq!(out.len(), 40);
25 assert!(out.iter().all(|&b| b == 7));
26 }
27
28 #[tokio::test]
29 async fn capped_read_allows_body_exactly_at_cap() {
30 // The abort condition is strictly `>`, so a body equal to the cap passes.
31 let stream = s3_storage::ByteStream::from(vec![1u8; 50]);
32 let out = read_bytestream_capped(stream, "k", 50).await.unwrap();
33 assert_eq!(out.len(), 50);
34 }
35
36 #[test]
37 fn extract_key_cdn_form() {
38 let key = extract_s3_key_from_url(
39 "https://cdn.makenot.work/projects/abc/image/cover.png",
40 "https://cdn.makenot.work",
41 None,
42 None,
43 );
44 assert_eq!(key.as_deref(), Some("projects/abc/image/cover.png"));
45 }
46
47 #[test]
48 fn extract_key_cdn_with_trailing_slash_in_base() {
49 let key = extract_s3_key_from_url(
50 "https://cdn.makenot.work/foo/bar",
51 "https://cdn.makenot.work/",
52 None,
53 None,
54 );
55 assert_eq!(key.as_deref(), Some("foo/bar"));
56 }
57
58 #[test]
59 fn extract_key_strips_query_string() {
60 let key = extract_s3_key_from_url(
61 "https://cdn.makenot.work/foo/bar?X-Amz-Signature=zzz",
62 "https://cdn.makenot.work",
63 None,
64 None,
65 );
66 assert_eq!(key.as_deref(), Some("foo/bar"));
67 }
68
69 #[test]
70 fn extract_key_path_style_s3() {
71 let key = extract_s3_key_from_url(
72 "https://fsn1.your-objectstorage.com/my-bucket/u/123/image/cover.png?X-Amz=...",
73 "",
74 Some("my-bucket"),
75 Some("https://fsn1.your-objectstorage.com"),
76 );
77 assert_eq!(key.as_deref(), Some("u/123/image/cover.png"));
78 }
79
80 #[test]
81 fn extract_key_path_style_rejects_attacker_host() {
82 // Attacker-controlled host with the legitimate bucket name in the
83 // path must NOT be accepted. The endpoint pin closes the gap.
84 let key = extract_s3_key_from_url(
85 "https://attacker.example/my-bucket/poisoned",
86 "",
87 Some("my-bucket"),
88 Some("https://fsn1.your-objectstorage.com"),
89 );
90 assert_eq!(key, None);
91 }
92
93 #[test]
94 fn extract_key_path_style_requires_endpoint() {
95 // Without the endpoint, the path-style branch must not fire, bucket
96 // name alone is not enough to identify a trustworthy host.
97 let key = extract_s3_key_from_url(
98 "https://fsn1.your-objectstorage.com/my-bucket/u/123/key",
99 "",
100 Some("my-bucket"),
101 None,
102 );
103 assert_eq!(key, None);
104 }
105
106 #[test]
107 fn extract_key_returns_none_when_no_prefix_matches() {
108 // Neither the CDN base nor the bucket name is present in the URL.
109 let key = extract_s3_key_from_url(
110 "https://random.example.com/foo/bar",
111 "https://cdn.makenot.work",
112 Some("my-bucket"),
113 Some("https://fsn1.your-objectstorage.com"),
114 );
115 assert_eq!(key, None);
116 }
117
118 #[test]
119 fn extract_key_does_not_misparse_keys_containing_projects_substring() {
120 // Regression: the old heuristic would have returned just
121 // "projects/x" from this URL, dropping the user-scoped prefix.
122 let key = extract_s3_key_from_url(
123 "https://cdn.makenot.work/u/me/projects/x",
124 "https://cdn.makenot.work",
125 None,
126 None,
127 );
128 assert_eq!(key.as_deref(), Some("u/me/projects/x"));
129 }
130
131 #[test]
132 fn test_generate_key() {
133 let user_id: UserId = "11111111-1111-1111-1111-111111111111".parse().unwrap();
134 let item_id: ItemId = "22222222-2222-2222-2222-222222222222".parse().unwrap();
135
136 let key = S3Client::generate_key(user_id, item_id, FileType::Audio, "episode.mp3");
137 assert_eq!(
138 key,
139 "11111111-1111-1111-1111-111111111111/22222222-2222-2222-2222-222222222222/audio/episode.mp3"
140 );
141 }
142
143 #[test]
144 fn test_generate_version_key_is_unique_per_version() {
145 let user_id: UserId = "11111111-1111-1111-1111-111111111111".parse().unwrap();
146 let item_id: ItemId = "22222222-2222-2222-2222-222222222222".parse().unwrap();
147 let v1: VersionId = "33333333-3333-3333-3333-333333333333".parse().unwrap();
148 let v2: VersionId = "44444444-4444-4444-4444-444444444444".parse().unwrap();
149
150 // Two versions of the SAME item sharing a filename must not collide.
151 let k1 = S3Client::generate_version_key(user_id, item_id, v1, "plugin.zip");
152 let k2 = S3Client::generate_version_key(user_id, item_id, v2, "plugin.zip");
153 assert_ne!(k1, k2, "same-filename versions must produce distinct keys");
154
155 assert_eq!(
156 k1,
157 "11111111-1111-1111-1111-111111111111/22222222-2222-2222-2222-222222222222/download/33333333-3333-3333-3333-333333333333/plugin.zip"
158 );
159 // Confirm-handler prefix check is `{user}/{item}/`; the woven key still
160 // satisfies it.
161 assert!(
162 k1.starts_with(
163 "11111111-1111-1111-1111-111111111111/22222222-2222-2222-2222-222222222222/"
164 )
165 );
166 // Filename is still the last path segment (confirm extracts it via rsplit).
167 assert_eq!(k1.rsplit('/').next(), Some("plugin.zip"));
168 }
169
170 #[test]
171 fn test_generate_version_key_sanitizes_filename() {
172 let user_id: UserId = "11111111-1111-1111-1111-111111111111".parse().unwrap();
173 let item_id: ItemId = "22222222-2222-2222-2222-222222222222".parse().unwrap();
174 let v1: VersionId = "33333333-3333-3333-3333-333333333333".parse().unwrap();
175
176 let key = S3Client::generate_version_key(user_id, item_id, v1, "my release (1).zip");
177 assert!(key.ends_with("/myrelease1.zip"));
178 }
179
180 #[test]
181 fn test_generate_key_sanitizes_filename() {
182 let user_id: UserId = "11111111-1111-1111-1111-111111111111".parse().unwrap();
183 let item_id: ItemId = "22222222-2222-2222-2222-222222222222".parse().unwrap();
184
185 let key = S3Client::generate_key(user_id, item_id, FileType::Audio, "my file (1).mp3");
186 assert!(key.ends_with("/myfile1.mp3"));
187 }
188
189 #[test]
190 fn test_validate_content_type() {
191 assert!(S3Client::validate_content_type(FileType::Audio, "audio/mpeg").is_ok());
192 assert!(S3Client::validate_content_type(FileType::Audio, "audio/wav").is_ok());
193 assert!(S3Client::validate_content_type(FileType::Audio, "image/png").is_err());
194
195 assert!(S3Client::validate_content_type(FileType::Cover, "image/png").is_ok());
196 assert!(S3Client::validate_content_type(FileType::Cover, "image/jpeg").is_ok());
197 assert!(S3Client::validate_content_type(FileType::Cover, "audio/mpeg").is_err());
198 }
199
200 #[test]
201 fn content_type_for_maps_allowed_extensions() {
202 // The multipart promote names the destination's type explicitly, so this
203 // must agree with the allow-list the upload was validated against.
204 assert_eq!(content_type_for(FileType::Video, "mp4"), "video/mp4");
205 assert_eq!(content_type_for(FileType::Video, "webm"), "video/webm");
206 assert_eq!(content_type_for(FileType::Cover, "png"), "image/png");
207 // Extensions arrive from a key, which may carry any case.
208 assert_eq!(content_type_for(FileType::Video, "MP4"), "video/mp4");
209 }
210
211 #[test]
212 fn content_type_for_falls_back_for_unknown_extension() {
213 // `key_extension` yields "bin" when a key has no extension; an unknown
214 // extension must degrade to a generic type, never panic or mis-label.
215 assert_eq!(
216 content_type_for(FileType::Video, "bin"),
217 "application/octet-stream"
218 );
219 assert_eq!(
220 content_type_for(FileType::Video, ""),
221 "application/octet-stream"
222 );
223 }
224
225 #[test]
226 fn single_copy_ceiling_matches_s3_and_is_distinct_from_the_browser_ceiling() {
227 // Different limits, and since the browser cap dropped to 2 GiB, different
228 // numbers too: the copy ceiling is what S3 enforces on a one-shot
229 // `CopyObject`, the browser cap is a product call about resumability.
230 // Pinned so a future change to one doesn't silently move the other.
231 assert_eq!(
232 crate::constants::S3_SINGLE_COPY_MAX_BYTES,
233 5 * 1024 * 1024 * 1024
234 );
235 assert_eq!(
236 crate::constants::BROWSER_UPLOAD_MAX_BYTES,
237 2 * 1024 * 1024 * 1024
238 );
239 }
240
241 #[test]
242 fn test_validate_extension() {
243 assert!(S3Client::validate_extension(FileType::Audio, "episode.mp3").is_ok());
244 assert!(S3Client::validate_extension(FileType::Audio, "episode.MP3").is_ok());
245 assert!(S3Client::validate_extension(FileType::Audio, "episode.png").is_err());
246
247 assert!(S3Client::validate_extension(FileType::Cover, "cover.jpg").is_ok());
248 assert!(S3Client::validate_extension(FileType::Cover, "cover.webp").is_ok());
249 assert!(S3Client::validate_extension(FileType::Cover, "cover.mp3").is_err());
250 }
251
252 #[test]
253 fn test_file_type_from_str() {
254 assert_eq!(FileType::from_str("audio"), Ok(FileType::Audio));
255 assert_eq!(FileType::from_str("AUDIO"), Ok(FileType::Audio));
256 assert_eq!(FileType::from_str("cover"), Ok(FileType::Cover));
257 assert_eq!(FileType::from_str("image"), Ok(FileType::Cover));
258 assert!(FileType::from_str("invalid").is_err());
259 }
260
261 #[test]
262 fn file_type_as_str() {
263 assert_eq!(FileType::Audio.as_str(), "audio");
264 assert_eq!(FileType::Cover.as_str(), "cover");
265 }
266
267 #[test]
268 fn file_type_max_size() {
269 assert_eq!(FileType::Audio.max_size(), 500 * 1024 * 1024);
270 assert_eq!(FileType::Cover.max_size(), 10 * 1024 * 1024);
271 }
272
273 #[test]
274 fn file_type_allowed_types_audio() {
275 let types = FileType::Audio.allowed_types();
276 let exts: Vec<&str> = types.iter().map(|(e, _)| *e).collect();
277 assert!(exts.contains(&"mp3"));
278 assert!(exts.contains(&"wav"));
279 assert!(exts.contains(&"flac"));
280 assert!(!exts.contains(&"png"));
281 }
282
283 #[test]
284 fn file_type_allowed_types_cover() {
285 let types = FileType::Cover.allowed_types();
286 let exts: Vec<&str> = types.iter().map(|(e, _)| *e).collect();
287 assert!(exts.contains(&"jpg"));
288 assert!(exts.contains(&"png"));
289 assert!(exts.contains(&"webp"));
290 assert!(!exts.contains(&"mp3"));
291 }
292
293 #[test]
294 fn generate_key_strips_path_traversal() {
295 let user_id: UserId = "11111111-1111-1111-1111-111111111111".parse().unwrap();
296 let item_id: ItemId = "22222222-2222-2222-2222-222222222222".parse().unwrap();
297
298 let key = S3Client::generate_key(user_id, item_id, FileType::Audio, "../../etc/passwd");
299 // Slashes are stripped, dots kept: "../../etc/passwd" -> "....etcpasswd"
300 assert!(key.ends_with("/audio/....etcpasswd"));
301 }
302
303 #[test]
304 fn generate_key_empty_filename_gets_fallback() {
305 let user_id: UserId = "11111111-1111-1111-1111-111111111111".parse().unwrap();
306 let item_id: ItemId = "22222222-2222-2222-2222-222222222222".parse().unwrap();
307
308 let key = S3Client::generate_key(user_id, item_id, FileType::Cover, "");
309 assert!(
310 key.ends_with("/cover/file"),
311 "expected fallback name 'file', got: {key}"
312 );
313 }
314
315 #[test]
316 fn validate_extension_no_extension() {
317 assert!(S3Client::validate_extension(FileType::Audio, "noext").is_err());
318 }
319
320 #[test]
321 fn validate_extension_double_dot() {
322 assert!(S3Client::validate_extension(FileType::Audio, "file.backup.mp3").is_ok());
323 }
324
325 #[test]
326 fn validate_content_type_empty() {
327 assert!(S3Client::validate_content_type(FileType::Audio, "").is_err());
328 }
329
330 #[test]
331 fn file_type_insertion_from_str() {
332 assert_eq!(FileType::from_str("insertion"), Ok(FileType::Insertion));
333 assert_eq!(FileType::from_str("INSERTION"), Ok(FileType::Insertion));
334 }
335
336 #[test]
337 fn file_type_insertion_as_str() {
338 assert_eq!(FileType::Insertion.as_str(), "insertion");
339 }
340
341 #[test]
342 fn file_type_insertion_max_size() {
343 assert_eq!(FileType::Insertion.max_size(), 500 * 1024 * 1024);
344 }
345
346 #[test]
347 fn validate_insertion_content_types() {
348 // Audio clips (the original use).
349 assert!(S3Client::validate_content_type(FileType::Insertion, "audio/mpeg").is_ok());
350 assert!(S3Client::validate_content_type(FileType::Insertion, "audio/wav").is_ok());
351 assert!(S3Client::validate_content_type(FileType::Insertion, "audio/flac").is_ok());
352 // Video clips (pre/mid/post-roll on video items).
353 assert!(S3Client::validate_content_type(FileType::Insertion, "video/mp4").is_ok());
354 assert!(S3Client::validate_content_type(FileType::Insertion, "video/webm").is_ok());
355 assert!(S3Client::validate_content_type(FileType::Insertion, "video/quicktime").is_ok());
356 // Neither audio nor video is rejected.
357 assert!(S3Client::validate_content_type(FileType::Insertion, "image/png").is_err());
358 }
359
360 #[test]
361 fn validate_insertion_extensions() {
362 assert!(S3Client::validate_extension(FileType::Insertion, "intro.mp3").is_ok());
363 assert!(S3Client::validate_extension(FileType::Insertion, "sponsor.wav").is_ok());
364 assert!(S3Client::validate_extension(FileType::Insertion, "outro.flac").is_ok());
365 assert!(S3Client::validate_extension(FileType::Insertion, "bumper.mp4").is_ok());
366 assert!(S3Client::validate_extension(FileType::Insertion, "bumper.webm").is_ok());
367 assert!(S3Client::validate_extension(FileType::Insertion, "clip.png").is_err());
368 }
369
370 #[test]
371 fn insertion_media_type_classifies_by_mime_family() {
372 assert_eq!(S3Client::insertion_media_type("audio/mpeg"), "audio");
373 assert_eq!(S3Client::insertion_media_type("audio/mp4"), "audio");
374 assert_eq!(S3Client::insertion_media_type("video/mp4"), "video");
375 assert_eq!(S3Client::insertion_media_type("video/webm"), "video");
376 }
377
378 #[test]
379 fn generate_insertion_key_format() {
380 let user_id: UserId = "11111111-1111-1111-1111-111111111111".parse().unwrap();
381 let key = S3Client::generate_insertion_key(user_id, "intro.mp3");
382 assert_eq!(
383 key,
384 "11111111-1111-1111-1111-111111111111/insertions/intro.mp3"
385 );
386 }
387
388 #[test]
389 fn generate_insertion_key_sanitizes() {
390 let user_id: UserId = "11111111-1111-1111-1111-111111111111".parse().unwrap();
391 let key = S3Client::generate_insertion_key(user_id, "my sponsor read (v2).mp3");
392 assert_eq!(
393 key,
394 "11111111-1111-1111-1111-111111111111/insertions/mysponsorreadv2.mp3"
395 );
396 }
397
398 #[test]
399 fn extension_for_cases() {
400 assert_eq!(extension_for("plugin.zip"), "zip");
401 assert_eq!(extension_for("LOUD.WAV"), "wav"); // lowercased
402 assert_eq!(extension_for("archive.tar.gz"), "gz"); // last segment only
403 assert_eq!(extension_for("noextension"), "bin"); // fallback
404 assert_eq!(extension_for("trailing."), "bin"); // empty ext → fallback
405 assert_eq!(extension_for("weird.z!p"), "zp"); // non-alnum stripped
406 }
407
408 #[test]
409 fn key_extension_cases() {
410 assert_eq!(key_extension("staging/2f9a.zip"), "zip");
411 assert_eq!(key_extension("uid/c/abcd1234.mp3"), "mp3");
412 assert_eq!(key_extension("staging/no-dot-basename"), "bin");
413 assert_eq!(key_extension("dir.with.dot/basename"), "bin"); // dot in dir, not basename
414 }
415
416 #[test]
417 fn staging_key_is_unserved_and_carries_extension() {
418 let key = S3Client::generate_staging_key("release.zip");
419 assert!(key.as_str().starts_with("staging/"), "staging key: {key}");
420 assert_eq!(key_extension(key.as_str()), "zip");
421 // Two calls never collide (random uuid), so a replayed PUT can't target
422 // another upload's staging object.
423 let key2 = S3Client::generate_staging_key("release.zip");
424 assert_ne!(key, key2);
425 }
426
427 #[test]
428 fn content_key_is_hash_addressed_and_owner_namespaced() {
429 let user_id: UserId = "11111111-1111-1111-1111-111111111111".parse().unwrap();
430 let sha = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
431 let key = S3Client::content_key(user_id, sha, "zip");
432 assert_eq!(
433 key,
434 "11111111-1111-1111-1111-111111111111/c/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.zip"
435 );
436 // Same bytes, different owner → different key (no cross-tenant sharing).
437 let other: UserId = "22222222-2222-2222-2222-222222222222".parse().unwrap();
438 assert_ne!(S3Client::content_key(other, sha, "zip"), key);
439 }
440
441 #[test]
442 fn generate_key_cover_type() {
443 let user_id: UserId = "11111111-1111-1111-1111-111111111111".parse().unwrap();
444 let item_id: ItemId = "22222222-2222-2222-2222-222222222222".parse().unwrap();
445
446 let key = S3Client::generate_key(user_id, item_id, FileType::Cover, "art.png");
447 assert!(key.contains("/cover/"));
448 assert!(key.ends_with("art.png"));
449 }
450
451 // FileType::Download tests
452
453 #[test]
454 fn file_type_download_from_str() {
455 assert_eq!(FileType::from_str("download"), Ok(FileType::Download));
456 assert_eq!(FileType::from_str("DOWNLOAD"), Ok(FileType::Download));
457 }
458
459 #[test]
460 fn file_type_download_as_str() {
461 assert_eq!(FileType::Download.as_str(), "download");
462 }
463
464 #[test]
465 fn file_type_download_max_size() {
466 assert_eq!(FileType::Download.max_size(), 500 * 1024 * 1024);
467 }
468
469 #[test]
470 fn validate_download_content_types() {
471 assert!(
472 S3Client::validate_content_type(FileType::Download, "application/octet-stream").is_ok()
473 );
474 assert!(S3Client::validate_content_type(FileType::Download, "application/zip").is_ok());
475 assert!(
476 S3Client::validate_content_type(FileType::Download, "application/x-apple-diskimage")
477 .is_ok()
478 );
479 assert!(S3Client::validate_content_type(FileType::Download, "application/gzip").is_ok());
480 assert!(S3Client::validate_content_type(FileType::Download, "application/x-tar").is_ok());
481 // Reject clearly wrong types
482 assert!(S3Client::validate_content_type(FileType::Download, "text/html").is_err());
483 assert!(S3Client::validate_content_type(FileType::Download, "image/png").is_err());
484 }
485
486 #[test]
487 fn validate_download_extensions() {
488 assert!(S3Client::validate_extension(FileType::Download, "app.zip").is_ok());
489 assert!(S3Client::validate_extension(FileType::Download, "app.dmg").is_ok());
490 assert!(S3Client::validate_extension(FileType::Download, "app.exe").is_ok());
491 assert!(S3Client::validate_extension(FileType::Download, "app.appimage").is_ok());
492 assert!(S3Client::validate_extension(FileType::Download, "app.deb").is_ok());
493 assert!(S3Client::validate_extension(FileType::Download, "app.tar.gz").is_ok());
494 assert!(S3Client::validate_extension(FileType::Download, "app.clap").is_ok());
495 assert!(S3Client::validate_extension(FileType::Download, "app.vst3").is_ok());
496 assert!(S3Client::validate_extension(FileType::Download, "App.ZIP").is_ok());
497 // Reject invalid extensions
498 assert!(S3Client::validate_extension(FileType::Download, "app.mp3").is_err());
499 assert!(S3Client::validate_extension(FileType::Download, "app.txt").is_err());
500 }
501
502 #[test]
503 fn generate_key_download_type() {
504 let user_id: UserId = "11111111-1111-1111-1111-111111111111".parse().unwrap();
505 let item_id: ItemId = "22222222-2222-2222-2222-222222222222".parse().unwrap();
506
507 let key = S3Client::generate_key(user_id, item_id, FileType::Download, "plugin-v1.0.zip");
508 assert!(key.contains("/download/"));
509 assert!(key.ends_with("plugin-v1.0.zip"));
510 }
511
512 // CDN tests
513
514 #[test]
515 fn cache_control_immutable_format() {
516 assert!(CACHE_CONTROL_IMMUTABLE.contains("public"));
517 assert!(CACHE_CONTROL_IMMUTABLE.contains("max-age=31536000"));
518 assert!(CACHE_CONTROL_IMMUTABLE.contains("immutable"));
519 }
520
521 #[test]
522 fn generate_project_image_key_format() {
523 let project_id: ProjectId = "33333333-3333-3333-3333-333333333333".parse().unwrap();
524 let key = S3Client::generate_project_image_key(project_id, "logo.png");
525 assert_eq!(
526 key,
527 "projects/33333333-3333-3333-3333-333333333333/image/logo.png"
528 );
529 }
530
531 #[test]
532 fn generate_project_image_key_sanitizes() {
533 let project_id: ProjectId = "33333333-3333-3333-3333-333333333333".parse().unwrap();
534 let key = S3Client::generate_project_image_key(project_id, "my logo (v2).png");
535 assert_eq!(
536 key,
537 "projects/33333333-3333-3333-3333-333333333333/image/mylogov2.png"
538 );
539 }
540
541 #[test]
542 fn cdn_url_from_s3_key() {
543 let cdn_base = "https://cdn.makenot.work";
544 let user_id: UserId = "11111111-1111-1111-1111-111111111111".parse().unwrap();
545 let item_id: ItemId = "22222222-2222-2222-2222-222222222222".parse().unwrap();
546 let key = S3Client::generate_key(user_id, item_id, FileType::Audio, "episode.mp3");
547 let cdn_url = format!("{cdn_base}/{key}");
548 assert_eq!(
549 cdn_url,
550 "https://cdn.makenot.work/11111111-1111-1111-1111-111111111111/22222222-2222-2222-2222-222222222222/audio/episode.mp3"
551 );
552 }
553
554 // FileType::Video tests
555
556 #[test]
557 fn file_type_video_from_str() {
558 assert_eq!(FileType::from_str("video"), Ok(FileType::Video));
559 assert_eq!(FileType::from_str("VIDEO"), Ok(FileType::Video));
560 }
561
562 #[test]
563 fn file_type_video_as_str() {
564 assert_eq!(FileType::Video.as_str(), "video");
565 }
566
567 #[test]
568 fn file_type_video_max_size() {
569 assert_eq!(FileType::Video.max_size(), 20 * 1024 * 1024 * 1024);
570 }
571
572 #[test]
573 fn validate_video_content_types() {
574 assert!(S3Client::validate_content_type(FileType::Video, "video/mp4").is_ok());
575 assert!(S3Client::validate_content_type(FileType::Video, "video/webm").is_ok());
576 assert!(S3Client::validate_content_type(FileType::Video, "video/quicktime").is_ok());
577 assert!(S3Client::validate_content_type(FileType::Video, "audio/mpeg").is_err());
578 assert!(S3Client::validate_content_type(FileType::Video, "application/octet-stream").is_err());
579 assert!(S3Client::validate_content_type(FileType::Video, "text/html").is_err());
580 }
581
582 #[test]
583 fn validate_video_extensions() {
584 assert!(S3Client::validate_extension(FileType::Video, "clip.mp4").is_ok());
585 assert!(S3Client::validate_extension(FileType::Video, "clip.webm").is_ok());
586 assert!(S3Client::validate_extension(FileType::Video, "clip.mov").is_ok());
587 assert!(S3Client::validate_extension(FileType::Video, "Clip.MP4").is_ok());
588 assert!(S3Client::validate_extension(FileType::Video, "clip.avi").is_err());
589 assert!(S3Client::validate_extension(FileType::Video, "clip.mp3").is_err());
590 }
591
592 #[test]
593 fn generate_key_video_type() {
594 let user_id: UserId = "11111111-1111-1111-1111-111111111111".parse().unwrap();
595 let item_id: ItemId = "22222222-2222-2222-2222-222222222222".parse().unwrap();
596
597 let key = S3Client::generate_key(user_id, item_id, FileType::Video, "tutorial.mp4");
598 assert!(key.contains("/video/"));
599 assert!(key.ends_with("tutorial.mp4"));
600 }
601