stemedb/crates/stemedb-api/tests/http_read_your_writes.rs
jordan 02ecac9a07 fix: merge upstream 10 commits, fix DashMap deadlock, deterministic sim ingestion
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>
2026-02-20 20:27:32 -07:00

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");
}