vti_common/trust_task/
router.rs1use std::sync::Arc;
27
28use axum::Router;
29use axum::routing::MethodRouter;
30
31use super::TrustTask;
32
33pub struct TrustTaskRouter<S = ()> {
36 inner: Router<S>,
37}
38
39impl<S> Default for TrustTaskRouter<S>
40where
41 S: Clone + Send + Sync + 'static,
42{
43 fn default() -> Self {
44 Self::new()
45 }
46}
47
48impl<S> TrustTaskRouter<S>
49where
50 S: Clone + Send + Sync + 'static,
51{
52 pub fn new() -> Self {
54 Self {
55 inner: Router::new(),
56 }
57 }
58
59 pub fn route_with_task(
67 mut self,
68 path: &str,
69 method_router: MethodRouter<S>,
70 task: TrustTask,
71 ) -> Self {
72 let task = Arc::new(task);
77 let layered = method_router.layer(axum::middleware::from_fn(move |request, next| {
78 let task = task.clone();
79 async move { super::extractor::validate_header(&task, request, next).await }
80 }));
81 self.inner = self.inner.route(path, layered);
82 self
83 }
84
85 pub fn route_exempt(mut self, path: &str, method_router: MethodRouter<S>) -> Self {
92 self.inner = self.inner.route(path, method_router);
93 self
94 }
95
96 pub fn into_router(self) -> Router<S> {
99 self.inner
100 }
101}
102
103impl<S> From<TrustTaskRouter<S>> for Router<S>
104where
105 S: Clone + Send + Sync + 'static,
106{
107 fn from(r: TrustTaskRouter<S>) -> Self {
108 r.into_router()
109 }
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115 use axum::body::Body;
116 use axum::http::{Request, StatusCode};
117 use axum::routing::{get, post};
118 use http_body_util::BodyExt;
119 use tower::ServiceExt;
120
121 async fn ok() -> &'static str {
122 "ok"
123 }
124
125 fn make_router() -> Router {
126 let claim = TrustTask::new("https://trusttasks.org/openvtc/vtc/install/claim/1.0").unwrap();
127 TrustTaskRouter::new()
128 .route_with_task("/v1/install/claim", post(ok), claim)
129 .route_exempt("/health", get(ok))
130 .into_router()
131 }
132
133 #[tokio::test]
134 async fn exact_match_succeeds() {
135 let app = make_router();
136 let resp = app
137 .oneshot(
138 Request::builder()
139 .method("POST")
140 .uri("/v1/install/claim")
141 .header(
142 HEADER_NAME,
143 "https://trusttasks.org/openvtc/vtc/install/claim/1.0",
144 )
145 .body(Body::empty())
146 .unwrap(),
147 )
148 .await
149 .unwrap();
150 assert_eq!(resp.status(), StatusCode::OK);
151 }
152
153 #[tokio::test]
154 async fn missing_header_returns_400() {
155 let app = make_router();
156 let resp = app
157 .oneshot(
158 Request::builder()
159 .method("POST")
160 .uri("/v1/install/claim")
161 .body(Body::empty())
162 .unwrap(),
163 )
164 .await
165 .unwrap();
166 assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
167
168 let bytes = resp.into_body().collect().await.unwrap().to_bytes();
169 let body: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
170 assert_eq!(body["error"], "TrustTaskMissing");
171 }
172
173 #[tokio::test]
174 async fn mismatched_header_returns_415_with_expected_field() {
175 let app = make_router();
176 let resp = app
177 .oneshot(
178 Request::builder()
179 .method("POST")
180 .uri("/v1/install/claim")
181 .header(
182 HEADER_NAME,
183 "https://trusttasks.org/openvtc/vtc/auth/login/1.0",
184 )
185 .body(Body::empty())
186 .unwrap(),
187 )
188 .await
189 .unwrap();
190 assert_eq!(resp.status(), StatusCode::UNSUPPORTED_MEDIA_TYPE);
191
192 let bytes = resp.into_body().collect().await.unwrap().to_bytes();
193 let body: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
194 assert_eq!(body["error"], "TrustTaskMismatch");
195 assert_eq!(
196 body["expected"],
197 "https://trusttasks.org/openvtc/vtc/install/claim/1.0"
198 );
199 assert_eq!(
200 body["received"],
201 "https://trusttasks.org/openvtc/vtc/auth/login/1.0"
202 );
203 }
204
205 #[tokio::test]
206 async fn exact_match_is_byte_strict_not_prefix() {
207 let app = make_router();
208 let resp = app
210 .oneshot(
211 Request::builder()
212 .method("POST")
213 .uri("/v1/install/claim")
214 .header(
215 HEADER_NAME,
216 "https://trusttasks.org/openvtc/vtc/install/claim/1.1",
217 )
218 .body(Body::empty())
219 .unwrap(),
220 )
221 .await
222 .unwrap();
223 assert_eq!(resp.status(), StatusCode::UNSUPPORTED_MEDIA_TYPE);
224 }
225
226 #[tokio::test]
227 async fn health_is_exempt() {
228 let app = make_router();
229 let resp = app
231 .oneshot(
232 Request::builder()
233 .method("GET")
234 .uri("/health")
235 .body(Body::empty())
236 .unwrap(),
237 )
238 .await
239 .unwrap();
240 assert_eq!(resp.status(), StatusCode::OK);
241 }
242
243 use super::super::HEADER_NAME;
244}