From 43430aef2750559f5231a781196d75a092dab2ba Mon Sep 17 00:00:00 2001 From: DevMuhdishaq Date: Sat, 27 Jun 2026 21:18:14 +0100 Subject: [PATCH 1/4] Add integration tests for GET /verify/:hash/history endpoint --- contract/tests/history_tests.rs | 155 ++++++++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 contract/tests/history_tests.rs diff --git a/contract/tests/history_tests.rs b/contract/tests/history_tests.rs new file mode 100644 index 0000000..e8ab9b2 --- /dev/null +++ b/contract/tests/history_tests.rs @@ -0,0 +1,155 @@ +use axum_test::TestServer; +use stellar_doc_verifier::{app, AppState, HistoryResponse, TransactionRecord}; +use stellar_doc_verifier::cache::{CacheBackend, InMemoryCache}; +use stellar_doc_verifier::stellar::StellarClient; +use stellar_doc_verifier::metrics::MetricsRegistry; +use stellar_doc_verifier::module::webhook::VerificationWebhookNotifier; +use std::sync::Arc; + +fn create_test_state() -> AppState { + let stellar = Arc::new(StellarClient::new("https://horizon-testnet.stellar.org")); + let cache = Arc::new(CacheBackend::InMemory(InMemoryCache::new())); + let metrics = Arc::new(MetricsRegistry::new()); + let notifier = Arc::new(VerificationWebhookNotifier::new()); + + AppState { + stellar, + cache, + metrics, + stellar_secret_key: "test_secret".to_string(), + notifier, + } +} + +fn valid_sha256_hash() -> &'static str { + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" +} + +#[tokio::test] +async fn test_hash_with_no_history_returns_200_empty_array() { + let state = create_test_state(); + let app = app(state); + let server = TestServer::new(app).unwrap(); + + let response = server.get(&format!("/verify/{}/history", valid_sha256_hash())).await; + + response.assert_status_ok(); + let history: HistoryResponse = response.json(); + assert_eq!(history.count, 0); + assert!(history.transactions.is_empty()); + assert_eq!(history.document_hash, valid_sha256_hash()); + assert!(!history.cached); +} + +#[tokio::test] +async fn test_hash_with_one_record_returns_count_one() { + let state = create_test_state(); + let cache_key = format!("history:{}", valid_sha256_hash()); + let transactions = vec![TransactionRecord { + transaction_id: "tx123".to_string(), + timestamp: 1620000000, + verified: true, + }]; + state.cache.set(&cache_key, &transactions, 3600).await.unwrap(); + + let app = app(state); + let server = TestServer::new(app).unwrap(); + + let response = server.get(&format!("/verify/{}/history", valid_sha256_hash())).await; + + response.assert_status_ok(); + let history: HistoryResponse = response.json(); + assert_eq!(history.count, 1); + assert_eq!(history.transactions.len(), 1); + assert_eq!(history.transactions[0].transaction_id, "tx123"); + assert!(history.cached); +} + +#[tokio::test] +async fn test_hash_with_multiple_transfers_returns_in_order() { + let state = create_test_state(); + let cache_key = format!("history:{}", valid_sha256_hash()); + let transactions = vec![ + TransactionRecord { + transaction_id: "tx1".to_string(), + timestamp: 1620000000, + verified: true, + }, + TransactionRecord { + transaction_id: "tx2".to_string(), + timestamp: 1630000000, + verified: true, + }, + TransactionRecord { + transaction_id: "tx3".to_string(), + timestamp: 1640000000, + verified: true, + }, + ]; + state.cache.set(&cache_key, &transactions, 3600).await.unwrap(); + + let app = app(state); + let server = TestServer::new(app).unwrap(); + + let response = server.get(&format!("/verify/{}/history", valid_sha256_hash())).await; + + response.assert_status_ok(); + let history: HistoryResponse = response.json(); + assert_eq!(history.count, 3); + assert_eq!(history.transactions.len(), 3); + assert_eq!(history.transactions[0].transaction_id, "tx1"); + assert_eq!(history.transactions[1].transaction_id, "tx2"); + assert_eq!(history.transactions[2].transaction_id, "tx3"); + assert!(history.cached); +} + +#[tokio::test] +async fn test_cache_hit_returns_cached_true() { + let state = create_test_state(); + let cache_key = format!("history:{}", valid_sha256_hash()); + let transactions = vec![TransactionRecord { + transaction_id: "tx_cache".to_string(), + timestamp: 1620000000, + verified: true, + }]; + state.cache.set(&cache_key, &transactions, 3600).await.unwrap(); + + let app = app(state); + let server = TestServer::new(app).unwrap(); + + let response = server.get(&format!("/verify/{}/history", valid_sha256_hash())).await; + + response.assert_status_ok(); + let history: HistoryResponse = response.json(); + assert!(history.cached); + assert_eq!(history.count, 1); +} + +#[tokio::test] +async fn test_invalid_hash_format_returns_400() { + let state = create_test_state(); + let app = app(state); + let server = TestServer::new(app).unwrap(); + + let invalid_hash = "invalid_hash"; // Not 64 hex chars + let response = server.get(&format!("/verify/{}/history", invalid_hash)).await; + + response.assert_status_bad_request(); + let error: serde_json::Value = response.json(); + assert!(error.get("error").is_some()); + assert!(error["error"].as_str().unwrap().contains("wrong length")); +} + +#[tokio::test] +async fn test_empty_hash_string_returns_400() { + let state = create_test_state(); + let app = app(state); + let server = TestServer::new(app).unwrap(); + + let response = server.get("/verify//history").await; + + response.assert_status_bad_request(); + let error: serde_json::Value = response.json(); + assert!(error.get("error").is_some()); + assert!(error["error"].as_str().unwrap().contains("must not be empty")); +} \ No newline at end of file From ed9b38af6213b961ec7a8e53f40af942e7ced5e2 Mon Sep 17 00:00:00 2001 From: DevMuhdishaq Date: Sat, 27 Jun 2026 21:21:05 +0100 Subject: [PATCH 2/4] updated --- contract/src/lib.rs | 18 +- .../module/middleware/hash_normalization.rs | 217 ++++++++++++++++++ contract/src/module/middleware/mod.rs | 1 + contract/src/module/mod.rs | 1 + 4 files changed, 229 insertions(+), 8 deletions(-) create mode 100644 contract/src/module/middleware/hash_normalization.rs create mode 100644 contract/src/module/middleware/mod.rs diff --git a/contract/src/lib.rs b/contract/src/lib.rs index d9e34af..994f034 100644 --- a/contract/src/lib.rs +++ b/contract/src/lib.rs @@ -176,6 +176,8 @@ fn map_validation_error(err: HashValidationError) -> (StatusCode, ValidationErro } pub fn app(state: AppState) -> Router { + use crate::module::middleware::hash_normalization::{hash_normalization_middleware, normalize}; + Router::new() .route("/health", get(health_check)) .route("/metrics", get(metrics_handler)) @@ -186,6 +188,7 @@ pub fn app(state: AppState) -> Router { .route("/submit", post(submit_document)) .route("/revoke", post(revoke_document)) .route("/transfer", post(record_transfer)) + .layer(axum::middleware::from_fn(hash_normalization_middleware)) .layer(TraceLayer::new_for_http()) .with_state(state) } @@ -337,7 +340,7 @@ pub async fn verify_document( State(state): State, Json(req): Json, ) -> Response { - let normalized_hash = HashValidator::normalize(&req.document_hash); + let normalized_hash = normalize(&req.document_hash); if let Err(err) = HashValidator::validate_sha256(&normalized_hash) { let (status, body) = map_validation_error(err); return (status, Json(body)).into_response(); @@ -397,7 +400,7 @@ pub async fn verify_document_history( State(state): State, Path(hash): Path, ) -> Response { - let normalized_hash = HashValidator::normalize(&hash); + let normalized_hash = normalize(&hash); if let Err(err) = HashValidator::validate_sha256(&normalized_hash) { let (status, body) = map_validation_error(err); return (status, Json(body)).into_response(); @@ -483,8 +486,7 @@ pub async fn batch_verify_documents( // Helper function to verify a single hash async fn verify_single_hash(state: &AppState, hash: String) -> BatchVerifyItem { - let normalized_hash = HashValidator::normalize(&hash); - + let normalized_hash = normalize(&hash); if let Err(err) = HashValidator::validate_sha256(&normalized_hash) { let error_msg = match err { HashValidationError::EmptyHash => "hash must not be empty".to_string(), @@ -572,7 +574,7 @@ pub async fn submit_document( State(state): State, Json(req): Json, ) -> impl IntoResponse { - let normalized_hash = HashValidator::normalize(&req.document_hash); + let normalized_hash = normalize(&req.document_hash); if let Err(err) = HashValidator::validate_sha256(&normalized_hash) { let (status, body) = map_validation_error(err); return (status, Json(body)).into_response(); @@ -639,7 +641,7 @@ pub async fn revoke_document( State(state): State, Json(req): Json, ) -> impl IntoResponse { - let normalized_hash = HashValidator::normalize(&req.document_hash); + let normalized_hash = normalize(&req.document_hash); if let Err(err) = HashValidator::validate_sha256(&normalized_hash) { let (status, body) = map_validation_error(err); return (status, Json(body)).into_response(); @@ -691,7 +693,7 @@ pub async fn revoke_document( } pub async fn transfer_document(Json(req): Json) -> impl IntoResponse { - let normalized_hash = HashValidator::normalize(&req.document_hash); + let normalized_hash = normalize(&req.document_hash); if let Err(err) = HashValidator::validate_sha256(&normalized_hash) { let (status, body) = map_validation_error(err); return (status, Json(body)); @@ -1061,4 +1063,4 @@ mod tests { assert_eq!(item.timestamp, None); assert_eq!(item.error, Some("invalid hash format".to_string())); } -} +} \ No newline at end of file diff --git a/contract/src/module/middleware/hash_normalization.rs b/contract/src/module/middleware/hash_normalization.rs new file mode 100644 index 0000000..14c92c0 --- /dev/null +++ b/contract/src/module/middleware/hash_normalization.rs @@ -0,0 +1,217 @@ +use axum::{ + extract::{Request, FromRequestParts}, + http::{StatusCode, response::IntoResponse}, + middleware::Next, + response::Response, + body::Bytes, +}; +use serde::{Deserialize, de::DeserializeOwned}; +use crate::hash_validator::{HashValidator, ValidationError}; +use crate::ValidationErrorResponse; + +/// Normalizes a hash string by trimming whitespace and converting to lowercase +pub fn normalize(hash: &str) -> String { + hash.trim().to_lowercase() +} + +/// Helper function to normalize hash fields in request bodies +pub fn normalize_request_body(body: &mut T) +where + T: HasDocumentHash +{ + if let Some(document_hash) = body.document_hash_mut() { + *document_hash = normalize(document_hash); + } +} + +/// Trait to extract and mutate the document_hash field from request types +pub trait HasDocumentHash { + fn document_hash(&self) -> Option<&str>; + fn document_hash_mut(&mut self) -> Option<&mut String>; +} + +// Implement HasDocumentHash for all request types that have a document_hash field +#[derive(Debug, Deserialize)] +#[deserialize_any] +struct AnyRequest; + +impl HasDocumentHash for crate::VerifyRequest { + fn document_hash(&self) -> Option<&str> { + Some(&self.document_hash) + } + fn document_hash_mut(&mut self) -> Option<&mut String> { + Some(&mut self.document_hash) + } +} + +impl HasDocumentHash for crate::SubmitRequest { + fn document_hash(&self) -> Option<&str> { + Some(&self.document_hash) + } + fn document_hash_mut(&mut self) -> Option<&mut String> { + Some(&mut self.document_hash) + } +} + +impl HasDocumentHash for crate::RevokeRequest { + fn document_hash(&self) -> Option<&str> { + Some(&self.document_hash) + } + fn document_hash_mut(&mut self) -> Option<&mut String> { + Some(&mut self.document_hash) + } +} + +impl HasDocumentHash for crate::TransferRequest { + fn document_hash(&self) -> Option<&str> { + Some(&self.document_hash) + } + fn document_hash_mut(&mut self) -> Option<&mut String> { + Some(&mut self.document_hash) + } +} + +/// Axum middleware that normalizes hash strings in request bodies and path parameters +pub async fn hash_normalization_middleware(mut req: Request, next: Next) -> Result { + // First, handle path parameters that might contain hashes (like /verify/:hash) + let uri = req.uri().clone(); + let path = uri.path(); + + // Check if this is an endpoint that has a hash in the path + if let Some(captures) = path.strip_prefix("/verify/").and_then(|p| p.strip_suffix("/history").or_else(|| Some(p))) { + if !captures.is_empty() && !captures.starts_with("batch") { + // This is /verify/:hash or /verify/:hash/history + // We'll handle the normalization in the handler itself since path params are extracted directly + // Middleware can't easily modify path params, so the existing normalize call in handlers is still used + // But we ensure it's using our shared normalize function + } + } + + // For request bodies, we can deserialize, normalize, then re-serialize + // This works for all JSON requests that have a document_hash field + let body = match hyper::body::to_bytes(req.body_mut()).await { + Ok(b) => b, + Err(_) => return Err((StatusCode::BAD_REQUEST, Json(ValidationErrorResponse { error: "Failed to read request body".to_string() }))), + }; + + // Try to deserialize and normalize if it's a request with document_hash + if let Ok(mut verify_req) = serde_json::from_slice::(&body) { + normalize_request_body(&mut verify_req); + // Validate after normalization + if let Err(err) = HashValidator::validate_sha256(&verify_req.document_hash) { + let (status, body) = map_validation_error(err); + return Err((status, Json(body))); + } + // Re-attach the normalized body + *req.body_mut() = serde_json::to_vec(&verify_req).unwrap().into(); + } else if let Ok(mut submit_req) = serde_json::from_slice::(&body) { + normalize_request_body(&mut submit_req); + if let Err(err) = HashValidator::validate_sha256(&submit_req.document_hash) { + let (status, body) = map_validation_error(err); + return Err((status, Json(body))); + } + *req.body_mut() = serde_json::to_vec(&submit_req).unwrap().into(); + } else if let Ok(mut revoke_req) = serde_json::from_slice::(&body) { + normalize_request_body(&mut revoke_req); + if let Err(err) = HashValidator::validate_sha256(&revoke_req.document_hash) { + let (status, body) = map_validation_error(err); + return Err((status, Json(body))); + } + *req.body_mut() = serde_json::to_vec(&revoke_req).unwrap().into(); + } else if let Ok(mut transfer_req) = serde_json::from_slice::(&body) { + normalize_request_body(&mut transfer_req); + if let Err(err) = HashValidator::validate_sha256(&transfer_req.document_hash) { + let (status, body) = map_validation_error(err); + return Err((status, Json(body))); + } + *req.body_mut() = serde_json::to_vec(&transfer_req).unwrap().into(); + } + + // Continue processing the request + Ok(next.run(req).await) +} + +fn map_validation_error(err: ValidationError) -> (StatusCode, ValidationErrorResponse) { + let message = match err { + ValidationError::EmptyHash => "hash must not be empty".to_string(), + ValidationError::WrongLength { expected, actual } => format!( + "hash has wrong length: expected {} characters, got {}", + expected, actual + ), + ValidationError::InvalidCharacter { + position, + character, + } => format!( + "hash contains invalid character '{}' at position {}", + character, position + ), + }; + + ( + StatusCode::BAD_REQUEST, + ValidationErrorResponse { error: message }, + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn sample_sha256_uppercase() -> &'static str { + "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855" + } + + fn sample_sha256_mixed() -> &'static str { + "e3b0c44298FC1c149AFbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + + fn sample_sha256_lowercase() -> &'static str { + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + + #[test] + fn lowercase_input_unchanged() { + let result = normalize(sample_sha256_lowercase()); + assert_eq!(result, sample_sha256_lowercase()); + assert_eq!(result.len(), 64); + } + + #[test] + fn uppercase_input_lowercased() { + let result = normalize(sample_sha256_uppercase()); + assert_eq!(result, sample_sha256_lowercase()); + assert_eq!(result.len(), 64); + } + + #[test] + fn mixed_case_input_lowercased() { + let result = normalize(sample_sha256_mixed()); + assert_eq!(result, sample_sha256_lowercase()); + assert_eq!(result.len(), 64); + } + + #[test] + fn leading_trailing_spaces_stripped() { + let input = format!(" {} ", sample_sha256_lowercase()); + let result = normalize(&input); + assert_eq!(result, sample_sha256_lowercase()); + assert_eq!(result.len(), 64); + } + + #[test] + fn combined_whitespace_and_uppercase() { + let input = format!(" {} ", sample_sha256_uppercase()); + let result = normalize(&input); + assert_eq!(result, sample_sha256_lowercase()); + assert_eq!(result.len(), 64); + } + + #[test] + fn sixtyfour_char_uppercase_normalizes_to_sixtyfour_lowercase() { + let input = sample_sha256_uppercase(); + assert_eq!(input.len(), 64); + let result = normalize(input); + assert_eq!(result.len(), 64); + assert_eq!(result, sample_sha256_lowercase()); + } +} \ No newline at end of file diff --git a/contract/src/module/middleware/mod.rs b/contract/src/module/middleware/mod.rs new file mode 100644 index 0000000..13d7439 --- /dev/null +++ b/contract/src/module/middleware/mod.rs @@ -0,0 +1 @@ +pub mod hash_normalization; \ No newline at end of file diff --git a/contract/src/module/mod.rs b/contract/src/module/mod.rs index ed702a1..cbd795d 100644 --- a/contract/src/module/mod.rs +++ b/contract/src/module/mod.rs @@ -1,2 +1,3 @@ pub mod cache_warmup; pub mod webhook; +pub mod middleware; \ No newline at end of file From f342ffbbdcce7b2d25257257c0bfdb438eaf7873 Mon Sep 17 00:00:00 2001 From: DevMuhdishaq Date: Sun, 28 Jun 2026 00:31:41 +0100 Subject: [PATCH 3/4] fix: resolve all compilation errors in hash normalization middleware --- contract/Cargo.toml | 3 +- contract/src/lib.rs | 17 +++---- .../module/middleware/hash_normalization.rs | 44 ++++++++++++------- 3 files changed, 39 insertions(+), 25 deletions(-) diff --git a/contract/Cargo.toml b/contract/Cargo.toml index e91debe..335d4c0 100644 --- a/contract/Cargo.toml +++ b/contract/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" [dependencies] # Web framework axum = "0.7" +http-body-util = "0.1" tokio = { version = "1.35", features = ["full"] } tower = "0.4" tower-http = { version = "0.5", features = ["trace", "cors"] } @@ -48,4 +49,4 @@ async-trait = "0.1" [dev-dependencies] httpmock = "0.7" -axum-test = "16.4.1" +axum-test = "16.4.1" \ No newline at end of file diff --git a/contract/src/lib.rs b/contract/src/lib.rs index 994f034..71f8512 100644 --- a/contract/src/lib.rs +++ b/contract/src/lib.rs @@ -25,6 +25,7 @@ use tracing::{info, warn}; use cache::CacheBackend; use hash_validator::{HashValidator, ValidationError as HashValidationError}; use metrics::MetricsRegistry; +use module::middleware::hash_normalization::normalize; use module::webhook::VerificationWebhookNotifier; use stellar::{StellarClient, TransactionRecord}; @@ -39,7 +40,7 @@ pub struct AppState { } // Request/Response types -#[derive(Debug, Deserialize)] +#[derive(Debug, Serialize, Deserialize)] pub struct VerifyRequest { pub document_hash: String, pub transaction_id: Option, @@ -54,7 +55,7 @@ pub struct VerifyResponse { } /// Request type for submitting a document hash to Stellar blockchain -#[derive(Debug, Deserialize)] +#[derive(Debug, Serialize, Deserialize)] pub struct SubmitRequest { pub document_hash: String, pub document_id: String, @@ -70,7 +71,7 @@ pub struct SubmitResponse { pub error: Option, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Serialize, Deserialize)] pub struct RevokeRequest { pub document_hash: String, pub reason: String, @@ -104,7 +105,7 @@ pub struct ValidationErrorResponse { pub error: String, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Serialize, Deserialize)] pub struct BatchVerifyRequest { pub hashes: Vec, } @@ -126,7 +127,7 @@ pub struct BatchVerifyItem { pub error: Option, } -#[derive(Debug, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct TransferRequest { pub document_hash: String, pub from_owner: String, @@ -176,8 +177,8 @@ fn map_validation_error(err: HashValidationError) -> (StatusCode, ValidationErro } pub fn app(state: AppState) -> Router { - use crate::module::middleware::hash_normalization::{hash_normalization_middleware, normalize}; - + use crate::module::middleware::hash_normalization::hash_normalization_middleware; + Router::new() .route("/health", get(health_check)) .route("/metrics", get(metrics_handler)) @@ -1063,4 +1064,4 @@ mod tests { assert_eq!(item.timestamp, None); assert_eq!(item.error, Some("invalid hash format".to_string())); } -} \ No newline at end of file +} diff --git a/contract/src/module/middleware/hash_normalization.rs b/contract/src/module/middleware/hash_normalization.rs index 14c92c0..a423286 100644 --- a/contract/src/module/middleware/hash_normalization.rs +++ b/contract/src/module/middleware/hash_normalization.rs @@ -1,13 +1,13 @@ +use crate::hash_validator::{HashValidator, ValidationError}; +use crate::ValidationErrorResponse; use axum::{ - extract::{Request, FromRequestParts}, - http::{StatusCode, response::IntoResponse}, + extract::Request, + http::StatusCode, middleware::Next, - response::Response, - body::Bytes, + response::{IntoResponse, Json, Response}, }; -use serde::{Deserialize, de::DeserializeOwned}; -use crate::hash_validator::{HashValidator, ValidationError}; -use crate::ValidationErrorResponse; +use http_body_util::BodyExt; +use serde::Deserialize; /// Normalizes a hash string by trimming whitespace and converting to lowercase pub fn normalize(hash: &str) -> String { @@ -17,7 +17,7 @@ pub fn normalize(hash: &str) -> String { /// Helper function to normalize hash fields in request bodies pub fn normalize_request_body(body: &mut T) where - T: HasDocumentHash + T: HasDocumentHash, { if let Some(document_hash) = body.document_hash_mut() { *document_hash = normalize(document_hash); @@ -32,7 +32,6 @@ pub trait HasDocumentHash { // Implement HasDocumentHash for all request types that have a document_hash field #[derive(Debug, Deserialize)] -#[deserialize_any] struct AnyRequest; impl HasDocumentHash for crate::VerifyRequest { @@ -72,13 +71,19 @@ impl HasDocumentHash for crate::TransferRequest { } /// Axum middleware that normalizes hash strings in request bodies and path parameters -pub async fn hash_normalization_middleware(mut req: Request, next: Next) -> Result { +pub async fn hash_normalization_middleware( + mut req: Request, + next: Next, +) -> Result { // First, handle path parameters that might contain hashes (like /verify/:hash) let uri = req.uri().clone(); let path = uri.path(); - + // Check if this is an endpoint that has a hash in the path - if let Some(captures) = path.strip_prefix("/verify/").and_then(|p| p.strip_suffix("/history").or_else(|| Some(p))) { + if let Some(captures) = path + .strip_prefix("/verify/") + .and_then(|p| p.strip_suffix("/history").or_else(|| Some(p))) + { if !captures.is_empty() && !captures.starts_with("batch") { // This is /verify/:hash or /verify/:hash/history // We'll handle the normalization in the handler itself since path params are extracted directly @@ -89,9 +94,16 @@ pub async fn hash_normalization_middleware(mut req: Request, next: Next) -> Resu // For request bodies, we can deserialize, normalize, then re-serialize // This works for all JSON requests that have a document_hash field - let body = match hyper::body::to_bytes(req.body_mut()).await { - Ok(b) => b, - Err(_) => return Err((StatusCode::BAD_REQUEST, Json(ValidationErrorResponse { error: "Failed to read request body".to_string() }))), + let body = match req.body_mut().collect().await { + Ok(b) => b.to_bytes(), + Err(_) => { + return Err(( + StatusCode::BAD_REQUEST, + Json(ValidationErrorResponse { + error: "Failed to read request body".to_string(), + }), + )) + } }; // Try to deserialize and normalize if it's a request with document_hash @@ -214,4 +226,4 @@ mod tests { assert_eq!(result.len(), 64); assert_eq!(result, sample_sha256_lowercase()); } -} \ No newline at end of file +} From d9600048d2cb354834ea9fccdc8eb4543debbe37 Mon Sep 17 00:00:00 2001 From: DevMuhdishaq Date: Sun, 28 Jun 2026 00:31:54 +0100 Subject: [PATCH 4/4] update --- contract/src/module/middleware/mod.rs | 2 +- contract/src/module/mod.rs | 2 +- contract/tests/history_tests.rs | 51 ++++++++++++++++++++------- 3 files changed, 40 insertions(+), 15 deletions(-) diff --git a/contract/src/module/middleware/mod.rs b/contract/src/module/middleware/mod.rs index 13d7439..d3c6093 100644 --- a/contract/src/module/middleware/mod.rs +++ b/contract/src/module/middleware/mod.rs @@ -1 +1 @@ -pub mod hash_normalization; \ No newline at end of file +pub mod hash_normalization; diff --git a/contract/src/module/mod.rs b/contract/src/module/mod.rs index cbd795d..777e5ad 100644 --- a/contract/src/module/mod.rs +++ b/contract/src/module/mod.rs @@ -1,3 +1,3 @@ pub mod cache_warmup; +pub mod middleware; pub mod webhook; -pub mod middleware; \ No newline at end of file diff --git a/contract/tests/history_tests.rs b/contract/tests/history_tests.rs index e8ab9b2..7521288 100644 --- a/contract/tests/history_tests.rs +++ b/contract/tests/history_tests.rs @@ -1,10 +1,10 @@ use axum_test::TestServer; -use stellar_doc_verifier::{app, AppState, HistoryResponse, TransactionRecord}; +use std::sync::Arc; use stellar_doc_verifier::cache::{CacheBackend, InMemoryCache}; -use stellar_doc_verifier::stellar::StellarClient; use stellar_doc_verifier::metrics::MetricsRegistry; use stellar_doc_verifier::module::webhook::VerificationWebhookNotifier; -use std::sync::Arc; +use stellar_doc_verifier::stellar::StellarClient; +use stellar_doc_verifier::{app, AppState, HistoryResponse, TransactionRecord}; fn create_test_state() -> AppState { let stellar = Arc::new(StellarClient::new("https://horizon-testnet.stellar.org")); @@ -31,7 +31,9 @@ async fn test_hash_with_no_history_returns_200_empty_array() { let app = app(state); let server = TestServer::new(app).unwrap(); - let response = server.get(&format!("/verify/{}/history", valid_sha256_hash())).await; + let response = server + .get(&format!("/verify/{}/history", valid_sha256_hash())) + .await; response.assert_status_ok(); let history: HistoryResponse = response.json(); @@ -50,12 +52,18 @@ async fn test_hash_with_one_record_returns_count_one() { timestamp: 1620000000, verified: true, }]; - state.cache.set(&cache_key, &transactions, 3600).await.unwrap(); + state + .cache + .set(&cache_key, &transactions, 3600) + .await + .unwrap(); let app = app(state); let server = TestServer::new(app).unwrap(); - let response = server.get(&format!("/verify/{}/history", valid_sha256_hash())).await; + let response = server + .get(&format!("/verify/{}/history", valid_sha256_hash())) + .await; response.assert_status_ok(); let history: HistoryResponse = response.json(); @@ -86,12 +94,18 @@ async fn test_hash_with_multiple_transfers_returns_in_order() { verified: true, }, ]; - state.cache.set(&cache_key, &transactions, 3600).await.unwrap(); + state + .cache + .set(&cache_key, &transactions, 3600) + .await + .unwrap(); let app = app(state); let server = TestServer::new(app).unwrap(); - let response = server.get(&format!("/verify/{}/history", valid_sha256_hash())).await; + let response = server + .get(&format!("/verify/{}/history", valid_sha256_hash())) + .await; response.assert_status_ok(); let history: HistoryResponse = response.json(); @@ -112,12 +126,18 @@ async fn test_cache_hit_returns_cached_true() { timestamp: 1620000000, verified: true, }]; - state.cache.set(&cache_key, &transactions, 3600).await.unwrap(); + state + .cache + .set(&cache_key, &transactions, 3600) + .await + .unwrap(); let app = app(state); let server = TestServer::new(app).unwrap(); - let response = server.get(&format!("/verify/{}/history", valid_sha256_hash())).await; + let response = server + .get(&format!("/verify/{}/history", valid_sha256_hash())) + .await; response.assert_status_ok(); let history: HistoryResponse = response.json(); @@ -132,7 +152,9 @@ async fn test_invalid_hash_format_returns_400() { let server = TestServer::new(app).unwrap(); let invalid_hash = "invalid_hash"; // Not 64 hex chars - let response = server.get(&format!("/verify/{}/history", invalid_hash)).await; + let response = server + .get(&format!("/verify/{}/history", invalid_hash)) + .await; response.assert_status_bad_request(); let error: serde_json::Value = response.json(); @@ -151,5 +173,8 @@ async fn test_empty_hash_string_returns_400() { response.assert_status_bad_request(); let error: serde_json::Value = response.json(); assert!(error.get("error").is_some()); - assert!(error["error"].as_str().unwrap().contains("must not be empty")); -} \ No newline at end of file + assert!(error["error"] + .as_str() + .unwrap() + .contains("must not be empty")); +}