use axum::extract::{Path as AxumPath, Request, State};
use axum::http::StatusCode;
use axum::middleware::Next;
use axum::response::{IntoResponse, Response};
use axum::routing::{get, post};
use axum::{Json, Router};
use mockforge_core::network_profiles::{NetworkProfile, NetworkProfileCatalog};
use serde::Serialize;
use std::sync::{Arc, RwLock};
#[derive(Clone)]
pub struct NetworkProfileRuntimeState {
inner: Arc<Inner>,
}
struct Inner {
catalog: NetworkProfileCatalog,
active: RwLock<Option<NetworkProfile>>,
}
impl NetworkProfileRuntimeState {
pub fn new(catalog: NetworkProfileCatalog) -> Self {
Self {
inner: Arc::new(Inner {
catalog,
active: RwLock::new(None),
}),
}
}
pub fn list(&self) -> Vec<(String, String)> {
self.inner.catalog.list_profiles_with_description()
}
pub fn active(&self) -> Option<NetworkProfile> {
self.inner.active.read().expect("network-profile state poisoned").clone()
}
pub fn activate(&self, name: &str) -> bool {
let profile = match self.inner.catalog.get(name) {
Some(p) => p.clone(),
None => return false,
};
*self.inner.active.write().expect("network-profile state poisoned") = Some(profile);
true
}
pub fn deactivate(&self) {
*self.inner.active.write().expect("network-profile state poisoned") = None;
}
}
pub async fn network_profile_middleware(
State(state): State<NetworkProfileRuntimeState>,
req: Request,
next: Next,
) -> Response {
if let Some(profile) = state.active() {
let delay = profile.latency.calculate_latency(&[]);
if !delay.is_zero() {
tokio::time::sleep(delay).await;
}
}
next.run(req).await
}
#[derive(Debug, Serialize)]
struct ProfileSummary {
name: String,
description: String,
}
#[derive(Debug, Serialize)]
struct ListResponse {
profiles: Vec<ProfileSummary>,
active: Option<String>,
}
async fn list_handler(State(state): State<NetworkProfileRuntimeState>) -> Json<ListResponse> {
Json(ListResponse {
profiles: state
.list()
.into_iter()
.map(|(name, description)| ProfileSummary { name, description })
.collect(),
active: state.active().map(|p| p.name),
})
}
async fn active_handler(State(state): State<NetworkProfileRuntimeState>) -> Response {
match state.active() {
Some(profile) => Json(profile).into_response(),
None => StatusCode::NO_CONTENT.into_response(),
}
}
async fn activate_handler(
State(state): State<NetworkProfileRuntimeState>,
AxumPath(name): AxumPath<String>,
) -> Response {
if state.activate(&name) {
Json(serde_json::json!({ "active": name })).into_response()
} else {
(
StatusCode::NOT_FOUND,
Json(serde_json::json!({
"error": "profile_not_found",
"message": format!("No network profile named '{}'", name),
})),
)
.into_response()
}
}
async fn deactivate_handler(State(state): State<NetworkProfileRuntimeState>) -> Response {
state.deactivate();
StatusCode::NO_CONTENT.into_response()
}
pub fn network_profile_api_router(state: NetworkProfileRuntimeState) -> Router {
Router::new()
.route("/", get(list_handler))
.route("/active", get(active_handler))
.route("/{name}/activate", post(activate_handler))
.route("/deactivate", post(deactivate_handler))
.with_state(state)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_active_until_activated() {
let state = NetworkProfileRuntimeState::new(NetworkProfileCatalog::new());
assert!(state.active().is_none());
}
#[test]
fn activate_unknown_returns_false() {
let state = NetworkProfileRuntimeState::new(NetworkProfileCatalog::new());
assert!(!state.activate("not_a_real_profile_name"));
assert!(state.active().is_none());
}
#[test]
fn list_includes_builtin_profiles() {
let state = NetworkProfileRuntimeState::new(NetworkProfileCatalog::new());
let names: Vec<String> = state.list().into_iter().map(|(n, _)| n).collect();
assert!(names.iter().any(|n| n.contains("3g") || n.contains("3G")));
assert!(!names.is_empty());
}
#[test]
fn activate_then_deactivate() {
let state = NetworkProfileRuntimeState::new(NetworkProfileCatalog::new());
let any_name = state.list().first().map(|(n, _)| n.clone()).expect("catalog non-empty");
assert!(state.activate(&any_name));
assert_eq!(state.active().map(|p| p.name), Some(any_name));
state.deactivate();
assert!(state.active().is_none());
}
}