mockforge_http/
network_profile_runtime.rs1use axum::extract::{Path as AxumPath, Request, State};
23use axum::http::StatusCode;
24use axum::middleware::Next;
25use axum::response::{IntoResponse, Response};
26use axum::routing::{get, post};
27use axum::{Json, Router};
28use mockforge_core::network_profiles::{NetworkProfile, NetworkProfileCatalog};
29use serde::Serialize;
30use std::sync::{Arc, RwLock};
31
32#[derive(Clone)]
35pub struct NetworkProfileRuntimeState {
36 inner: Arc<Inner>,
37}
38
39struct Inner {
40 catalog: NetworkProfileCatalog,
41 active: RwLock<Option<NetworkProfile>>,
42}
43
44impl NetworkProfileRuntimeState {
45 pub fn new(catalog: NetworkProfileCatalog) -> Self {
48 Self {
49 inner: Arc::new(Inner {
50 catalog,
51 active: RwLock::new(None),
52 }),
53 }
54 }
55
56 pub fn list(&self) -> Vec<(String, String)> {
58 self.inner.catalog.list_profiles_with_description()
59 }
60
61 pub fn active(&self) -> Option<NetworkProfile> {
63 self.inner.active.read().expect("network-profile state poisoned").clone()
64 }
65
66 pub fn activate(&self, name: &str) -> bool {
69 let profile = match self.inner.catalog.get(name) {
70 Some(p) => p.clone(),
71 None => return false,
72 };
73 *self.inner.active.write().expect("network-profile state poisoned") = Some(profile);
74 true
75 }
76
77 pub fn deactivate(&self) {
79 *self.inner.active.write().expect("network-profile state poisoned") = None;
80 }
81}
82
83pub async fn network_profile_middleware(
87 State(state): State<NetworkProfileRuntimeState>,
88 req: Request,
89 next: Next,
90) -> Response {
91 if let Some(profile) = state.active() {
92 let delay = profile.latency.calculate_latency(&[]);
93 if !delay.is_zero() {
94 tokio::time::sleep(delay).await;
95 }
96 }
97 next.run(req).await
98}
99
100#[derive(Debug, Serialize)]
101struct ProfileSummary {
102 name: String,
103 description: String,
104}
105
106#[derive(Debug, Serialize)]
107struct ListResponse {
108 profiles: Vec<ProfileSummary>,
109 active: Option<String>,
110}
111
112async fn list_handler(State(state): State<NetworkProfileRuntimeState>) -> Json<ListResponse> {
113 Json(ListResponse {
114 profiles: state
115 .list()
116 .into_iter()
117 .map(|(name, description)| ProfileSummary { name, description })
118 .collect(),
119 active: state.active().map(|p| p.name),
120 })
121}
122
123async fn active_handler(State(state): State<NetworkProfileRuntimeState>) -> Response {
124 match state.active() {
125 Some(profile) => Json(profile).into_response(),
126 None => StatusCode::NO_CONTENT.into_response(),
127 }
128}
129
130async fn activate_handler(
131 State(state): State<NetworkProfileRuntimeState>,
132 AxumPath(name): AxumPath<String>,
133) -> Response {
134 if state.activate(&name) {
135 Json(serde_json::json!({ "active": name })).into_response()
136 } else {
137 (
138 StatusCode::NOT_FOUND,
139 Json(serde_json::json!({
140 "error": "profile_not_found",
141 "message": format!("No network profile named '{}'", name),
142 })),
143 )
144 .into_response()
145 }
146}
147
148async fn deactivate_handler(State(state): State<NetworkProfileRuntimeState>) -> Response {
149 state.deactivate();
150 StatusCode::NO_CONTENT.into_response()
151}
152
153pub fn network_profile_api_router(state: NetworkProfileRuntimeState) -> Router {
156 Router::new()
157 .route("/", get(list_handler))
158 .route("/active", get(active_handler))
159 .route("/{name}/activate", post(activate_handler))
160 .route("/deactivate", post(deactivate_handler))
161 .with_state(state)
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167
168 #[test]
169 fn empty_active_until_activated() {
170 let state = NetworkProfileRuntimeState::new(NetworkProfileCatalog::new());
171 assert!(state.active().is_none());
172 }
173
174 #[test]
175 fn activate_unknown_returns_false() {
176 let state = NetworkProfileRuntimeState::new(NetworkProfileCatalog::new());
177 assert!(!state.activate("not_a_real_profile_name"));
178 assert!(state.active().is_none());
179 }
180
181 #[test]
182 fn list_includes_builtin_profiles() {
183 let state = NetworkProfileRuntimeState::new(NetworkProfileCatalog::new());
184 let names: Vec<String> = state.list().into_iter().map(|(n, _)| n).collect();
185 assert!(names.iter().any(|n| n.contains("3g") || n.contains("3G")));
187 assert!(!names.is_empty());
188 }
189
190 #[test]
191 fn activate_then_deactivate() {
192 let state = NetworkProfileRuntimeState::new(NetworkProfileCatalog::new());
193 let any_name = state.list().first().map(|(n, _)| n.clone()).expect("catalog non-empty");
194 assert!(state.activate(&any_name));
195 assert_eq!(state.active().map(|p| p.name), Some(any_name));
196 state.deactivate();
197 assert!(state.active().is_none());
198 }
199}