1#![cfg_attr(coverage, allow(unused_imports, dead_code))]
8
9use std::net::SocketAddr;
10use std::sync::Arc;
11
12use axum::{
13 routing::{get, post},
14 Router,
15};
16use tower_http::cors::{Any, CorsLayer};
17use tower_http::trace::TraceLayer;
18use tracing::info;
19
20use super::handlers;
21
22#[derive(Clone)]
24pub struct ApiConfig {
25 pub host: String,
26 pub port: u16,
27}
28
29impl Default for ApiConfig {
30 fn default() -> Self {
31 Self {
32 host: "127.0.0.1".to_string(),
33 port: 8080,
34 }
35 }
36}
37
38#[derive(Clone)]
40pub struct AppState {
41 pub version: String,
42}
43
44#[cfg(not(coverage))]
55pub async fn run_api_server(config: ApiConfig) -> anyhow::Result<()> {
56 tracing_subscriber::fmt()
58 .with_env_filter(
59 tracing_subscriber::EnvFilter::try_from_default_env()
60 .unwrap_or_else(|_| "forge=info,tower_http=info".into()),
61 )
62 .init();
63
64 let state = Arc::new(AppState {
65 version: env!("CARGO_PKG_VERSION").to_string(),
66 });
67
68 let cors = CorsLayer::new()
70 .allow_origin(Any)
71 .allow_methods(Any)
72 .allow_headers(Any);
73
74 let app = Router::new()
76 .route("/", get(handlers::root))
78 .route("/health", get(handlers::health))
79 .route("/version", get(handlers::version))
80 .route("/api/v1/validate", post(handlers::validate))
82 .route("/api/v1/calculate", post(handlers::calculate))
83 .route("/api/v1/audit", post(handlers::audit))
84 .route("/api/v1/export", post(handlers::export))
85 .route("/api/v1/import", post(handlers::import_excel))
86 .with_state(state)
88 .layer(cors)
89 .layer(TraceLayer::new_for_http());
90
91 let addr: SocketAddr = format!("{}:{}", config.host, config.port).parse()?;
92 info!("🔥 Forge API Server starting on http://{}", addr);
93 info!(" Endpoints: /api/v1/validate, /api/v1/calculate, /api/v1/audit, /api/v1/export, /api/v1/import");
94 info!(" Health: /health, Version: /version");
95
96 let listener = tokio::net::TcpListener::bind(addr).await?;
97 axum::serve(listener, app)
98 .with_graceful_shutdown(shutdown_signal())
99 .await?;
100
101 info!("Forge API Server shutdown complete");
102 Ok(())
103}
104
105#[cfg(coverage)]
107pub async fn run_api_server(_config: ApiConfig) -> anyhow::Result<()> {
108 Ok(())
109}
110
111#[cfg(not(coverage))]
116async fn shutdown_signal() {
117 let ctrl_c = async {
118 tokio::signal::ctrl_c()
119 .await
120 .expect("failed to install Ctrl+C handler");
121 };
122
123 #[cfg(unix)]
124 let terminate = async {
125 tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
126 .expect("failed to install signal handler")
127 .recv()
128 .await;
129 };
130
131 #[cfg(not(unix))]
132 let terminate = std::future::pending::<()>();
133
134 tokio::select! {
135 () = ctrl_c => {},
136 () = terminate => {},
137 }
138
139 info!("Shutdown signal received, stopping server...");
140}
141
142#[cfg(coverage)]
144async fn shutdown_signal() {}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149
150 #[test]
153 fn test_default_config() {
154 let config = ApiConfig::default();
155 assert_eq!(config.host, "127.0.0.1");
156 assert_eq!(config.port, 8080);
157 }
158
159 #[test]
160 fn test_config_custom_values() {
161 let config = ApiConfig {
162 host: "0.0.0.0".to_string(),
163 port: 3000,
164 };
165 assert_eq!(config.host, "0.0.0.0");
166 assert_eq!(config.port, 3000);
167 }
168
169 #[test]
170 fn test_config_clone() {
171 let config1 = ApiConfig::default();
172 let config2 = config1.clone();
173 assert_eq!(config1.host, config2.host);
174 assert_eq!(config1.port, config2.port);
175 }
176
177 #[test]
178 fn test_config_address_format() {
179 let config = ApiConfig {
180 host: "192.168.1.100".to_string(),
181 port: 9090,
182 };
183 let addr_str = format!("{}:{}", config.host, config.port);
184 assert_eq!(addr_str, "192.168.1.100:9090");
185
186 let addr: SocketAddr = addr_str.parse().unwrap();
188 assert_eq!(addr.port(), 9090);
189 }
190
191 #[test]
194 fn test_app_state_version() {
195 let state = AppState {
196 version: "2.0.0".to_string(),
197 };
198 assert_eq!(state.version, "2.0.0");
199 }
200
201 #[test]
202 fn test_app_state_clone() {
203 let state1 = AppState {
204 version: "2.0.0".to_string(),
205 };
206 let state2 = state1.clone();
207 assert_eq!(state1.version, state2.version);
208 }
209
210 #[test]
211 fn test_app_state_in_arc() {
212 let state = Arc::new(AppState {
213 version: "2.0.0".to_string(),
214 });
215 let state_clone = Arc::clone(&state);
216 assert_eq!(state.version, state_clone.version);
217 assert_eq!(Arc::strong_count(&state), 2);
218 }
219
220 #[test]
223 fn test_build_router() {
224 let state = Arc::new(AppState {
225 version: "5.0.0".to_string(),
226 });
227
228 let _app: Router = Router::new()
230 .route("/", get(handlers::root))
231 .route("/health", get(handlers::health))
232 .route("/version", get(handlers::version))
233 .route("/api/v1/validate", post(handlers::validate))
234 .route("/api/v1/calculate", post(handlers::calculate))
235 .route("/api/v1/audit", post(handlers::audit))
236 .route("/api/v1/export", post(handlers::export))
237 .route("/api/v1/import", post(handlers::import_excel))
238 .with_state(state);
239
240 }
242
243 #[test]
244 fn test_socket_addr_parsing() {
245 let config = ApiConfig::default();
246 let addr_str = format!("{}:{}", config.host, config.port);
247 let addr: Result<SocketAddr, _> = addr_str.parse();
248 assert!(addr.is_ok());
249 assert_eq!(addr.unwrap().port(), 8080);
250 }
251
252 #[test]
253 fn test_socket_addr_parsing_ipv6() {
254 let config = ApiConfig {
255 host: "::1".to_string(),
256 port: 8080,
257 };
258 let addr_str = format!("[{}]:{}", config.host, config.port);
259 let addr: Result<SocketAddr, _> = addr_str.parse();
260 assert!(addr.is_ok());
261 }
262
263 #[test]
264 fn test_cors_layer_creation() {
265 let cors = CorsLayer::new()
266 .allow_origin(Any)
267 .allow_methods(Any)
268 .allow_headers(Any);
269 let _ = cors;
271 }
272
273 #[tokio::test]
276 async fn test_router_health_endpoint() {
277 use axum::body::Body;
278 use axum::http::{Request, StatusCode};
279 use tower::ServiceExt;
280
281 let state = Arc::new(AppState {
282 version: "5.0.0".to_string(),
283 });
284
285 let app = Router::new()
286 .route("/health", get(handlers::health))
287 .with_state(state);
288
289 let response = app
290 .oneshot(
291 Request::builder()
292 .uri("/health")
293 .body(Body::empty())
294 .unwrap(),
295 )
296 .await
297 .unwrap();
298
299 assert_eq!(response.status(), StatusCode::OK);
300 }
301
302 #[tokio::test]
303 async fn test_router_version_endpoint() {
304 use axum::body::Body;
305 use axum::http::{Request, StatusCode};
306 use tower::ServiceExt;
307
308 let state = Arc::new(AppState {
309 version: "5.0.0".to_string(),
310 });
311
312 let app = Router::new()
313 .route("/version", get(handlers::version))
314 .with_state(state);
315
316 let response = app
317 .oneshot(
318 Request::builder()
319 .uri("/version")
320 .body(Body::empty())
321 .unwrap(),
322 )
323 .await
324 .unwrap();
325
326 assert_eq!(response.status(), StatusCode::OK);
327 }
328
329 #[tokio::test]
330 async fn test_router_root_endpoint() {
331 use axum::body::Body;
332 use axum::http::{Request, StatusCode};
333 use tower::ServiceExt;
334
335 let state = Arc::new(AppState {
336 version: "5.0.0".to_string(),
337 });
338
339 let app = Router::new()
340 .route("/", get(handlers::root))
341 .with_state(state);
342
343 let response = app
344 .oneshot(Request::builder().uri("/").body(Body::empty()).unwrap())
345 .await
346 .unwrap();
347
348 assert_eq!(response.status(), StatusCode::OK);
349 }
350
351 #[tokio::test]
352 async fn test_router_validate_endpoint() {
353 use axum::body::Body;
354 use axum::http::{header, Method, Request, StatusCode};
355 use tower::ServiceExt;
356
357 let state = Arc::new(AppState {
358 version: "5.0.0".to_string(),
359 });
360
361 let app = Router::new()
362 .route("/api/v1/validate", post(handlers::validate))
363 .with_state(state);
364
365 let body = r#"{"file_path": "test-data/budget.yaml"}"#;
366
367 let response = app
368 .oneshot(
369 Request::builder()
370 .method(Method::POST)
371 .uri("/api/v1/validate")
372 .header(header::CONTENT_TYPE, "application/json")
373 .body(Body::from(body))
374 .unwrap(),
375 )
376 .await
377 .unwrap();
378
379 assert_eq!(response.status(), StatusCode::OK);
380 }
381
382 #[tokio::test]
383 async fn test_router_calculate_endpoint() {
384 use axum::body::Body;
385 use axum::http::{header, Method, Request, StatusCode};
386 use tower::ServiceExt;
387
388 let state = Arc::new(AppState {
389 version: "5.0.0".to_string(),
390 });
391
392 let app = Router::new()
393 .route("/api/v1/calculate", post(handlers::calculate))
394 .with_state(state);
395
396 let body = r#"{"file_path": "test-data/budget.yaml", "dry_run": true}"#;
397
398 let response = app
399 .oneshot(
400 Request::builder()
401 .method(Method::POST)
402 .uri("/api/v1/calculate")
403 .header(header::CONTENT_TYPE, "application/json")
404 .body(Body::from(body))
405 .unwrap(),
406 )
407 .await
408 .unwrap();
409
410 assert_eq!(response.status(), StatusCode::OK);
411 }
412
413 #[tokio::test]
414 async fn test_router_not_found() {
415 use axum::body::Body;
416 use axum::http::{Request, StatusCode};
417 use tower::ServiceExt;
418
419 let state = Arc::new(AppState {
420 version: "5.0.0".to_string(),
421 });
422
423 let app = Router::new()
424 .route("/health", get(handlers::health))
425 .with_state(state);
426
427 let response = app
428 .oneshot(
429 Request::builder()
430 .uri("/nonexistent")
431 .body(Body::empty())
432 .unwrap(),
433 )
434 .await
435 .unwrap();
436
437 assert_eq!(response.status(), StatusCode::NOT_FOUND);
438 }
439}