- Add per-query and total timeout enforcement to MultiSearchExecutor - Improve SearchResult with helper methods (ok, err, timeout, is_success) - Fix ModeACoordinator feature-gate compilation issues - Add comprehensive acceptance tests for multi-search: - 5-query batch completion - Slow query doesn't block fast queries (parallel execution) - Partial failure handling - Per-query timeout - Total timeout - 100-query batch completion Closes: miroir-uhj.11
626 lines
21 KiB
Rust
626 lines
21 KiB
Rust
// Miroir API Compatibility Tests
|
|
//
|
|
// Runs the same scenarios against both Miroir and a real Meilisearch instance,
|
|
// asserting semantic equivalence of responses.
|
|
//
|
|
// Per plan §8: Every Meilisearch error code must be verified byte-identical
|
|
// in the compat suite, including {message,code,type,link} shape.
|
|
//
|
|
// Prerequisites:
|
|
// - docker-compose-dev stack running (Miroir on port 7700)
|
|
// - Meilisearch running on port 7704 (or set MEILISEARCH_PORT)
|
|
//
|
|
// Run:
|
|
// MEILISEARCH_PORT=7704 cargo test --test api_compatibility
|
|
|
|
use meilisearch_sdk::{client::Client, indexes::Indexes, task::Task};
|
|
use serde_json::json;
|
|
use serde_json::Value;
|
|
use std::env;
|
|
use std::time::Duration;
|
|
|
|
const MIROIR_PORT: u16 = 7700;
|
|
const DEFAULT_MEILISEARCH_PORT: u16 = 7704;
|
|
|
|
/// Error shape from Meilisearch/Miroir responses
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
struct ErrorShape {
|
|
message: String,
|
|
code: String,
|
|
error_type: String,
|
|
link: Option<String>,
|
|
}
|
|
|
|
impl ErrorShape {
|
|
/// Parse error from JSON response
|
|
fn from_json(value: &Value) -> Option<Self> {
|
|
Some(Self {
|
|
message: value.get("message")?.as_str()?.to_string(),
|
|
code: value.get("code")?.as_str()?.to_string(),
|
|
error_type: value.get("type")?.as_str()?.to_string(),
|
|
link: value.get("link").and_then(|v| v.as_str()).map(|s| s.to_string()),
|
|
})
|
|
}
|
|
|
|
/// Check if this is a Miroir-specific error code
|
|
fn is_miroir_code(&self) -> bool {
|
|
self.code.starts_with("miroir_")
|
|
}
|
|
}
|
|
|
|
/// Assert two error shapes are byte-identical for critical fields
|
|
fn assert_error_equivalent(miroir: &ErrorShape, meili: &ErrorShape, msg: &str) {
|
|
// For Miroir-specific codes, only validate against Miroir
|
|
if miroir.is_miroir_code() {
|
|
assert_eq!(
|
|
miroir.error_type, meili.error_type,
|
|
"{}: error_type mismatch (miroir={}, meili={})",
|
|
msg, miroir.error_type, meili.error_type
|
|
);
|
|
return;
|
|
}
|
|
|
|
// For Meilisearch-native codes, all fields must match
|
|
assert_eq!(
|
|
miroir.code, meili.code,
|
|
"{}: code mismatch (miroir={}, meili={})",
|
|
msg, miroir.code, meili.code
|
|
);
|
|
|
|
assert_eq!(
|
|
miroir.error_type, meili.error_type,
|
|
"{}: error_type mismatch (miroir={}, meili={})",
|
|
msg, miroir.error_type, meili.error_type
|
|
);
|
|
|
|
// Link should match if present on either side
|
|
if miroir.link.is_some() || meili.link.is_some() {
|
|
assert_eq!(
|
|
miroir.link, meili.link,
|
|
"{}: link mismatch (miroir={:?}, meili={:?})",
|
|
msg, miroir.link, meili.link
|
|
);
|
|
}
|
|
}
|
|
|
|
fn get_miroir_client() -> Client {
|
|
let url = format!("http://localhost:{}", MIROIR_PORT);
|
|
Client::new(url, Some("dev-key".to_string()))
|
|
}
|
|
|
|
fn get_meilisearch_client() -> Client {
|
|
let port = env::var("MEILISEARCH_PORT")
|
|
.ok()
|
|
.and_then(|p| p.parse().ok())
|
|
.unwrap_or(DEFAULT_MEILISEARCH_PORT);
|
|
let url = format!("http://localhost:{}", port);
|
|
Client::new(url, Some("dev-node-key".to_string()))
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct CompatibilityTest {
|
|
name: &'static str,
|
|
run: Box<dyn Fn() -> Result<(), Box<dyn std::error::Error>>>,
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn create_and_search_index() -> Result<(), Box<dyn std::error::Error>> {
|
|
let miroir = get_miroir_client();
|
|
let meili = get_meilisearch_client();
|
|
|
|
let index_name = "compat_create_search";
|
|
|
|
// Clean up first
|
|
let _ = miroir.delete_index(index_name).await;
|
|
let _ = meili.delete_index(index_name).await;
|
|
|
|
// Create index on both
|
|
let miroir_task = miroir.create_index(index_name, Some("id")).await?;
|
|
let meili_task = meili.create_index(index_name, Some("id")).await?;
|
|
|
|
// Wait for both
|
|
miroir.wait_for_task(miroir_task.uid(), None, None).await?;
|
|
meili.wait_for_task(meili_task.uid(), None, None).await?;
|
|
|
|
// Add documents to both
|
|
let docs = json!([
|
|
{"id": 1, "title": "Test Document", "value": 42},
|
|
{"id": 2, "title": "Another Document", "value": 100},
|
|
]);
|
|
|
|
let miroir_task = miroir.index(index_name).add_documents(&docs, None).await?;
|
|
let meili_task = meili.index(index_name).add_documents(&docs, None).await?;
|
|
|
|
miroir.wait_for_task(miroir_task.uid(), None, None).await?;
|
|
meili.wait_for_task(meili_task.uid(), None, None).await?;
|
|
|
|
// Search both
|
|
let miroir_results = miroir.index(index_name).search()
|
|
.with_query("test")
|
|
.execute::<serde_json::Value>()
|
|
.await?;
|
|
|
|
let meili_results = meili.index(index_name).search()
|
|
.with_query("test")
|
|
.execute::<serde_json::Value>()
|
|
.await?;
|
|
|
|
// Assert equivalence
|
|
assert_eq!(
|
|
miroir_results.hits.len(),
|
|
meili_results.hits.len(),
|
|
"Hit count mismatch"
|
|
);
|
|
|
|
if !miroir_results.hits.is_empty() && !meili_results.hits.is_empty() {
|
|
let miroir_title = miroir_results.hits[0]["title"].as_str();
|
|
let meili_title = meili_results.hits[0]["title"].as_str();
|
|
assert_eq!(miroir_title, meili_title, "First hit title mismatch");
|
|
}
|
|
|
|
// Clean up
|
|
let _ = miroir.delete_index(index_name).await;
|
|
let _ = meili.delete_index(index_name).await;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn settings_update_and_search() -> Result<(), Box<dyn std::error::Error>> {
|
|
let miroir = get_miroir_client();
|
|
let meili = get_meilisearch_client();
|
|
|
|
let index_name = "compat_settings";
|
|
|
|
// Clean up and create
|
|
let _ = miroir.delete_index(index_name).await;
|
|
let _ = meili.delete_index(index_name).await;
|
|
|
|
let miroir_task = miroir.create_index(index_name, Some("id")).await?;
|
|
let meili_task = meili.create_index(index_name, Some("id")).await?;
|
|
|
|
miroir.wait_for_task(miroir_task.uid(), None, None).await?;
|
|
meili.wait_for_task(meili_task.uid(), None, None).await?;
|
|
|
|
// Update settings
|
|
use meilisearch_sdk::settings::Settings;
|
|
|
|
let settings = Settings::new()
|
|
.with_searchable_attributes(["title", "description"])
|
|
.with_filterable_attributes(["category"]);
|
|
|
|
let miroir_task = miroir.index(index_name).set_settings(&settings).await?;
|
|
let meili_task = meili.index(index_name).set_settings(&settings).await?;
|
|
|
|
miroir.wait_for_task(miroir_task.uid(), None, None).await?;
|
|
meili.wait_for_task(meili_task.uid(), None, None).await?;
|
|
|
|
// Verify settings are retrievable
|
|
let miroir_settings = miroir.index(index_name).get_settings().await?;
|
|
let meili_settings = meili.index(index_name).get_settings().await?;
|
|
|
|
assert_eq!(
|
|
miroir_settings.searchable_attributes,
|
|
meili_settings.searchable_attributes,
|
|
"Searchable attributes mismatch"
|
|
);
|
|
|
|
// Clean up
|
|
let _ = miroir.delete_index(index_name).await;
|
|
let _ = meili.delete_index(index_name).await;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn error_response_format() -> Result<(), Box<dyn std::error::Error>> {
|
|
let miroir = get_miroir_client();
|
|
let meili = get_meilisearch_client();
|
|
|
|
let index_name = "compat_errors";
|
|
|
|
// Clean up first
|
|
let _ = miroir.delete_index(index_name).await;
|
|
let _ = meili.delete_index(index_name).await;
|
|
|
|
// Try to get a non-existent index
|
|
let miroir_err = miroir.index(index_name).get_stats().await;
|
|
let meili_err = meili.index(index_name).get_stats().await;
|
|
|
|
// Both should fail
|
|
assert!(miroir_err.is_err(), "Miroir should return error for non-existent index");
|
|
assert!(meili_err.is_err(), "Meilisearch should return error for non-existent index");
|
|
|
|
// Check error shapes match
|
|
let miroir_err_msg = format!("{:?}", miroir_err.unwrap_err());
|
|
let meili_err_msg = format!("{:?}", meili_err.unwrap_err());
|
|
|
|
// Both should mention "index not found" or similar
|
|
assert!(
|
|
miroir_err_msg.to_lowercase().contains("index") || miroir_err_msg.to_lowercase().contains("not found"),
|
|
"Miroir error should mention index not found: {}",
|
|
miroir_err_msg
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn filter_and_facet() -> Result<(), Box<dyn std::error::Error>> {
|
|
let miroir = get_miroir_client();
|
|
let meili = get_meilisearch_client();
|
|
|
|
let index_name = "compat_filter_facet";
|
|
|
|
// Clean up and create
|
|
let _ = miroir.delete_index(index_name).await;
|
|
let _ = meili.delete_index(index_name).await;
|
|
|
|
let miroir_task = miroir.create_index(index_name, Some("id")).await?;
|
|
let meili_task = meili.create_index(index_name, Some("id")).await?;
|
|
|
|
miroir.wait_for_task(miroir_task.uid(), None, None).await?;
|
|
meili.wait_for_task(meili_task.uid(), None, None).await?;
|
|
|
|
// Add faceted documents
|
|
let docs = json!([
|
|
{"id": 1, "title": "Product A", "category": "electronics", "price": 100},
|
|
{"id": 2, "title": "Product B", "category": "books", "price": 20},
|
|
{"id": 3, "title": "Product C", "category": "electronics", "price": 200},
|
|
]);
|
|
|
|
let miroir_task = miroir.index(index_name).add_documents(&docs, None).await?;
|
|
let meili_task = meili.index(index_name).add_documents(&docs, None).await?;
|
|
|
|
miroir.wait_for_task(miroir_task.uid(), None, None).await?;
|
|
meili.wait_for_task(meili_task.uid(), None, None).await?;
|
|
|
|
// Update settings for facets
|
|
use meilisearch_sdk::settings::Settings;
|
|
|
|
let settings = Settings::new()
|
|
.with_filterable_attributes(["category", "price"]);
|
|
|
|
let miroir_task = miroir.index(index_name).set_settings(&settings).await?;
|
|
let meili_task = meili.index(index_name).set_settings(&settings).await?;
|
|
|
|
miroir.wait_for_task(miroir_task.uid(), None, None).await?;
|
|
meili.wait_for_task(meili_task.uid(), None, None).await?;
|
|
|
|
// Filter by category
|
|
let miroir_results = miroir.index(index_name).search()
|
|
.with_query("product")
|
|
.with_filter("category = electronics")
|
|
.execute::<serde_json::Value>()
|
|
.await?;
|
|
|
|
let meili_results = meili.index(index_name).search()
|
|
.with_query("product")
|
|
.with_filter("category = electronics")
|
|
.execute::<serde_json::Value>()
|
|
.await?;
|
|
|
|
// Should both return 2 electronics products
|
|
assert_eq!(miroir_results.hits.len(), 2, "Miroir should return 2 electronics products");
|
|
assert_eq!(meili_results.hits.len(), 2, "Meilisearch should return 2 electronics products");
|
|
|
|
// Clean up
|
|
let _ = miroir.delete_index(index_name).await;
|
|
let _ = meili.delete_index(index_name).await;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// ============================================================================
|
|
// Comprehensive Error Code Tests (plan §8 requirement)
|
|
// ============================================================================
|
|
|
|
/// Helper: Make a raw HTTP request and parse the error response
|
|
async fn get_error_from_raw_request(
|
|
url: &str,
|
|
method: reqwest::Method,
|
|
path: &str,
|
|
body: Option<&Value>,
|
|
headers: &[(&str, &str)],
|
|
) -> Option<ErrorShape> {
|
|
let client = reqwest::Client::new();
|
|
let mut req = client.request(method.clone(), format!("{}{}", url, path));
|
|
|
|
for (key, value) in headers {
|
|
req = req.header(*key, *value);
|
|
}
|
|
|
|
if let Some(b) = body {
|
|
req = req.json(b);
|
|
}
|
|
|
|
let resp = req.send().await.ok()?;
|
|
let status = resp.status();
|
|
|
|
// Only process error responses
|
|
if status.is_success() {
|
|
return None;
|
|
}
|
|
|
|
let json: Value = resp.json().await.ok()?;
|
|
ErrorShape::from_json(&json)
|
|
}
|
|
|
|
/// Test: index_not_found error code (Meilisearch-native)
|
|
#[tokio::test]
|
|
async fn error_code_index_not_found() -> Result<(), Box<dyn std::error::Error>> {
|
|
let index_name = "compat_index_not_found";
|
|
|
|
// Try to get stats on non-existent index via raw HTTP
|
|
let miroir_err = get_error_from_raw_request(
|
|
"http://localhost:7700",
|
|
reqwest::Method::GET,
|
|
&format!("/indexes/{}/stats", index_name),
|
|
None,
|
|
&[("Authorization", "Bearer dev-key")],
|
|
).await;
|
|
|
|
let meili_err = get_error_from_raw_request(
|
|
&format!("http://localhost:{}", env::var("MEILISEARCH_PORT").unwrap_or_else(|_| "7704".to_string())),
|
|
reqwest::Method::GET,
|
|
&format!("/indexes/{}/stats", index_name),
|
|
None,
|
|
&[("Authorization", "Bearer dev-node-key")],
|
|
).await;
|
|
|
|
assert!(miroir_err.is_some(), "Miroir should return index_not_found error");
|
|
assert!(meili_err.is_some(), "Meilisearch should return index_not_found error");
|
|
|
|
let miroir_err = miroir_err.unwrap();
|
|
let meili_err = meili_err.unwrap();
|
|
|
|
// Verify code is index_not_found
|
|
assert_eq!(miroir_err.code, "index_not_found");
|
|
assert_eq!(meili_err.code, "index_not_found");
|
|
|
|
// Verify error shapes match
|
|
assert_error_equivalent(&miroir_err, &meili_err, "index_not_found");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Test: invalid_index_uid error code (Meilisearch-native)
|
|
#[tokio::test]
|
|
async fn error_code_invalid_index_uid() -> Result<(), Box<dyn std::error::Error>> {
|
|
// Try to create an index with invalid UID (contains capital letters)
|
|
let invalid_index = "InvalidIndex";
|
|
|
|
let miroir_err = get_error_from_raw_request(
|
|
"http://localhost:7700",
|
|
reqwest::Method::POST,
|
|
"/indexes",
|
|
Some(&json!({"uid": invalid_index, "primaryKey": "id"})),
|
|
&[("Authorization", "Bearer dev-key")],
|
|
).await;
|
|
|
|
let meili_err = get_error_from_raw_request(
|
|
&format!("http://localhost:{}", env::var("MEILISEARCH_PORT").unwrap_or_else(|_| "7704".to_string())),
|
|
reqwest::Method::POST,
|
|
"/indexes",
|
|
Some(&json!({"uid": invalid_index, "primaryKey": "id"})),
|
|
&[("Authorization", "Bearer dev-node-key")],
|
|
).await;
|
|
|
|
assert!(miroir_err.is_some(), "Miroir should return invalid_index_uid error");
|
|
assert!(meili_err.is_some(), "Meilisearch should return invalid_index_uid error");
|
|
|
|
let miroir_err = miroir_err.unwrap();
|
|
let meili_err = meili_err.unwrap();
|
|
|
|
// Verify code
|
|
assert_eq!(miroir_err.code, "invalid_index_uid");
|
|
assert_eq!(meili_err.code, "invalid_index_uid");
|
|
|
|
// Verify error shapes match
|
|
assert_error_equivalent(&miroir_err, &meili_err, "invalid_index_uid");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Test: missing_authorization_header error code (Meilisearch-native)
|
|
#[tokio::test]
|
|
async fn error_code_missing_authorization_header() -> Result<(), Box<dyn std::error::Error>> {
|
|
let miroir_err = get_error_from_raw_request(
|
|
"http://localhost:7700",
|
|
reqwest::Method::GET,
|
|
"/indexes",
|
|
None,
|
|
&[], // No auth header
|
|
).await;
|
|
|
|
let meili_err = get_error_from_raw_request(
|
|
&format!("http://localhost:{}", env::var("MEILISEARCH_PORT").unwrap_or_else(|_| "7704".to_string())),
|
|
reqwest::Method::GET,
|
|
"/indexes",
|
|
None,
|
|
&[], // No auth header
|
|
).await;
|
|
|
|
// Both should return an auth error (code may vary by version)
|
|
assert!(miroir_err.is_some(), "Miroir should return auth error");
|
|
assert!(meili_err.is_some(), "Meilisearch should return auth error");
|
|
|
|
let miroir_err = miroir_err.unwrap();
|
|
let meili_err = meili_err.unwrap();
|
|
|
|
// Verify error type is auth
|
|
assert_eq!(miroir_err.error_type, "auth");
|
|
assert_eq!(meili_err.error_type, "auth");
|
|
|
|
// Verify error shapes match
|
|
assert_error_equivalent(&miroir_err, &meili_err, "missing_authorization_header");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Test: invalid_api_key error code (Meilisearch-native)
|
|
#[tokio::test]
|
|
async fn error_code_invalid_api_key() -> Result<(), Box<dyn std::error::Error>> {
|
|
let miroir_err = get_error_from_raw_request(
|
|
"http://localhost:7700",
|
|
reqwest::Method::GET,
|
|
"/indexes",
|
|
None,
|
|
&[("Authorization", "Bearer invalid-key-12345")],
|
|
).await;
|
|
|
|
let meili_err = get_error_from_raw_request(
|
|
&format!("http://localhost:{}", env::var("MEILISEARCH_PORT").unwrap_or_else(|_| "7704".to_string())),
|
|
reqwest::Method::GET,
|
|
"/indexes",
|
|
None,
|
|
&[("Authorization", "Bearer invalid-key-12345")],
|
|
).await;
|
|
|
|
assert!(miroir_err.is_some(), "Miroir should return invalid_api_key error");
|
|
assert!(meili_err.is_some(), "Meilisearch should return invalid_api_key error");
|
|
|
|
let miroir_err = miroir_err.unwrap();
|
|
let meili_err = meili_err.unwrap();
|
|
|
|
// Verify error type is auth
|
|
assert_eq!(miroir_err.error_type, "auth");
|
|
assert_eq!(meili_err.error_type, "auth");
|
|
|
|
// Verify error shapes match
|
|
assert_error_equivalent(&miroir_err, &meili_err, "invalid_api_key");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Test: invalid_search_filter error code (Meilisearch-native)
|
|
#[tokio::test]
|
|
async fn error_code_invalid_search_filter() -> Result<(), Box<dyn std::error::Error>> {
|
|
let index_name = "compat_invalid_filter";
|
|
let miroir = get_miroir_client();
|
|
let meili = get_meilisearch_client();
|
|
|
|
// Clean up and create index
|
|
let _ = miroir.delete_index(index_name).await;
|
|
let _ = meili.delete_index(index_name).await;
|
|
|
|
let _ = miroir.create_index(index_name, Some("id")).await;
|
|
let _ = meili.create_index(index_name, Some("id")).await;
|
|
|
|
// Wait for index creation
|
|
tokio::time::sleep(Duration::from_secs(2)).await;
|
|
|
|
// Try invalid filter syntax
|
|
let miroir_err = get_error_from_raw_request(
|
|
"http://localhost:7700",
|
|
reqwest::Method::POST,
|
|
&format!("/indexes/{}/search", index_name),
|
|
Some(&json!({"q": "test", "filter": "invalid syntax"})),
|
|
&[("Authorization", "Bearer dev-key")],
|
|
).await;
|
|
|
|
let meili_err = get_error_from_raw_request(
|
|
&format!("http://localhost:{}", env::var("MEILISEARCH_PORT").unwrap_or_else(|_| "7704".to_string())),
|
|
reqwest::Method::POST,
|
|
&format!("/indexes/{}/search", index_name),
|
|
Some(&json!({"q": "test", "filter": "invalid syntax"})),
|
|
&[("Authorization", "Bearer dev-node-key")],
|
|
).await;
|
|
|
|
// Both should return an invalid request error
|
|
assert!(miroir_err.is_some(), "Miroir should return invalid request error");
|
|
assert!(meili_err.is_some(), "Meilisearch should return invalid request error");
|
|
|
|
let miroir_err = miroir_err.unwrap();
|
|
let meili_err = meili_err.unwrap();
|
|
|
|
// Verify error type is invalid_request
|
|
assert_eq!(miroir_err.error_type, "invalid_request");
|
|
assert_eq!(meili_err.error_type, "invalid_request");
|
|
|
|
// Verify error shapes match
|
|
assert_error_equivalent(&miroir_err, &meili_err, "invalid_search_filter");
|
|
|
|
// Clean up
|
|
let _ = miroir.delete_index(index_name).await;
|
|
let _ = meili.delete_index(index_name).await;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Test: miroir_reserved_field error code (Miroir-specific)
|
|
#[tokio::test]
|
|
async fn error_code_miroir_reserved_field() -> Result<(), Box<dyn std::error::Error>> {
|
|
let index_name = "compat_reserved_field";
|
|
let miroir = get_miroir_client();
|
|
|
|
// Clean up and create index
|
|
let _ = miroir.delete_index(index_name).await;
|
|
let _ = miroir.create_index(index_name, Some("id")).await;
|
|
|
|
// Wait for index creation
|
|
tokio::time::sleep(Duration::from_secs(2)).await;
|
|
|
|
// Try to add a document with a reserved field
|
|
let miroir_err = get_error_from_raw_request(
|
|
"http://localhost:7700",
|
|
reqwest::Method::POST,
|
|
&format!("/indexes/{}/documents", index_name),
|
|
Some(&json!([{"id": 1, "_miroir_shard": 0}])),
|
|
&[("Authorization", "Bearer dev-key")],
|
|
).await;
|
|
|
|
assert!(miroir_err.is_some(), "Miroir should return miroir_reserved_field error");
|
|
|
|
let err = miroir_err.unwrap();
|
|
|
|
// Verify Miroir-specific error code
|
|
assert_eq!(err.code, "miroir_reserved_field");
|
|
assert_eq!(err.error_type, "invalid_request");
|
|
assert!(err.link.as_ref().unwrap().contains("miroir_reserved_field"));
|
|
|
|
// Clean up
|
|
let _ = miroir.delete_index(index_name).await;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Test: Error code consistency across all endpoints
|
|
#[tokio::test]
|
|
async fn error_code_consistency_across_endpoints() -> Result<(), Box<dyn std::error::Error>> {
|
|
let index_name = "compat_consistency";
|
|
|
|
// Test 1: Non-existent index on different endpoints
|
|
for (endpoint, method, body) in [
|
|
("/stats", reqwest::Method::GET, None::<Value>),
|
|
("/settings", reqwest::Method::GET, None::<Value>),
|
|
("/documents", reqwest::Method::GET, None::<Value>),
|
|
] {
|
|
let miroir_err = get_error_from_raw_request(
|
|
"http://localhost:7700",
|
|
method.clone(),
|
|
&format!("/indexes/{}{}", index_name, endpoint),
|
|
body.as_ref(),
|
|
&[("Authorization", "Bearer dev-key")],
|
|
).await;
|
|
|
|
let meili_err = get_error_from_raw_request(
|
|
&format!("http://localhost:{}", env::var("MEILISEARCH_PORT").unwrap_or_else(|_| "7704".to_string())),
|
|
method,
|
|
&format!("/indexes/{}{}", index_name, endpoint),
|
|
body.as_ref(),
|
|
&[("Authorization", "Bearer dev-node-key")],
|
|
).await;
|
|
|
|
if let (Some(m_err), Some(e_err)) = (miroir_err, meili_err) {
|
|
// Both should return index_not_found
|
|
assert_eq!(m_err.code, "index_not_found", "Endpoint: {}", endpoint);
|
|
assert_eq!(e_err.code, "index_not_found", "Endpoint: {}", endpoint);
|
|
|
|
// Error shapes should match
|
|
assert_error_equivalent(&m_err, &e_err, endpoint);
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|