1pub mod api;
25pub mod monitor;
26
27use crate::chains::DefaultClientFactory;
28use crate::config::Config;
29use axum::Router;
30use axum::response::IntoResponse;
31use std::net::SocketAddr;
32use std::sync::Arc;
33use tower_http::cors::{Any, CorsLayer};
34
35pub struct AppState {
37 pub config: Config,
39 pub factory: DefaultClientFactory,
41}
42
43pub fn build_router(state: Arc<AppState>) -> Router {
45 let cors = CorsLayer::new()
46 .allow_origin(Any)
47 .allow_methods(Any)
48 .allow_headers(Any);
49
50 let api_routes = api::routes(state.clone());
51
52 Router::new()
53 .nest("/api", api_routes)
54 .route("/ws/monitor", axum::routing::get(monitor::ws_handler))
55 .fallback(axum::routing::get(serve_ui))
56 .layer(cors)
57 .with_state(state)
58}
59
60async fn serve_ui(uri: axum::http::Uri) -> impl axum::response::IntoResponse {
62 let path = uri.path().trim_start_matches('/');
63
64 match path {
66 "" | "index.html" => {
67 axum::response::Html(include_str!("static/index.html")).into_response()
68 }
69 "app.js" => (
70 [(axum::http::header::CONTENT_TYPE, "application/javascript")],
71 include_str!("static/app.js"),
72 )
73 .into_response(),
74 "style.css" => (
75 [(axum::http::header::CONTENT_TYPE, "text/css")],
76 include_str!("static/style.css"),
77 )
78 .into_response(),
79 "favicon.svg" => (
80 [(axum::http::header::CONTENT_TYPE, "image/svg+xml")],
81 include_str!("static/favicon.svg"),
82 )
83 .into_response(),
84 "favicon.ico" => {
85 axum::response::Redirect::permanent("/favicon.svg").into_response()
87 }
88 _ => axum::response::Html(include_str!("static/index.html")).into_response(),
90 }
91}
92
93pub async fn start_server(addr: SocketAddr, config: Config) -> anyhow::Result<()> {
97 let http: std::sync::Arc<dyn crate::http::HttpClient> = if config.ghola.enabled {
98 match crate::http::GholaHttpClient::new(config.ghola.stealth) {
99 Ok(client) => std::sync::Arc::new(client),
100 Err(_) => std::sync::Arc::new(
101 crate::http::NativeHttpClient::new().expect("Failed to create HTTP client"),
102 ),
103 }
104 } else {
105 std::sync::Arc::new(
106 crate::http::NativeHttpClient::new().expect("Failed to create HTTP client"),
107 )
108 };
109 let factory = DefaultClientFactory {
110 chains_config: config.chains.clone(),
111 http,
112 };
113 let state = Arc::new(AppState { config, factory });
114 let app = build_router(state);
115
116 tracing::info!("Scope web server listening on http://{}", addr);
117 eprintln!("Scope web server listening on http://{}", addr);
118
119 let listener = tokio::net::TcpListener::bind(addr).await?;
120 axum::serve(listener, app).await?;
121
122 Ok(())
123}
124
125pub fn pid_file_path() -> std::path::PathBuf {
131 Config::default_data_dir().join("scope-web.pid")
132}
133
134pub fn log_file_path() -> std::path::PathBuf {
136 Config::default_data_dir().join("scope-web.log")
137}
138
139pub fn stop_daemon() -> anyhow::Result<()> {
141 let pid_path = pid_file_path();
142 if !pid_path.exists() {
143 eprintln!("No daemon PID file found at {}", pid_path.display());
144 eprintln!("Is the daemon running?");
145 return Ok(());
146 }
147
148 let pid_str = std::fs::read_to_string(&pid_path)?;
149 let pid: u32 = pid_str
150 .trim()
151 .parse()
152 .map_err(|e| anyhow::anyhow!("Invalid PID in {}: {}", pid_path.display(), e))?;
153
154 eprintln!("Stopping Scope web daemon (PID {})...", pid);
155
156 #[cfg(unix)]
157 {
158 let result = unsafe { libc::kill(pid as libc::pid_t, libc::SIGTERM) };
160 if result == 0 {
161 eprintln!("Daemon stopped.");
162 } else {
163 eprintln!("Failed to stop daemon (process may have already exited).");
164 }
165 }
166
167 #[cfg(not(unix))]
168 {
169 eprintln!("Daemon stop is only supported on Unix systems.");
170 eprintln!("Please manually terminate PID {}.", pid);
171 }
172
173 let _ = std::fs::remove_file(&pid_path);
175 Ok(())
176}
177
178#[cfg(unix)]
183pub fn start_daemon(addr: SocketAddr, config: Config) -> anyhow::Result<()> {
184 use std::io::Write;
185
186 let _ = config; let data_dir = Config::default_data_dir();
189 std::fs::create_dir_all(&data_dir)?;
190
191 let pid_path = pid_file_path();
192 let log_path = log_file_path();
193
194 eprintln!("Starting Scope web daemon...");
195 eprintln!(" URL: http://{}", addr);
196 eprintln!(" PID: {}", pid_path.display());
197 eprintln!(" Log: {}", log_path.display());
198
199 let log_file = std::fs::OpenOptions::new()
201 .create(true)
202 .append(true)
203 .open(&log_path)?;
204 let log_file_err = log_file.try_clone()?;
205
206 let current_exe = std::env::current_exe()?;
207 let child = std::process::Command::new(current_exe)
208 .args([
209 "web",
210 "--port",
211 &addr.port().to_string(),
212 "--bind",
213 &addr.ip().to_string(),
214 ])
215 .env("SCOPE_WEB_DAEMON_CHILD", "1")
216 .stdout(log_file)
217 .stderr(log_file_err)
218 .stdin(std::process::Stdio::null())
219 .spawn()?;
220
221 let pid = child.id();
222
223 let mut f = std::fs::File::create(&pid_path)?;
225 write!(f, "{}", pid)?;
226
227 eprintln!("Daemon started with PID {}", pid);
228 eprintln!("Stop with: scope web --stop");
229
230 Ok(())
231}
232
233#[cfg(not(unix))]
235pub fn start_daemon(addr: SocketAddr, config: Config) -> anyhow::Result<()> {
236 eprintln!("Daemon mode is only supported on Unix systems.");
237 eprintln!("Starting in foreground instead...");
238 let rt = tokio::runtime::Runtime::new()?;
239 rt.block_on(start_server(addr, config))
240}
241
242pub fn is_daemon_child() -> bool {
244 std::env::var("SCOPE_WEB_DAEMON_CHILD").is_ok()
245}
246
247#[cfg(test)]
252mod tests {
253 use super::*;
254 use axum::body;
255
256 #[test]
257 fn test_pid_file_path() {
258 let path = pid_file_path();
259 assert!(path.to_string_lossy().contains("scope-web.pid"));
260 }
261
262 #[test]
263 fn test_log_file_path() {
264 let path = log_file_path();
265 assert!(path.to_string_lossy().contains("scope-web.log"));
266 }
267
268 #[test]
269 fn test_build_router() {
270 let config = Config::default();
271 let http: std::sync::Arc<dyn crate::http::HttpClient> =
272 std::sync::Arc::new(crate::http::NativeHttpClient::new().unwrap());
273 let factory = DefaultClientFactory {
274 chains_config: config.chains.clone(),
275 http,
276 };
277 let state = Arc::new(AppState { config, factory });
278 let _router = build_router(state);
279 }
280
281 #[test]
282 fn test_is_daemon_child_default() {
283 let _ = is_daemon_child();
286 }
287
288 #[tokio::test]
289 async fn test_serve_ui_index() {
290 let response = serve_ui(axum::http::Uri::from_static("/"))
291 .await
292 .into_response();
293 assert_eq!(response.status(), axum::http::StatusCode::OK);
294 let body = body::to_bytes(response.into_body(), 1_000_000)
295 .await
296 .unwrap();
297 let html = String::from_utf8(body.to_vec()).unwrap();
298 assert!(html.contains("<!DOCTYPE html>"));
299 assert!(html.contains("Scope"));
300 }
301
302 #[tokio::test]
303 async fn test_serve_ui_index_html() {
304 let response = serve_ui(axum::http::Uri::from_static("/index.html"))
305 .await
306 .into_response();
307 assert_eq!(response.status(), axum::http::StatusCode::OK);
308 }
309
310 #[tokio::test]
311 async fn test_serve_ui_app_js() {
312 let response = serve_ui(axum::http::Uri::from_static("/app.js"))
313 .await
314 .into_response();
315 assert_eq!(response.status(), axum::http::StatusCode::OK);
316 let body = body::to_bytes(response.into_body(), 1_000_000)
317 .await
318 .unwrap();
319 let js = String::from_utf8(body.to_vec()).unwrap();
320 assert!(js.contains("function") || js.contains("const") || js.contains("var"));
321 }
322
323 #[tokio::test]
324 async fn test_serve_ui_style_css() {
325 let response = serve_ui(axum::http::Uri::from_static("/style.css"))
326 .await
327 .into_response();
328 assert_eq!(response.status(), axum::http::StatusCode::OK);
329 let body = body::to_bytes(response.into_body(), 1_000_000)
330 .await
331 .unwrap();
332 let css = String::from_utf8(body.to_vec()).unwrap();
333 assert!(css.contains(":root") || css.contains("body") || css.contains("nav"));
334 }
335
336 #[tokio::test]
337 async fn test_serve_ui_spa_fallback() {
338 let response = serve_ui(axum::http::Uri::from_static("/some/random/path"))
340 .await
341 .into_response();
342 assert_eq!(response.status(), axum::http::StatusCode::OK);
343 let body = body::to_bytes(response.into_body(), 1_000_000)
344 .await
345 .unwrap();
346 let html = String::from_utf8(body.to_vec()).unwrap();
347 assert!(html.contains("<!DOCTYPE html>"));
348 }
349
350 #[tokio::test]
351 async fn test_serve_ui_favicon_svg() {
352 let response = serve_ui(axum::http::Uri::from_static("/favicon.svg"))
353 .await
354 .into_response();
355 assert_eq!(response.status(), axum::http::StatusCode::OK);
356 let headers = response.headers();
357 let ct = headers.get(axum::http::header::CONTENT_TYPE).unwrap();
358 assert_eq!(ct, "image/svg+xml");
359 }
360
361 #[tokio::test]
362 async fn test_serve_ui_favicon_ico_redirect() {
363 let response = serve_ui(axum::http::Uri::from_static("/favicon.ico"))
364 .await
365 .into_response();
366 assert_eq!(
367 response.status(),
368 axum::http::StatusCode::PERMANENT_REDIRECT
369 );
370 let headers = response.headers();
371 let loc = headers.get(axum::http::header::LOCATION).unwrap();
372 assert_eq!(loc, "/favicon.svg");
373 }
374
375 #[tokio::test]
376 async fn test_route_ws_monitor_handshake() {
377 use axum::http::{Request, StatusCode};
378 use tower::ServiceExt;
379
380 let state = test_state();
381 let app = build_router(state);
382
383 let req = Request::builder()
385 .uri("/ws/monitor?token=USDC")
386 .method("GET")
387 .header("Connection", "Upgrade")
388 .header("Upgrade", "websocket")
389 .header("Sec-WebSocket-Key", "dGhlIHNhbXBsZSBub25jZQ==")
390 .header("Sec-WebSocket-Version", "13")
391 .body(axum::body::Body::empty())
392 .unwrap();
393
394 let response = app.oneshot(req).await.unwrap();
395 let status = response.status();
397 assert!(
398 status == StatusCode::SWITCHING_PROTOCOLS
399 || status == StatusCode::UPGRADE_REQUIRED
400 || status.is_success(),
401 "Unexpected status: {}",
402 status
403 );
404 }
405
406 #[tokio::test]
407 async fn test_route_ws_monitor_missing_token() {
408 use axum::http::Request;
409 use tower::ServiceExt;
410
411 let state = test_state();
412 let app = build_router(state);
413
414 let req = Request::builder()
415 .uri("/ws/monitor")
416 .method("GET")
417 .body(axum::body::Body::empty())
418 .unwrap();
419
420 let response = app.oneshot(req).await.unwrap();
421 assert!(response.status().is_client_error());
423 }
424
425 #[test]
426 fn test_pid_file_path_in_data_dir() {
427 let path = pid_file_path();
428 assert!(path.ends_with("scope-web.pid"));
429 assert!(path.parent().is_some());
430 }
431
432 #[test]
433 fn test_log_file_path_in_data_dir() {
434 let path = log_file_path();
435 assert!(path.ends_with("scope-web.log"));
436 assert!(path.parent().is_some());
437 }
438
439 #[test]
440 fn test_app_state_construction() {
441 let config = Config::default();
442 let http: std::sync::Arc<dyn crate::http::HttpClient> =
443 std::sync::Arc::new(crate::http::NativeHttpClient::new().unwrap());
444 let factory = DefaultClientFactory {
445 chains_config: config.chains.clone(),
446 http,
447 };
448 let state = AppState { config, factory };
449 let _ = state.config.chains.api_keys.len();
451 }
452
453 #[test]
454 fn test_stop_daemon_no_pid_file() {
455 let pid = pid_file_path();
459 let log = log_file_path();
460 assert_eq!(pid.parent(), log.parent());
461 }
462
463 #[test]
464 fn test_stop_daemon_invoked_no_pid_file() {
465 let result = stop_daemon();
467 assert!(result.is_ok());
468 }
469
470 fn test_state() -> Arc<AppState> {
472 let config = Config::default();
473 let http: std::sync::Arc<dyn crate::http::HttpClient> =
474 std::sync::Arc::new(crate::http::NativeHttpClient::new().unwrap());
475 let factory = DefaultClientFactory {
476 chains_config: config.chains.clone(),
477 http,
478 };
479 Arc::new(AppState { config, factory })
480 }
481
482 #[tokio::test]
487 async fn test_route_get_config_status() {
488 use axum::http::{Request, StatusCode};
489 use tower::ServiceExt;
490
491 let state = test_state();
492 let app = build_router(state);
493
494 let req = Request::builder()
495 .uri("/api/config/status")
496 .method("GET")
497 .body(axum::body::Body::empty())
498 .unwrap();
499
500 let response = app.oneshot(req).await.unwrap();
501 assert_eq!(response.status(), StatusCode::OK);
502
503 let body = body::to_bytes(response.into_body(), 1_000_000)
504 .await
505 .unwrap();
506 let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
507 assert!(json.get("config_path").is_some());
508 assert!(json.get("api_keys").is_some());
509 assert!(json.get("version").is_some());
510 }
511
512 #[tokio::test]
513 async fn test_route_post_config_save() {
514 use axum::http::{Request, StatusCode, header};
515 use tower::ServiceExt;
516
517 let state = test_state();
518 let app = build_router(state);
519
520 let payload = serde_json::json!({
521 "api_keys": {},
522 "rpc_endpoints": {}
523 });
524
525 let req = Request::builder()
526 .uri("/api/config")
527 .method("POST")
528 .header(header::CONTENT_TYPE, "application/json")
529 .body(axum::body::Body::from(payload.to_string()))
530 .unwrap();
531
532 let response = app.oneshot(req).await.unwrap();
533 let status = response.status();
535 assert!(status == StatusCode::OK || status == StatusCode::INTERNAL_SERVER_ERROR);
536 }
537
538 #[tokio::test]
539 async fn test_route_post_address() {
540 use axum::http::{Request, StatusCode, header};
541 use tower::ServiceExt;
542
543 let state = test_state();
544 let app = build_router(state);
545
546 let payload = serde_json::json!({
547 "address": "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
548 "chain": "ethereum"
549 });
550
551 let req = Request::builder()
552 .uri("/api/address")
553 .method("POST")
554 .header(header::CONTENT_TYPE, "application/json")
555 .body(axum::body::Body::from(payload.to_string()))
556 .unwrap();
557
558 let response = app.oneshot(req).await.unwrap();
559 let status = response.status();
561 assert!(
562 status == StatusCode::OK
563 || status == StatusCode::BAD_REQUEST
564 || status == StatusCode::INTERNAL_SERVER_ERROR
565 );
566 }
567
568 #[tokio::test]
569 async fn test_route_post_tx() {
570 use axum::http::{Request, StatusCode, header};
571 use tower::ServiceExt;
572
573 let state = test_state();
574 let app = build_router(state);
575
576 let payload = serde_json::json!({
577 "hash": "0xabc123def456789012345678901234567890123456789012345678901234abcd",
578 "chain": "ethereum"
579 });
580
581 let req = Request::builder()
582 .uri("/api/tx")
583 .method("POST")
584 .header(header::CONTENT_TYPE, "application/json")
585 .body(axum::body::Body::from(payload.to_string()))
586 .unwrap();
587
588 let response = app.oneshot(req).await.unwrap();
589 let status = response.status();
590 assert!(
591 status == StatusCode::OK
592 || status == StatusCode::BAD_REQUEST
593 || status == StatusCode::INTERNAL_SERVER_ERROR
594 );
595 }
596
597 #[tokio::test]
598 async fn test_route_post_crawl() {
599 use axum::http::{Request, StatusCode, header};
600 use tower::ServiceExt;
601
602 let state = test_state();
603 let app = build_router(state);
604
605 let payload = serde_json::json!({
606 "token": "USDC",
607 "chain": "ethereum"
608 });
609
610 let req = Request::builder()
611 .uri("/api/crawl")
612 .method("POST")
613 .header(header::CONTENT_TYPE, "application/json")
614 .body(axum::body::Body::from(payload.to_string()))
615 .unwrap();
616
617 let response = app.oneshot(req).await.unwrap();
618 let status = response.status();
619 assert!(
620 status == StatusCode::OK
621 || status == StatusCode::BAD_REQUEST
622 || status == StatusCode::INTERNAL_SERVER_ERROR
623 );
624 }
625
626 #[tokio::test]
627 async fn test_route_get_discover() {
628 use axum::http::{Request, StatusCode};
629 use tower::ServiceExt;
630
631 let state = test_state();
632 let app = build_router(state);
633
634 let req = Request::builder()
635 .uri("/api/discover?source=profiles&limit=5")
636 .method("GET")
637 .body(axum::body::Body::empty())
638 .unwrap();
639
640 let response = app.oneshot(req).await.unwrap();
641 let status = response.status();
642 assert!(status == StatusCode::OK || status == StatusCode::INTERNAL_SERVER_ERROR);
643 }
644
645 #[tokio::test]
646 async fn test_route_post_insights() {
647 use axum::http::{Request, StatusCode, header};
648 use tower::ServiceExt;
649
650 let state = test_state();
651 let app = build_router(state);
652
653 let payload = serde_json::json!({
654 "target": "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2"
655 });
656
657 let req = Request::builder()
658 .uri("/api/insights")
659 .method("POST")
660 .header(header::CONTENT_TYPE, "application/json")
661 .body(axum::body::Body::from(payload.to_string()))
662 .unwrap();
663
664 let response = app.oneshot(req).await.unwrap();
665 let status = response.status();
666 assert!(
667 status == StatusCode::OK
668 || status == StatusCode::BAD_REQUEST
669 || status == StatusCode::INTERNAL_SERVER_ERROR
670 );
671 }
672
673 #[tokio::test]
674 async fn test_route_post_compliance_risk() {
675 use axum::http::{Request, StatusCode, header};
676 use tower::ServiceExt;
677
678 let state = test_state();
679 let app = build_router(state);
680
681 let payload = serde_json::json!({
682 "address": "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
683 "chain": "ethereum",
684 "detailed": true
685 });
686
687 let req = Request::builder()
688 .uri("/api/compliance/risk")
689 .method("POST")
690 .header(header::CONTENT_TYPE, "application/json")
691 .body(axum::body::Body::from(payload.to_string()))
692 .unwrap();
693
694 let response = app.oneshot(req).await.unwrap();
695 let status = response.status();
696 assert!(
697 status == StatusCode::OK
698 || status == StatusCode::BAD_REQUEST
699 || status == StatusCode::INTERNAL_SERVER_ERROR
700 );
701 }
702
703 #[tokio::test]
704 async fn test_route_post_export() {
705 use axum::http::{Request, StatusCode, header};
706 use tower::ServiceExt;
707
708 let state = test_state();
709 let app = build_router(state);
710
711 let payload = serde_json::json!({
712 "address": "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
713 "chain": "ethereum",
714 "format": "json"
715 });
716
717 let req = Request::builder()
718 .uri("/api/export")
719 .method("POST")
720 .header(header::CONTENT_TYPE, "application/json")
721 .body(axum::body::Body::from(payload.to_string()))
722 .unwrap();
723
724 let response = app.oneshot(req).await.unwrap();
725 let status = response.status();
726 assert!(
727 status == StatusCode::OK
728 || status == StatusCode::BAD_REQUEST
729 || status == StatusCode::INTERNAL_SERVER_ERROR
730 );
731 }
732
733 #[tokio::test]
734 async fn test_route_post_token_health() {
735 use axum::http::{Request, StatusCode, header};
736 use tower::ServiceExt;
737
738 let state = test_state();
739 let app = build_router(state);
740
741 let payload = serde_json::json!({
742 "token": "USDC",
743 "chain": "ethereum",
744 "with_market": false
745 });
746
747 let req = Request::builder()
748 .uri("/api/token-health")
749 .method("POST")
750 .header(header::CONTENT_TYPE, "application/json")
751 .body(axum::body::Body::from(payload.to_string()))
752 .unwrap();
753
754 let response = app.oneshot(req).await.unwrap();
755 let status = response.status();
756 assert!(
757 status == StatusCode::OK
758 || status == StatusCode::BAD_REQUEST
759 || status == StatusCode::INTERNAL_SERVER_ERROR
760 );
761 }
762
763 #[tokio::test]
764 async fn test_route_post_contract() {
765 use axum::http::{Request, StatusCode, header};
766 use tower::ServiceExt;
767
768 let state = test_state();
769 let app = build_router(state);
770
771 let payload = serde_json::json!({
772 "address": "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
773 "chain": "ethereum"
774 });
775
776 let req = Request::builder()
777 .uri("/api/contract")
778 .method("POST")
779 .header(header::CONTENT_TYPE, "application/json")
780 .body(axum::body::Body::from(payload.to_string()))
781 .unwrap();
782
783 let response = app.oneshot(req).await.unwrap();
784 let status = response.status();
785 assert!(
786 status == StatusCode::OK
787 || status == StatusCode::BAD_REQUEST
788 || status == StatusCode::INTERNAL_SERVER_ERROR
789 );
790 }
791
792 #[tokio::test]
793 async fn test_route_post_market_summary() {
794 use axum::http::{Request, StatusCode, header};
795 use tower::ServiceExt;
796
797 let state = test_state();
798 let app = build_router(state);
799
800 let payload = serde_json::json!({
801 "pair": "USDC",
802 "market_venue": "binance"
803 });
804
805 let req = Request::builder()
806 .uri("/api/market/summary")
807 .method("POST")
808 .header(header::CONTENT_TYPE, "application/json")
809 .body(axum::body::Body::from(payload.to_string()))
810 .unwrap();
811
812 let response = app.oneshot(req).await.unwrap();
813 let status = response.status();
814 assert!(
815 status == StatusCode::OK
816 || status == StatusCode::BAD_REQUEST
817 || status == StatusCode::INTERNAL_SERVER_ERROR
818 );
819 }
820
821 #[tokio::test]
822 async fn test_route_get_address_book_list() {
823 use axum::http::{Request, StatusCode};
824 use tower::ServiceExt;
825
826 let state = test_state();
827 let app = build_router(state);
828
829 let req = Request::builder()
830 .uri("/api/address-book/list")
831 .method("GET")
832 .body(axum::body::Body::empty())
833 .unwrap();
834
835 let response = app.oneshot(req).await.unwrap();
836 let status = response.status();
837 assert!(status == StatusCode::OK || status == StatusCode::INTERNAL_SERVER_ERROR);
838 }
839
840 #[tokio::test]
841 async fn test_route_post_address_book_add() {
842 use axum::http::{Request, StatusCode, header};
843 use tower::ServiceExt;
844
845 let state = test_state();
846 let app = build_router(state);
847
848 let payload = serde_json::json!({
849 "address": "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
850 "chain": "ethereum",
851 "label": "Test Wallet"
852 });
853
854 let req = Request::builder()
855 .uri("/api/address-book/add")
856 .method("POST")
857 .header(header::CONTENT_TYPE, "application/json")
858 .body(axum::body::Body::from(payload.to_string()))
859 .unwrap();
860
861 let response = app.oneshot(req).await.unwrap();
862 let status = response.status();
863 assert!(
864 status == StatusCode::OK
865 || status == StatusCode::BAD_REQUEST
866 || status == StatusCode::INTERNAL_SERVER_ERROR
867 );
868 }
869
870 #[tokio::test]
871 async fn test_route_invalid_json_body() {
872 use axum::http::{Request, header};
873 use tower::ServiceExt;
874
875 let state = test_state();
876 let app = build_router(state);
877
878 let req = Request::builder()
879 .uri("/api/address")
880 .method("POST")
881 .header(header::CONTENT_TYPE, "application/json")
882 .body(axum::body::Body::from("not json"))
883 .unwrap();
884
885 let response = app.oneshot(req).await.unwrap();
886 assert!(response.status().is_client_error());
888 }
889
890 #[tokio::test]
891 async fn test_route_missing_required_field() {
892 use axum::http::{Request, header};
893 use tower::ServiceExt;
894
895 let state = test_state();
896 let app = build_router(state);
897
898 let payload = serde_json::json!({ "chain": "ethereum" });
900
901 let req = Request::builder()
902 .uri("/api/address")
903 .method("POST")
904 .header(header::CONTENT_TYPE, "application/json")
905 .body(axum::body::Body::from(payload.to_string()))
906 .unwrap();
907
908 let response = app.oneshot(req).await.unwrap();
909 assert!(response.status().is_client_error());
910 }
911
912 #[tokio::test]
913 async fn test_route_static_fallback() {
914 use axum::http::{Request, StatusCode};
915 use tower::ServiceExt;
916
917 let state = test_state();
918 let app = build_router(state);
919
920 let req = Request::builder()
921 .uri("/nonexistent-path")
922 .method("GET")
923 .body(axum::body::Body::empty())
924 .unwrap();
925
926 let response = app.oneshot(req).await.unwrap();
927 assert_eq!(response.status(), StatusCode::OK);
928 }
929}