use std::{net::SocketAddr, sync::Arc}; use axum::{ body::Body, extract::ConnectInfo, http::{Request, StatusCode}, }; use http_body_util::BodyExt; use pinakes_core::{ cache::CacheLayer, config::{ AccountsConfig, AnalyticsConfig, CloudConfig, Config, DirectoryConfig, EnrichmentConfig, JobsConfig, ManagedStorageConfig, PhotoConfig, PluginsConfig, ScanningConfig, ServerConfig, SharingConfig, SqliteConfig, StorageBackendType, StorageConfig, SyncConfig, ThumbnailConfig, TlsConfig, TranscodingConfig, TrashConfig, UiConfig, UserAccount, UserRole, WebhookConfig, }, jobs::JobQueue, storage::{StorageBackend, sqlite::SqliteBackend}, }; use tokio::sync::RwLock; use tower::ServiceExt; /// Fake socket address for tests (governor needs ConnectInfo) fn test_addr() -> ConnectInfo { ConnectInfo("127.0.0.1:9999".parse().unwrap()) } /// Build a GET request with ConnectInfo for rate limiter compatibility fn get(uri: &str) -> Request { let mut req = Request::builder().uri(uri).body(Body::empty()).unwrap(); req.extensions_mut().insert(test_addr()); req } /// Build a POST request with ConnectInfo fn post_json(uri: &str, body: &str) -> Request { let mut req = Request::builder() .method("POST") .uri(uri) .header("content-type", "application/json") .body(Body::from(body.to_string())) .unwrap(); req.extensions_mut().insert(test_addr()); req } /// Build a GET request with Bearer auth fn get_authed(uri: &str, token: &str) -> Request { let mut req = Request::builder() .uri(uri) .header("authorization", format!("Bearer {}", token)) .body(Body::empty()) .unwrap(); req.extensions_mut().insert(test_addr()); req } /// Build a POST JSON request with Bearer auth fn post_json_authed(uri: &str, body: &str, token: &str) -> Request { let mut req = Request::builder() .method("POST") .uri(uri) .header("content-type", "application/json") .header("authorization", format!("Bearer {}", token)) .body(Body::from(body.to_string())) .unwrap(); req.extensions_mut().insert(test_addr()); req } /// Build a DELETE request with Bearer auth fn delete_authed(uri: &str, token: &str) -> Request { let mut req = Request::builder() .method("DELETE") .uri(uri) .header("authorization", format!("Bearer {}", token)) .body(Body::empty()) .unwrap(); req.extensions_mut().insert(test_addr()); req } /// Build a PATCH JSON request with Bearer auth fn patch_json_authed(uri: &str, body: &str, token: &str) -> Request { let mut req = Request::builder() .method("PATCH") .uri(uri) .header("content-type", "application/json") .header("authorization", format!("Bearer {}", token)) .body(Body::from(body.to_string())) .unwrap(); req.extensions_mut().insert(test_addr()); req } fn default_config() -> Config { Config { storage: StorageConfig { backend: StorageBackendType::Sqlite, sqlite: Some(SqliteConfig { path: ":memory:".into(), }), postgres: None, }, directories: DirectoryConfig { roots: vec![] }, scanning: ScanningConfig { watch: false, poll_interval_secs: 300, ignore_patterns: vec![], import_concurrency: 8, }, server: ServerConfig { host: "127.0.0.1".to_string(), port: 3000, api_key: None, tls: TlsConfig::default(), authentication_disabled: true, }, ui: UiConfig::default(), accounts: AccountsConfig::default(), jobs: JobsConfig::default(), thumbnails: ThumbnailConfig::default(), webhooks: Vec::::new(), scheduled_tasks: vec![], plugins: PluginsConfig::default(), transcoding: TranscodingConfig::default(), enrichment: EnrichmentConfig::default(), cloud: CloudConfig::default(), analytics: AnalyticsConfig::default(), photos: PhotoConfig::default(), managed_storage: ManagedStorageConfig::default(), sync: SyncConfig::default(), sharing: SharingConfig::default(), trash: TrashConfig::default(), } } async fn setup_app() -> axum::Router { let backend = SqliteBackend::in_memory().expect("in-memory SQLite"); backend.run_migrations().await.expect("migrations"); let storage = Arc::new(backend) as pinakes_core::storage::DynStorageBackend; let config = default_config(); let job_queue = JobQueue::new(1, |_id, _kind, _cancel, _jobs| tokio::spawn(async {})); let config = Arc::new(RwLock::new(config)); let scheduler = pinakes_core::scheduler::TaskScheduler::new( job_queue.clone(), tokio_util::sync::CancellationToken::new(), config.clone(), None, ); let state = pinakes_server::state::AppState { storage, config, config_path: None, scan_progress: pinakes_core::scan::ScanProgress::new(), job_queue, cache: Arc::new(CacheLayer::new(60)), scheduler: Arc::new(scheduler), plugin_manager: None, transcode_service: None, managed_storage: None, chunked_upload_manager: None, }; pinakes_server::app::create_router(state) } /// Hash a password for test user accounts fn hash_password(password: &str) -> String { pinakes_core::users::auth::hash_password(password).unwrap() } /// Set up an app with accounts enabled and three pre-seeded users. /// Returns (Router, admin_token, editor_token, viewer_token). async fn setup_app_with_auth() -> (axum::Router, String, String, String) { let backend = SqliteBackend::in_memory().expect("in-memory SQLite"); backend.run_migrations().await.expect("migrations"); let storage = Arc::new(backend) as pinakes_core::storage::DynStorageBackend; // Create users in database so resolve_user_id works let users_to_create = vec![ ("admin", "adminpass", UserRole::Admin), ("editor", "editorpass", UserRole::Editor), ("viewer", "viewerpass", UserRole::Viewer), ]; for (username, password, role) in &users_to_create { let password_hash = hash_password(password); storage .create_user(username, &password_hash, *role, None) .await .expect("create user"); } let mut config = default_config(); config.server.authentication_disabled = false; // Enable authentication for these tests config.accounts.enabled = true; config.accounts.users = vec![ UserAccount { username: "admin".to_string(), password_hash: hash_password("adminpass"), role: UserRole::Admin, }, UserAccount { username: "editor".to_string(), password_hash: hash_password("editorpass"), role: UserRole::Editor, }, UserAccount { username: "viewer".to_string(), password_hash: hash_password("viewerpass"), role: UserRole::Viewer, }, ]; let job_queue = JobQueue::new(1, |_id, _kind, _cancel, _jobs| tokio::spawn(async {})); let config = Arc::new(RwLock::new(config)); let scheduler = pinakes_core::scheduler::TaskScheduler::new( job_queue.clone(), tokio_util::sync::CancellationToken::new(), config.clone(), None, ); let state = pinakes_server::state::AppState { storage, config, config_path: None, scan_progress: pinakes_core::scan::ScanProgress::new(), job_queue, cache: Arc::new(CacheLayer::new(60)), scheduler: Arc::new(scheduler), plugin_manager: None, transcode_service: None, managed_storage: None, chunked_upload_manager: None, }; let app = pinakes_server::app::create_router(state); // Login each user to get tokens let admin_token = login_user(app.clone(), "admin", "adminpass").await; let editor_token = login_user(app.clone(), "editor", "editorpass").await; let viewer_token = login_user(app.clone(), "viewer", "viewerpass").await; (app, admin_token, editor_token, viewer_token) } async fn login_user( app: axum::Router, username: &str, password: &str, ) -> String { let body = format!(r#"{{"username":"{}","password":"{}"}}"#, username, password); let response = app .oneshot(post_json("/api/v1/auth/login", &body)) .await .unwrap(); assert_eq!( response.status(), StatusCode::OK, "login failed for user {}", username ); let body = response.into_body().collect().await.unwrap().to_bytes(); let result: serde_json::Value = serde_json::from_slice(&body).unwrap(); result["token"].as_str().unwrap().to_string() } async fn response_body( response: axum::response::Response, ) -> serde_json::Value { let body = response.into_body().collect().await.unwrap().to_bytes(); serde_json::from_slice(&body).unwrap_or(serde_json::Value::Null) } #[tokio::test] async fn test_list_media_empty() { let app = setup_app().await; let response = app.oneshot(get("/api/v1/media")).await.unwrap(); assert_eq!(response.status(), StatusCode::OK); let body = response.into_body().collect().await.unwrap().to_bytes(); let items: Vec = serde_json::from_slice(&body).unwrap(); assert_eq!(items.len(), 0); } #[tokio::test] async fn test_create_and_list_tags() { let app = setup_app().await; // Create a tag let response = app .clone() .oneshot(post_json("/api/v1/tags", r#"{"name":"Music"}"#)) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); // List tags let response = app.oneshot(get("/api/v1/tags")).await.unwrap(); assert_eq!(response.status(), StatusCode::OK); let body = response.into_body().collect().await.unwrap().to_bytes(); let tags: Vec = serde_json::from_slice(&body).unwrap(); assert_eq!(tags.len(), 1); assert_eq!(tags[0]["name"], "Music"); } #[tokio::test] async fn test_search_empty() { let app = setup_app().await; let response = app.oneshot(get("/api/v1/search?q=test")).await.unwrap(); assert_eq!(response.status(), StatusCode::OK); let body = response.into_body().collect().await.unwrap().to_bytes(); let result: serde_json::Value = serde_json::from_slice(&body).unwrap(); assert_eq!(result["total_count"], 0); } #[tokio::test] async fn test_media_not_found() { let app = setup_app().await; let response = app .oneshot(get("/api/v1/media/00000000-0000-0000-0000-000000000000")) .await .unwrap(); assert_eq!(response.status(), StatusCode::NOT_FOUND); } #[tokio::test] async fn test_collections_crud() { let app = setup_app().await; // Create collection let response = app .clone() .oneshot(post_json( "/api/v1/collections", r#"{"name":"Favorites","kind":"manual"}"#, )) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); // List collections let response = app.oneshot(get("/api/v1/collections")).await.unwrap(); assert_eq!(response.status(), StatusCode::OK); let body = response.into_body().collect().await.unwrap().to_bytes(); let cols: Vec = serde_json::from_slice(&body).unwrap(); assert_eq!(cols.len(), 1); assert_eq!(cols[0]["name"], "Favorites"); } #[tokio::test] async fn test_statistics_endpoint() { let app = setup_app().await; let response = app.oneshot(get("/api/v1/statistics")).await.unwrap(); assert_eq!(response.status(), StatusCode::OK); let body = response.into_body().collect().await.unwrap().to_bytes(); let stats: serde_json::Value = serde_json::from_slice(&body).unwrap(); assert_eq!(stats["total_media"], 0); assert_eq!(stats["total_size_bytes"], 0); } #[tokio::test] async fn test_scheduled_tasks_endpoint() { let app = setup_app().await; let response = app.oneshot(get("/api/v1/tasks/scheduled")).await.unwrap(); assert_eq!(response.status(), StatusCode::OK); let body = response.into_body().collect().await.unwrap().to_bytes(); let tasks: Vec = serde_json::from_slice(&body).unwrap(); assert!(!tasks.is_empty(), "should have default scheduled tasks"); // Verify structure of first task assert!(tasks[0]["id"].is_string()); assert!(tasks[0]["name"].is_string()); assert!(tasks[0]["schedule"].is_string()); } #[tokio::test] async fn test_user_management_crud() { let app = setup_app().await; // Create a user let response = app .clone() .oneshot(post_json( "/api/v1/users", r#"{"username":"testuser","password":"password123","role":"viewer"}"#, )) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); let body = response.into_body().collect().await.unwrap().to_bytes(); let user: serde_json::Value = serde_json::from_slice(&body).unwrap(); assert_eq!(user["username"], "testuser"); assert_eq!(user["role"], "viewer"); let user_id = user["id"].as_str().unwrap(); // List users let response = app.clone().oneshot(get("/api/v1/users")).await.unwrap(); assert_eq!(response.status(), StatusCode::OK); let body = response.into_body().collect().await.unwrap().to_bytes(); let users: Vec = serde_json::from_slice(&body).unwrap(); assert_eq!(users.len(), 1); assert_eq!(users[0]["username"], "testuser"); // Get specific user let response = app .clone() .oneshot(get(&format!("/api/v1/users/{}", user_id))) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); let body = response.into_body().collect().await.unwrap().to_bytes(); let retrieved_user: serde_json::Value = serde_json::from_slice(&body).unwrap(); assert_eq!(retrieved_user["username"], "testuser"); // Delete user let mut req = Request::builder() .method("DELETE") .uri(&format!("/api/v1/users/{}", user_id)) .body(Body::empty()) .unwrap(); req.extensions_mut().insert(test_addr()); let response = app.clone().oneshot(req).await.unwrap(); assert_eq!(response.status(), StatusCode::OK); // Verify user is deleted let response = app .oneshot(get(&format!("/api/v1/users/{}", user_id))) .await .unwrap(); assert_eq!(response.status(), StatusCode::NOT_FOUND); } #[tokio::test] async fn test_health_endpoint() { let app = setup_app().await; // Health endpoint should be publicly accessible let response = app.oneshot(get("/api/v1/health")).await.unwrap(); assert_eq!(response.status(), StatusCode::OK); } #[tokio::test] async fn test_user_duplicate_username() { let app = setup_app().await; // Create first user let response = app .clone() .oneshot(post_json( "/api/v1/users", r#"{"username":"duplicate","password":"password1","role":"viewer"}"#, )) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); // Try to create user with same username let response = app .oneshot(post_json( "/api/v1/users", r#"{"username":"duplicate","password":"password2","role":"viewer"}"#, )) .await .unwrap(); // Should fail with conflict (409) for duplicate username assert_eq!(response.status(), StatusCode::CONFLICT); } #[tokio::test] async fn test_unauthenticated_request_rejected() { let (app, ..) = setup_app_with_auth().await; // Request without Bearer token let response = app.oneshot(get("/api/v1/media")).await.unwrap(); assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } #[tokio::test] async fn test_invalid_token_rejected() { let (app, ..) = setup_app_with_auth().await; let response = app .oneshot(get_authed("/api/v1/media", "totally-invalid-token")) .await .unwrap(); assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } #[tokio::test] async fn test_login_valid_credentials() { let (app, ..) = setup_app_with_auth().await; let response = app .oneshot(post_json( "/api/v1/auth/login", r#"{"username":"admin","password":"adminpass"}"#, )) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); let body = response_body(response).await; assert!(body["token"].is_string()); assert_eq!(body["username"], "admin"); assert_eq!(body["role"], "admin"); } #[tokio::test] async fn test_login_invalid_password() { let (app, ..) = setup_app_with_auth().await; let response = app .oneshot(post_json( "/api/v1/auth/login", r#"{"username":"admin","password":"wrongpassword"}"#, )) .await .unwrap(); assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } #[tokio::test] async fn test_login_unknown_user() { let (app, ..) = setup_app_with_auth().await; let response = app .oneshot(post_json( "/api/v1/auth/login", r#"{"username":"nonexistent","password":"whatever"}"#, )) .await .unwrap(); assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } #[tokio::test] async fn test_auth_me_endpoint() { let (app, admin_token, ..) = setup_app_with_auth().await; let response = app .oneshot(get_authed("/api/v1/auth/me", &admin_token)) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); let body = response_body(response).await; assert_eq!(body["username"], "admin"); assert_eq!(body["role"], "admin"); } #[tokio::test] async fn test_logout() { let (app, admin_token, ..) = setup_app_with_auth().await; // Logout let response = app .clone() .oneshot(post_json_authed("/api/v1/auth/logout", "", &admin_token)) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); // Subsequent requests with same token should fail let response = app .oneshot(get_authed("/api/v1/media", &admin_token)) .await .unwrap(); assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } #[tokio::test] async fn test_viewer_cannot_access_editor_routes() { let (app, _, _, viewer_token) = setup_app_with_auth().await; // POST /tags is an editor-only route let response = app .oneshot(post_json_authed( "/api/v1/tags", r#"{"name":"test"}"#, &viewer_token, )) .await .unwrap(); assert_eq!(response.status(), StatusCode::FORBIDDEN); } #[tokio::test] async fn test_viewer_cannot_access_admin_routes() { let (app, _, _, viewer_token) = setup_app_with_auth().await; // GET /users is an admin-only route let response = app .oneshot(get_authed("/api/v1/users", &viewer_token)) .await .unwrap(); assert_eq!(response.status(), StatusCode::FORBIDDEN); } #[tokio::test] async fn test_editor_cannot_access_admin_routes() { let (app, _, editor_token, _) = setup_app_with_auth().await; let response = app .oneshot(get_authed("/api/v1/users", &editor_token)) .await .unwrap(); assert_eq!(response.status(), StatusCode::FORBIDDEN); } #[tokio::test] async fn test_editor_can_write() { let (app, _, editor_token, _) = setup_app_with_auth().await; let response = app .oneshot(post_json_authed( "/api/v1/tags", r#"{"name":"EditorTag"}"#, &editor_token, )) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); } #[tokio::test] async fn test_admin_can_access_all() { let (app, admin_token, ..) = setup_app_with_auth().await; // Viewer route let response = app .clone() .oneshot(get_authed("/api/v1/media", &admin_token)) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); // Editor route let response = app .clone() .oneshot(post_json_authed( "/api/v1/tags", r#"{"name":"AdminTag"}"#, &admin_token, )) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); // Admin route let response = app .oneshot(get_authed("/api/v1/users", &admin_token)) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); } #[tokio::test] async fn test_rating_invalid_stars_zero() { let (app, _, editor_token, _) = setup_app_with_auth().await; let response = app .oneshot(post_json_authed( "/api/v1/media/00000000-0000-0000-0000-000000000000/ratings", r#"{"stars":0}"#, &editor_token, )) .await .unwrap(); assert_eq!(response.status(), StatusCode::BAD_REQUEST); } #[tokio::test] async fn test_rating_invalid_stars_six() { let (app, _, editor_token, _) = setup_app_with_auth().await; let response = app .oneshot(post_json_authed( "/api/v1/media/00000000-0000-0000-0000-000000000000/ratings", r#"{"stars":6}"#, &editor_token, )) .await .unwrap(); assert_eq!(response.status(), StatusCode::BAD_REQUEST); } #[tokio::test] async fn test_comment_empty_text() { let (app, _, editor_token, _) = setup_app_with_auth().await; let response = app .oneshot(post_json_authed( "/api/v1/media/00000000-0000-0000-0000-000000000000/comments", r#"{"text":""}"#, &editor_token, )) .await .unwrap(); assert_eq!(response.status(), StatusCode::BAD_REQUEST); } #[tokio::test] async fn test_favorites_list_empty() { let (app, _, _, viewer_token) = setup_app_with_auth().await; let response = app .oneshot(get_authed("/api/v1/favorites", &viewer_token)) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); let body = response_body(response).await; assert!(body.as_array().unwrap().is_empty()); } #[tokio::test] async fn test_playlist_crud() { let (app, _, editor_token, _) = setup_app_with_auth().await; // Create let response = app .clone() .oneshot(post_json_authed( "/api/v1/playlists", r#"{"name":"My Playlist"}"#, &editor_token, )) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); let body = response_body(response).await; let playlist_id = body["id"].as_str().unwrap().to_string(); assert_eq!(body["name"], "My Playlist"); // List let response = app .clone() .oneshot(get_authed("/api/v1/playlists", &editor_token)) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); let body = response_body(response).await; assert_eq!(body.as_array().unwrap().len(), 1); // Get let response = app .clone() .oneshot(get_authed( &format!("/api/v1/playlists/{}", playlist_id), &editor_token, )) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); // Update let response = app .clone() .oneshot(patch_json_authed( &format!("/api/v1/playlists/{}", playlist_id), r#"{"name":"Updated Playlist","description":"A test description"}"#, &editor_token, )) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); let body = response_body(response).await; assert_eq!(body["name"], "Updated Playlist"); // Delete let response = app .clone() .oneshot(delete_authed( &format!("/api/v1/playlists/{}", playlist_id), &editor_token, )) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); } #[tokio::test] async fn test_playlist_empty_name() { let (app, _, editor_token, _) = setup_app_with_auth().await; let response = app .oneshot(post_json_authed( "/api/v1/playlists", r#"{"name":""}"#, &editor_token, )) .await .unwrap(); assert_eq!(response.status(), StatusCode::BAD_REQUEST); } #[tokio::test] async fn test_most_viewed_empty() { let (app, _, _, viewer_token) = setup_app_with_auth().await; let response = app .oneshot(get_authed("/api/v1/analytics/most-viewed", &viewer_token)) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); let body = response_body(response).await; assert!(body.as_array().unwrap().is_empty()); } #[tokio::test] async fn test_record_event_and_query() { let (app, _, editor_token, _) = setup_app_with_auth().await; // Record an event let response = app .clone() .oneshot(post_json_authed( "/api/v1/analytics/events", r#"{"event_type":"view","duration_secs":5.0}"#, &editor_token, )) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); let body = response_body(response).await; assert_eq!(body["recorded"], true); } #[tokio::test] async fn test_transcode_session_not_found() { let (app, _, _, viewer_token) = setup_app_with_auth().await; let response = app .oneshot(get_authed( "/api/v1/transcode/00000000-0000-0000-0000-000000000000", &viewer_token, )) .await .unwrap(); // Should be 404 or 500 (not found in DB) assert!( response.status() == StatusCode::NOT_FOUND || response.status() == StatusCode::INTERNAL_SERVER_ERROR ); } #[tokio::test] async fn test_transcode_list_empty() { let (app, _, _, viewer_token) = setup_app_with_auth().await; let response = app .oneshot(get_authed("/api/v1/transcode", &viewer_token)) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); let body = response_body(response).await; assert!(body.as_array().unwrap().is_empty()); } #[tokio::test] async fn test_hls_segment_no_session() { let (app, _, _, viewer_token) = setup_app_with_auth().await; let response = app .oneshot(get_authed( "/api/v1/media/00000000-0000-0000-0000-000000000000/stream/hls/720p/\ segment0.ts", &viewer_token, )) .await .unwrap(); // Should fail because media doesn't exist or no transcode session assert!( response.status() == StatusCode::BAD_REQUEST || response.status() == StatusCode::NOT_FOUND || response.status() == StatusCode::INTERNAL_SERVER_ERROR ); } #[tokio::test] async fn test_subtitles_list() { let (app, _, _, viewer_token) = setup_app_with_auth().await; // Should return empty for nonexistent media (or not found) let response = app .oneshot(get_authed( "/api/v1/media/00000000-0000-0000-0000-000000000000/subtitles", &viewer_token, )) .await .unwrap(); assert!( response.status() == StatusCode::OK || response.status() == StatusCode::NOT_FOUND || response.status() == StatusCode::INTERNAL_SERVER_ERROR ); } #[tokio::test] async fn test_health_public() { let (app, ..) = setup_app_with_auth().await; // Health endpoint should be accessible without auth even when accounts // enabled let response = app.oneshot(get("/api/v1/health")).await.unwrap(); assert_eq!(response.status(), StatusCode::OK); } #[tokio::test] async fn test_invalid_uuid_in_path() { let (app, _, _, viewer_token) = setup_app_with_auth().await; let response = app .oneshot(get_authed("/api/v1/media/not-a-uuid", &viewer_token)) .await .unwrap(); assert_eq!(response.status(), StatusCode::BAD_REQUEST); } #[tokio::test] async fn test_oversized_comment() { let (app, _, editor_token, _) = setup_app_with_auth().await; let long_text: String = "x".repeat(10_001); let body = format!(r#"{{"text":"{}"}}"#, long_text); let response = app .oneshot(post_json_authed( "/api/v1/media/00000000-0000-0000-0000-000000000000/comments", &body, &editor_token, )) .await .unwrap(); assert_eq!(response.status(), StatusCode::BAD_REQUEST); } #[tokio::test] async fn test_share_link_expired() { // Uses no-auth setup since share links are complex to test with auth // (need real media items). Verify the expire check logic works. let app = setup_app().await; // First import a dummy file to get a media_id, but we can't without a real // file. So let's test the public share access endpoint with a nonexistent // token. let response = app .oneshot(get("/api/v1/s/nonexistent_token")) .await .unwrap(); // Should fail with not found or internal error (no such share link) assert!( response.status() == StatusCode::NOT_FOUND || response.status() == StatusCode::INTERNAL_SERVER_ERROR ); }