diff --git a/contract/src/handlers/verify.rs b/contract/src/handlers/verify.rs index 0bad7399..549f2c28 100644 --- a/contract/src/handlers/verify.rs +++ b/contract/src/handlers/verify.rs @@ -192,6 +192,7 @@ pub async fn verify_single_hash(state: &AppState, hash: String) -> BatchVerifyIt "hash contains invalid character '{}' at position {}", character, position ), + HashValidationError::InvalidUtf8 => "hash contains invalid UTF-8 bytes".to_string(), }; return BatchVerifyItem { diff --git a/contract/src/hash_validator.rs b/contract/src/hash_validator.rs index 655414b6..089ca180 100644 --- a/contract/src/hash_validator.rs +++ b/contract/src/hash_validator.rs @@ -2,14 +2,43 @@ //! hashes. /// Reasons a candidate hash string fails validation. -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq, Clone)] pub enum ValidationError { WrongLength { expected: usize, actual: usize }, InvalidCharacter { position: usize, character: char }, EmptyHash, + InvalidUtf8, } -#[derive(Debug, PartialEq, Eq)] +impl std::fmt::Display for ValidationError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ValidationError::EmptyHash => write!(f, "hash must not be empty"), + ValidationError::WrongLength { expected, actual } => { + write!( + f, + "hash has wrong length: expected {} characters, got {}", + expected, actual + ) + } + ValidationError::InvalidCharacter { + position, + character, + } => { + write!( + f, + "hash contains invalid character '{}' at position {}", + character, position + ) + } + ValidationError::InvalidUtf8 => write!(f, "hash contains invalid UTF-8 bytes"), + } + } +} + +impl std::error::Error for ValidationError {} + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum HashAlgorithm { SHA256, SHA512, @@ -32,6 +61,22 @@ impl HashValidator { Self::validate_with_length(hash, 128) } + /// Validates raw bytes representing a UTF-8 hex-encoded SHA-256 hash. + pub fn validate_sha256_bytes(bytes: &[u8]) -> Result<(), ValidationError> { + Self::validate_bytes(bytes, 64) + } + + /// Validates raw bytes representing a UTF-8 hex-encoded SHA-512 hash. + pub fn validate_sha512_bytes(bytes: &[u8]) -> Result<(), ValidationError> { + Self::validate_bytes(bytes, 128) + } + + /// Validates raw bytes representing a UTF-8 hex-encoded hash against an expected length. + pub fn validate_bytes(bytes: &[u8], expected_len: usize) -> Result<(), ValidationError> { + let s = std::str::from_utf8(bytes).map_err(|_| ValidationError::InvalidUtf8)?; + Self::validate_with_length(s, expected_len) + } + fn validate_with_length(hash: &str, expected_len: usize) -> Result<(), ValidationError> { let normalized = Self::normalize(hash); @@ -100,23 +145,179 @@ mod tests { assert!(HashValidator::validate_sha512(sample_sha512()).is_ok()); } + // ── Non-UTF8 and binary input tests ───────────────────────── + #[test] - fn wrong_length_error_for_63_char_hash() { - let hash = "a".repeat(63); - match HashValidator::validate_sha256(&hash) { - Err(ValidationError::WrongLength { expected, actual }) => { - assert_eq!(expected, 64); - assert_eq!(actual, 63); + fn validate_sha256_bytes_rejects_invalid_utf8_binary_sequences() { + let invalid_utf8_cases: Vec<&[u8]> = vec![ + &[0xFF, 0xFE, 0xFD], + &[0x80, 0x81, 0x82], + &[0xC3, 0x28], // Invalid 2-byte sequence + &[0xE2, 0x28, 0xA1], // Invalid 3-byte sequence + &[0xF0, 0x90, 0x28, 0xBC], // Invalid 4-byte sequence + b"e3b0c442\xFF\xFE98fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + ]; + + for bytes in invalid_utf8_cases { + assert_eq!( + HashValidator::validate_sha256_bytes(bytes), + Err(ValidationError::InvalidUtf8), + "expected InvalidUtf8 for {:?}", + bytes + ); + } + } + + #[test] + fn validate_sha512_bytes_rejects_invalid_utf8_binary_sequences() { + let invalid_utf8_cases: Vec<&[u8]> = vec![ + &[0xFF, 0xAA, 0xBB], + &[0xC0, 0xAF], // Overlong sequence + &[0xED, 0xA0, 0x80], // UTF-16 surrogate + b"cf83e135\xFF\xAAeefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e", + ]; + + for bytes in invalid_utf8_cases { + assert_eq!( + HashValidator::validate_sha512_bytes(bytes), + Err(ValidationError::InvalidUtf8), + "expected InvalidUtf8 for {:?}", + bytes + ); + } + } + + #[test] + fn validate_bytes_valid_utf8_hashes() { + assert!(HashValidator::validate_sha256_bytes(sample_sha256().as_bytes()).is_ok()); + assert!(HashValidator::validate_sha512_bytes(sample_sha512().as_bytes()).is_ok()); + } + + #[test] + fn sha256_rejects_non_ascii_unicode_and_binary_characters() { + // Unicode character (snowflake/snowman) embedded in 64-char string + let char_count_64_with_unicode = format!("{}☃{}", "a".repeat(30), "b".repeat(33)); + assert_eq!(char_count_64_with_unicode.chars().count(), 64); + match HashValidator::validate_sha256(&char_count_64_with_unicode) { + Err(ValidationError::InvalidCharacter { position, character }) => { + assert_eq!(position, 30); + assert_eq!(character, '☃'); + } + other => panic!("expected InvalidCharacter error, got {:?}", other), + } + + // Binary null byte in 64-char string + let with_null = format!("{}\0{}", "a".repeat(30), "b".repeat(33)); + match HashValidator::validate_sha256(&with_null) { + Err(ValidationError::InvalidCharacter { position, character }) => { + assert_eq!(position, 30); + assert_eq!(character, '\0'); } - other => panic!("expected WrongLength error, got {:?}", other), + other => panic!("expected InvalidCharacter error for null byte, got {:?}", other), } } #[test] - fn empty_hash_errors() { - match HashValidator::validate_sha256("") { - Err(ValidationError::EmptyHash) => {} - other => panic!("expected EmptyHash error, got {:?}", other), + fn sha512_rejects_non_ascii_unicode_and_binary_characters() { + // Unicode character (crab) embedded in 128-char string + let with_unicode = format!("{}🦀{}", "a".repeat(60), "b".repeat(67)); + assert_eq!(with_unicode.chars().count(), 128); + match HashValidator::validate_sha512(&with_unicode) { + Err(ValidationError::InvalidCharacter { position, character }) => { + assert_eq!(position, 60); + assert_eq!(character, '🦀'); + } + other => panic!("expected InvalidCharacter error, got {:?}", other), + } + + // Binary null byte in 128-char string + let with_null = format!("{}\0{}", "a".repeat(60), "b".repeat(67)); + match HashValidator::validate_sha512(&with_null) { + Err(ValidationError::InvalidCharacter { position, character }) => { + assert_eq!(position, 60); + assert_eq!(character, '\0'); + } + other => panic!("expected InvalidCharacter error for null byte, got {:?}", other), + } + } + + // ── Unexpected length tests for SHA-256 (too short / too long) ────── + + #[test] + fn sha256_unexpected_length_too_short() { + // Empty hash + assert_eq!(HashValidator::validate_sha256(""), Err(ValidationError::EmptyHash)); + assert_eq!(HashValidator::validate_sha256(" "), Err(ValidationError::EmptyHash)); + + // Various short lengths: 1, 10, 32, 63 + for len in [1, 10, 32, 63] { + let short_hash = "a".repeat(len); + assert_eq!( + HashValidator::validate_sha256(&short_hash), + Err(ValidationError::WrongLength { + expected: 64, + actual: len, + }), + "expected WrongLength for length {}", + len + ); + } + } + + #[test] + fn sha256_unexpected_length_too_long() { + // Various long lengths: 65, 100, 128 (SHA-512 length), 256 + for len in [65, 100, 128, 256] { + let long_hash = "a".repeat(len); + assert_eq!( + HashValidator::validate_sha256(&long_hash), + Err(ValidationError::WrongLength { + expected: 64, + actual: len, + }), + "expected WrongLength for length {}", + len + ); + } + } + + // ── Unexpected length tests for SHA-512 (too short / too long) ────── + + #[test] + fn sha512_unexpected_length_too_short() { + // Empty hash + assert_eq!(HashValidator::validate_sha512(""), Err(ValidationError::EmptyHash)); + assert_eq!(HashValidator::validate_sha512(" "), Err(ValidationError::EmptyHash)); + + // Various short lengths: 1, 32, 64 (SHA-256 length), 127 + for len in [1, 32, 64, 127] { + let short_hash = "a".repeat(len); + assert_eq!( + HashValidator::validate_sha512(&short_hash), + Err(ValidationError::WrongLength { + expected: 128, + actual: len, + }), + "expected WrongLength for length {}", + len + ); + } + } + + #[test] + fn sha512_unexpected_length_too_long() { + // Various long lengths: 129, 150, 200, 256 + for len in [129, 150, 200, 256] { + let long_hash = "a".repeat(len); + assert_eq!( + HashValidator::validate_sha512(&long_hash), + Err(ValidationError::WrongLength { + expected: 128, + actual: len, + }), + "expected WrongLength for length {}", + len + ); } } @@ -125,6 +326,10 @@ mod tests { let upper = sample_sha256().to_uppercase(); let normalized = HashValidator::normalize(&upper); assert!(HashValidator::validate_sha256(&normalized).is_ok()); + + let upper_512 = sample_sha512().to_uppercase(); + let normalized_512 = HashValidator::normalize(&upper_512); + assert!(HashValidator::validate_sha512(&normalized_512).is_ok()); } #[test] @@ -142,6 +347,20 @@ mod tests { } other => panic!("expected InvalidCharacter error, got {:?}", other), } + + let mut hash512 = sample_sha512().to_string(); + hash512.replace_range(100..101, "z"); // 'z' is not a valid hex digit + + match HashValidator::validate_sha512(&hash512) { + Err(ValidationError::InvalidCharacter { + position, + character, + }) => { + assert_eq!(position, 100); + assert_eq!(character, 'z'); + } + other => panic!("expected InvalidCharacter error, got {:?}", other), + } } #[test] @@ -160,5 +379,10 @@ mod tests { fn detect_algorithm_returns_none_for_other_lengths() { let algo = HashValidator::detect_algorithm("abc123"); assert_eq!(algo, None); + assert_eq!(HashValidator::detect_algorithm(""), None); + assert_eq!(HashValidator::detect_algorithm(&"a".repeat(63)), None); + assert_eq!(HashValidator::detect_algorithm(&"a".repeat(65)), None); + assert_eq!(HashValidator::detect_algorithm(&"a".repeat(127)), None); + assert_eq!(HashValidator::detect_algorithm(&"a".repeat(129)), None); } } diff --git a/contract/src/lib.rs b/contract/src/lib.rs index 1d758e0b..265ff09b 100644 --- a/contract/src/lib.rs +++ b/contract/src/lib.rs @@ -217,6 +217,7 @@ fn map_validation_error(err: HashValidationError) -> (StatusCode, ValidationErro "hash contains invalid character '{}' at position {}", character, position ), + HashValidationError::InvalidUtf8 => "hash contains invalid UTF-8 bytes".to_string(), }; ( @@ -733,6 +734,7 @@ async fn verify_single_hash(state: &AppState, hash: String) -> BatchVerifyItem { "hash contains invalid character '{}' at position {}", character, position ), + HashValidationError::InvalidUtf8 => "hash contains invalid UTF-8 bytes".to_string(), }; return BatchVerifyItem { diff --git a/contract/src/tests/integration.rs b/contract/src/tests/integration.rs index 86659ff1..0f51123a 100644 --- a/contract/src/tests/integration.rs +++ b/contract/src/tests/integration.rs @@ -401,3 +401,151 @@ async fn transfer_of_revoked_hash_returns_409() { resp.assert_status(StatusCode::CONFLICT); assert!(resp.text().contains("has been revoked")); } + +// ───────────────────────────────────────────────────────────────────────────── +// 11. Non-UTF8 / Malformed Binary Input (CT's SHA-256 / SHA-512 support) +// Confirm Axum extractor rejects non-UTF8/binary payload cleanly with 4xx, +// never panicking or returning 500. +// ───────────────────────────────────────────────────────────────────────────── + +#[tokio::test] +async fn verify_rejects_invalid_utf8_binary_body_with_4xx() { + let state = make_state("http://127.0.0.1:1"); + let server = TestServer::new(app(state)).unwrap(); + + // Raw bytes containing non-UTF8 binary byte sequences in the JSON body + let invalid_utf8_payload: &[u8] = b"{\"document_hash\": \"\xFF\xFE\xFD\"}"; + + let resp = server + .post("/verify") + .content_type("application/json") + .bytes(invalid_utf8_payload.to_vec().into()) + .await; + + let status = resp.status_code(); + assert!( + status.is_client_error(), + "expected a 4xx client error status when submitting non-UTF8 binary bytes, got {}", + status + ); + assert_ne!(status, StatusCode::INTERNAL_SERVER_ERROR); +} + +#[tokio::test] +async fn endpoints_reject_raw_malformed_binary_with_4xx() { + let state = make_state("http://127.0.0.1:1"); + let server = TestServer::new(app(state)).unwrap(); + + let raw_binary: &[u8] = &[0xFF, 0xFE, 0xFD, 0x80, 0x00]; + + for path in ["/verify", "/submit", "/revoke", "/transfer", "/verify/batch"] { + let resp = server + .post(path) + .content_type("application/json") + .bytes(raw_binary.to_vec().into()) + .await; + + let status = resp.status_code(); + assert!( + status.is_client_error(), + "path {} expected 4xx client error status for raw binary, got {}", + path, + status + ); + assert_ne!( + status, + StatusCode::INTERNAL_SERVER_ERROR, + "path {} must not return 500 for malformed binary", + path + ); + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// 12. Hash Unexpected Length Tests (Too Short / Too Long) +// Confirm Axum validator rejects too-short / too-long hash strings with 400. +// ───────────────────────────────────────────────────────────────────────────── + +#[tokio::test] +async fn verify_rejects_hash_of_unexpected_length_with_400() { + let state = make_state("http://127.0.0.1:1"); + let server = TestServer::new(app(state)).unwrap(); + + // Hashes that are too short (1, 10, 32, 63 chars) + for len in [1, 10, 32, 63] { + let short_hash = "a".repeat(len); + let resp = server + .post("/verify") + .json(&json!({ "document_hash": short_hash })) + .await; + + resp.assert_status(StatusCode::BAD_REQUEST); + let body: Value = resp.json(); + assert!( + body["error"].as_str().unwrap().contains("wrong length"), + "expected wrong length error for short hash (len {}): {:?}", + len, + body + ); + } + + // Hashes that are too long (65, 100, 128 chars for sha256 endpoint, 200 chars) + for len in [65, 100, 128, 200] { + let long_hash = "a".repeat(len); + let resp = server + .post("/verify") + .json(&json!({ "document_hash": long_hash })) + .await; + + resp.assert_status(StatusCode::BAD_REQUEST); + let body: Value = resp.json(); + assert!( + body["error"].as_str().unwrap().contains("wrong length"), + "expected wrong length error for long hash (len {}): {:?}", + len, + body + ); + } + + // Empty hash + let resp_empty = server + .post("/verify") + .json(&json!({ "document_hash": "" })) + .await; + resp_empty.assert_status(StatusCode::BAD_REQUEST); + let body_empty: Value = resp_empty.json(); + assert!(body_empty["error"].as_str().unwrap().contains("empty")); +} + +#[tokio::test] +async fn submit_and_revoke_reject_unexpected_hash_length_with_400() { + let state = make_state("http://127.0.0.1:1"); + let server = TestServer::new(app(state)).unwrap(); + + // Test too short and too long on /submit + for bad_hash in ["a".repeat(10), "a".repeat(65), "".to_string()] { + let resp = server + .post("/submit") + .json(&json!({ + "document_hash": bad_hash, + "document_id": "doc-1", + "submitter": "tester" + })) + .await; + resp.assert_status(StatusCode::BAD_REQUEST); + } + + // Test too short and too long on /revoke + for bad_hash in ["a".repeat(10), "a".repeat(65), "".to_string()] { + let resp = server + .post("/revoke") + .json(&json!({ + "document_hash": bad_hash, + "reason": "testing", + "revoked_by": "tester" + })) + .await; + resp.assert_status(StatusCode::BAD_REQUEST); + } +} + diff --git a/contract/src/types.rs b/contract/src/types.rs index 9eb248c9..01128334 100644 --- a/contract/src/types.rs +++ b/contract/src/types.rs @@ -149,6 +149,7 @@ pub fn map_validation_error(err: HashValidationError) -> (StatusCode, Validation "hash contains invalid character '{}' at position {}", character, position ), + HashValidationError::InvalidUtf8 => "hash contains invalid UTF-8 bytes".to_string(), }; ( diff --git a/contract/tests/hash_validation.rs b/contract/tests/hash_validation.rs new file mode 100644 index 00000000..f6ff7b1d --- /dev/null +++ b/contract/tests/hash_validation.rs @@ -0,0 +1,233 @@ +//! Integration tests for hash validation, non-UTF8 / binary input, and unexpected hash lengths. + +use axum::http::StatusCode; +use axum_test::TestServer; +use serde_json::{json, Value}; +use std::sync::Arc; +use stellar_doc_verifier::app; +use stellar_doc_verifier::cache::{CacheBackend, InMemoryCache}; +use stellar_doc_verifier::hash_validator::{HashAlgorithm, HashValidator, ValidationError}; +use stellar_doc_verifier::metrics::MetricsRegistry; +use stellar_doc_verifier::rate_limit::build_rate_limiter; +use stellar_doc_verifier::stellar::StellarClient; +use stellar_doc_verifier::AppState; + +const SECRET: &str = "SAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; + +fn test_state() -> AppState { + AppState { + stellar: Arc::new(StellarClient::new("https://horizon-testnet.stellar.org")), + cache: Arc::new(CacheBackend::InMemory(InMemoryCache::new())), + metrics: Arc::new(MetricsRegistry::new()), + stellar_secret_key: SECRET.to_string(), + rate_limiter: build_rate_limiter(1000, 1000), + webhook_urls: Vec::new(), + webhook_secret: None, + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// 1. Non-UTF8 and binary input tests on HashValidator directly +// ───────────────────────────────────────────────────────────────────────────── + +#[test] +fn test_hash_validator_rejects_invalid_utf8_binary_bytes() { + let invalid_bytes: Vec<&[u8]> = vec![ + &[0xFF, 0xFE, 0xFD], + &[0x80, 0x81, 0x82], + &[0xC3, 0x28], + &[0xE2, 0x28, 0xA1], + &[0xF0, 0x90, 0x28, 0xBC], + b"\x00\xFF\xFErawbinarydata", + ]; + + for bytes in invalid_bytes { + assert_eq!( + HashValidator::validate_sha256_bytes(bytes), + Err(ValidationError::InvalidUtf8) + ); + assert_eq!( + HashValidator::validate_sha512_bytes(bytes), + Err(ValidationError::InvalidUtf8) + ); + assert_eq!( + HashValidator::validate_bytes(bytes, 64), + Err(ValidationError::InvalidUtf8) + ); + } +} + +#[test] +fn test_hash_validator_unexpected_length_sha256() { + // Too short + assert_eq!( + HashValidator::validate_sha256(""), + Err(ValidationError::EmptyHash) + ); + for len in [1, 10, 32, 63] { + let h = "f".repeat(len); + assert_eq!( + HashValidator::validate_sha256(&h), + Err(ValidationError::WrongLength { + expected: 64, + actual: len, + }) + ); + } + + // Too long + for len in [65, 100, 128, 256] { + let h = "f".repeat(len); + assert_eq!( + HashValidator::validate_sha256(&h), + Err(ValidationError::WrongLength { + expected: 64, + actual: len, + }) + ); + } +} + +#[test] +fn test_hash_validator_unexpected_length_sha512() { + // Too short + assert_eq!( + HashValidator::validate_sha512(""), + Err(ValidationError::EmptyHash) + ); + for len in [1, 32, 64, 127] { + let h = "f".repeat(len); + assert_eq!( + HashValidator::validate_sha512(&h), + Err(ValidationError::WrongLength { + expected: 128, + actual: len, + }) + ); + } + + // Too long + for len in [129, 150, 200, 256] { + let h = "f".repeat(len); + assert_eq!( + HashValidator::validate_sha512(&h), + Err(ValidationError::WrongLength { + expected: 128, + actual: len, + }) + ); + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// 2. Axum extractor/validator rejection of non-UTF8 / malformed binary input +// ───────────────────────────────────────────────────────────────────────────── + +#[tokio::test] +async fn test_axum_verify_endpoint_rejects_invalid_utf8_binary_body_with_4xx() { + let server = TestServer::new(app(test_state())).unwrap(); + + // Body with non-UTF8 byte sequence in JSON string + let invalid_utf8_in_json: &[u8] = b"{\"document_hash\": \"\xFF\xFE\xFD\"}"; + + let resp = server + .post("/verify") + .content_type("application/json") + .bytes(invalid_utf8_in_json.to_vec().into()) + .await; + + let status = resp.status_code(); + assert!( + status.is_client_error(), + "expected a 4xx client error status for invalid UTF-8 in hash field, got {}", + status + ); + assert_ne!(status, StatusCode::INTERNAL_SERVER_ERROR); +} + +#[tokio::test] +async fn test_axum_endpoints_reject_malformed_binary_payload_cleanly() { + let server = TestServer::new(app(test_state())).unwrap(); + + let raw_binary_body: &[u8] = &[0xDE, 0xAD, 0xBE, 0xEF, 0xFF, 0x00, 0xFE]; + + for endpoint in ["/verify", "/submit", "/revoke", "/transfer", "/verify/batch"] { + let resp = server + .post(endpoint) + .content_type("application/json") + .bytes(raw_binary_body.to_vec().into()) + .await; + + let status = resp.status_code(); + assert!( + status.is_client_error(), + "endpoint {} must return 4xx for raw binary payload, got {}", + endpoint, + status + ); + assert_ne!( + status, + StatusCode::INTERNAL_SERVER_ERROR, + "endpoint {} must not panic or 500 on raw binary payload", + endpoint + ); + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// 3. Axum endpoint tests for unexpected hash lengths +// ───────────────────────────────────────────────────────────────────────────── + +#[tokio::test] +async fn test_axum_endpoints_reject_unexpected_length_hashes() { + let server = TestServer::new(app(test_state())).unwrap(); + + // 1) Verify endpoint: too short + let resp_short = server + .post("/verify") + .json(&json!({ "document_hash": "abcdef" })) + .await; + assert_eq!(resp_short.status_code(), StatusCode::BAD_REQUEST); + let body_short: Value = resp_short.json(); + assert!(body_short["error"].as_str().unwrap().contains("wrong length")); + + // 2) Verify endpoint: too long (e.g. 65 chars or SHA-512 length 128 chars) + let resp_long = server + .post("/verify") + .json(&json!({ "document_hash": "a".repeat(65) })) + .await; + assert_eq!(resp_long.status_code(), StatusCode::BAD_REQUEST); + let body_long: Value = resp_long.json(); + assert!(body_long["error"].as_str().unwrap().contains("wrong length")); + + // 3) Verify endpoint: empty + let resp_empty = server + .post("/verify") + .json(&json!({ "document_hash": "" })) + .await; + assert_eq!(resp_empty.status_code(), StatusCode::BAD_REQUEST); + let body_empty: Value = resp_empty.json(); + assert!(body_empty["error"].as_str().unwrap().contains("empty")); + + // 4) Submit endpoint: unexpected length + let resp_submit = server + .post("/submit") + .json(&json!({ + "document_hash": "12345", + "document_id": "doc-1", + "submitter": "tester" + })) + .await; + assert_eq!(resp_submit.status_code(), StatusCode::BAD_REQUEST); + + // 5) Revoke endpoint: unexpected length + let resp_revoke = server + .post("/revoke") + .json(&json!({ + "document_hash": "a".repeat(128), // 128 chars is invalid for sha256 revoke + "reason": "testing", + "revoked_by": "tester" + })) + .await; + assert_eq!(resp_revoke.status_code(), StatusCode::BAD_REQUEST); +}