//! Tests for [`super`]. use super::*; use crate::db::{ItemId, ProjectId, UserId, VersionId}; use crate::error::AppError; use std::str::FromStr; #[tokio::test] async fn capped_read_aborts_when_body_exceeds_cap() { // A recorded-small object whose real body is larger must not aggregate // past the cap (Run 22 Perf, the buffered scan-download ceiling). let stream = s3_storage::ByteStream::from(vec![0u8; 100]); let err = read_bytestream_capped(stream, "k", 50).await.unwrap_err(); assert!( matches!(err, AppError::Storage(_)), "expected Storage error, got {err:?}" ); } #[tokio::test] async fn capped_read_allows_body_within_cap() { let stream = s3_storage::ByteStream::from(vec![7u8; 40]); let out = read_bytestream_capped(stream, "k", 50).await.unwrap(); assert_eq!(out.len(), 40); assert!(out.iter().all(|&b| b == 7)); } #[tokio::test] async fn capped_read_allows_body_exactly_at_cap() { // The abort condition is strictly `>`, so a body equal to the cap passes. let stream = s3_storage::ByteStream::from(vec![1u8; 50]); let out = read_bytestream_capped(stream, "k", 50).await.unwrap(); assert_eq!(out.len(), 50); } #[test] fn extract_key_cdn_form() { let key = extract_s3_key_from_url( "https://cdn.makenot.work/projects/abc/image/cover.png", "https://cdn.makenot.work", None, None, ); assert_eq!(key.as_deref(), Some("projects/abc/image/cover.png")); } #[test] fn extract_key_cdn_with_trailing_slash_in_base() { let key = extract_s3_key_from_url( "https://cdn.makenot.work/foo/bar", "https://cdn.makenot.work/", None, None, ); assert_eq!(key.as_deref(), Some("foo/bar")); } #[test] fn extract_key_strips_query_string() { let key = extract_s3_key_from_url( "https://cdn.makenot.work/foo/bar?X-Amz-Signature=zzz", "https://cdn.makenot.work", None, None, ); assert_eq!(key.as_deref(), Some("foo/bar")); } #[test] fn extract_key_path_style_s3() { let key = extract_s3_key_from_url( "https://fsn1.your-objectstorage.com/my-bucket/u/123/image/cover.png?X-Amz=...", "", Some("my-bucket"), Some("https://fsn1.your-objectstorage.com"), ); assert_eq!(key.as_deref(), Some("u/123/image/cover.png")); } #[test] fn extract_key_path_style_rejects_attacker_host() { // Attacker-controlled host with the legitimate bucket name in the // path must NOT be accepted. The endpoint pin closes the gap. let key = extract_s3_key_from_url( "https://attacker.example/my-bucket/poisoned", "", Some("my-bucket"), Some("https://fsn1.your-objectstorage.com"), ); assert_eq!(key, None); } #[test] fn extract_key_path_style_requires_endpoint() { // Without the endpoint, the path-style branch must not fire, bucket // name alone is not enough to identify a trustworthy host. let key = extract_s3_key_from_url( "https://fsn1.your-objectstorage.com/my-bucket/u/123/key", "", Some("my-bucket"), None, ); assert_eq!(key, None); } #[test] fn extract_key_returns_none_when_no_prefix_matches() { // Neither the CDN base nor the bucket name is present in the URL. let key = extract_s3_key_from_url( "https://random.example.com/foo/bar", "https://cdn.makenot.work", Some("my-bucket"), Some("https://fsn1.your-objectstorage.com"), ); assert_eq!(key, None); } #[test] fn extract_key_does_not_misparse_keys_containing_projects_substring() { // Regression: the old heuristic would have returned just // "projects/x" from this URL, dropping the user-scoped prefix. let key = extract_s3_key_from_url( "https://cdn.makenot.work/u/me/projects/x", "https://cdn.makenot.work", None, None, ); assert_eq!(key.as_deref(), Some("u/me/projects/x")); } #[test] fn test_generate_key() { let user_id: UserId = "11111111-1111-1111-1111-111111111111".parse().unwrap(); let item_id: ItemId = "22222222-2222-2222-2222-222222222222".parse().unwrap(); let key = S3Client::generate_key(user_id, item_id, FileType::Audio, "episode.mp3"); assert_eq!( key, "11111111-1111-1111-1111-111111111111/22222222-2222-2222-2222-222222222222/audio/episode.mp3" ); } #[test] fn test_generate_version_key_is_unique_per_version() { let user_id: UserId = "11111111-1111-1111-1111-111111111111".parse().unwrap(); let item_id: ItemId = "22222222-2222-2222-2222-222222222222".parse().unwrap(); let v1: VersionId = "33333333-3333-3333-3333-333333333333".parse().unwrap(); let v2: VersionId = "44444444-4444-4444-4444-444444444444".parse().unwrap(); // Two versions of the SAME item sharing a filename must not collide. let k1 = S3Client::generate_version_key(user_id, item_id, v1, "plugin.zip"); let k2 = S3Client::generate_version_key(user_id, item_id, v2, "plugin.zip"); assert_ne!(k1, k2, "same-filename versions must produce distinct keys"); assert_eq!( k1, "11111111-1111-1111-1111-111111111111/22222222-2222-2222-2222-222222222222/download/33333333-3333-3333-3333-333333333333/plugin.zip" ); // Confirm-handler prefix check is `{user}/{item}/`; the woven key still // satisfies it. assert!( k1.starts_with( "11111111-1111-1111-1111-111111111111/22222222-2222-2222-2222-222222222222/" ) ); // Filename is still the last path segment (confirm extracts it via rsplit). assert_eq!(k1.rsplit('/').next(), Some("plugin.zip")); } #[test] fn test_generate_version_key_sanitizes_filename() { let user_id: UserId = "11111111-1111-1111-1111-111111111111".parse().unwrap(); let item_id: ItemId = "22222222-2222-2222-2222-222222222222".parse().unwrap(); let v1: VersionId = "33333333-3333-3333-3333-333333333333".parse().unwrap(); let key = S3Client::generate_version_key(user_id, item_id, v1, "my release (1).zip"); assert!(key.ends_with("/myrelease1.zip")); } #[test] fn test_generate_key_sanitizes_filename() { let user_id: UserId = "11111111-1111-1111-1111-111111111111".parse().unwrap(); let item_id: ItemId = "22222222-2222-2222-2222-222222222222".parse().unwrap(); let key = S3Client::generate_key(user_id, item_id, FileType::Audio, "my file (1).mp3"); assert!(key.ends_with("/myfile1.mp3")); } #[test] fn test_validate_content_type() { assert!(S3Client::validate_content_type(FileType::Audio, "audio/mpeg").is_ok()); assert!(S3Client::validate_content_type(FileType::Audio, "audio/wav").is_ok()); assert!(S3Client::validate_content_type(FileType::Audio, "image/png").is_err()); assert!(S3Client::validate_content_type(FileType::Cover, "image/png").is_ok()); assert!(S3Client::validate_content_type(FileType::Cover, "image/jpeg").is_ok()); assert!(S3Client::validate_content_type(FileType::Cover, "audio/mpeg").is_err()); } #[test] fn content_type_for_maps_allowed_extensions() { // The multipart promote names the destination's type explicitly, so this // must agree with the allow-list the upload was validated against. assert_eq!(content_type_for(FileType::Video, "mp4"), "video/mp4"); assert_eq!(content_type_for(FileType::Video, "webm"), "video/webm"); assert_eq!(content_type_for(FileType::Cover, "png"), "image/png"); // Extensions arrive from a key, which may carry any case. assert_eq!(content_type_for(FileType::Video, "MP4"), "video/mp4"); } #[test] fn content_type_for_falls_back_for_unknown_extension() { // `key_extension` yields "bin" when a key has no extension; an unknown // extension must degrade to a generic type, never panic or mis-label. assert_eq!( content_type_for(FileType::Video, "bin"), "application/octet-stream" ); assert_eq!( content_type_for(FileType::Video, ""), "application/octet-stream" ); } #[test] fn single_copy_ceiling_matches_s3_and_is_distinct_from_the_browser_ceiling() { // Different limits, and since the browser cap dropped to 2 GiB, different // numbers too: the copy ceiling is what S3 enforces on a one-shot // `CopyObject`, the browser cap is a product call about resumability. // Pinned so a future change to one doesn't silently move the other. assert_eq!( crate::constants::S3_SINGLE_COPY_MAX_BYTES, 5 * 1024 * 1024 * 1024 ); assert_eq!( crate::constants::BROWSER_UPLOAD_MAX_BYTES, 2 * 1024 * 1024 * 1024 ); } #[test] fn test_validate_extension() { assert!(S3Client::validate_extension(FileType::Audio, "episode.mp3").is_ok()); assert!(S3Client::validate_extension(FileType::Audio, "episode.MP3").is_ok()); assert!(S3Client::validate_extension(FileType::Audio, "episode.png").is_err()); assert!(S3Client::validate_extension(FileType::Cover, "cover.jpg").is_ok()); assert!(S3Client::validate_extension(FileType::Cover, "cover.webp").is_ok()); assert!(S3Client::validate_extension(FileType::Cover, "cover.mp3").is_err()); } #[test] fn test_file_type_from_str() { assert_eq!(FileType::from_str("audio"), Ok(FileType::Audio)); assert_eq!(FileType::from_str("AUDIO"), Ok(FileType::Audio)); assert_eq!(FileType::from_str("cover"), Ok(FileType::Cover)); assert_eq!(FileType::from_str("image"), Ok(FileType::Cover)); assert!(FileType::from_str("invalid").is_err()); } #[test] fn file_type_as_str() { assert_eq!(FileType::Audio.as_str(), "audio"); assert_eq!(FileType::Cover.as_str(), "cover"); } #[test] fn file_type_max_size() { assert_eq!(FileType::Audio.max_size(), 500 * 1024 * 1024); assert_eq!(FileType::Cover.max_size(), 10 * 1024 * 1024); } #[test] fn file_type_allowed_types_audio() { let types = FileType::Audio.allowed_types(); let exts: Vec<&str> = types.iter().map(|(e, _)| *e).collect(); assert!(exts.contains(&"mp3")); assert!(exts.contains(&"wav")); assert!(exts.contains(&"flac")); assert!(!exts.contains(&"png")); } #[test] fn file_type_allowed_types_cover() { let types = FileType::Cover.allowed_types(); let exts: Vec<&str> = types.iter().map(|(e, _)| *e).collect(); assert!(exts.contains(&"jpg")); assert!(exts.contains(&"png")); assert!(exts.contains(&"webp")); assert!(!exts.contains(&"mp3")); } #[test] fn generate_key_strips_path_traversal() { let user_id: UserId = "11111111-1111-1111-1111-111111111111".parse().unwrap(); let item_id: ItemId = "22222222-2222-2222-2222-222222222222".parse().unwrap(); let key = S3Client::generate_key(user_id, item_id, FileType::Audio, "../../etc/passwd"); // Slashes are stripped, dots kept: "../../etc/passwd" -> "....etcpasswd" assert!(key.ends_with("/audio/....etcpasswd")); } #[test] fn generate_key_empty_filename_gets_fallback() { let user_id: UserId = "11111111-1111-1111-1111-111111111111".parse().unwrap(); let item_id: ItemId = "22222222-2222-2222-2222-222222222222".parse().unwrap(); let key = S3Client::generate_key(user_id, item_id, FileType::Cover, ""); assert!( key.ends_with("/cover/file"), "expected fallback name 'file', got: {key}" ); } #[test] fn validate_extension_no_extension() { assert!(S3Client::validate_extension(FileType::Audio, "noext").is_err()); } #[test] fn validate_extension_double_dot() { assert!(S3Client::validate_extension(FileType::Audio, "file.backup.mp3").is_ok()); } #[test] fn validate_content_type_empty() { assert!(S3Client::validate_content_type(FileType::Audio, "").is_err()); } #[test] fn file_type_insertion_from_str() { assert_eq!(FileType::from_str("insertion"), Ok(FileType::Insertion)); assert_eq!(FileType::from_str("INSERTION"), Ok(FileType::Insertion)); } #[test] fn file_type_insertion_as_str() { assert_eq!(FileType::Insertion.as_str(), "insertion"); } #[test] fn file_type_insertion_max_size() { assert_eq!(FileType::Insertion.max_size(), 500 * 1024 * 1024); } #[test] fn validate_insertion_content_types() { // Audio clips (the original use). assert!(S3Client::validate_content_type(FileType::Insertion, "audio/mpeg").is_ok()); assert!(S3Client::validate_content_type(FileType::Insertion, "audio/wav").is_ok()); assert!(S3Client::validate_content_type(FileType::Insertion, "audio/flac").is_ok()); // Video clips (pre/mid/post-roll on video items). assert!(S3Client::validate_content_type(FileType::Insertion, "video/mp4").is_ok()); assert!(S3Client::validate_content_type(FileType::Insertion, "video/webm").is_ok()); assert!(S3Client::validate_content_type(FileType::Insertion, "video/quicktime").is_ok()); // Neither audio nor video is rejected. assert!(S3Client::validate_content_type(FileType::Insertion, "image/png").is_err()); } #[test] fn validate_insertion_extensions() { assert!(S3Client::validate_extension(FileType::Insertion, "intro.mp3").is_ok()); assert!(S3Client::validate_extension(FileType::Insertion, "sponsor.wav").is_ok()); assert!(S3Client::validate_extension(FileType::Insertion, "outro.flac").is_ok()); assert!(S3Client::validate_extension(FileType::Insertion, "bumper.mp4").is_ok()); assert!(S3Client::validate_extension(FileType::Insertion, "bumper.webm").is_ok()); assert!(S3Client::validate_extension(FileType::Insertion, "clip.png").is_err()); } #[test] fn insertion_media_type_classifies_by_mime_family() { assert_eq!(S3Client::insertion_media_type("audio/mpeg"), "audio"); assert_eq!(S3Client::insertion_media_type("audio/mp4"), "audio"); assert_eq!(S3Client::insertion_media_type("video/mp4"), "video"); assert_eq!(S3Client::insertion_media_type("video/webm"), "video"); } #[test] fn generate_insertion_key_format() { let user_id: UserId = "11111111-1111-1111-1111-111111111111".parse().unwrap(); let key = S3Client::generate_insertion_key(user_id, "intro.mp3"); assert_eq!( key, "11111111-1111-1111-1111-111111111111/insertions/intro.mp3" ); } #[test] fn generate_insertion_key_sanitizes() { let user_id: UserId = "11111111-1111-1111-1111-111111111111".parse().unwrap(); let key = S3Client::generate_insertion_key(user_id, "my sponsor read (v2).mp3"); assert_eq!( key, "11111111-1111-1111-1111-111111111111/insertions/mysponsorreadv2.mp3" ); } #[test] fn extension_for_cases() { assert_eq!(extension_for("plugin.zip"), "zip"); assert_eq!(extension_for("LOUD.WAV"), "wav"); // lowercased assert_eq!(extension_for("archive.tar.gz"), "gz"); // last segment only assert_eq!(extension_for("noextension"), "bin"); // fallback assert_eq!(extension_for("trailing."), "bin"); // empty ext → fallback assert_eq!(extension_for("weird.z!p"), "zp"); // non-alnum stripped } #[test] fn key_extension_cases() { assert_eq!(key_extension("staging/2f9a.zip"), "zip"); assert_eq!(key_extension("uid/c/abcd1234.mp3"), "mp3"); assert_eq!(key_extension("staging/no-dot-basename"), "bin"); assert_eq!(key_extension("dir.with.dot/basename"), "bin"); // dot in dir, not basename } #[test] fn staging_key_is_unserved_and_carries_extension() { let key = S3Client::generate_staging_key("release.zip"); assert!(key.as_str().starts_with("staging/"), "staging key: {key}"); assert_eq!(key_extension(key.as_str()), "zip"); // Two calls never collide (random uuid), so a replayed PUT can't target // another upload's staging object. let key2 = S3Client::generate_staging_key("release.zip"); assert_ne!(key, key2); } #[test] fn content_key_is_hash_addressed_and_owner_namespaced() { let user_id: UserId = "11111111-1111-1111-1111-111111111111".parse().unwrap(); let sha = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; let key = S3Client::content_key(user_id, sha, "zip"); assert_eq!( key, "11111111-1111-1111-1111-111111111111/c/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.zip" ); // Same bytes, different owner → different key (no cross-tenant sharing). let other: UserId = "22222222-2222-2222-2222-222222222222".parse().unwrap(); assert_ne!(S3Client::content_key(other, sha, "zip"), key); } #[test] fn generate_key_cover_type() { let user_id: UserId = "11111111-1111-1111-1111-111111111111".parse().unwrap(); let item_id: ItemId = "22222222-2222-2222-2222-222222222222".parse().unwrap(); let key = S3Client::generate_key(user_id, item_id, FileType::Cover, "art.png"); assert!(key.contains("/cover/")); assert!(key.ends_with("art.png")); } // FileType::Download tests #[test] fn file_type_download_from_str() { assert_eq!(FileType::from_str("download"), Ok(FileType::Download)); assert_eq!(FileType::from_str("DOWNLOAD"), Ok(FileType::Download)); } #[test] fn file_type_download_as_str() { assert_eq!(FileType::Download.as_str(), "download"); } #[test] fn file_type_download_max_size() { assert_eq!(FileType::Download.max_size(), 500 * 1024 * 1024); } #[test] fn validate_download_content_types() { assert!( S3Client::validate_content_type(FileType::Download, "application/octet-stream").is_ok() ); assert!(S3Client::validate_content_type(FileType::Download, "application/zip").is_ok()); assert!( S3Client::validate_content_type(FileType::Download, "application/x-apple-diskimage") .is_ok() ); assert!(S3Client::validate_content_type(FileType::Download, "application/gzip").is_ok()); assert!(S3Client::validate_content_type(FileType::Download, "application/x-tar").is_ok()); // Reject clearly wrong types assert!(S3Client::validate_content_type(FileType::Download, "text/html").is_err()); assert!(S3Client::validate_content_type(FileType::Download, "image/png").is_err()); } #[test] fn validate_download_extensions() { assert!(S3Client::validate_extension(FileType::Download, "app.zip").is_ok()); assert!(S3Client::validate_extension(FileType::Download, "app.dmg").is_ok()); assert!(S3Client::validate_extension(FileType::Download, "app.exe").is_ok()); assert!(S3Client::validate_extension(FileType::Download, "app.appimage").is_ok()); assert!(S3Client::validate_extension(FileType::Download, "app.deb").is_ok()); assert!(S3Client::validate_extension(FileType::Download, "app.tar.gz").is_ok()); assert!(S3Client::validate_extension(FileType::Download, "app.clap").is_ok()); assert!(S3Client::validate_extension(FileType::Download, "app.vst3").is_ok()); assert!(S3Client::validate_extension(FileType::Download, "App.ZIP").is_ok()); // Reject invalid extensions assert!(S3Client::validate_extension(FileType::Download, "app.mp3").is_err()); assert!(S3Client::validate_extension(FileType::Download, "app.txt").is_err()); } #[test] fn generate_key_download_type() { let user_id: UserId = "11111111-1111-1111-1111-111111111111".parse().unwrap(); let item_id: ItemId = "22222222-2222-2222-2222-222222222222".parse().unwrap(); let key = S3Client::generate_key(user_id, item_id, FileType::Download, "plugin-v1.0.zip"); assert!(key.contains("/download/")); assert!(key.ends_with("plugin-v1.0.zip")); } // CDN tests #[test] fn cache_control_immutable_format() { assert!(CACHE_CONTROL_IMMUTABLE.contains("public")); assert!(CACHE_CONTROL_IMMUTABLE.contains("max-age=31536000")); assert!(CACHE_CONTROL_IMMUTABLE.contains("immutable")); } #[test] fn generate_project_image_key_format() { let project_id: ProjectId = "33333333-3333-3333-3333-333333333333".parse().unwrap(); let key = S3Client::generate_project_image_key(project_id, "logo.png"); assert_eq!( key, "projects/33333333-3333-3333-3333-333333333333/image/logo.png" ); } #[test] fn generate_project_image_key_sanitizes() { let project_id: ProjectId = "33333333-3333-3333-3333-333333333333".parse().unwrap(); let key = S3Client::generate_project_image_key(project_id, "my logo (v2).png"); assert_eq!( key, "projects/33333333-3333-3333-3333-333333333333/image/mylogov2.png" ); } #[test] fn cdn_url_from_s3_key() { let cdn_base = "https://cdn.makenot.work"; let user_id: UserId = "11111111-1111-1111-1111-111111111111".parse().unwrap(); let item_id: ItemId = "22222222-2222-2222-2222-222222222222".parse().unwrap(); let key = S3Client::generate_key(user_id, item_id, FileType::Audio, "episode.mp3"); let cdn_url = format!("{cdn_base}/{key}"); assert_eq!( cdn_url, "https://cdn.makenot.work/11111111-1111-1111-1111-111111111111/22222222-2222-2222-2222-222222222222/audio/episode.mp3" ); } // FileType::Video tests #[test] fn file_type_video_from_str() { assert_eq!(FileType::from_str("video"), Ok(FileType::Video)); assert_eq!(FileType::from_str("VIDEO"), Ok(FileType::Video)); } #[test] fn file_type_video_as_str() { assert_eq!(FileType::Video.as_str(), "video"); } #[test] fn file_type_video_max_size() { assert_eq!(FileType::Video.max_size(), 20 * 1024 * 1024 * 1024); } #[test] fn validate_video_content_types() { assert!(S3Client::validate_content_type(FileType::Video, "video/mp4").is_ok()); assert!(S3Client::validate_content_type(FileType::Video, "video/webm").is_ok()); assert!(S3Client::validate_content_type(FileType::Video, "video/quicktime").is_ok()); assert!(S3Client::validate_content_type(FileType::Video, "audio/mpeg").is_err()); assert!(S3Client::validate_content_type(FileType::Video, "application/octet-stream").is_err()); assert!(S3Client::validate_content_type(FileType::Video, "text/html").is_err()); } #[test] fn validate_video_extensions() { assert!(S3Client::validate_extension(FileType::Video, "clip.mp4").is_ok()); assert!(S3Client::validate_extension(FileType::Video, "clip.webm").is_ok()); assert!(S3Client::validate_extension(FileType::Video, "clip.mov").is_ok()); assert!(S3Client::validate_extension(FileType::Video, "Clip.MP4").is_ok()); assert!(S3Client::validate_extension(FileType::Video, "clip.avi").is_err()); assert!(S3Client::validate_extension(FileType::Video, "clip.mp3").is_err()); } #[test] fn generate_key_video_type() { let user_id: UserId = "11111111-1111-1111-1111-111111111111".parse().unwrap(); let item_id: ItemId = "22222222-2222-2222-2222-222222222222".parse().unwrap(); let key = S3Client::generate_key(user_id, item_id, FileType::Video, "tutorial.mp4"); assert!(key.contains("/video/")); assert!(key.ends_with("tutorial.mp4")); }