use std::sync::Arc;
use axum::body::Body;
use axum::http::{Request, StatusCode};
use http_body_util::BodyExt;
use tower::ServiceExt;
use mnemo_core::embedding::DeterministicEmbedding;
use mnemo_core::index::usearch::UsearchIndex;
use mnemo_core::query::MnemoEngine;
use mnemo_core::storage::duckdb::DuckDbStorage;
fn create_test_engine() -> Arc<MnemoEngine> {
let storage = Arc::new(DuckDbStorage::open_in_memory().unwrap());
let index = Arc::new(UsearchIndex::new(128).unwrap());
let embedding = Arc::new(DeterministicEmbedding::new(128));
Arc::new(MnemoEngine::new(
storage,
index,
embedding,
"test-agent".to_string(),
None,
))
}
#[tokio::test]
async fn test_rest_health() {
let engine = create_test_engine();
let app = mnemo_rest::router(engine);
let response = app
.oneshot(
Request::builder()
.uri("/v1/health")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = response.into_body().collect().await.unwrap().to_bytes();
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(json["status"], "ok");
}
#[tokio::test]
async fn test_rest_remember_endpoint() {
let engine = create_test_engine();
let app = mnemo_rest::router(engine);
let body = serde_json::json!({
"content": "REST API test memory",
"importance": 0.85
});
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/v1/memories")
.header("content-type", "application/json")
.body(Body::from(serde_json::to_string(&body).unwrap()))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = response.into_body().collect().await.unwrap().to_bytes();
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert!(json["id"].is_string());
assert!(json["content_hash"].is_string());
}
#[tokio::test]
async fn test_rest_recall_endpoint() {
let engine = create_test_engine();
engine
.remember(mnemo_core::query::remember::RememberRequest {
content: "Recall test content for REST".to_string(),
agent_id: None,
memory_type: None,
scope: None,
importance: Some(0.7),
tags: None,
metadata: None,
source_type: None,
source_id: None,
related_to: None,
org_id: None,
thread_id: None,
ttl_seconds: None,
decay_rate: None,
created_by: None,
})
.await
.unwrap();
let app = mnemo_rest::router(engine);
let response = app
.oneshot(
Request::builder()
.uri("/v1/memories?query=recall+test&strategy=exact")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = response.into_body().collect().await.unwrap().to_bytes();
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert!(json["total"].as_u64().unwrap() >= 1);
assert!(json["memories"].is_array());
}
#[tokio::test]
async fn test_rest_forget_endpoint() {
let engine = create_test_engine();
let mem = engine
.remember(mnemo_core::query::remember::RememberRequest {
content: "Memory to forget via REST".to_string(),
agent_id: None,
memory_type: None,
scope: None,
importance: Some(0.5),
tags: None,
metadata: None,
source_type: None,
source_id: None,
related_to: None,
org_id: None,
thread_id: None,
ttl_seconds: None,
decay_rate: None,
created_by: None,
})
.await
.unwrap();
let app = mnemo_rest::router(engine);
let response = app
.oneshot(
Request::builder()
.method("DELETE")
.uri(format!("/v1/memories/{}", mem.id))
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = response.into_body().collect().await.unwrap().to_bytes();
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert!(json["forgotten"].is_array());
assert_eq!(json["forgotten"].as_array().unwrap().len(), 1);
}
#[tokio::test]
async fn test_rest_get_memory_endpoint() {
let engine = create_test_engine();
let mem = engine
.remember(mnemo_core::query::remember::RememberRequest {
content: "Get by ID via REST".to_string(),
agent_id: None,
memory_type: None,
scope: None,
importance: Some(0.6),
tags: None,
metadata: None,
source_type: None,
source_id: None,
related_to: None,
org_id: None,
thread_id: None,
ttl_seconds: None,
decay_rate: None,
created_by: None,
})
.await
.unwrap();
let app = mnemo_rest::router(engine);
let response = app
.oneshot(
Request::builder()
.uri(format!("/v1/memories/{}", mem.id))
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = response.into_body().collect().await.unwrap().to_bytes();
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(json["content"], "Get by ID via REST");
assert_eq!(json["id"], mem.id.to_string());
}
#[tokio::test]
async fn test_rest_verify_endpoint() {
let engine = create_test_engine();
engine
.remember(mnemo_core::query::remember::RememberRequest {
content: "Chain link 1".to_string(),
agent_id: None,
memory_type: None,
scope: None,
importance: Some(0.7),
tags: None,
metadata: None,
source_type: None,
source_id: None,
related_to: None,
org_id: None,
thread_id: None,
ttl_seconds: None,
decay_rate: None,
created_by: None,
})
.await
.unwrap();
let app = mnemo_rest::router(engine);
let body = serde_json::json!({
"agent_id": "test-agent"
});
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/v1/verify")
.header("content-type", "application/json")
.body(Body::from(serde_json::to_string(&body).unwrap()))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = response.into_body().collect().await.unwrap().to_bytes();
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert!(json["valid"].as_bool().unwrap());
assert_eq!(json["status"], "verified");
}
#[tokio::test]
async fn test_rest_not_found_memory() {
let engine = create_test_engine();
let app = mnemo_rest::router(engine);
let fake_id = uuid::Uuid::now_v7();
let response = app
.oneshot(
Request::builder()
.uri(format!("/v1/memories/{}", fake_id))
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn auth_rejects_missing_token() {
let app = mnemo_rest::router_with_auth(create_test_engine(), Some("s3cret".to_string()));
let response = app
.oneshot(
Request::builder()
.uri("/v1/memories?query=x&limit=1")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn auth_rejects_wrong_token() {
let app = mnemo_rest::router_with_auth(create_test_engine(), Some("s3cret".to_string()));
let response = app
.oneshot(
Request::builder()
.uri("/v1/memories?query=x&limit=1")
.header("authorization", "Bearer nope")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn auth_accepts_correct_token() {
let app = mnemo_rest::router_with_auth(create_test_engine(), Some("s3cret".to_string()));
let response = app
.oneshot(
Request::builder()
.uri("/v1/memories?query=x&limit=1")
.header("authorization", "Bearer s3cret")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
}
#[tokio::test]
async fn auth_exempts_health_endpoint() {
let app = mnemo_rest::router_with_auth(create_test_engine(), Some("s3cret".to_string()));
let response = app
.oneshot(
Request::builder()
.uri("/v1/health")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
}
#[tokio::test]
async fn no_token_runs_open() {
let app = mnemo_rest::router_with_auth(create_test_engine(), None);
let response = app
.oneshot(
Request::builder()
.uri("/v1/memories?query=x&limit=1")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
}
#[tokio::test]
async fn test_rest_provenance_read_and_forget() {
let engine = create_test_engine();
let app = mnemo_rest::router(engine);
let body = serde_json::json!({
"content": "provenance over REST",
"created_by": "alice",
"thread_id": "sess-rest"
});
let resp = app
.clone()
.oneshot(
Request::builder()
.method("POST")
.uri("/v1/memories")
.header("content-type", "application/json")
.body(Body::from(serde_json::to_string(&body).unwrap()))
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let bytes = resp.into_body().collect().await.unwrap().to_bytes();
let created: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
let id = created["id"].as_str().unwrap().to_string();
let resp = app
.clone()
.oneshot(
Request::builder()
.uri(format!("/v1/memories/{id}/provenance"))
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let bytes = resp.into_body().collect().await.unwrap().to_bytes();
let prov: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
assert_eq!(prov["principal"], "alice");
assert_eq!(prov["op"], "remember");
assert_eq!(prov["memory_id"], id);
let resp = app
.clone()
.oneshot(
Request::builder()
.uri("/v1/provenance/principal/alice")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let bytes = resp.into_body().collect().await.unwrap().to_bytes();
let writes: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
assert_eq!(writes.as_array().unwrap().len(), 1);
let forget_body = serde_json::json!({"principal": "alice", "strategy": "hard_delete"});
let resp = app
.clone()
.oneshot(
Request::builder()
.method("POST")
.uri("/v1/provenance/forget")
.header("content-type", "application/json")
.body(Body::from(serde_json::to_string(&forget_body).unwrap()))
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let bytes = resp.into_body().collect().await.unwrap().to_bytes();
let forgotten: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
assert_eq!(forgotten["forgotten"].as_array().unwrap().len(), 1);
let resp = app
.oneshot(
Request::builder()
.uri(format!("/v1/memories/{id}"))
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn test_rest_forget_by_provenance_requires_selector() {
let app = mnemo_rest::router(create_test_engine());
let resp = app
.oneshot(
Request::builder()
.method("POST")
.uri("/v1/provenance/forget")
.header("content-type", "application/json")
.body(Body::from(
serde_json::json!({"strategy": "soft_delete"}).to_string(),
))
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}