#![allow(clippy::doc_markdown)]
mod common;
use axum::{
body::Body,
http::{Request, StatusCode},
};
use common::create_test_app;
use serde_json::{json, Value};
use tempfile::TempDir;
use tower::ServiceExt;
const TEST_COLLECTION: &str = "admin_endpoints_bdd";
const DIM: usize = 4;
async fn seed_collection(app: axum::Router, n: usize) {
let response = app
.clone()
.oneshot(
Request::builder()
.method("POST")
.uri("/collections")
.header("Content-Type", "application/json")
.body(Body::from(
json!({
"name": TEST_COLLECTION,
"dimension": DIM,
"metric": "cosine"
})
.to_string(),
))
.expect("test: build create request"),
)
.await
.expect("test: collection create request");
assert_eq!(
response.status(),
StatusCode::CREATED,
"test setup: failed to create collection"
);
if n == 0 {
return;
}
let points: Vec<Value> = (0..n)
.map(|i| {
#[allow(clippy::cast_precision_loss)]
let v = (i as f32) / 100.0;
json!({
"id": u64::try_from(i).expect("test: idx fits in u64") + 1,
"vector": vec![v; DIM],
"payload": { "idx": i }
})
})
.collect();
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri(format!("/collections/{TEST_COLLECTION}/points"))
.header("Content-Type", "application/json")
.body(Body::from(json!({ "points": points }).to_string()))
.expect("test: build upsert request"),
)
.await
.expect("test: upsert request");
assert_eq!(
response.status(),
StatusCode::OK,
"test setup: failed to seed points"
);
}
async fn read_json(response: axum::response::Response) -> Value {
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.expect("test: read body");
serde_json::from_slice(&body).expect("test: response is valid JSON")
}
#[tokio::test]
async fn bulk_delete_nominal_returns_200_with_deleted_count() {
let temp_dir = TempDir::new().expect("test: temp dir");
let app = create_test_app(&temp_dir);
seed_collection(app.clone(), 5).await;
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri(format!("/collections/{TEST_COLLECTION}/points/delete"))
.header("Content-Type", "application/json")
.body(Body::from(json!({ "ids": [1, 2, 3] }).to_string()))
.expect("test: build delete request"),
)
.await
.expect("test: delete request");
assert_eq!(response.status(), StatusCode::OK);
let json = read_json(response).await;
assert_eq!(json["deleted_count"], 3);
assert_eq!(json["collection"], TEST_COLLECTION);
}
#[tokio::test]
async fn bulk_delete_empty_payload_returns_200_noop() {
let temp_dir = TempDir::new().expect("test: temp dir");
let app = create_test_app(&temp_dir);
seed_collection(app.clone(), 3).await;
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri(format!("/collections/{TEST_COLLECTION}/points/delete"))
.header("Content-Type", "application/json")
.body(Body::from(json!({ "ids": [] }).to_string()))
.expect("test: build delete request"),
)
.await
.expect("test: delete request");
assert_eq!(response.status(), StatusCode::OK);
let json = read_json(response).await;
assert_eq!(json["deleted_count"], 0);
}
#[tokio::test]
async fn bulk_delete_unknown_ids_silently_skipped() {
let temp_dir = TempDir::new().expect("test: temp dir");
let app = create_test_app(&temp_dir);
seed_collection(app.clone(), 2).await;
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri(format!("/collections/{TEST_COLLECTION}/points/delete"))
.header("Content-Type", "application/json")
.body(Body::from(json!({ "ids": [999, 1000] }).to_string()))
.expect("test: build delete request"),
)
.await
.expect("test: delete request");
assert_eq!(response.status(), StatusCode::OK);
let json = read_json(response).await;
assert_eq!(json["deleted_count"], 2);
}
#[tokio::test]
async fn bulk_delete_oversized_batch_returns_400() {
let temp_dir = TempDir::new().expect("test: temp dir");
let app = create_test_app(&temp_dir);
seed_collection(app.clone(), 1).await;
let oversized: Vec<u64> = (1..=10_001).collect();
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri(format!("/collections/{TEST_COLLECTION}/points/delete"))
.header("Content-Type", "application/json")
.body(Body::from(json!({ "ids": oversized }).to_string()))
.expect("test: build delete request"),
)
.await
.expect("test: delete request");
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
}
#[tokio::test]
async fn bulk_delete_unknown_collection_returns_404() {
let temp_dir = TempDir::new().expect("test: temp dir");
let app = create_test_app(&temp_dir);
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/collections/ghost_collection/points/delete")
.header("Content-Type", "application/json")
.body(Body::from(json!({ "ids": [1, 2] }).to_string()))
.expect("test: build delete request"),
)
.await
.expect("test: delete request");
assert_eq!(response.status(), StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn bulk_delete_malformed_payload_returns_400() {
let temp_dir = TempDir::new().expect("test: temp dir");
let app = create_test_app(&temp_dir);
seed_collection(app.clone(), 1).await;
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri(format!("/collections/{TEST_COLLECTION}/points/delete"))
.header("Content-Type", "application/json")
.body(Body::from(json!({ "wrong_field": [1, 2] }).to_string()))
.expect("test: build delete request"),
)
.await
.expect("test: delete request");
assert!(
response.status().is_client_error(),
"expected 4xx for malformed payload, got {}",
response.status()
);
}
#[tokio::test]
async fn vacuum_nominal_returns_200_with_compacted_count() {
let temp_dir = TempDir::new().expect("test: temp dir");
let app = create_test_app(&temp_dir);
seed_collection(app.clone(), 4).await;
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri(format!("/collections/{TEST_COLLECTION}/vacuum"))
.header("Content-Type", "application/json")
.body(Body::empty())
.expect("test: build vacuum request"),
)
.await
.expect("test: vacuum request");
assert_eq!(response.status(), StatusCode::OK);
let json = read_json(response).await;
assert_eq!(json["message"], "Index vacuumed");
assert_eq!(json["collection"], TEST_COLLECTION);
assert!(
json["compacted_entries"].is_number(),
"compacted_entries must be a number, got {:?}",
json["compacted_entries"]
);
}
#[tokio::test]
async fn vacuum_empty_collection_returns_200() {
let temp_dir = TempDir::new().expect("test: temp dir");
let app = create_test_app(&temp_dir);
seed_collection(app.clone(), 0).await;
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri(format!("/collections/{TEST_COLLECTION}/vacuum"))
.body(Body::empty())
.expect("test: build vacuum request"),
)
.await
.expect("test: vacuum request");
assert_eq!(response.status(), StatusCode::OK);
}
#[tokio::test]
async fn vacuum_unknown_collection_returns_404() {
let temp_dir = TempDir::new().expect("test: temp dir");
let app = create_test_app(&temp_dir);
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/collections/ghost_collection/vacuum")
.body(Body::empty())
.expect("test: build vacuum request"),
)
.await
.expect("test: vacuum request");
assert_eq!(response.status(), StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn compact_nominal_returns_200_with_bytes_reclaimed() {
let temp_dir = TempDir::new().expect("test: temp dir");
let app = create_test_app(&temp_dir);
seed_collection(app.clone(), 6).await;
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri(format!("/collections/{TEST_COLLECTION}/compact"))
.body(Body::empty())
.expect("test: build compact request"),
)
.await
.expect("test: compact request");
assert_eq!(response.status(), StatusCode::OK);
let json = read_json(response).await;
assert_eq!(json["message"], "Storage compacted");
assert_eq!(json["collection"], TEST_COLLECTION);
assert!(
json["bytes_reclaimed"].is_number(),
"bytes_reclaimed must be a number, got {:?}",
json["bytes_reclaimed"]
);
}
#[tokio::test]
async fn compact_empty_collection_returns_200() {
let temp_dir = TempDir::new().expect("test: temp dir");
let app = create_test_app(&temp_dir);
seed_collection(app.clone(), 0).await;
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri(format!("/collections/{TEST_COLLECTION}/compact"))
.body(Body::empty())
.expect("test: build compact request"),
)
.await
.expect("test: compact request");
assert_eq!(response.status(), StatusCode::OK);
}
#[tokio::test]
async fn compact_unknown_collection_returns_404() {
let temp_dir = TempDir::new().expect("test: temp dir");
let app = create_test_app(&temp_dir);
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/collections/ghost_collection/compact")
.body(Body::empty())
.expect("test: build compact request"),
)
.await
.expect("test: compact request");
assert_eq!(response.status(), StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn reorder_nominal_returns_200() {
let temp_dir = TempDir::new().expect("test: temp dir");
let app = create_test_app(&temp_dir);
seed_collection(app.clone(), 6).await;
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri(format!("/collections/{TEST_COLLECTION}/locality/reorder"))
.body(Body::empty())
.expect("test: build reorder request"),
)
.await
.expect("test: reorder request");
assert_eq!(response.status(), StatusCode::OK);
let json = read_json(response).await;
assert_eq!(json["message"], "Locality reordered");
assert_eq!(json["collection"], TEST_COLLECTION);
}
#[tokio::test]
async fn reorder_empty_collection_returns_200() {
let temp_dir = TempDir::new().expect("test: temp dir");
let app = create_test_app(&temp_dir);
seed_collection(app.clone(), 0).await;
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri(format!("/collections/{TEST_COLLECTION}/locality/reorder"))
.body(Body::empty())
.expect("test: build reorder request"),
)
.await
.expect("test: reorder request");
assert_eq!(response.status(), StatusCode::OK);
}
#[tokio::test]
async fn reorder_unknown_collection_returns_404() {
let temp_dir = TempDir::new().expect("test: temp dir");
let app = create_test_app(&temp_dir);
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/collections/ghost_collection/locality/reorder")
.body(Body::empty())
.expect("test: build reorder request"),
)
.await
.expect("test: reorder request");
assert_eq!(response.status(), StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn lifecycle_delete_then_vacuum_then_compact_succeeds() {
let temp_dir = TempDir::new().expect("test: temp dir");
let app = create_test_app(&temp_dir);
seed_collection(app.clone(), 8).await;
let response = app
.clone()
.oneshot(
Request::builder()
.method("POST")
.uri(format!("/collections/{TEST_COLLECTION}/points/delete"))
.header("Content-Type", "application/json")
.body(Body::from(json!({ "ids": [1, 2, 3, 4] }).to_string()))
.expect("test: build delete request"),
)
.await
.expect("test: delete request");
assert_eq!(response.status(), StatusCode::OK);
let response = app
.clone()
.oneshot(
Request::builder()
.method("POST")
.uri(format!("/collections/{TEST_COLLECTION}/vacuum"))
.body(Body::empty())
.expect("test: build vacuum request"),
)
.await
.expect("test: vacuum request");
assert_eq!(response.status(), StatusCode::OK);
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri(format!("/collections/{TEST_COLLECTION}/compact"))
.body(Body::empty())
.expect("test: build compact request"),
)
.await
.expect("test: compact request");
assert_eq!(response.status(), StatusCode::OK);
}