//! HTTP integration tests for DTO validation. //! //! Coverage: //! - Confidence validation (0.0 to 1.0 range) //! - Weight validation (0.0 to 1.0 range) //! - Hex string validation (length and character validation) //! - Conflict score validation (0.0 to 1.0 range) //! - Query parameter validation //! - Error 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; // ============================================================================ // Confidence Validation Tests // ============================================================================ #[tokio::test] async fn test_dto_confidence_validation() { let env = common::create_test_env().await; let app = create_router(env.state); // Test confidence > 1.0 let invalid_assertion = json!({ "subject": "Tesla", "predicate": "has_revenue", "object": {"type": "Number", "value": 100.0}, "confidence": 1.5, "signatures": [{ "agent_id": "a".repeat(64), "signature": "b".repeat(128), "timestamp": 1234567890 }], "source_hash": "c".repeat(64) }); let request = Request::builder() .uri("/v1/assert") .method("POST") .header("content-type", "application/json") .body(Body::from(serde_json::to_vec(&invalid_assertion).expect("JSON"))) .expect("Request"); let response = app.clone().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("Confidence must be between 0.0 and 1.0")); // Test confidence < 0.0 let invalid_assertion = json!({ "subject": "Tesla", "predicate": "has_revenue", "object": {"type": "Number", "value": 100.0}, "confidence": -0.5, "signatures": [{ "agent_id": "a".repeat(64), "signature": "b".repeat(128), "timestamp": 1234567890 }], "source_hash": "c".repeat(64) }); let request = Request::builder() .uri("/v1/assert") .method("POST") .header("content-type", "application/json") .body(Body::from(serde_json::to_vec(&invalid_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("Confidence must be between 0.0 and 1.0")); } // ============================================================================ // Weight Validation Tests // ============================================================================ #[tokio::test] async fn test_dto_weight_validation() { let env = common::create_test_env().await; let app = create_router(env.state); // Test weight > 1.0 let invalid_vote = json!({ "assertion_hash": "a".repeat(64), "agent_id": "b".repeat(64), "weight": 1.5, "signature": "c".repeat(128) }); let request = Request::builder() .uri("/v1/vote") .method("POST") .header("content-type", "application/json") .body(Body::from(serde_json::to_vec(&invalid_vote).expect("JSON"))) .expect("Request"); let response = app.clone().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")); // Test weight < 0.0 let invalid_vote = json!({ "assertion_hash": "a".repeat(64), "agent_id": "b".repeat(64), "weight": -0.5, "signature": "c".repeat(128) }); let request = Request::builder() .uri("/v1/vote") .method("POST") .header("content-type", "application/json") .body(Body::from(serde_json::to_vec(&invalid_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")); } // ============================================================================ // Hex Validation Tests // ============================================================================ #[tokio::test] async fn test_hex_decode_wrong_length() { let env = common::create_test_env().await; let app = create_router(env.state); // Test assertion with wrong-length source_hash let invalid_assertion = json!({ "subject": "Tesla", "predicate": "has_revenue", "object": {"type": "Number", "value": 100.0}, "confidence": 0.9, "signatures": [{ "agent_id": "a".repeat(64), "signature": "b".repeat(128), "timestamp": 1234567890 }], "source_hash": "abc" // Too short }); let request = Request::builder() .uri("/v1/assert") .method("POST") .header("content-type", "application/json") .body(Body::from(serde_json::to_vec(&invalid_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"); let error_msg = json["error"].as_str().expect("error"); assert!(error_msg.contains("Expected 64 hex characters")); assert!(error_msg.contains("got 3")); } #[tokio::test] async fn test_hex_decode_valid() { let env = common::create_test_env().await; let app = create_router(env.state); // Test assertion with valid hex lengths // Note: This will succeed in parsing/validation but may fail in the ingest worker // We're primarily testing that hex validation accepts correct-length strings let valid_assertion = json!({ "subject": "Tesla", "predicate": "has_revenue", "object": {"type": "Number", "value": 100.0}, "confidence": 0.9, "signatures": [{ "agent_id": "a".repeat(64), "signature": "b".repeat(128), "timestamp": 1234567890 }], "source_hash": "c".repeat(64) }); let request = Request::builder() .uri("/v1/assert") .method("POST") .header("content-type", "application/json") .body(Body::from(serde_json::to_vec(&valid_assertion).expect("JSON"))) .expect("Request"); let response = app.oneshot(request).await.expect("Request"); // Accept 201 (success), 400 (invalid signature crypto — hex format was valid, but // verify_assertion_signatures rejects non-Ed25519-valid bytes), or 500 (ingest worker // not running in test). We're primarily testing that hex-format validation accepts // correct-length strings without rejecting them at the parsing layer. assert!( response.status() == StatusCode::CREATED || response.status() == StatusCode::BAD_REQUEST || response.status() == StatusCode::INTERNAL_SERVER_ERROR, "Expected 201, 400, or 500, got {}", response.status() ); } #[tokio::test] async fn test_dto_to_assertion_invalid_hex() { let env = common::create_test_env().await; let app = create_router(env.state); // Test assertion with invalid hex characters let invalid_assertion = json!({ "subject": "Tesla", "predicate": "has_revenue", "object": {"type": "Number", "value": 100.0}, "confidence": 0.9, "signatures": [{ "agent_id": "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", // 'z' is not a valid hex character (64 z's) "signature": "b".repeat(128), "timestamp": 1234567890 }], "source_hash": "c".repeat(64) }); let request = Request::builder() .uri("/v1/assert") .method("POST") .header("content-type", "application/json") .body(Body::from(serde_json::to_vec(&invalid_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"); let error = json["error"].as_str().expect("error"); // The error could be "Invalid hex encoding" or similar - just check it's a bad request error assert!(!error.is_empty(), "Error message should not be empty"); } // ============================================================================ // Query Parameter Validation Tests // ============================================================================ #[tokio::test] async fn test_query_empty_results() { let env = common::create_test_env().await; let app = create_router(env.state); let request = Request::builder() .uri("/v1/query?subject=nonexistent") .method("GET") .body(Body::empty()) .expect("Request"); let response = app.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 json: serde_json::Value = serde_json::from_slice(&body).expect("JSON"); assert_eq!(json["assertions"], json!([])); assert_eq!(json["total_count"], 0); assert_eq!(json["has_more"], false); } #[tokio::test] async fn test_query_conflict_score_validation() { let env = common::create_test_env().await; let app = create_router(env.state); // Test: min_conflict_score > 1.0 (invalid) let request = Request::builder() .uri("/v1/query?subject=Test&predicate=test&min_conflict_score=1.5") .method("GET") .body(Body::empty()) .expect("Request"); let response = app.clone().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("min_conflict_score must be between 0.0 and 1.0")); // Test: min_conflict_score < 0.0 (invalid) let request = Request::builder() .uri("/v1/query?subject=Test&predicate=test&min_conflict_score=-0.1") .method("GET") .body(Body::empty()) .expect("Request"); let response = app.clone().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("min_conflict_score must be between 0.0 and 1.0")); // Test: max_conflict_score > 1.0 (invalid) let request = Request::builder() .uri("/v1/query?subject=Test&predicate=test&max_conflict_score=2.0") .method("GET") .body(Body::empty()) .expect("Request"); let response = app.clone().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("max_conflict_score must be between 0.0 and 1.0")); // Test: valid conflict scores (should succeed even if no results) let request = Request::builder() .uri("/v1/query?subject=Test&predicate=test&min_conflict_score=0.3&max_conflict_score=0.7") .method("GET") .body(Body::empty()) .expect("Request"); let response = app.clone().oneshot(request).await.expect("Request"); assert_eq!(response.status(), StatusCode::OK); }