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