Merged 10 upstream commits (MemTable, read-your-writes tests, feed endpoint, security hardening, signed assertions, source registry, dashboard enhancements) and fixed all test failures across the full workspace (2656/2656 passing). Key fixes: - fix(cluster): DashMap deadlock in swim.rs suspect_node/fail_node/alive_node - DashMap::get_mut RefMut + iter() on same map = non-reentrant write lock deadlock - Fix: extract clone in scoped block to drop RefMut before calling update_node_gauges() - 6 previously-hanging SWIM tests now pass in <2s - fix(sim): replace background-task+polling ingestion with synchronous process_pending() - smoke_high_volume_simulation was CPU-starved under 2656 parallel tests - Removed ingestor.start() + wait_until_ingested() pattern throughout sim - All arena functions now call ingestor.process_pending() directly (deterministic) - fix(test): v2 signature helper used wrong hash (rkyv vs canonical compute_content_hash_v2) - fix(test): quota test signed "test" but v1 requires "subject:predicate" format - fix(test): http_validation now accepts 400 for valid-format-but-invalid-crypto hex - fix(test): scale_adaptive micro tier assertions updated (auto_promote upstream change) - config: add nextest.toml with slow-timeout for background-task-tests group Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
237 lines
9.2 KiB
Rust
237 lines
9.2 KiB
Rust
//! HTTP integration tests for read-your-writes consistency.
|
|
//!
|
|
//! These tests verify that after POST /assert returns 201, an immediate
|
|
//! GET /query returns the assertion without waiting for background indexing.
|
|
//!
|
|
//! The MemTable provides this consistency by storing assertions after WAL
|
|
//! commit, and merging them with KVStore results during query.
|
|
|
|
#![allow(clippy::expect_used)]
|
|
|
|
mod common;
|
|
|
|
use axum::{
|
|
body::Body,
|
|
http::{Request, StatusCode},
|
|
};
|
|
use tower::ServiceExt;
|
|
|
|
use stemedb_api::create_router;
|
|
|
|
// ============================================================================
|
|
// Read-Your-Writes Consistency Tests
|
|
// ============================================================================
|
|
|
|
#[tokio::test]
|
|
async fn test_read_your_writes_immediate() {
|
|
let env = common::create_test_env().await;
|
|
let app = create_router(env.state);
|
|
|
|
// Create a unique subject for this test
|
|
let subject = format!(
|
|
"TestSubject_{}",
|
|
std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.expect("time")
|
|
.as_nanos()
|
|
);
|
|
let predicate = "test_predicate";
|
|
|
|
// 1. POST /assert with unique subject
|
|
let assertion_json = common::create_signed_assertion_json(&subject, predicate, 42.0);
|
|
|
|
let request = Request::builder()
|
|
.uri("/v1/assert")
|
|
.method("POST")
|
|
.header("Content-Type", "application/json")
|
|
.body(Body::from(assertion_json.to_string()))
|
|
.expect("Request");
|
|
|
|
let response = app.clone().oneshot(request).await.expect("Request");
|
|
assert_eq!(response.status(), StatusCode::CREATED, "POST /assert should return 201");
|
|
|
|
// Parse the response to get the hash
|
|
let body = axum::body::to_bytes(response.into_body(), usize::MAX).await.expect("Body");
|
|
let create_response: serde_json::Value = serde_json::from_slice(&body).expect("JSON");
|
|
let created_hash = create_response["hash"].as_str().expect("hash field");
|
|
|
|
// 2. IMMEDIATELY query for the same subject (no sleep!)
|
|
let query_uri = format!("/v1/query?subject={}", subject);
|
|
let request =
|
|
Request::builder().uri(&query_uri).method("GET").body(Body::empty()).expect("Request");
|
|
|
|
let response = app.clone().oneshot(request).await.expect("Request");
|
|
assert_eq!(response.status(), StatusCode::OK, "GET /query should return 200");
|
|
|
|
let body = axum::body::to_bytes(response.into_body(), usize::MAX).await.expect("Body");
|
|
let query_result: serde_json::Value = serde_json::from_slice(&body).expect("JSON");
|
|
|
|
// 3. Assert the assertion is returned immediately
|
|
let assertions = query_result["assertions"].as_array().expect("assertions array");
|
|
assert!(!assertions.is_empty(), "Query should return the just-created assertion immediately");
|
|
|
|
// Verify we got the correct assertion
|
|
let found = assertions.iter().any(|a| a["subject"].as_str() == Some(subject.as_str()));
|
|
assert!(found, "The queried assertion should match the created subject");
|
|
|
|
// Note: resolved_hash is only set for MV hits; for MemTable lookups it may not be set
|
|
// So we just verify we found the assertion by subject
|
|
let _ = created_hash; // Suppress unused variable warning
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_read_your_writes_with_predicate() {
|
|
let env = common::create_test_env().await;
|
|
let app = create_router(env.state);
|
|
|
|
// Create unique identifiers
|
|
let subject = format!(
|
|
"Company_{}",
|
|
std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.expect("time")
|
|
.as_nanos()
|
|
);
|
|
let predicate = "revenue";
|
|
|
|
// 1. POST /assert
|
|
let assertion_json = common::create_signed_assertion_json(&subject, predicate, 1_000_000.0);
|
|
|
|
let request = Request::builder()
|
|
.uri("/v1/assert")
|
|
.method("POST")
|
|
.header("Content-Type", "application/json")
|
|
.body(Body::from(assertion_json.to_string()))
|
|
.expect("Request");
|
|
|
|
let response = app.clone().oneshot(request).await.expect("Request");
|
|
assert_eq!(response.status(), StatusCode::CREATED);
|
|
|
|
// 2. IMMEDIATELY query with both subject and predicate
|
|
let query_uri = format!("/v1/query?subject={}&predicate={}", subject, predicate);
|
|
let request =
|
|
Request::builder().uri(&query_uri).method("GET").body(Body::empty()).expect("Request");
|
|
|
|
let response = app.clone().oneshot(request).await.expect("Request");
|
|
assert_eq!(response.status(), StatusCode::OK);
|
|
|
|
let body = axum::body::to_bytes(response.into_body(), usize::MAX).await.expect("Body");
|
|
let query_result: serde_json::Value = serde_json::from_slice(&body).expect("JSON");
|
|
|
|
// 3. Verify assertion is found
|
|
let assertions = query_result["assertions"].as_array().expect("assertions array");
|
|
assert!(!assertions.is_empty(), "Query should find the assertion immediately");
|
|
|
|
let found = assertions.iter().any(|a| {
|
|
a["subject"].as_str() == Some(subject.as_str())
|
|
&& a["predicate"].as_str() == Some(predicate)
|
|
});
|
|
assert!(found, "The assertion should match subject and predicate");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_read_your_writes_multiple_assertions() {
|
|
let env = common::create_test_env().await;
|
|
let app = create_router(env.state);
|
|
|
|
// Create unique subject
|
|
let subject = format!(
|
|
"MultiAssert_{}",
|
|
std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.expect("time")
|
|
.as_nanos()
|
|
);
|
|
|
|
// Create multiple assertions with different predicates
|
|
let predicates = vec!["revenue", "profit", "employees"];
|
|
let values = vec![100.0, 20.0, 50.0];
|
|
|
|
for (predicate, value) in predicates.iter().zip(values.iter()) {
|
|
let assertion_json = common::create_signed_assertion_json(&subject, predicate, *value);
|
|
|
|
let request = Request::builder()
|
|
.uri("/v1/assert")
|
|
.method("POST")
|
|
.header("Content-Type", "application/json")
|
|
.body(Body::from(assertion_json.to_string()))
|
|
.expect("Request");
|
|
|
|
let response = app.clone().oneshot(request).await.expect("Request");
|
|
assert_eq!(response.status(), StatusCode::CREATED);
|
|
}
|
|
|
|
// Query for all assertions with this subject
|
|
let query_uri = format!("/v1/query?subject={}", subject);
|
|
let request =
|
|
Request::builder().uri(&query_uri).method("GET").body(Body::empty()).expect("Request");
|
|
|
|
let response = app.clone().oneshot(request).await.expect("Request");
|
|
assert_eq!(response.status(), StatusCode::OK);
|
|
|
|
let body = axum::body::to_bytes(response.into_body(), usize::MAX).await.expect("Body");
|
|
let query_result: serde_json::Value = serde_json::from_slice(&body).expect("JSON");
|
|
|
|
let assertions = query_result["assertions"].as_array().expect("assertions array");
|
|
assert_eq!(assertions.len(), 3, "Should find all 3 assertions immediately");
|
|
|
|
// Verify each predicate was found
|
|
for predicate in &predicates {
|
|
let found = assertions.iter().any(|a| a["predicate"].as_str() == Some(*predicate));
|
|
assert!(found, "Should find assertion with predicate: {}", predicate);
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_read_your_writes_does_not_affect_other_subjects() {
|
|
let env = common::create_test_env().await;
|
|
let app = create_router(env.state);
|
|
|
|
let timestamp = std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.expect("time")
|
|
.as_nanos();
|
|
|
|
let subject1 = format!("Subject1_{}", timestamp);
|
|
let subject2 = format!("Subject2_{}", timestamp);
|
|
|
|
// Create assertion for subject1
|
|
let assertion_json = common::create_signed_assertion_json(&subject1, "test", 1.0);
|
|
|
|
let request = Request::builder()
|
|
.uri("/v1/assert")
|
|
.method("POST")
|
|
.header("Content-Type", "application/json")
|
|
.body(Body::from(assertion_json.to_string()))
|
|
.expect("Request");
|
|
|
|
let response = app.clone().oneshot(request).await.expect("Request");
|
|
assert_eq!(response.status(), StatusCode::CREATED);
|
|
|
|
// Query for subject2 (should be empty)
|
|
let query_uri = format!("/v1/query?subject={}", subject2);
|
|
let request =
|
|
Request::builder().uri(&query_uri).method("GET").body(Body::empty()).expect("Request");
|
|
|
|
let response = app.clone().oneshot(request).await.expect("Request");
|
|
assert_eq!(response.status(), StatusCode::OK);
|
|
|
|
let body = axum::body::to_bytes(response.into_body(), usize::MAX).await.expect("Body");
|
|
let query_result: serde_json::Value = serde_json::from_slice(&body).expect("JSON");
|
|
|
|
let assertions = query_result["assertions"].as_array().expect("assertions array");
|
|
assert!(assertions.is_empty(), "Subject2 should have no assertions");
|
|
|
|
// Query for subject1 (should have one assertion)
|
|
let query_uri = format!("/v1/query?subject={}", subject1);
|
|
let request =
|
|
Request::builder().uri(&query_uri).method("GET").body(Body::empty()).expect("Request");
|
|
|
|
let response = app.clone().oneshot(request).await.expect("Request");
|
|
let body = axum::body::to_bytes(response.into_body(), usize::MAX).await.expect("Body");
|
|
let query_result: serde_json::Value = serde_json::from_slice(&body).expect("JSON");
|
|
|
|
let assertions = query_result["assertions"].as_array().expect("assertions array");
|
|
assert_eq!(assertions.len(), 1, "Subject1 should have one assertion");
|
|
}
|