1pub mod auth;
22pub mod handlers;
23pub mod metrics;
24pub mod server;
25pub mod state;
26pub mod types;
27
28pub use server::{create_router, serve};
30pub use state::AppState;
31pub use types::HttpAdapterError;
32
33#[cfg(test)]
36mod tests {
37 use std::{num::NonZeroUsize, sync::Arc};
38
39 use axum::{
40 body::Body,
41 http::{self, Request},
42 routing::{get, post},
43 Router,
44 };
45 use tokio::sync::Mutex;
46 use tower::util::ServiceExt;
47 use uuid::Uuid;
48
49 use super::handlers::*;
50 use super::metrics::Metrics;
51 use super::state::{AppState, CACHE_CAPACITY};
52 use super::types::*;
53
54 async fn make_router() -> (Router, tempfile::TempDir, String) {
56 let temp = tempfile::tempdir().unwrap();
57 let mut config = brain_core::BrainConfig::default();
58 config.brain.data_dir = temp.path().to_str().unwrap().to_string();
59 let api_key = config.access.api_keys.first().unwrap().key.clone();
60 let api_keys = config.access.api_keys.clone();
61 let processor = signal::SignalProcessor::new(config).await.unwrap();
62 let router = crate::create_router(
63 Arc::new(processor),
64 std::collections::HashMap::new(),
65 api_keys,
66 true,
67 );
68 (router, temp, api_key)
69 }
70
71 #[test]
72 fn test_parse_source_defaults_to_http() {
73 assert_eq!(
74 signal::SignalSource::parse(None, signal::SignalSource::Http),
75 signal::SignalSource::Http
76 );
77 assert_eq!(
78 signal::SignalSource::parse(Some("http"), signal::SignalSource::Http),
79 signal::SignalSource::Http
80 );
81 }
82
83 #[test]
84 fn test_parse_source_all_variants() {
85 assert_eq!(
86 signal::SignalSource::parse(Some("cli"), signal::SignalSource::Http),
87 signal::SignalSource::Cli
88 );
89 assert_eq!(
90 signal::SignalSource::parse(Some("ws"), signal::SignalSource::Http),
91 signal::SignalSource::WebSocket
92 );
93 assert_eq!(
94 signal::SignalSource::parse(Some("mcp"), signal::SignalSource::Http),
95 signal::SignalSource::Mcp
96 );
97 assert_eq!(
98 signal::SignalSource::parse(Some("grpc"), signal::SignalSource::Http),
99 signal::SignalSource::Grpc
100 );
101 }
102
103 #[test]
104 fn test_health_response_serializes() {
105 let h = HealthResponse {
106 status: "ok",
107 version: "1.0.0",
108 };
109 let json = serde_json::to_string(&h).unwrap();
110 assert!(json.contains("\"status\":\"ok\""));
111 assert!(json.contains("\"version\""));
112 }
113
114 #[test]
115 fn test_fact_json_serializes() {
116 let f = FactJson {
117 id: "abc".into(),
118 namespace: "personal".into(),
119 category: "personal".into(),
120 subject: "user".into(),
121 predicate: "likes".into(),
122 object: "Rust".into(),
123 confidence: 0.9,
124 distance: Some(0.05),
125 };
126 let json = serde_json::to_string(&f).unwrap();
127 assert!(json.contains("\"subject\":\"user\""));
128 assert!(json.contains("\"namespace\":\"personal\""));
129 assert!(json.contains("\"distance\":0.05"));
130 }
131
132 #[tokio::test]
134 async fn test_openapi_endpoint() {
135 let (router, _tmp, _api_key) = make_router().await;
136
137 let request = Request::builder()
138 .method(http::Method::GET)
139 .uri("/openapi.json")
140 .body(Body::empty())
141 .unwrap();
142
143 let response = router.oneshot(request).await.unwrap();
144 assert_eq!(response.status(), http::StatusCode::OK);
145
146 let bytes = axum::body::to_bytes(response.into_body(), usize::MAX)
147 .await
148 .unwrap();
149 let spec: serde_json::Value = serde_json::from_slice(&bytes).expect("valid JSON");
150 assert_eq!(spec["openapi"], "3.0.3");
151 assert!(
152 spec["paths"]["/v1/signals"].is_object(),
153 "missing /v1/signals path"
154 );
155 assert!(
156 spec["components"]["schemas"]["FactJson"].is_object(),
157 "missing FactJson schema"
158 );
159 }
160
161 #[tokio::test]
163 async fn test_swagger_ui_endpoint() {
164 let (router, _tmp, _api_key) = make_router().await;
165
166 let request = Request::builder()
167 .method(http::Method::GET)
168 .uri("/api")
169 .body(Body::empty())
170 .unwrap();
171
172 let response = router.oneshot(request).await.unwrap();
173 assert_eq!(response.status(), http::StatusCode::OK);
174
175 let bytes = axum::body::to_bytes(response.into_body(), usize::MAX)
176 .await
177 .unwrap();
178 let body = std::str::from_utf8(&bytes).unwrap();
179 assert!(body.contains("swagger-ui"), "missing Swagger UI element");
180 assert!(body.contains("/openapi.json"), "missing spec URL reference");
181 }
182
183 #[tokio::test]
185 async fn test_ui_endpoint() {
186 let (router, _tmp, _api_key) = make_router().await;
187
188 let request = Request::builder()
189 .method(http::Method::GET)
190 .uri("/ui")
191 .body(Body::empty())
192 .unwrap();
193
194 let response = router.oneshot(request).await.unwrap();
195 assert_eq!(response.status(), http::StatusCode::OK);
196
197 let ct = response
198 .headers()
199 .get("content-type")
200 .unwrap()
201 .to_str()
202 .unwrap();
203 assert!(ct.contains("text/html"), "expected text/html, got: {ct}");
204
205 let bytes = axum::body::to_bytes(response.into_body(), usize::MAX)
206 .await
207 .unwrap();
208 let body = std::str::from_utf8(&bytes).unwrap();
209 assert!(body.contains("Brain Memory Explorer"), "missing page title");
210 assert!(
211 body.contains("/v1/memory/search"),
212 "missing API endpoint reference"
213 );
214 }
215
216 #[tokio::test]
218 async fn test_metrics_endpoint() {
219 let (router, _tmp, _api_key) = make_router().await;
220
221 let request = Request::builder()
222 .method(http::Method::GET)
223 .uri("/metrics")
224 .body(Body::empty())
225 .unwrap();
226
227 let response = router.oneshot(request).await.unwrap();
228 assert_eq!(response.status(), http::StatusCode::OK);
229
230 let ct = response
231 .headers()
232 .get("content-type")
233 .unwrap()
234 .to_str()
235 .unwrap();
236 assert!(ct.contains("text/plain"), "expected text/plain, got: {ct}");
237
238 let bytes = axum::body::to_bytes(response.into_body(), usize::MAX)
239 .await
240 .unwrap();
241 let body = std::str::from_utf8(&bytes).unwrap();
242 assert!(
243 body.contains("brain_signals_total"),
244 "missing counter in metrics output"
245 );
246 assert!(
247 body.contains("brain_search_total"),
248 "missing search counter"
249 );
250 }
251
252 #[tokio::test]
254 async fn test_health_endpoint() {
255 let (router, _tmp, _api_key) = make_router().await;
256
257 let request = Request::builder()
258 .method(http::Method::GET)
259 .uri("/health")
260 .body(Body::empty())
261 .unwrap();
262
263 let response = router.oneshot(request).await.unwrap();
264 assert_eq!(response.status(), http::StatusCode::OK);
265
266 let bytes = axum::body::to_bytes(response.into_body(), usize::MAX)
267 .await
268 .unwrap();
269 let body: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
270 assert_eq!(body["status"], "ok");
271 }
272
273 #[tokio::test]
275 async fn test_post_signal_no_auth_returns_401() {
276 let (router, _tmp, _api_key) = make_router().await;
277
278 let payload = serde_json::json!({"content": "Remember Rust is fast"});
279 let request = Request::builder()
280 .method(http::Method::POST)
281 .uri("/v1/signals")
282 .header("content-type", "application/json")
283 .body(Body::from(serde_json::to_string(&payload).unwrap()))
284 .unwrap();
285
286 let response = router.oneshot(request).await.unwrap();
287 assert_eq!(response.status(), http::StatusCode::UNAUTHORIZED);
288 }
289
290 #[tokio::test]
292 async fn test_post_signal_invalid_key_returns_401() {
293 let (router, _tmp, _api_key) = make_router().await;
294
295 let payload = serde_json::json!({"content": "Remember Rust is fast"});
296 let request = Request::builder()
297 .method(http::Method::POST)
298 .uri("/v1/signals")
299 .header("content-type", "application/json")
300 .header("authorization", "Bearer wrong-key")
301 .body(Body::from(serde_json::to_string(&payload).unwrap()))
302 .unwrap();
303
304 let response = router.oneshot(request).await.unwrap();
305 assert_eq!(response.status(), http::StatusCode::UNAUTHORIZED);
306 }
307
308 #[tokio::test]
310 async fn test_post_signal_store_fact_with_auth() {
311 let (router, _tmp, api_key) = make_router().await;
312
313 let payload = serde_json::json!({"content": "Remember that Rust is fast"});
314 let request = Request::builder()
315 .method(http::Method::POST)
316 .uri("/v1/signals")
317 .header("content-type", "application/json")
318 .header("authorization", format!("Bearer {api_key}"))
319 .body(Body::from(serde_json::to_string(&payload).unwrap()))
320 .unwrap();
321
322 let response = router.oneshot(request).await.unwrap();
323 assert_eq!(response.status(), http::StatusCode::OK);
324
325 let bytes = axum::body::to_bytes(response.into_body(), usize::MAX)
326 .await
327 .unwrap();
328 let resp: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
329 assert_eq!(resp["status"], "Ok");
330 }
331
332 #[tokio::test]
334 async fn test_get_facts_no_auth_returns_401() {
335 let (router, _tmp, _api_key) = make_router().await;
336
337 let request = Request::builder()
338 .method(http::Method::GET)
339 .uri("/v1/memory/facts")
340 .body(Body::empty())
341 .unwrap();
342
343 let response = router.oneshot(request).await.unwrap();
344 assert_eq!(response.status(), http::StatusCode::UNAUTHORIZED);
345 }
346
347 #[tokio::test]
349 async fn test_get_facts_endpoint_with_auth() {
350 let (router, _tmp, api_key) = make_router().await;
351
352 let request = Request::builder()
353 .method(http::Method::GET)
354 .uri("/v1/memory/facts")
355 .header("authorization", format!("Bearer {api_key}"))
356 .body(Body::empty())
357 .unwrap();
358
359 let response = router.oneshot(request).await.unwrap();
360 assert_eq!(response.status(), http::StatusCode::OK);
361
362 let bytes = axum::body::to_bytes(response.into_body(), usize::MAX)
363 .await
364 .unwrap();
365 let body: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
366 assert!(body.is_array());
367 }
368
369 #[tokio::test]
371 async fn test_search_with_read_only_key() {
372 let temp = tempfile::tempdir().unwrap();
373 let mut config = brain_core::BrainConfig::default();
374 config.brain.data_dir = temp.path().to_str().unwrap().to_string();
375 config.access.api_keys.push(brain_core::ApiKeyConfig {
376 key: "read-only-key".to_string(),
377 name: "Read Only".to_string(),
378 permissions: vec!["read".to_string()],
379 });
380 let api_keys = config.access.api_keys.clone();
381 let processor = signal::SignalProcessor::new(config).await.unwrap();
382 let router = crate::create_router(
383 Arc::new(processor),
384 std::collections::HashMap::new(),
385 api_keys,
386 true,
387 );
388
389 let payload = serde_json::json!({"query": "Rust", "top_k": 5});
390 let request = Request::builder()
391 .method(http::Method::POST)
392 .uri("/v1/memory/search")
393 .header("content-type", "application/json")
394 .header("authorization", "Bearer read-only-key")
395 .body(Body::from(serde_json::to_string(&payload).unwrap()))
396 .unwrap();
397
398 let response = router.oneshot(request).await.unwrap();
399 assert_eq!(response.status(), http::StatusCode::OK);
400 }
401
402 #[tokio::test]
404 async fn test_post_signal_read_only_key_returns_401() {
405 let temp = tempfile::tempdir().unwrap();
406 let mut config = brain_core::BrainConfig::default();
407 config.brain.data_dir = temp.path().to_str().unwrap().to_string();
408 config.access.api_keys.push(brain_core::ApiKeyConfig {
409 key: "read-only-key".to_string(),
410 name: "Read Only".to_string(),
411 permissions: vec!["read".to_string()],
412 });
413 let api_keys = config.access.api_keys.clone();
414 let processor = signal::SignalProcessor::new(config).await.unwrap();
415 let router = crate::create_router(
416 Arc::new(processor),
417 std::collections::HashMap::new(),
418 api_keys,
419 true,
420 );
421
422 let payload = serde_json::json!({"content": "Remember something"});
423 let request = Request::builder()
424 .method(http::Method::POST)
425 .uri("/v1/signals")
426 .header("content-type", "application/json")
427 .header("authorization", "Bearer read-only-key")
428 .body(Body::from(serde_json::to_string(&payload).unwrap()))
429 .unwrap();
430
431 let response = router.oneshot(request).await.unwrap();
432 assert_eq!(response.status(), http::StatusCode::UNAUTHORIZED);
433 }
434
435 #[tokio::test]
437 async fn test_http_store_signal_fact_persisted_in_db() {
438 let temp = tempfile::tempdir().unwrap();
439 let mut config = brain_core::BrainConfig::default();
440 config.brain.data_dir = temp.path().to_str().unwrap().to_string();
441 let api_key = config.access.api_keys.first().unwrap().key.clone();
442 let api_keys = config.access.api_keys.clone();
443 let processor = Arc::new(signal::SignalProcessor::new(config).await.unwrap());
444 let state = Arc::new(AppState {
445 processor,
446 webhook_handlers: std::collections::HashMap::new(),
447 cache: Mutex::new(lru::LruCache::new(
448 NonZeroUsize::new(CACHE_CAPACITY).unwrap(),
449 )),
450 api_keys,
451 metrics: Arc::new(Metrics::default()),
452 });
453
454 let payload = serde_json::json!({"content": "Remember that Rust is fast"});
455 let post_req = Request::builder()
456 .method(http::Method::POST)
457 .uri("/v1/signals")
458 .header("content-type", "application/json")
459 .header("authorization", format!("Bearer {api_key}"))
460 .body(Body::from(serde_json::to_string(&payload).unwrap()))
461 .unwrap();
462
463 let router = Router::new()
464 .route("/v1/signals", post(post_signal_handler))
465 .route("/v1/memory/facts", get(get_facts_handler))
466 .with_state(state.clone());
467
468 let post_resp = router.clone().oneshot(post_req).await.unwrap();
469 assert_eq!(post_resp.status(), http::StatusCode::OK);
470
471 let bytes = axum::body::to_bytes(post_resp.into_body(), usize::MAX)
472 .await
473 .unwrap();
474 let resp_json: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
475 assert_eq!(resp_json["status"], "Ok");
476 assert!(resp_json["memory_context"].is_object());
477
478 let get_req = Request::builder()
479 .method(http::Method::GET)
480 .uri("/v1/memory/facts")
481 .header("authorization", format!("Bearer {api_key}"))
482 .body(Body::empty())
483 .unwrap();
484
485 let get_resp = router.oneshot(get_req).await.unwrap();
486 assert_eq!(get_resp.status(), http::StatusCode::OK);
487
488 let bytes = axum::body::to_bytes(get_resp.into_body(), usize::MAX)
489 .await
490 .unwrap();
491 let facts: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
492 assert!(facts.is_array(), "Expected array of facts");
493 assert!(
494 !facts.as_array().unwrap().is_empty(),
495 "Stored fact should appear in GET /v1/memory/facts"
496 );
497 }
498
499 #[tokio::test]
501 async fn test_http_memory_search_returns_stored_fact() {
502 let temp = tempfile::tempdir().unwrap();
503 let mut config = brain_core::BrainConfig::default();
504 config.brain.data_dir = temp.path().to_str().unwrap().to_string();
505 let api_key = config.access.api_keys.first().unwrap().key.clone();
506 let api_keys = config.access.api_keys.clone();
507 let processor = Arc::new(signal::SignalProcessor::new(config).await.unwrap());
508
509 let _ = processor
510 .store_fact_direct("personal", "test", "Ferris", "is", "the Rust mascot", None)
511 .await
512 .unwrap();
513
514 let state = Arc::new(AppState {
515 processor,
516 webhook_handlers: std::collections::HashMap::new(),
517 cache: Mutex::new(lru::LruCache::new(
518 NonZeroUsize::new(CACHE_CAPACITY).unwrap(),
519 )),
520 api_keys,
521 metrics: Arc::new(Metrics::default()),
522 });
523
524 let router = Router::new()
525 .route("/v1/memory/search", post(search_memory_handler))
526 .with_state(state);
527
528 let payload = serde_json::json!({"query": "Ferris Rust mascot", "top_k": 5});
529 let request = Request::builder()
530 .method(http::Method::POST)
531 .uri("/v1/memory/search")
532 .header("content-type", "application/json")
533 .header("authorization", format!("Bearer {api_key}"))
534 .body(Body::from(serde_json::to_string(&payload).unwrap()))
535 .unwrap();
536
537 let response = router.oneshot(request).await.unwrap();
538 assert_eq!(response.status(), http::StatusCode::OK);
539
540 let bytes = axum::body::to_bytes(response.into_body(), usize::MAX)
541 .await
542 .unwrap();
543 let results: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
544 assert!(results.is_array(), "Expected array of search results");
545 }
546
547 #[tokio::test]
549 async fn test_get_cached_signal_with_auth() {
550 let temp = tempfile::tempdir().unwrap();
551 let mut config = brain_core::BrainConfig::default();
552 config.brain.data_dir = temp.path().to_str().unwrap().to_string();
553 let api_key = config.access.api_keys.first().unwrap().key.clone();
554 let api_keys = config.access.api_keys.clone();
555 let processor = Arc::new(signal::SignalProcessor::new(config).await.unwrap());
556 let state = Arc::new(AppState {
557 processor,
558 webhook_handlers: std::collections::HashMap::new(),
559 cache: Mutex::new(lru::LruCache::new(
560 NonZeroUsize::new(CACHE_CAPACITY).unwrap(),
561 )),
562 api_keys,
563 metrics: Arc::new(Metrics::default()),
564 });
565
566 let id = Uuid::new_v4();
567 let fake_resp = signal::SignalResponse::ok(id, "test response");
568 state.cache.lock().await.put(id, fake_resp);
569
570 let router = Router::new()
571 .route("/v1/signals/:id", get(get_signal_handler))
572 .with_state(state);
573
574 let request = Request::builder()
575 .method(http::Method::GET)
576 .uri(format!("/v1/signals/{id}"))
577 .header("authorization", format!("Bearer {api_key}"))
578 .body(Body::empty())
579 .unwrap();
580
581 let response = router.oneshot(request).await.unwrap();
582 assert_eq!(response.status(), http::StatusCode::OK);
583 }
584}