//! HTTP integration tests for CRUD operations. //! //! Coverage: //! - POST /v1/assert - Assertion creation with signatures //! - POST /v1/vote - Vote submission with weight validation //! - Error responses (400 Bad Request) //! - Response structure validation #![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; // ============================================================================ // POST /v1/assert - Create Assertion Tests // ============================================================================ #[tokio::test] async fn test_assert_valid_creation() { let env = common::create_test_env().await; let app = create_router(env.state); let subject = "Test_Entity"; let predicate = "test_property"; let message = format!("{}:{}", subject, predicate); let (agent_id, signature) = common::sign_message(&message); let assertion = json!({ "subject": subject, "predicate": predicate, "object": {"type": "Number", "value": 42.0}, "confidence": 0.95, "signatures": [{ "agent_id": hex::encode(agent_id), "signature": hex::encode(signature), "timestamp": 1234567890 }], "source_hash": hex::encode([0u8; 32]) }); let request = Request::builder() .uri("/v1/assert") .method("POST") .header("content-type", "application/json") .body(Body::from(serde_json::to_vec(&assertion).expect("JSON serialization"))) .expect("Failed to build request"); let response = app.oneshot(request).await.expect("Request failed"); assert_eq!(response.status(), StatusCode::CREATED); let body = axum::body::to_bytes(response.into_body(), usize::MAX).await.expect("Failed to read body"); let json: serde_json::Value = serde_json::from_slice(&body).expect("Failed to parse JSON"); assert_eq!(json["status"], "created"); assert!(json["hash"].is_string()); assert_eq!(json["hash"].as_str().expect("hash").len(), 64); // BLAKE3 = 32 bytes = 64 hex } #[tokio::test] async fn test_assert_response_includes_hash() { let env = common::create_test_env().await; let app = create_router(env.state); let (agent_id, signature) = common::sign_message("Test:test"); let assertion = json!({ "subject": "Test", "predicate": "test", "object": {"type": "Text", "value": "test_value"}, "confidence": 0.8, "signatures": [{ "agent_id": hex::encode(agent_id), "signature": hex::encode(signature), "timestamp": 1000000000 }], "source_hash": hex::encode([1u8; 32]) }); let request = Request::builder() .uri("/v1/assert") .method("POST") .header("content-type", "application/json") .body(Body::from(serde_json::to_vec(&assertion).expect("JSON"))) .expect("Request build"); 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 parse"); // Verify hash is present and correctly formatted let hash = json["hash"].as_str().expect("hash should be present"); assert_eq!(hash.len(), 64); assert!(hash.chars().all(|c| c.is_ascii_hexdigit())); } #[tokio::test] async fn test_assert_missing_signature_fails() { let env = common::create_test_env().await; let app = create_router(env.state); let assertion = json!({ "subject": "Test", "predicate": "test", "object": {"type": "Number", "value": 1.0}, "confidence": 0.9, "signatures": [], // Empty signatures "source_hash": hex::encode([0u8; 32]) }); let request = Request::builder() .uri("/v1/assert") .method("POST") .header("content-type", "application/json") .body(Body::from(serde_json::to_vec(&assertion).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 message") .contains("At least one signature is required")); } #[tokio::test] async fn test_assert_invalid_confidence_fails() { let env = common::create_test_env().await; let app = create_router(env.state); let (agent_id, signature) = common::sign_message("test"); let assertion = json!({ "subject": "Test", "predicate": "test", "object": {"type": "Number", "value": 1.0}, "confidence": 2.0, // Invalid: > 1.0 "signatures": [{ "agent_id": hex::encode(agent_id), "signature": hex::encode(signature), "timestamp": 1000 }], "source_hash": hex::encode([0u8; 32]) }); let request = Request::builder() .uri("/v1/assert") .method("POST") .header("content-type", "application/json") .body(Body::from(serde_json::to_vec(&assertion).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"); // Verify error response structure matches ErrorResponse DTO assert!(json.get("error").is_some()); assert!(json.get("code").is_some()); assert!(json["error"].as_str().expect("error message").contains("Confidence")); } #[tokio::test] async fn test_hex_validation_wrong_length() { let env = common::create_test_env().await; let app = create_router(env.state); let (agent_id, signature) = common::sign_message("test"); let assertion = json!({ "subject": "Test", "predicate": "test", "object": {"type": "Number", "value": 1.0}, "confidence": 0.9, "signatures": [{ "agent_id": hex::encode(agent_id), "signature": hex::encode(signature), "timestamp": 1000 }], "source_hash": "abc" // Too short - should be 64 hex chars }); let request = Request::builder() .uri("/v1/assert") .method("POST") .header("content-type", "application/json") .body(Body::from(serde_json::to_vec(&assertion).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("Expected 64 hex characters")); } #[tokio::test] async fn test_invalid_json_payload() { let env = common::create_test_env().await; let app = create_router(env.state); // Invalid JSON payload let invalid_json = b"{invalid json}"; let request = Request::builder() .uri("/v1/assert") .method("POST") .header("content-type", "application/json") .body(Body::from(invalid_json.to_vec())) .expect("Request"); let response = app.oneshot(request).await.expect("Request"); assert_eq!(response.status(), StatusCode::BAD_REQUEST); } // ============================================================================ // POST /v1/vote - Submit Vote Tests // ============================================================================ #[tokio::test] async fn test_vote_valid_submission() { let env = common::create_test_env().await; let app = create_router(env.state); let (agent_id, signature) = common::sign_message("vote_message"); let vote = json!({ "assertion_hash": hex::encode([0u8; 32]), "agent_id": hex::encode(agent_id), "weight": 0.75, "signature": hex::encode(signature) }); let request = Request::builder() .uri("/v1/vote") .method("POST") .header("content-type", "application/json") .body(Body::from(serde_json::to_vec(&vote).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"); assert!(json["hash"].is_string()); assert_eq!(json["hash"].as_str().expect("hash").len(), 64); } #[tokio::test] async fn test_vote_response_structure() { let env = common::create_test_env().await; let app = create_router(env.state); let (agent_id, signature) = common::sign_message("test"); let vote = json!({ "assertion_hash": hex::encode([1u8; 32]), "agent_id": hex::encode(agent_id), "weight": 1.0, "signature": hex::encode(signature) }); let request = Request::builder() .uri("/v1/vote") .method("POST") .header("content-type", "application/json") .body(Body::from(serde_json::to_vec(&vote).expect("JSON"))) .expect("Request"); let response = app.oneshot(request).await.expect("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"); // Verify response structure matches CreateResponse assert!(json.get("hash").is_some()); assert!(json.get("status").is_some()); assert_eq!(json.as_object().expect("object").len(), 2); } #[tokio::test] async fn test_vote_invalid_weight_fails() { let env = common::create_test_env().await; let app = create_router(env.state); let (agent_id, signature) = common::sign_message("test"); // Test weight > 1.0 let vote = json!({ "assertion_hash": hex::encode([0u8; 32]), "agent_id": hex::encode(agent_id), "weight": 1.5, "signature": hex::encode(signature) }); let request = Request::builder() .uri("/v1/vote") .method("POST") .header("content-type", "application/json") .body(Body::from(serde_json::to_vec(&vote).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("Weight must be between 0.0 and 1.0")); } #[tokio::test] async fn test_vote_with_provenance_fields() { let env = common::create_test_env().await; let app = create_router(env.state); let (agent_id, signature) = common::sign_message("vote_with_provenance"); let vote = json!({ "assertion_hash": hex::encode([0u8; 32]), "agent_id": hex::encode(agent_id), "weight": 0.9, "signature": hex::encode(signature), "source_url": "https://example.com/article", "observed_context": "The study found that coffee consumption..." }); let request = Request::builder() .uri("/v1/vote") .method("POST") .header("content-type", "application/json") .body(Body::from(serde_json::to_vec(&vote).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"); assert!(json["hash"].is_string()); assert_eq!(json["hash"].as_str().expect("hash").len(), 64); } #[tokio::test] async fn test_vote_without_provenance_fields_backward_compat() { // Ensure votes without provenance fields still work (backward compatibility) let env = common::create_test_env().await; let app = create_router(env.state); let (agent_id, signature) = common::sign_message("vote_no_provenance"); let vote = json!({ "assertion_hash": hex::encode([0u8; 32]), "agent_id": hex::encode(agent_id), "weight": 0.8, "signature": hex::encode(signature) // No source_url or observed_context }); let request = Request::builder() .uri("/v1/vote") .method("POST") .header("content-type", "application/json") .body(Body::from(serde_json::to_vec(&vote).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"); assert!(json["hash"].is_string()); } #[tokio::test] async fn test_vote_source_url_too_long_fails() { let env = common::create_test_env().await; let app = create_router(env.state); let (agent_id, signature) = common::sign_message("test"); // Test source_url > 2048 characters let long_url = format!("https://example.com/{}", "x".repeat(2049)); let vote = json!({ "assertion_hash": hex::encode([0u8; 32]), "agent_id": hex::encode(agent_id), "weight": 0.8, "signature": hex::encode(signature), "source_url": long_url }); let request = Request::builder() .uri("/v1/vote") .method("POST") .header("content-type", "application/json") .body(Body::from(serde_json::to_vec(&vote).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("2048")); } #[tokio::test] async fn test_vote_source_url_empty_fails() { let env = common::create_test_env().await; let app = create_router(env.state); let (agent_id, signature) = common::sign_message("test"); // Test empty source_url let vote = json!({ "assertion_hash": hex::encode([0u8; 32]), "agent_id": hex::encode(agent_id), "weight": 0.8, "signature": hex::encode(signature), "source_url": "" }); let request = Request::builder() .uri("/v1/vote") .method("POST") .header("content-type", "application/json") .body(Body::from(serde_json::to_vec(&vote).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("empty")); } #[tokio::test] async fn test_vote_observed_context_too_large_fails() { let env = common::create_test_env().await; let app = create_router(env.state); let (agent_id, signature) = common::sign_message("test"); // Test observed_context > 64KB let large_context = "c".repeat(65537); let vote = json!({ "assertion_hash": hex::encode([0u8; 32]), "agent_id": hex::encode(agent_id), "weight": 0.8, "signature": hex::encode(signature), "observed_context": large_context }); let request = Request::builder() .uri("/v1/vote") .method("POST") .header("content-type", "application/json") .body(Body::from(serde_json::to_vec(&vote).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("64KB")); }