1use std::collections::HashMap;
4use std::sync::Arc;
5use std::time::Duration;
6
7use axum::extract::State;
8use axum::http::StatusCode;
9use axum::routing::post;
10use axum::{Json, Router};
11use serde::Deserialize;
12
13use crate::context::ContextStore;
14
15#[derive(Debug, Deserialize)]
16pub struct BindRequest {
17 pub values: HashMap<String, String>,
18 #[serde(default)]
19 pub ttl_seconds: Option<u64>,
20}
21
22pub fn admin_router(context: Arc<ContextStore>) -> Router {
23 Router::new()
24 .route("/internal/bind", post(bind).delete(unbind))
25 .with_state(context)
26}
27
28async fn bind(State(ctx): State<Arc<ContextStore>>, Json(req): Json<BindRequest>) -> StatusCode {
29 ctx.push(req.values, req.ttl_seconds.map(Duration::from_secs));
30 StatusCode::NO_CONTENT
31}
32
33async fn unbind(State(ctx): State<Arc<ContextStore>>) -> StatusCode {
34 ctx.clear();
35 StatusCode::NO_CONTENT
36}
37
38#[cfg(test)]
39mod tests {
40 use super::*;
41 use http_body_util::BodyExt;
42 use tower::ServiceExt;
43
44 #[tokio::test]
45 async fn bind_then_unbind_updates_context() {
46 let ctx = Arc::new(ContextStore::new(HashMap::new()));
47 let app = admin_router(ctx.clone());
48 let body =
49 serde_json::json!({ "values": { "org": "pushed" }, "ttl_seconds": 3600 }).to_string();
50 let resp = app
51 .clone()
52 .oneshot(
53 axum::http::Request::builder()
54 .method("POST")
55 .uri("/internal/bind")
56 .header("content-type", "application/json")
57 .body(axum::body::Body::from(body))
58 .unwrap(),
59 )
60 .await
61 .unwrap();
62 assert_eq!(resp.status(), 204);
63 let _ = resp.into_body().collect().await;
64 assert_eq!(ctx.resolve().get("org"), Some("pushed"));
65
66 let resp = app
67 .oneshot(
68 axum::http::Request::builder()
69 .method("DELETE")
70 .uri("/internal/bind")
71 .body(axum::body::Body::empty())
72 .unwrap(),
73 )
74 .await
75 .unwrap();
76 assert_eq!(resp.status(), 204);
77 assert_eq!(ctx.resolve().get("org"), None);
78 }
79}