use std::sync::Arc;
use axum::Router;
use axum::routing::MethodRouter;
use super::TrustTask;
pub struct TrustTaskRouter<S = ()> {
inner: Router<S>,
}
impl<S> Default for TrustTaskRouter<S>
where
S: Clone + Send + Sync + 'static,
{
fn default() -> Self {
Self::new()
}
}
impl<S> TrustTaskRouter<S>
where
S: Clone + Send + Sync + 'static,
{
pub fn new() -> Self {
Self {
inner: Router::new(),
}
}
pub fn route_with_task(
mut self,
path: &str,
method_router: MethodRouter<S>,
task: TrustTask,
) -> Self {
let task = Arc::new(task);
let layered = method_router.layer(axum::middleware::from_fn(move |request, next| {
let task = task.clone();
async move { super::extractor::validate_header(&task, request, next).await }
}));
self.inner = self.inner.route(path, layered);
self
}
pub fn route_exempt(mut self, path: &str, method_router: MethodRouter<S>) -> Self {
self.inner = self.inner.route(path, method_router);
self
}
pub fn into_router(self) -> Router<S> {
self.inner
}
}
impl<S> From<TrustTaskRouter<S>> for Router<S>
where
S: Clone + Send + Sync + 'static,
{
fn from(r: TrustTaskRouter<S>) -> Self {
r.into_router()
}
}
#[cfg(test)]
mod tests {
use super::*;
use axum::body::Body;
use axum::http::{Request, StatusCode};
use axum::routing::{get, post};
use http_body_util::BodyExt;
use tower::ServiceExt;
async fn ok() -> &'static str {
"ok"
}
fn make_router() -> Router {
let claim = TrustTask::new("https://trusttasks.org/spec/vtc/install/claim/0.1").unwrap();
TrustTaskRouter::new()
.route_with_task("/v1/install/claim", post(ok), claim)
.route_exempt("/health", get(ok))
.into_router()
}
#[tokio::test]
async fn exact_match_succeeds() {
let app = make_router();
let resp = app
.oneshot(
Request::builder()
.method("POST")
.uri("/v1/install/claim")
.header(
HEADER_NAME,
"https://trusttasks.org/spec/vtc/install/claim/0.1",
)
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
}
#[tokio::test]
async fn missing_header_returns_400() {
let app = make_router();
let resp = app
.oneshot(
Request::builder()
.method("POST")
.uri("/v1/install/claim")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let bytes = resp.into_body().collect().await.unwrap().to_bytes();
let body: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
assert_eq!(body["error"], "TrustTaskMissing");
}
#[tokio::test]
async fn mismatched_header_returns_415_with_expected_field() {
let app = make_router();
let resp = app
.oneshot(
Request::builder()
.method("POST")
.uri("/v1/install/claim")
.header(
HEADER_NAME,
"https://trusttasks.org/spec/vtc/auth/login/0.1",
)
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::UNSUPPORTED_MEDIA_TYPE);
let bytes = resp.into_body().collect().await.unwrap().to_bytes();
let body: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
assert_eq!(body["error"], "TrustTaskMismatch");
assert_eq!(
body["expected"],
"https://trusttasks.org/spec/vtc/install/claim/0.1"
);
assert_eq!(
body["received"],
"https://trusttasks.org/spec/vtc/auth/login/0.1"
);
}
#[tokio::test]
async fn exact_match_is_byte_strict_not_prefix() {
let app = make_router();
let resp = app
.oneshot(
Request::builder()
.method("POST")
.uri("/v1/install/claim")
.header(
HEADER_NAME,
"https://trusttasks.org/spec/vtc/install/claim/0.2",
)
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::UNSUPPORTED_MEDIA_TYPE);
}
#[tokio::test]
async fn health_is_exempt() {
let app = make_router();
let resp = app
.oneshot(
Request::builder()
.method("GET")
.uri("/health")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
}
use super::super::HEADER_NAME;
}