use std::sync::Arc;
use axum::body::Body;
use axum::http::{Request, StatusCode};
use polars::prelude::*;
use serde_json::json;
use taxa_core::{Backend, FrameBackend, FrameDataset, FrameSource};
use taxa_server::{build_router, build_router_ext};
use tower::ServiceExt;
fn app() -> axum::Router {
let df = df![
"fullname" => ["microsoft/vscode", "google/go"], "owner" => ["microsoft", "google"],
"dt" => Series::new("dt".into(), [20454i32, 20454]).cast(&DataType::Date).unwrap(),
"stars" => [163000i64, 123000],
]
.unwrap();
let ds: FrameDataset = serde_json::from_value(json!({
"source": "r", "id_column": "fullname", "label_column": "fullname",
"timestamp_column": "dt",
"axes": [{"id": "by_owner", "levels": ["owner", "fullname"]}],
"metrics": [
{"id": "stars", "agg": "sum", "column": "stars", "unit": "count"},
{"id": "n", "agg": "count", "unit": "count"}
],
"filters": [{"id": "owner", "column": "owner", "type": "categorical"}],
"default_axis": "by_owner", "default_size_by": "stars"
}))
.unwrap();
let backend: Arc<dyn Backend> = Arc::new(FrameBackend::new(Arc::new(FrameSource::new(df))));
build_router(ds, backend, None)
}
async fn status(req: Request<Body>) -> StatusCode {
app().oneshot(req).await.unwrap().status()
}
fn post(path: &str, body: serde_json::Value) -> Request<Body> {
Request::builder()
.method("POST")
.uri(path)
.header("content-type", "application/json")
.body(Body::from(body.to_string()))
.unwrap()
}
#[tokio::test]
async fn slash_entity_id_matches() {
let resp = app()
.oneshot(
Request::builder()
.uri("/api/entity/microsoft/vscode")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let bytes = axum::body::to_bytes(resp.into_body(), usize::MAX)
.await
.unwrap();
let v: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
assert_eq!(v["label"], json!("microsoft/vscode"));
}
#[tokio::test]
async fn unknown_entity_is_404() {
assert_eq!(
status(
Request::builder()
.uri("/api/entity/nope/nope")
.body(Body::empty())
.unwrap()
)
.await,
StatusCode::NOT_FOUND
);
}
#[tokio::test]
async fn invalid_params_are_400() {
assert_eq!(
status(post(
"/api/series",
json!({"axis": "by_owner", "metric": "stars", "agg": "bogus"})
))
.await,
StatusCode::BAD_REQUEST
);
assert_eq!(
status(post(
"/api/series",
json!({"axis": "by_owner", "metric": "stars", "resolution": "zzz"})
))
.await,
StatusCode::BAD_REQUEST
);
assert_eq!(
status(post(
"/api/series",
json!({"axis": "by_owner", "metric": "stars", "window": "nope"})
))
.await,
StatusCode::BAD_REQUEST
);
assert_eq!(
status(post(
"/api/series",
json!({"axis": "nope", "metric": "stars"})
))
.await,
StatusCode::BAD_REQUEST
);
assert_eq!(
status(post(
"/api/treemap",
json!({"axis": "by_owner", "filters": {"nope": "x"}})
))
.await,
StatusCode::BAD_REQUEST
);
assert_eq!(
status(post(
"/api/treemap",
json!({"axis": "by_owner", "size_by": "bogus"})
))
.await,
StatusCode::BAD_REQUEST
);
}
#[tokio::test]
async fn treemap_is_focus_bounded() {
let resp = app()
.oneshot(post(
"/api/treemap",
json!({"axis": "by_owner", "focus": ["root", "microsoft"], "depth": 1}),
))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let bytes = axum::body::to_bytes(resp.into_body(), usize::MAX)
.await
.unwrap();
let t: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
let names: Vec<&str> = t["children"]
.as_array()
.unwrap()
.iter()
.map(|c| c["name"].as_str().unwrap())
.collect();
assert_eq!(names, ["microsoft/vscode"]); assert_eq!(t["stars"], json!(163000)); }
#[tokio::test]
async fn valid_requests_are_200() {
assert_eq!(
status(post(
"/api/treemap",
json!({"axis": "by_owner", "filters": {}})
))
.await,
StatusCode::OK
);
assert_eq!(
status(post(
"/api/treemap",
json!({"axis": "by_owner", "filters": {"owner": ["microsoft"]}})
))
.await,
StatusCode::OK
);
assert_eq!(
status(post(
"/api/treemap",
json!({"axis": "by_owner", "size_by": "n"})
))
.await,
StatusCode::OK
);
assert_eq!(
status(post(
"/api/series",
json!({"axis": "by_owner", "metric": "stars", "agg": "median", "resolution": "w"})
))
.await,
StatusCode::OK
);
assert_eq!(
status(post(
"/api/series",
json!({"axis": "by_owner", "metric": "stars", "size_by": "stars", "top_k": 3})
))
.await,
StatusCode::OK
);
assert_eq!(
status(post(
"/api/series",
json!({"axis": "by_owner", "metric": "stars", "size_by": "bogus"})
))
.await,
StatusCode::OK
);
assert_eq!(
status(post(
"/api/series",
json!({"axis": "by_owner", "metric": "stars", "top_k": 100000})
))
.await,
StatusCode::OK
);
}
fn series_app() -> axum::Router {
let main_df = df![
"fullname" => ["microsoft/vscode", "google/go"],
"owner" => ["microsoft", "google"],
"stars" => [163000i64, 123000],
]
.unwrap();
let main_ds: FrameDataset = serde_json::from_value(json!({
"source": "r", "id_column": "fullname", "label_column": "fullname",
"axes": [{"id": "by_owner", "levels": ["owner", "fullname"]}],
"metrics": [{"id": "stars", "agg": "sum", "column": "stars", "unit": "count"}],
"filters": [{"id": "owner", "column": "owner", "type": "categorical"}],
"default_axis": "by_owner", "default_size_by": "stars"
}))
.unwrap();
let main_backend: Arc<dyn Backend> =
Arc::new(FrameBackend::new(Arc::new(FrameSource::new(main_df))));
let series_df = df![
"fullname" => ["microsoft/vscode", "microsoft/vscode", "google/go"],
"owner" => ["microsoft", "microsoft", "google"],
"snap" => Series::new("snap".into(), [20454i32, 20461, 20454]).cast(&DataType::Date).unwrap(),
"price" => [10.0f64, 12.0, 8.0],
]
.unwrap();
let series_ds: FrameDataset = serde_json::from_value(json!({
"source": "s", "id_column": "fullname", "label_column": "fullname",
"timestamp_column": "snap",
"axes": [{"id": "by_owner", "levels": ["owner", "fullname"]}],
"metrics": [{"id": "price", "agg": "sum", "column": "price", "unit": "money"}],
"default_axis": "by_owner", "default_size_by": "price"
}))
.unwrap();
let series_backend: Arc<dyn Backend> =
Arc::new(FrameBackend::new(Arc::new(FrameSource::new(series_df))));
build_router(main_ds, main_backend, Some((series_ds, series_backend)))
}
#[tokio::test]
async fn series_routes_to_series_backend() {
let resp = series_app()
.oneshot(post("/api/series", json!({
"axis": "by_owner", "metric": "price", "size_by": "price", "top_k": 5, "window": "1y"
})))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let bytes = axum::body::to_bytes(resp.into_body(), usize::MAX)
.await
.unwrap();
let v: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
assert_eq!(v["meta"]["metric_id"], json!("price"));
assert!(!v["series"].as_array().unwrap().is_empty());
assert_eq!(
series_app()
.oneshot(post(
"/api/series",
json!({"axis": "by_owner", "metric": "stars"})
))
.await
.unwrap()
.status(),
StatusCode::BAD_REQUEST
);
}
#[tokio::test]
async fn entity_series_returns_one_line() {
let resp = series_app()
.oneshot(
Request::builder()
.uri("/api/entity/microsoft/vscode/series?metric=price&window=1y&resolution=w")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let bytes = axum::body::to_bytes(resp.into_body(), usize::MAX)
.await
.unwrap();
let v: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
let vals: Vec<f64> = v["values"]
.as_array()
.unwrap()
.iter()
.map(|x| x.as_f64().unwrap())
.collect();
assert_eq!(vals, vec![10.0, 12.0]);
assert_eq!(v["dates"].as_array().unwrap().len(), 2);
assert_eq!(v["unit"], json!("money"));
assert_eq!(v["metric_id"], json!("price"));
}
#[tokio::test]
async fn entity_ohlc_is_404() {
let resp = series_app()
.oneshot(
Request::builder()
.uri("/api/entity/microsoft/vscode/ohlc?window=1y&resolution=w")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn series_non_series_size_by_falls_back_not_400() {
let resp = series_app()
.oneshot(post(
"/api/series",
json!({
"axis": "by_owner", "metric": "price", "size_by": "stars"
}),
))
.await
.unwrap();
assert_eq!(
resp.status(),
StatusCode::OK,
"non-series size_by must fall back, not 400"
);
let bytes = axum::body::to_bytes(resp.into_body(), usize::MAX)
.await
.unwrap();
let v: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
assert_eq!(v["meta"]["metric_id"], json!("price"));
}
#[tokio::test]
async fn entity_id_ending_in_subroute_word_is_reachable() {
let resp = app()
.oneshot(
Request::builder()
.uri("/api/entity/series")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(
resp.status(),
StatusCode::NOT_FOUND,
"bare /series id is a detail lookup, not a subroute"
);
let resp = app()
.oneshot(
Request::builder()
.uri("/api/entity/ohlc")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}
fn frames_views_app() -> axum::Router {
let snap_df = df![
"symbol" => ["A", "B", "C"],
"sector" => ["tech", "fin", "energy"],
"mcap" => [1000.0f64, 500.0, 100.0],
]
.unwrap();
let snap_ds: FrameDataset = serde_json::from_value(json!({
"source": "snap", "id_column": "symbol", "label_column": "symbol",
"axes": [{"id": "sector", "levels": ["sector", "symbol"]}],
"metrics": [{"id": "mcap", "agg": "sum", "column": "mcap", "unit": "money"}],
"default_axis": "sector", "default_size_by": "mcap"
}))
.unwrap();
let snap_backend: Arc<dyn Backend> =
Arc::new(FrameBackend::new(Arc::new(FrameSource::new(snap_df))));
let series_df = df![
"symbol" => ["A", "B", "C"],
"sector" => ["tech", "fin", "energy"],
"date" => Series::new("date".into(), [20454i32, 20454, 20454]).cast(&DataType::Date).unwrap(),
"flow" => [1.0f64, 2.0, 999.0],
]
.unwrap();
let series_ds: FrameDataset = serde_json::from_value(json!({
"source": "facts", "id_column": "symbol", "label_column": "symbol", "timestamp_column": "date",
"axes": [{"id": "sector", "levels": ["sector", "symbol"]}],
"metrics": [{"id": "flow", "agg": "sum", "column": "flow", "unit": "number"}],
"default_axis": "sector", "default_size_by": "flow"
}))
.unwrap();
let series_backend: Arc<dyn Backend> =
Arc::new(FrameBackend::new(Arc::new(FrameSource::new(series_df))));
build_router_ext(
snap_ds,
snap_backend,
Some((series_ds, series_backend)),
true,
)
}
#[tokio::test]
async fn frames_views_series_follows_treemap_branch_set() {
let resp = frames_views_app()
.oneshot(post(
"/api/series",
json!({"axis": "sector", "metric": "flow", "size_by": "mcap", "top_k": 2}),
))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let bytes = axum::body::to_bytes(resp.into_body(), usize::MAX)
.await
.unwrap();
let v: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
let keys: Vec<&str> = v["meta"]["branch_keys"]
.as_array()
.unwrap()
.iter()
.map(|x| x.as_str().unwrap())
.collect();
assert_eq!(keys, ["tech", "fin", "__other__"]);
assert_eq!(v["meta"]["metric_id"], json!("flow"));
}
fn weekly_only_series_app() -> axum::Router {
let main_df = df![
"fullname" => ["microsoft/vscode", "google/go"],
"owner" => ["microsoft", "google"],
"stars" => [163000i64, 123000],
]
.unwrap();
let main_ds: FrameDataset = serde_json::from_value(json!({
"source": "r", "id_column": "fullname", "label_column": "fullname",
"axes": [{"id": "by_owner", "levels": ["owner", "fullname"]}],
"metrics": [{"id": "stars", "agg": "sum", "column": "stars", "unit": "count"}],
"default_axis": "by_owner", "default_size_by": "stars"
}))
.unwrap();
let main_backend: Arc<dyn Backend> =
Arc::new(FrameBackend::new(Arc::new(FrameSource::new(main_df))));
let series_df = df![
"fullname" => ["microsoft/vscode", "google/go"],
"owner" => ["microsoft", "google"],
"snap" => Series::new("snap".into(), [20454i32, 20454]).cast(&DataType::Date).unwrap(),
"price" => [10.0f64, 8.0],
]
.unwrap();
let series_ds: FrameDataset = serde_json::from_value(json!({
"source": "s", "id_column": "fullname", "label_column": "fullname",
"timestamp_column": "snap",
"axes": [{"id": "by_owner", "levels": ["owner", "fullname"]}],
"metrics": [{"id": "price", "agg": "sum", "column": "price", "unit": "money"}],
"series_resolutions": ["w"],
"default_axis": "by_owner", "default_size_by": "price"
}))
.unwrap();
let series_backend: Arc<dyn Backend> =
Arc::new(FrameBackend::new(Arc::new(FrameSource::new(series_df))));
build_router(main_ds, main_backend, Some((series_ds, series_backend)))
}
#[tokio::test]
async fn series_resolution_validates_against_frame_not_global() {
assert_eq!(
weekly_only_series_app()
.oneshot(post(
"/api/series",
json!({"axis": "by_owner", "metric": "price", "resolution": "d"})
))
.await
.unwrap()
.status(),
StatusCode::BAD_REQUEST,
"a frame-disallowed resolution must 400"
);
assert_eq!(
weekly_only_series_app()
.oneshot(post(
"/api/series",
json!({"axis": "by_owner", "metric": "price", "resolution": "w"})
))
.await
.unwrap()
.status(),
StatusCode::OK
);
let resp = weekly_only_series_app()
.oneshot(post(
"/api/series",
json!({"axis": "by_owner", "metric": "price"}),
))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let bytes = axum::body::to_bytes(resp.into_body(), usize::MAX)
.await
.unwrap();
let v: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
assert_eq!(
v["meta"]["cadence"],
json!("weekly"),
"absent resolution must default to the frame's first allowed ('w')"
);
}
#[tokio::test]
async fn embedded_assets_etag_revalidation() {
let resp = app()
.oneshot(Request::builder().uri("/").body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
resp.headers().get("cache-control").unwrap(),
"no-cache",
"embedded assets must revalidate, never cache blindly"
);
let etag = resp.headers().get("etag").expect("ETag present").clone();
let etag = etag.to_str().unwrap();
assert!(
etag.starts_with('"') && etag.ends_with('"'),
"strong quoted ETag: {etag}"
);
let resp = app()
.oneshot(
Request::builder()
.uri("/")
.header("if-none-match", etag)
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::NOT_MODIFIED);
assert!(
resp.headers().get("etag").is_some(),
"304 re-states the ETag"
);
let bytes = axum::body::to_bytes(resp.into_body(), usize::MAX)
.await
.unwrap();
assert!(bytes.is_empty(), "304 must not carry a body");
let resp = app()
.oneshot(
Request::builder()
.uri("/")
.header("if-none-match", "\"deadbeef\"")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let resp = app()
.oneshot(
Request::builder()
.uri("/static/vendor/world-countries-med.json")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(resp.headers().get("cache-control").unwrap(), "no-cache");
let etag = resp
.headers()
.get("etag")
.unwrap()
.to_str()
.unwrap()
.to_string();
let resp = app()
.oneshot(
Request::builder()
.uri("/static/vendor/world-countries-med.json")
.header("if-none-match", format!("W/{etag}")) .body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::NOT_MODIFIED);
}
#[tokio::test]
async fn entity_series_unknown_metric_is_400() {
let resp = series_app()
.oneshot(
Request::builder()
.uri("/api/entity/microsoft/vscode/series?metric=bogus&window=1y")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}