// 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, types::LookupGuildResponse},
build_router,
config::{AdminConfig, ProxyConfig, RuntimeEnv},
session,
};
use serde_json::{Value, json};
use std::sync::{
Arc, Mutex,
atomic::{AtomicUsize, Ordering},
};
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 admin_api_key_create_form_renders_the_one_time_secret() {
let app = setup().await;
let (headers, page) = get_with_headers(&app, "/admin-api-keys", &[]).await;
let csrf_token = csrf_cookie(&headers)
.unwrap_or_else(|| panic!("Admin API keys page did not set csrf_token cookie\n{page}"));
assert!(page.contains(r#"data-admin-result-form="true""#), "{page}");
let (status, _, response_body) = post_form_with_headers(
&app,
"/admin-api-keys?action=create",
&[
("HX-Request", "true"),
("HX-Boosted", "true"),
("HX-Target", "body"),
(
"Cookie",
&format!("{}; csrf_token={}", app.session_cookie, csrf_token),
),
],
&format!("_csrf={csrf_token}&name=Acceptance+Key&acls=*"),
)
.await;
assert_eq!(status, StatusCode::OK, "{response_body}");
assert_full_layout(&response_body);
assert!(response_body.contains(r#"hx-history="false""#));
assert!(response_body.contains(ADMIN_API_KEY_SECRET));
}
#[tokio::test]
async fn search_routes_return_layout_or_fragments_by_hx_target() {
let app = setup().await;
let cases = [
SearchCase {
path: "/users?ids=1500000000000000001",
result_target: "users-results",
mismatch_target: "guilds-results",
result_text: "SearchedUser",
},
SearchCase {
path: "/guilds?ids=1600000000000000001",
result_target: "guilds-results",
mismatch_target: "users-results",
result_text: "Searched Guild",
},
SearchCase {
path: "/applications?owner_id=1500000000000000001",
result_target: "applications-results",
mismatch_target: "users-results",
result_text: "Mock Application",
},
];
for case in cases {
let full = get(&app, case.path, &[]).await;
assert_full_layout(&full);
assert!(
full.contains(case.result_text),
"full response for {}",
case.path
);
let boosted = get(
&app,
case.path,
&[
("HX-Request", "true"),
("HX-Boosted", "true"),
("HX-Target", "body"),
],
)
.await;
assert_full_layout(&boosted);
assert!(
boosted.contains(case.result_text),
"boosted response for {}",
case.path
);
let fragment = get(
&app,
case.path,
&[("HX-Request", "true"), ("HX-Target", case.result_target)],
)
.await;
assert_fragment(&fragment);
assert!(
fragment.contains(case.result_text),
"fragment response for {}",
case.path
);
let mismatch = get(
&app,
case.path,
&[("HX-Request", "true"), ("HX-Target", case.mismatch_target)],
)
.await;
assert_full_layout(&mismatch);
assert!(
mismatch.contains(case.result_text),
"mismatched target response for {}",
case.path
);
}
}
#[tokio::test]
async fn detail_tab_routes_return_layout_or_fragments_by_route_shape() {
let app = setup().await;
let cases = [
TabCase {
full_path: "/users/1500000000000000001?tab=applications",
fragment_path: "/users/1500000000000000001/tabs/applications",
detail_text: "SearchedUser",
tab_text: "Mock Application",
},
TabCase {
full_path: "/guilds/1600000000000000001?tab=applications",
fragment_path: "/guilds/1600000000000000001/tabs/applications",
detail_text: "Searched Guild",
tab_text: "Mock Application",
},
];
for case in cases {
let full = get(&app, case.full_path, &[]).await;
assert_full_layout(&full);
assert!(
full.contains(case.detail_text),
"full tab URL for {}",
case.full_path
);
assert!(
full.contains(case.tab_text),
"full tab URL for {}",
case.full_path
);
let fragment = get(&app, case.fragment_path, &[]).await;
assert_fragment(&fragment);
assert!(
fragment.contains(case.tab_text),
"tab endpoint fragment for {}",
case.fragment_path
);
}
}
#[tokio::test]
async fn target_audit_log_tabs_request_write_entries_only() {
let app = setup().await;
for path in [
"/users/1500000000000000001/tabs/audit_logs",
"/guilds/1600000000000000001/tabs/audit_logs",
] {
let fragment = get(&app, path, &[]).await;
assert!(fragment.contains("Temp ban"), "{path}\n{fragment}");
assert!(!fragment.contains("Get user"), "{path}\n{fragment}");
}
}
#[tokio::test]
async fn audit_log_page_forwards_the_access_filter() {
let app = setup().await;
let all = get(&app, "/audit-logs", &[]).await;
assert!(all.contains("Temp ban"), "{all}");
assert!(all.contains("Get user"), "{all}");
assert!(
all.contains(r#""#),
"{all}"
);
let reads = get(&app, "/audit-logs?access=read", &[]).await;
assert!(reads.contains("Get user"), "{reads}");
assert!(!reads.contains("Temp ban"), "{reads}");
assert!(
reads.contains(r#""#),
"{reads}"
);
}
#[tokio::test]
async fn report_routes_keep_layout_and_fragment_contract() {
let app = setup().await;
let reports = get(&app, "/reports?q=mock", &[]).await;
assert_full_layout(&reports);
assert!(reports.contains("1800000000000000001"), "{reports}");
assert!(
!reports.contains(r#"hx-post="/reports/bulk-resolve""#),
"{reports}"
);
assert!(!reports.contains(r#"name="report_ids[]""#), "{reports}");
let boosted = get(
&app,
"/reports?q=mock",
&[
("HX-Request", "true"),
("HX-Boosted", "true"),
("HX-Target", "body"),
],
)
.await;
assert_full_layout(&boosted);
assert!(boosted.contains("1800000000000000001"), "{boosted}");
let detail = get(&app, "/reports/1800000000000000001", &[]).await;
assert_full_layout(&detail);
assert!(detail.contains("Mock report details"), "{detail}");
let fragment = get(&app, "/reports/1800000000000000001/fragment", &[]).await;
assert_fragment(&fragment);
assert!(fragment.contains("Mock report details"), "{fragment}");
let message_fragment = get(&app, "/reports/1800000000000000002/fragment", &[]).await;
assert_fragment(&message_fragment);
assert!(
message_fragment.contains("Message Context"),
"{message_fragment}"
);
assert!(
message_fragment.contains("Reported message in drawer"),
"{message_fragment}"
);
}
#[tokio::test]
async fn report_list_reason_select_is_rendered_and_wired() {
let app = setup().await;
let unfiltered = get(&app, "/reports", &[]).await;
assert!(
unfiltered.contains(r#"