stemedb/crates/stemedb-api/tests/http_epoch.rs
jordan 55349845d0 refactor: Split all files to enforce 500-line max
Break monolith source files into focused modules:
- stemedb-core/types.rs → types/ directory (assertion, source, gold_standard, etc.)
- stemedb-storage: audit_store, quota_store, trust_rank_store, vector_index, vote_store → module directories
- stemedb-ingest/worker.rs → worker/ with separate test modules
- stemedb-query: engine, materializer, query → module directories
- stemedb-lens: epoch_aware, skeptic → module directories
- stemedb-sim/lib.rs → agent, arenas/, helpers, runner, strategy, types
- stemedb-api/tests: integration_tests → http_basic, http_validation, http_epoch, http_pipeline
- stemedb-api/tests: e2e_flow_test → e2e_full_pipeline, e2e_lens_resolution
- stemedb-query/tests: e2e_pipeline → e2e_pipeline + e2e_decay

Also adds new features: gold standard verification, escalation handlers,
admin endpoints, concept hierarchy spec, arena roadmap, and Go SDK.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 01:13:45 -07:00

274 lines
9.6 KiB
Rust

//! HTTP integration tests for Epoch endpoint operations.
//!
//! Coverage:
//! - POST /v1/epoch - Epoch creation with validation
//! - Epoch name validation (empty, whitespace)
//! - Supersession validation (supersedes hash + supersession_type)
//! - Hex validation for supersedes field
//! - Deterministic ID generation (content-addressed)
//! - Default timestamp handling
#![allow(clippy::expect_used)]
mod common;
use axum::{
body::Body,
http::{Request, StatusCode},
};
use serde_json::json;
use tower::ServiceExt;
use stemedb_api::create_router;
// ============================================================================
// Epoch Creation Tests
// ============================================================================
#[tokio::test]
async fn test_create_epoch_success() {
let env = common::create_test_env().await;
let app = create_router(env.state);
let epoch = json!({
"name": "GAAP-2024",
"start_timestamp": 1704067200
});
let request = Request::builder()
.uri("/v1/epoch")
.method("POST")
.header("content-type", "application/json")
.body(Body::from(serde_json::to_vec(&epoch).expect("JSON")))
.expect("Request");
let response = app.oneshot(request).await.expect("Request");
let status = response.status();
let body = axum::body::to_bytes(response.into_body(), usize::MAX).await.expect("Body");
let json: serde_json::Value = serde_json::from_slice(&body).expect("JSON");
assert_eq!(status, StatusCode::CREATED, "Body: {:?}", json);
assert_eq!(json["status"], "created");
// BLAKE3 hash = 32 bytes = 64 hex characters
assert_eq!(json["hash"].as_str().expect("hash").len(), 64);
}
// ============================================================================
// Epoch Name Validation Tests
// ============================================================================
#[tokio::test]
async fn test_create_epoch_empty_name_fails() {
let env = common::create_test_env().await;
let app = create_router(env.state);
let epoch = json!({
"name": "",
"start_timestamp": 1704067200
});
let request = Request::builder()
.uri("/v1/epoch")
.method("POST")
.header("content-type", "application/json")
.body(Body::from(serde_json::to_vec(&epoch).expect("JSON")))
.expect("Request");
let response = app.oneshot(request).await.expect("Request");
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
let body = axum::body::to_bytes(response.into_body(), usize::MAX).await.expect("Body");
let json: serde_json::Value = serde_json::from_slice(&body).expect("JSON");
assert!(json["error"].as_str().expect("error").contains("cannot be empty"));
}
#[tokio::test]
async fn test_create_epoch_whitespace_name_fails() {
let env = common::create_test_env().await;
let app = create_router(env.state);
let epoch = json!({
"name": " ",
"start_timestamp": 1704067200
});
let request = Request::builder()
.uri("/v1/epoch")
.method("POST")
.header("content-type", "application/json")
.body(Body::from(serde_json::to_vec(&epoch).expect("JSON")))
.expect("Request");
let response = app.oneshot(request).await.expect("Request");
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
let body = axum::body::to_bytes(response.into_body(), usize::MAX).await.expect("Body");
let json: serde_json::Value = serde_json::from_slice(&body).expect("JSON");
assert!(json["error"].as_str().expect("error").contains("cannot be empty"));
}
// ============================================================================
// Epoch Supersession Validation Tests
// ============================================================================
#[tokio::test]
async fn test_create_epoch_with_supersedes() {
let env = common::create_test_env().await;
let app = create_router(env.state);
// Supersession requires both supersedes hash AND supersession_type
let epoch = json!({
"name": "GAAP-2025",
"supersedes": "a".repeat(64),
"supersession_type": "Temporal",
"start_timestamp": 1735689600
});
let request = Request::builder()
.uri("/v1/epoch")
.method("POST")
.header("content-type", "application/json")
.body(Body::from(serde_json::to_vec(&epoch).expect("JSON")))
.expect("Request");
let response = app.oneshot(request).await.expect("Request");
assert_eq!(response.status(), StatusCode::CREATED);
let body = axum::body::to_bytes(response.into_body(), usize::MAX).await.expect("Body");
let json: serde_json::Value = serde_json::from_slice(&body).expect("JSON");
assert_eq!(json["status"], "created");
}
#[tokio::test]
async fn test_create_epoch_supersedes_without_type_fails() {
let env = common::create_test_env().await;
let app = create_router(env.state);
// Providing supersedes but NOT supersession_type should fail
let epoch = json!({
"name": "GAAP-2025",
"supersedes": "a".repeat(64),
// Missing supersession_type!
"start_timestamp": 1735689600
});
let request = Request::builder()
.uri("/v1/epoch")
.method("POST")
.header("content-type", "application/json")
.body(Body::from(serde_json::to_vec(&epoch).expect("JSON")))
.expect("Request");
let response = app.oneshot(request).await.expect("Request");
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
let body = axum::body::to_bytes(response.into_body(), usize::MAX).await.expect("Body");
let json: serde_json::Value = serde_json::from_slice(&body).expect("JSON");
assert!(json["error"].as_str().expect("error").contains("supersession_type is required"));
}
#[tokio::test]
async fn test_create_epoch_invalid_supersedes_hex() {
let env = common::create_test_env().await;
let app = create_router(env.state);
let epoch = json!({
"name": "GAAP-2025",
"supersedes": "invalid", // Too short, should be 64 hex chars
"supersession_type": "Invalidate",
"start_timestamp": 1735689600
});
let request = Request::builder()
.uri("/v1/epoch")
.method("POST")
.header("content-type", "application/json")
.body(Body::from(serde_json::to_vec(&epoch).expect("JSON")))
.expect("Request");
let response = app.oneshot(request).await.expect("Request");
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
let body = axum::body::to_bytes(response.into_body(), usize::MAX).await.expect("Body");
let json: serde_json::Value = serde_json::from_slice(&body).expect("JSON");
// Should complain about hex length
assert!(json["error"].as_str().expect("error").contains("Expected 64 hex characters"));
}
// ============================================================================
// Epoch Content-Addressed ID Tests
// ============================================================================
#[tokio::test]
async fn test_create_epoch_deterministic_id() {
let env = common::create_test_env().await;
let app = create_router(env.state);
// Create the same epoch twice - should get the same ID
let epoch = json!({
"name": "Deterministic-Test",
"start_timestamp": 1000
});
let request1 = Request::builder()
.uri("/v1/epoch")
.method("POST")
.header("content-type", "application/json")
.body(Body::from(serde_json::to_vec(&epoch).expect("JSON")))
.expect("Request");
let response1 = app.clone().oneshot(request1).await.expect("Request");
assert_eq!(response1.status(), StatusCode::CREATED);
let body1 = axum::body::to_bytes(response1.into_body(), usize::MAX).await.expect("Body");
let json1: serde_json::Value = serde_json::from_slice(&body1).expect("JSON");
let hash1 = json1["hash"].as_str().expect("hash");
let request2 = Request::builder()
.uri("/v1/epoch")
.method("POST")
.header("content-type", "application/json")
.body(Body::from(serde_json::to_vec(&epoch).expect("JSON")))
.expect("Request");
let response2 = app.oneshot(request2).await.expect("Request");
assert_eq!(response2.status(), StatusCode::CREATED);
let body2 = axum::body::to_bytes(response2.into_body(), usize::MAX).await.expect("Body");
let json2: serde_json::Value = serde_json::from_slice(&body2).expect("JSON");
let hash2 = json2["hash"].as_str().expect("hash");
// Same input should produce same ID
assert_eq!(hash1, hash2, "Same epoch input should produce deterministic ID");
}
// ============================================================================
// Epoch Timestamp Default Tests
// ============================================================================
#[tokio::test]
async fn test_create_epoch_defaults_timestamp() {
let env = common::create_test_env().await;
let app = create_router(env.state);
// Omit start_timestamp - should default to current time
let epoch = json!({
"name": "Default-Timestamp-Test"
});
let request = Request::builder()
.uri("/v1/epoch")
.method("POST")
.header("content-type", "application/json")
.body(Body::from(serde_json::to_vec(&epoch).expect("JSON")))
.expect("Request");
let response = app.oneshot(request).await.expect("Request");
assert_eq!(response.status(), StatusCode::CREATED);
// If we got 201, the timestamp defaulted successfully
let body = axum::body::to_bytes(response.into_body(), usize::MAX).await.expect("Body");
let json: serde_json::Value = serde_json::from_slice(&body).expect("JSON");
assert_eq!(json["status"], "created");
}