featherbit/admin/
env_vars.rs1use std::sync::Arc;
5
6use axum::response::IntoResponse;
7use axum::routing::get;
8use axum::{Json, Router};
9
10use crate::state::SharedState;
11
12pub fn router() -> Router<Arc<SharedState>> {
14 Router::new().route("/api/env-vars", get(list_env_vars))
15}
16
17async fn list_env_vars() -> impl IntoResponse {
25 let mut names: Vec<String> = std::env::vars_os()
26 .map(|(k, _)| k.to_string_lossy().into_owned())
27 .collect();
28 names.sort();
29 Json(serde_json::json!({ "names": names }))
30}
31
32#[cfg(test)]
33mod tests {
34 use super::*;
35 use axum::body::Body;
36 use axum::http::{Request, StatusCode};
37 use tower::ServiceExt;
38
39 #[tokio::test]
40 async fn test_list_env_vars_shape_and_secret_safety() {
41 std::env::set_var("FB_TEST_SECRET_VALUE_CANARY", "s3cr3t");
43
44 let app: Router = Router::new().route("/api/env-vars", get(list_env_vars));
46 let resp = app
47 .oneshot(Request::get("/api/env-vars").body(Body::empty()).unwrap())
48 .await
49 .unwrap();
50
51 assert_eq!(resp.status(), StatusCode::OK);
52
53 let bytes = axum::body::to_bytes(resp.into_body(), usize::MAX)
54 .await
55 .unwrap();
56 let body_str = String::from_utf8(bytes.to_vec()).unwrap();
57
58 let v: serde_json::Value = serde_json::from_str(&body_str).unwrap();
60 let names = v["names"].as_array().unwrap();
61
62 assert!(!names.is_empty());
64 let names_vec: Vec<String> = names
65 .iter()
66 .filter_map(|n| n.as_str().map(String::from))
67 .collect();
68 let mut sorted_names = names_vec.clone();
69 sorted_names.sort();
70 assert_eq!(names_vec, sorted_names, "env var names not sorted");
71
72 assert!(
74 !body_str.contains("s3cr3t"),
75 "secret value should not appear in response body"
76 );
77
78 assert!(
80 names_vec.iter().any(|n| n == "FB_TEST_SECRET_VALUE_CANARY"),
81 "test env var name should be in response"
82 );
83 }
84}