fenrix_server/
lib.rs

1use axum::{
2    extract::Path,
3    http::StatusCode,
4    response::Json,
5    routing::{get, post},
6    Router,
7};
8use serde_json::{json, Value};
9use std::net::SocketAddr;
10use std::path::PathBuf;
11use tower_http::services::ServeDir;
12use tracing::info;
13
14/// The main configuration for the Fenrix server.
15#[derive(Clone, Debug)]
16pub struct ServerConfig {
17    /// The IP address and port to bind the server to.
18    pub addr: SocketAddr,
19    /// The path to the directory containing the client-side assets (e.g., HTML, JS, WASM).
20    pub assets_path: PathBuf,
21}
22
23/// Starts the Fenrix server.
24///
25/// This function initializes an `axum` server to host the application. It serves the
26/// static client-side files and provides API endpoints for server functions.
27///
28/// # Arguments
29///
30/// * `config` - A `ServerConfig` struct containing the server's configuration.
31///
32/// # Panics
33///
34/// This function will panic if the server fails to start.
35pub async fn start_server(config: ServerConfig) {
36    let app = Router::new()
37        // API route for server functions.
38        .route("/api/:name", post(handle_api))
39        // Route to serve static assets.
40        .nest_service("/", ServeDir::new(config.assets_path.clone()));
41
42    info!("Starting server at http://{}", config.addr);
43    info!(
44        "Serving static assets from: {}",
45        config.assets_path.display()
46    );
47
48    let listener = tokio::net::TcpListener::bind(config.addr).await.unwrap();
49    axum::serve(listener, app).await.unwrap();
50}
51
52/// A placeholder handler for server function API calls.
53///
54/// This function will eventually be replaced by a more robust system that
55/// dynamically registers and calls server functions.
56async fn handle_api(Path(name): Path<String>) -> (StatusCode, Json<Value>) {
57    info!("Received API call for function: {}", name);
58    let response = json!({
59        "status": "success",
60        "message": format!("Called server function: '{}'", name),
61    });
62    (StatusCode::OK, Json(response))
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68    use std::time::Duration;
69    use tokio::time::timeout;
70
71    // Helper to get an available port.
72    fn get_available_port() -> u16 {
73        std::net::TcpListener::bind("127.0.0.1:0")
74            .unwrap()
75            .local_addr()
76            .unwrap()
77            .port()
78    }
79
80    #[tokio::test]
81    async fn server_starts_and_responds_to_api_call() {
82        let port = get_available_port();
83        let addr = SocketAddr::from(([127, 0, 0, 1], port));
84
85        // Create a temporary directory for assets.
86        let temp_dir = tempfile::tempdir().unwrap();
87        let assets_path = temp_dir.path().to_path_buf();
88        std::fs::File::create(assets_path.join("index.html")).unwrap();
89
90        let config = ServerConfig { addr, assets_path };
91
92        // Run the server in the background.
93        let server_handle = tokio::spawn(start_server(config));
94
95        // Give the server a moment to start.
96        tokio::time::sleep(Duration::from_millis(100)).await;
97
98        // Test the API endpoint.
99        let client = reqwest::Client::new();
100        let res = client
101            .post(format!("http://{}/api/test_function", addr))
102            .send()
103            .await
104            .expect("Failed to send request");
105
106        assert_eq!(res.status(), reqwest::StatusCode::OK);
107
108        let body: Value = res.json().await.expect("Failed to parse JSON response");
109        assert_eq!(
110            body,
111            json!({
112                "status": "success",
113                "message": "Called server function: 'test_function'",
114            })
115        );
116
117        // Test the static file serving.
118        let res_static = client
119            .get(format!("http://{}/index.html", addr))
120            .send()
121            .await
122            .expect("Failed to request static file");
123
124        assert_eq!(res_static.status(), reqwest::StatusCode::OK);
125
126        // Shut down the server.
127        server_handle.abort();
128        let _ = timeout(Duration::from_secs(1), server_handle).await;
129    }
130}