// SPDX-License-Identifier: AGPL-3.0-or-later #![recursion_limit = "256"] use axum::{ Json, Router, body::{Body, to_bytes}, http::{HeaderMap, Method, Request, StatusCode, Uri, header}, response::{IntoResponse, Response}, routing, }; use fluxer_admin::{ api::generated::types as generated_types, build_router, config::{AdminConfig, ProxyConfig}, session, }; use serde_json::{Value, json}; use std::sync::{Arc, Mutex}; use tokio::net::TcpListener; use tower::ServiceExt; const SECRET_KEY: &str = "htmx-acceptance-test-secret"; const ADMIN_API_KEY_SECRET: &str = "fa_1900000000000000001_OneTimeSecretForAcceptance"; struct TestApp { router: Router, session_cookie: String, } #[tokio::test] async fn report_legal_hold_form_places_and_clears_the_hold() { let received = Arc::new(Mutex::new(Vec::::new())); let sink = received.clone(); let api = Router::new() .route( "/admin/reports/1800000000000000001/legal-hold", routing::post(move |Json(body): Json| { let sink = sink.clone(); async move { sink.lock().unwrap().push(body.clone()); if body["legal_hold_reason"] == "rejected" { return ( StatusCode::BAD_REQUEST, Json(json!({"code": "INVALID_FORM_BODY", "message": "Invalid"})), ) .into_response(); } json_response(json!({ "report_id": "1800000000000000001", "legal_hold_until": body["legal_hold_until"], "legal_hold_reason": body["legal_hold_reason"] })) } }), ) .fallback(mock_api); let app = setup_with_api(api).await; let (headers, page) = get_with_headers(&app, "/reports/1800000000000000001", &[]).await; let csrf_token = csrf_cookie(&headers) .unwrap_or_else(|| panic!("report page did not set csrf_token cookie\n{page}")); assert_form_has_csrf( &page, "/reports/1800000000000000001/legal-hold", &csrf_token, ); let cookie = format!("{}; csrf_token={}", app.session_cookie, csrf_token); let post = |body: String| { let app = &app; let cookie = cookie.clone(); async move { let (status, headers, text) = post_form_with_headers( app, "/reports/1800000000000000001/legal-hold", &[ ("HX-Request", "true"), ("HX-Target", "flash-container"), ("Cookie", &cookie), ], &body, ) .await; assert_eq!(status, StatusCode::NO_CONTENT, "{text}"); headers .get("X-Fluxer-Admin-Toast") .and_then(|value| value.to_str().ok()) .unwrap_or_else(|| panic!("missing toast header\n{text}")) .to_owned() } }; let toast = post(format!( "_csrf={csrf_token}&legal_hold_until=2027-01-31&legal_hold_reason=Court+order+42" )) .await; assert!(toast.contains("success"), "{toast}"); assert!( toast.contains("Legal hold placed until Jan 31, 2027, 11:59 PM UTC"), "{toast}" ); let toast = post(format!( "_csrf={csrf_token}&clear=1&legal_hold_reason=ignored" )) .await; assert!(toast.contains("Legal hold cleared"), "{toast}"); let toast = post(format!( "_csrf={csrf_token}&legal_hold_until=2027-01-31&legal_hold_reason=rejected" )) .await; assert!(toast.contains("error"), "{toast}"); assert!(toast.contains("The hold must end in the future"), "{toast}"); assert!(!toast.contains("needs a reason"), "{toast}"); let toast = post(format!( "_csrf={csrf_token}&legal_hold_until=2020-01-01&legal_hold_reason=Past+date" )) .await; assert!(toast.contains("error"), "{toast}"); assert!(toast.contains("The hold must end in the future"), "{toast}"); { let received = received.lock().unwrap(); assert_eq!( *received, vec![ json!({"legal_hold_until": "2027-01-31T23:59:59.999Z", "legal_hold_reason": "Court order 42"}), json!({"legal_hold_until": null, "legal_hold_reason": null}), json!({"legal_hold_until": "2027-01-31T23:59:59.999Z", "legal_hold_reason": "rejected"}), ] ); } let toast = post(format!( "_csrf={csrf_token}&legal_hold_until=2027-01-31&legal_hold_reason=+" )) .await; assert!(toast.contains("Give a reason for the hold"), "{toast}"); let toast = post(format!( "_csrf={csrf_token}&legal_hold_until=soon&legal_hold_reason=x" )) .await; assert!(toast.contains("Choose the date the hold ends"), "{toast}"); assert_eq!(received.lock().unwrap().len(), 3); } #[tokio::test] async fn instance_config_legal_form_round_trips_the_guidelines_url() { let patches = Arc::new(Mutex::new(Vec::::new())); let sink = patches.clone(); let configured = || { let mut config = instance_config(); config["self_hosted"] = json!(true); config["app_public"] = json!({ "branding": {"product_name": "Fluxer", "premium_product_name": "Premium"}, "setup": {"configured": true}, "legal": { "terms_url": "https://example.com/terms", "privacy_url": null, "guidelines_url": "https://example.com/community-guidelines" }, "registration": {"collect_date_of_birth": true} }); config }; let api = Router::new() .route( "/admin/instance/config", routing::get(move || async move { json_response(configured()) }).patch( move |Json(body): Json| { let sink = sink.clone(); async move { sink.lock().unwrap().push(body); json_response(configured()) } }, ), ) .fallback(mock_api); let app = setup_with_api(api).await; let (headers, page) = get_with_headers(&app, "/instance-config", &[]).await; assert!( page.contains(r#"id="app_guidelines_url" name="app_guidelines_url" value="https://example.com/community-guidelines""#), "{page}" ); assert!(page.contains("Community Guidelines URL"), "{page}"); let csrf_token = csrf_cookie(&headers) .unwrap_or_else(|| panic!("instance config page did not set csrf_token cookie\n{page}")); assert_form_has_csrf( &page, "/instance-config?action=update_app_legal", &csrf_token, ); let cookie = format!("{}; csrf_token={}", app.session_cookie, csrf_token); for form in [ format!( "_csrf={csrf_token}&app_terms_url=https%3A%2F%2Fexample.com%2Fterms&app_privacy_url=&app_guidelines_url=https%3A%2F%2Frules.example.org%2Fguidelines" ), format!("_csrf={csrf_token}&app_terms_url=&app_privacy_url=&app_guidelines_url="), ] { let (status, _, body) = post_form_with_headers( &app, "/instance-config?action=update_app_legal", &[("Cookie", &cookie)], &form, ) .await; assert!( status.is_redirection() || status.is_success(), "{status} {body}" ); } let patches = patches.lock().unwrap(); assert_eq!( *patches, vec![ json!({"app_public": {"legal": { "terms_url": "https://example.com/terms", "privacy_url": null, "guidelines_url": "https://rules.example.org/guidelines" }}}), json!({"app_public": {"legal": { "terms_url": null, "privacy_url": null, "guidelines_url": null }}}), ] ); } #[tokio::test] async fn report_resolve_form_sends_the_chosen_resolution_and_refuses_none() { let received = Arc::new(Mutex::new(Vec::::new())); let sink = received.clone(); let api = Router::new() .route( "/admin/reports/1800000000000000001", routing::patch(move |Json(body): Json| { let sink = sink.clone(); async move { sink.lock().unwrap().push(body); json_response(json!({ "report_id": "1800000000000000001", "status": 1, "resolved_at": "2026-05-26T12:03:00.000Z", "public_comment": null })) } }) .get(|| async { json_response(searched_report()) }), ) .fallback(mock_api); let app = setup_with_api(api).await; let (headers, page) = get_with_headers(&app, "/reports/1800000000000000001", &[]).await; let csrf_token = csrf_cookie(&headers) .unwrap_or_else(|| panic!("report page did not set csrf_token cookie\n{page}")); assert_form_has_csrf(&page, "/reports/1800000000000000001/resolve", &csrf_token); assert!( page.contains(r#"