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