this/server/exposure/rest/
mod.rs1pub mod notifications;
10pub mod sse;
11
12use super::super::host::ServerHost;
13use crate::links::handlers::AppState;
14use crate::server::router::build_link_routes;
15use anyhow::Result;
16use axum::{Json, Router, routing::get};
17use serde_json::{Value, json};
18use std::sync::Arc;
19
20pub struct RestExposure;
26
27impl RestExposure {
28 pub fn build_router(host: Arc<ServerHost>, custom_routes: Vec<Router>) -> Result<Router> {
46 let link_state = AppState {
48 link_service: host.link_service.clone(),
49 config: host.config.clone(),
50 registry: host.registry.clone(),
51 entity_fetchers: host.entity_fetchers.clone(),
52 entity_creators: host.entity_creators.clone(),
53 event_bus: host.event_bus.clone(),
54 };
55
56 let health_routes = Self::health_routes();
58 let entity_routes = host.entity_registry.build_routes();
59 let link_routes = build_link_routes(link_state.clone());
60
61 let mut app = health_routes.merge(entity_routes);
63
64 for custom_router in custom_routes {
65 app = app.merge(custom_router);
66 }
67
68 app = app.merge(link_routes);
69
70 if let Some(event_bus) = &host.event_bus {
72 let sse_routes = Router::new()
73 .route("/events/stream", get(sse::sse_handler))
74 .with_state(event_bus.clone());
75 app = app.merge(sse_routes);
76 }
77
78 if let (Some(notification_store), Some(preferences_store), Some(device_token_store)) = (
81 &host.notification_store,
82 &host.preferences_store,
83 &host.device_token_store,
84 ) {
85 let notif_state = notifications::NotificationState {
86 notification_store: notification_store.clone(),
87 preferences_store: preferences_store.clone(),
88 device_token_store: device_token_store.clone(),
89 };
90 app = app.merge(notifications::notification_routes(notif_state));
91 }
92
93 Ok(app)
94 }
95
96 fn health_routes() -> Router {
98 Router::new()
99 .route("/health", get(Self::health_check))
100 .route("/healthz", get(Self::health_check))
101 }
102
103 async fn health_check() -> Json<Value> {
105 Json(json!({
106 "status": "ok",
107 "service": "this-rs"
108 }))
109 }
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115 use crate::config::LinksConfig;
116 use crate::server::entity_registry::EntityRegistry;
117 use crate::server::host::ServerHost;
118 use crate::storage::InMemoryLinkService;
119 use axum::body::Body;
120 use axum::http::{Request, StatusCode};
121 use std::collections::HashMap;
122 use tower::ServiceExt;
123
124 fn test_host() -> Arc<ServerHost> {
126 let host = ServerHost::from_builder_components(
127 Arc::new(InMemoryLinkService::new()),
128 LinksConfig::default_config(),
129 EntityRegistry::new(),
130 HashMap::new(),
131 HashMap::new(),
132 )
133 .expect("should build host");
134 Arc::new(host)
135 }
136
137 #[test]
138 fn test_health_routes_builds_router() {
139 let router = RestExposure::health_routes();
140 let _ = router;
141 }
142
143 #[tokio::test]
144 async fn test_health_endpoint_returns_ok() {
145 let router = RestExposure::health_routes();
146 let response = router
147 .oneshot(
148 Request::builder()
149 .uri("/health")
150 .body(Body::empty())
151 .expect("request should build"),
152 )
153 .await
154 .expect("response should succeed");
155 assert_eq!(response.status(), StatusCode::OK);
156
157 let body = axum::body::to_bytes(response.into_body(), 1024)
158 .await
159 .expect("body should read");
160 let json: serde_json::Value =
161 serde_json::from_slice(&body).expect("body should be valid JSON");
162 assert_eq!(json["status"], "ok");
163 assert_eq!(json["service"], "this-rs");
164 }
165
166 #[tokio::test]
167 async fn test_healthz_endpoint_returns_ok() {
168 let router = RestExposure::health_routes();
169 let response = router
170 .oneshot(
171 Request::builder()
172 .uri("/healthz")
173 .body(Body::empty())
174 .expect("request should build"),
175 )
176 .await
177 .expect("response should succeed");
178 assert_eq!(response.status(), StatusCode::OK);
179
180 let body = axum::body::to_bytes(response.into_body(), 1024)
181 .await
182 .expect("body should read");
183 let json: serde_json::Value =
184 serde_json::from_slice(&body).expect("body should be valid JSON");
185 assert_eq!(json["status"], "ok");
186 }
187
188 #[test]
189 fn test_build_router_succeeds_with_host() {
190 let host = test_host();
191 let router = RestExposure::build_router(host, vec![]);
192 assert!(router.is_ok());
193 }
194
195 #[test]
196 fn test_build_router_with_custom_routes() {
197 use axum::routing::get;
198
199 let host = test_host();
200 let custom = Router::new().route("/custom", get(|| async { "custom" }));
201 let router = RestExposure::build_router(host, vec![custom]);
202 assert!(router.is_ok());
203 }
204
205 #[tokio::test]
206 async fn test_build_router_health_endpoint_reachable() {
207 let host = test_host();
208 let router = RestExposure::build_router(host, vec![]).expect("build should succeed");
209 let response = router
210 .oneshot(
211 Request::builder()
212 .uri("/health")
213 .body(Body::empty())
214 .expect("request should build"),
215 )
216 .await
217 .expect("response should succeed");
218 assert_eq!(response.status(), StatusCode::OK);
219 }
220}