108 lines
3.3 KiB
Rust
108 lines
3.3 KiB
Rust
//! The daemon serves the browser SPA off its own listener, so the browser
|
|
//! talks to it directly instead of through a reverse proxy.
|
|
|
|
#![cfg(feature = "web")]
|
|
|
|
mod common;
|
|
|
|
use std::net::{Ipv4Addr, SocketAddr};
|
|
|
|
use common::DaemonFixture;
|
|
use maestro_daemon::local_api;
|
|
use tokio::net::TcpListener;
|
|
use tokio::sync::oneshot;
|
|
|
|
struct Harness {
|
|
addr: SocketAddr,
|
|
_shutdown: oneshot::Sender<()>,
|
|
_fx: DaemonFixture,
|
|
}
|
|
|
|
async fn spawn() -> Harness {
|
|
let fx = DaemonFixture::new();
|
|
let router = local_api::router(local_api::state_for(
|
|
fx.manager.clone(),
|
|
"testhost",
|
|
"/home/test",
|
|
maestro_config::SpawnSection::default().resolve().unwrap(),
|
|
std::sync::Arc::new(maestro_daemon::projects::ProjectScanner::disabled()),
|
|
maestro_config::DEFAULT_LEADER,
|
|
));
|
|
let listener = TcpListener::bind(SocketAddr::from((Ipv4Addr::LOCALHOST, 0)))
|
|
.await
|
|
.unwrap();
|
|
let addr = listener.local_addr().unwrap();
|
|
let (tx, rx) = oneshot::channel::<()>();
|
|
tokio::spawn(async move {
|
|
let _ = axum::serve(listener, router)
|
|
.with_graceful_shutdown(async move {
|
|
let _ = rx.await;
|
|
})
|
|
.await;
|
|
});
|
|
Harness {
|
|
addr,
|
|
_shutdown: tx,
|
|
_fx: fx,
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn root_serves_the_spa_shell() {
|
|
let h = spawn().await;
|
|
let resp = reqwest::get(format!("http://{}/", h.addr)).await.unwrap();
|
|
assert_eq!(resp.status(), 200);
|
|
assert_eq!(
|
|
resp.headers()
|
|
.get(reqwest::header::CONTENT_TYPE)
|
|
.and_then(|v| v.to_str().ok())
|
|
.map(|v| v.starts_with("text/html")),
|
|
Some(true)
|
|
);
|
|
assert!(resp.text().await.unwrap().contains(r#"id="root""#));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn client_side_route_falls_back_to_the_shell() {
|
|
let h = spawn().await;
|
|
let resp = reqwest::get(format!("http://{}/sessions/whatever", h.addr))
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(resp.status(), 200);
|
|
assert!(resp.text().await.unwrap().contains(r#"id="root""#));
|
|
}
|
|
|
|
/// An unmatched API path must 404 rather than fall through to the SPA shell:
|
|
/// a client asking for JSON would otherwise get HTML and a parse error that
|
|
/// says nothing about the route being wrong.
|
|
#[tokio::test]
|
|
async fn unknown_api_route_404s_instead_of_serving_html() {
|
|
let h = spawn().await;
|
|
for path in ["/api", "/api/nope", "/api/sessions/not-a-uuid/bogus"] {
|
|
let resp = reqwest::get(format!("http://{}{path}", h.addr))
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(resp.status(), 404, "{path}");
|
|
let ct = resp
|
|
.headers()
|
|
.get(reqwest::header::CONTENT_TYPE)
|
|
.and_then(|v| v.to_str().ok())
|
|
.unwrap_or("")
|
|
.to_owned();
|
|
assert!(!ct.starts_with("text/html"), "{path} returned HTML: {ct}");
|
|
}
|
|
}
|
|
|
|
/// The API and the SPA share one listener; mounting the fallback must not
|
|
/// shadow a real route.
|
|
#[tokio::test]
|
|
async fn api_routes_still_answer() {
|
|
let h = spawn().await;
|
|
let resp = reqwest::get(format!("http://{}/api/info", h.addr))
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(resp.status(), 200);
|
|
let info: maestro_protocol::DaemonInfo = resp.json().await.unwrap();
|
|
assert_eq!(info.hostname, "testhost");
|
|
}
|