#![cfg(feature = "server")]
use esdiag::{
exporter::Exporter,
server::{RuntimeMode, Server},
};
use reqwest::Client;
use std::time::Duration;
use tokio::time::sleep;
async fn start_server(mode: RuntimeMode) -> (Server, Client, String) {
let default_web_features = match mode {
RuntimeMode::User => Some("advanced"),
RuntimeMode::Service => Some(""),
};
start_server_with_features(mode, default_web_features).await
}
async fn start_server_with_features(mode: RuntimeMode, web_features: Option<&str>) -> (Server, Client, String) {
let (server, bound_addr) = Server::start_with_web_features(
[127, 0, 0, 1],
0,
Exporter::default(),
String::new(),
mode,
web_features,
)
.await
.expect("start local server");
let client = Client::new();
let base = format!("http://127.0.0.1:{}", bound_addr.port());
for _ in 0..40 {
if client.get(format!("{base}/")).send().await.is_ok() {
return (server, client, base);
}
sleep(Duration::from_millis(25)).await;
}
panic!("server did not become reachable in time");
}
#[tokio::test]
async fn service_mode_requires_iap_header_for_web_access() {
let (mut server, client, base) = start_server(RuntimeMode::Service).await;
let unauthorized = client
.get(format!("{base}/"))
.send()
.await
.expect("service mode request");
assert_eq!(unauthorized.status(), reqwest::StatusCode::UNAUTHORIZED);
let authorized = client
.get(format!("{base}/"))
.header("X-Goog-Authenticated-User-Email", "accounts.google.com:ops@example.com")
.send()
.await
.expect("service mode authorized request");
assert_eq!(authorized.status(), reqwest::StatusCode::OK);
let body = authorized.text().await.expect("authorized body");
assert!(body.contains("ops@example.com"));
server.shutdown().await;
}
#[tokio::test]
async fn service_mode_does_not_mount_advanced_or_jobs_routes() {
let (mut server, client, base) = start_server(RuntimeMode::Service).await;
let advanced_response = client
.get(format!("{base}/advanced"))
.header("X-Goog-Authenticated-User-Email", "accounts.google.com:ops@example.com")
.send()
.await
.expect("service mode advanced request");
assert_eq!(advanced_response.status(), reqwest::StatusCode::NOT_FOUND);
let jobs_response = client
.get(format!("{base}/jobs"))
.header("X-Goog-Authenticated-User-Email", "accounts.google.com:ops@example.com")
.send()
.await
.expect("service mode jobs request");
assert_eq!(jobs_response.status(), reqwest::StatusCode::NOT_FOUND);
server.shutdown().await;
}
#[tokio::test]
async fn user_mode_default_nav_exposes_advanced_not_job_builder() {
let (mut server, client, base) = start_server(RuntimeMode::User).await;
let response = client.get(format!("{base}/")).send().await.expect("user mode request");
assert_eq!(response.status(), reqwest::StatusCode::OK);
let body = response.text().await.expect("user mode body");
assert!(body.contains("<header-nav>"));
assert!(body.contains(">Process</a>"));
assert!(body.contains("href=\"/advanced\""));
assert!(body.contains(">Advanced</a>"));
assert!(!body.contains("href=\"/jobs\""));
assert!(!body.contains(">Job Builder</a>"));
let advanced = client
.get(format!("{base}/advanced"))
.send()
.await
.expect("advanced route");
assert_eq!(advanced.status(), reqwest::StatusCode::OK);
let workflow = client
.get(format!("{base}/workflow"))
.send()
.await
.expect("workflow route");
assert_eq!(workflow.status(), reqwest::StatusCode::NOT_FOUND);
let jobs = client.get(format!("{base}/jobs")).send().await.expect("jobs route");
assert_eq!(jobs.status(), reqwest::StatusCode::NOT_FOUND);
server.shutdown().await;
}
#[cfg(feature = "keystore")]
#[tokio::test]
async fn explicit_job_builder_feature_mounts_jobs_and_omits_advanced() {
let (mut server, client, base) = start_server_with_features(RuntimeMode::User, Some("job-builder")).await;
let response = client.get(format!("{base}/")).send().await.expect("user mode request");
assert_eq!(response.status(), reqwest::StatusCode::OK);
let body = response.text().await.expect("user mode body");
assert!(body.contains("<header-nav>"));
assert!(body.contains(">Process</a>"));
assert!(!body.contains("href=\"/advanced\""));
assert!(body.contains("href=\"/jobs\""));
assert!(body.contains(">Job Builder</a>"));
let advanced = client
.get(format!("{base}/advanced"))
.send()
.await
.expect("advanced route");
assert_eq!(advanced.status(), reqwest::StatusCode::NOT_FOUND);
let jobs = client.get(format!("{base}/jobs")).send().await.expect("jobs route");
assert_eq!(jobs.status(), reqwest::StatusCode::OK);
server.shutdown().await;
}
#[cfg(feature = "keystore")]
#[tokio::test]
async fn explicit_advanced_and_job_builder_features_mount_both_pages() {
let (mut server, client, base) = start_server_with_features(RuntimeMode::User, Some("advanced,job-builder")).await;
let response = client.get(format!("{base}/")).send().await.expect("user mode request");
assert_eq!(response.status(), reqwest::StatusCode::OK);
let body = response.text().await.expect("user mode body");
assert!(body.contains("<header-nav>"));
assert!(body.contains(">Process</a>"));
assert!(body.contains("href=\"/advanced\""));
assert!(body.contains("href=\"/jobs\""));
let advanced = client
.get(format!("{base}/advanced"))
.send()
.await
.expect("advanced route");
assert_eq!(advanced.status(), reqwest::StatusCode::OK);
let jobs = client.get(format!("{base}/jobs")).send().await.expect("jobs route");
assert_eq!(jobs.status(), reqwest::StatusCode::OK);
server.shutdown().await;
}
#[tokio::test]
async fn empty_web_features_omit_optional_nav_and_routes() {
let (mut server, client, base) = start_server_with_features(RuntimeMode::User, Some("")).await;
let response = client.get(format!("{base}/")).send().await.expect("user mode request");
assert_eq!(response.status(), reqwest::StatusCode::OK);
let body = response.text().await.expect("user mode body");
assert!(!body.contains("<header-nav>"));
assert!(!body.contains(">Process</a>"));
assert!(!body.contains("href=\"/advanced\""));
assert!(!body.contains("href=\"/jobs\""));
let advanced = client
.get(format!("{base}/advanced"))
.send()
.await
.expect("advanced route");
assert_eq!(advanced.status(), reqwest::StatusCode::NOT_FOUND);
let jobs = client.get(format!("{base}/jobs")).send().await.expect("jobs route");
assert_eq!(jobs.status(), reqwest::StatusCode::NOT_FOUND);
server.shutdown().await;
}
#[tokio::test]
async fn service_mode_requires_iap_header_for_settings_update() {
let (mut server, client, base) = start_server(RuntimeMode::Service).await;
let unauthorized = client
.post(format!("{base}/api/settings/update"))
.body(r#"{"settings":{"kibana_url":"https://kibana.example"}}"#)
.header("content-type", "application/json")
.send()
.await
.expect("service mode settings update without header");
assert_eq!(unauthorized.status(), reqwest::StatusCode::UNAUTHORIZED);
let authorized = client
.post(format!("{base}/api/settings/update"))
.body(r#"{"settings":{"kibana_url":"https://kibana.example"}}"#)
.header("content-type", "application/json")
.header("X-Goog-Authenticated-User-Email", "accounts.google.com:ops@example.com")
.send()
.await
.expect("service mode settings update with header");
assert_ne!(authorized.status(), reqwest::StatusCode::UNAUTHORIZED);
server.shutdown().await;
}
#[tokio::test]
async fn service_mode_does_not_mount_keystore_routes() {
let (mut server, client, base) = start_server(RuntimeMode::Service).await;
let response = client
.post(format!("{base}/keystore/unlock"))
.form(&[("password", "pw")])
.send()
.await
.expect("service mode keystore request without header");
assert_eq!(response.status(), reqwest::StatusCode::UNAUTHORIZED);
let response = client
.post(format!("{base}/keystore/unlock"))
.header("X-Goog-Authenticated-User-Email", "accounts.google.com:ops@example.com")
.form(&[("password", "pw")])
.send()
.await
.expect("service mode keystore request");
assert_eq!(response.status(), reqwest::StatusCode::NOT_FOUND);
let response = client
.get(format!("{base}/keystore/modal"))
.header("X-Goog-Authenticated-User-Email", "accounts.google.com:ops@example.com")
.send()
.await
.expect("service mode keystore modal request");
assert_eq!(response.status(), reqwest::StatusCode::NOT_FOUND);
server.shutdown().await;
}
#[cfg(feature = "keystore")]
#[tokio::test]
async fn user_mode_mounts_keystore_routes_when_feature_enabled() {
let (mut server, client, base) = start_server(RuntimeMode::User).await;
let response = client
.get(format!("{base}/keystore/modal"))
.send()
.await
.expect("user mode keystore modal request");
assert_eq!(response.status(), reqwest::StatusCode::NO_CONTENT);
server.shutdown().await;
}
#[cfg(not(feature = "keystore"))]
#[tokio::test]
async fn user_mode_does_not_mount_keystore_routes_when_feature_disabled() {
let (mut server, client, base) = start_server(RuntimeMode::User).await;
let response = client
.post(format!("{base}/keystore/unlock"))
.form(&[("password", "pw")])
.send()
.await
.expect("user mode keystore unlock request");
assert_eq!(response.status(), reqwest::StatusCode::NOT_FOUND);
let response = client
.get(format!("{base}/keystore/modal"))
.send()
.await
.expect("user mode keystore modal request");
assert_eq!(response.status(), reqwest::StatusCode::NOT_FOUND);
server.shutdown().await;
}
#[tokio::test]
async fn user_mode_allows_anonymous_web_access() {
let (mut server, client, base) = start_server(RuntimeMode::User).await;
let response = client.get(format!("{base}/")).send().await.expect("user mode request");
assert_eq!(response.status(), reqwest::StatusCode::OK);
let body = response.text().await.expect("user mode body");
assert!(body.contains("Anonymous"));
assert!(body.contains("Process Diagnostics"));
assert!(!body.contains("id=\"workflow-go-button\""));
server.shutdown().await;
}
#[tokio::test]
async fn user_mode_workflow_shows_known_host_collect_and_local_save_defaults() {
let (mut server, client, base) = start_server(RuntimeMode::User).await;
let response = client
.get(format!("{base}/advanced"))
.send()
.await
.expect("user mode request");
assert_eq!(response.status(), reqwest::StatusCode::OK);
let body = response.text().await.expect("user mode body");
assert!(body.contains("id=\"collect-known-host\""));
assert!(body.contains("id=\"known-host-button\""));
assert!(body.contains("data-signals:job.collect.mode=\"'upload'\""));
assert!(body.contains("data-signals:job.collect.source=\"'upload-file'\""));
assert!(body.contains("id=\"collect-save-toggle\""));
assert!(body.contains("Download Archive"));
assert!(body.contains("id=\"upload-form\""));
assert!(
body.contains("data-attr:disabled=\"$job.collect.mode === 'upload' && $job.collect.source === 'upload-file'\"")
);
assert!(body.contains("placeholder=\"/"));
server.shutdown().await;
}
#[tokio::test]
async fn advanced_page_shows_process_controls_and_forward_remote_input() {
let (mut server, client, base) = start_server(RuntimeMode::User).await;
let response = client
.get(format!("{base}/advanced"))
.send()
.await
.expect("advanced page");
assert_eq!(response.status(), reqwest::StatusCode::OK);
let body = response.text().await.expect("advanced page body");
assert!(body.contains("id=\"process-product\""));
assert!(body.contains("id=\"process-type\""));
assert!(body.contains("Diagnostic Processors"));
assert!(body.contains("<option value=\"custom\">custom</option>"));
assert!(body.contains("id=\"process-custom-processors\""));
assert!(body.contains("id=\"process-enabled-toggle\""));
assert!(body.contains("id=\"process-option-list\""));
assert!(body.contains("id=\"process-selected-options\""));
assert!(body.contains("id=\"send-forward-upload\""));
assert!(body.contains("Forward preserves the raw diagnostic archive"));
server.shutdown().await;
}
#[tokio::test]
async fn advanced_page_embeds_send_target_disable_and_auto_save_rules() {
let (mut server, client, base) = start_server(RuntimeMode::User).await;
let response = client
.get(format!("{base}/advanced"))
.send()
.await
.expect("advanced page");
assert_eq!(response.status(), reqwest::StatusCode::OK);
let body = response.text().await.expect("advanced page body");
assert!(body.contains("data-bind:job.process.enabled"));
assert!(body.contains("$job.process.mode = $job.process.enabled ? 'process' : 'forward'"));
assert!(body.contains("$job.send.mode = 'local'"));
assert!(body.contains("Download the archive from <strong>Collect</strong>."));
assert!(body.contains("$job.collect.source === 'known-host'"));
assert!(body.contains(
"$job.collect.mode === 'collect' && $job.collect.source === 'known-host' && $job.collect.known_host !== ''"
));
assert!(body.contains("id=\"send-local-directory\""));
assert!(body.contains("Local directory"));
assert!(body.contains("id=\"job-go-button\""));
assert!(body.contains("id=\"known-host-button\""));
assert!(body.contains("<path d=\"M6 3L11 8L6 13\"></path>"));
server.shutdown().await;
}
#[tokio::test]
async fn user_mode_workflow_exposes_browser_download_binding_and_local_directory_default() {
let (mut server, client, base) = start_server(RuntimeMode::User).await;
let response = client
.get(format!("{base}/advanced"))
.send()
.await
.expect("advanced page");
assert_eq!(response.status(), reqwest::StatusCode::OK);
let body = response.text().await.expect("advanced page body");
assert!(body.contains("data-signals:archive.download_token=\"''\""));
assert!(body.contains("id=\"job-download-anchor\""));
assert!(body.contains("data-on:change=\"if (evt.target.checked)"));
assert!(body.contains("crypto.randomUUID()"));
assert!(body.contains(
"data-attr:href=\"$archive.download_token ? `/advanced/download/${$archive.download_token}` : null\""
));
assert!(body.contains("/Downloads"));
server.shutdown().await;
}