Skip to content

Commit e139e23

Browse files
benthecarmanclaude
andcommitted
Add thorough tests for gRPC encoding utilities
Adds exhaustive and varied-input tests for encode/decode roundtrip (13 payload sizes including boundary values), compression flag rejection (all 255 non-zero values), short input rejection, percent-encoding (full ASCII range + multi-byte UTF-8), and timeout parsing (zero values, large values, invalid formats). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent d1b3c25 commit e139e23

1 file changed

Lines changed: 75 additions & 0 deletions

File tree

ldk-server/src/grpc.rs

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,81 @@ mod tests {
357357
assert_eq!(result.unwrap_err().code, GRPC_STATUS_INVALID_ARGUMENT);
358358
}
359359

360+
#[test]
361+
fn test_encode_decode_roundtrip_varied_sizes() {
362+
// Test encode/decode roundtrip with many payload sizes including edge cases
363+
let sizes = [0, 1, 2, 4, 5, 127, 128, 255, 256, 1000, 4096, 65535, 65536];
364+
for &size in &sizes {
365+
let payload: Vec<u8> = (0..size).map(|i| (i % 256) as u8).collect();
366+
let encoded = encode_grpc_frame(&payload);
367+
assert_eq!(encoded.len(), 5 + size, "wrong frame size for payload len {size}");
368+
assert_eq!(encoded[0], 0, "compression flag should be 0");
369+
let decoded_len =
370+
u32::from_be_bytes([encoded[1], encoded[2], encoded[3], encoded[4]]) as usize;
371+
assert_eq!(decoded_len, size, "length header mismatch for payload len {size}");
372+
let decoded = decode_grpc_body(&encoded).unwrap();
373+
assert_eq!(decoded, &payload[..], "roundtrip failed for payload len {size}");
374+
}
375+
}
376+
377+
#[test]
378+
fn test_decode_rejects_all_nonzero_compression_flags() {
379+
for flag in 1..=255u8 {
380+
let data = vec![flag, 0, 0, 0, 1, 42];
381+
let result = decode_grpc_body(&data);
382+
assert!(result.is_err(), "should reject compression flag {flag}");
383+
assert_eq!(result.unwrap_err().code, GRPC_STATUS_UNIMPLEMENTED);
384+
}
385+
}
386+
387+
#[test]
388+
fn test_decode_rejects_all_short_inputs() {
389+
for len in 0..5 {
390+
let data = vec![0u8; len];
391+
assert!(decode_grpc_body(&data).is_err(), "should reject {len}-byte input");
392+
}
393+
}
394+
395+
#[test]
396+
fn test_percent_encode_ascii_range() {
397+
// Verify every ASCII byte is either passed through or percent-encoded
398+
for b in 0u8..=127 {
399+
let s = String::from(b as char);
400+
let encoded = percent_encode(&s);
401+
let is_unreserved = b.is_ascii_alphanumeric()
402+
|| b == b'-' || b == b'_'
403+
|| b == b'.' || b == b'~'
404+
|| b == b' ';
405+
if is_unreserved {
406+
assert_eq!(encoded, s, "byte {b:#04x} ({}) should pass through", b as char);
407+
} else {
408+
assert_eq!(encoded, format!("%{b:02X}"), "byte {b:#04x} should be percent-encoded");
409+
}
410+
}
411+
}
412+
413+
#[test]
414+
fn test_percent_encode_multibyte_utf8() {
415+
// Each byte of multi-byte UTF-8 chars should be individually encoded
416+
let encoded = percent_encode("café");
417+
assert_eq!(encoded, "caf%C3%A9");
418+
}
419+
420+
#[test]
421+
fn test_parse_grpc_timeout_boundary_values() {
422+
use std::time::Duration;
423+
// Zero values
424+
assert_eq!(parse_grpc_timeout("0S"), Some(Duration::from_secs(0)));
425+
assert_eq!(parse_grpc_timeout("0m"), Some(Duration::from_millis(0)));
426+
// Large values
427+
assert_eq!(parse_grpc_timeout("99999999S"), Some(Duration::from_secs(99_999_999)));
428+
// Various invalid formats
429+
assert_eq!(parse_grpc_timeout("5"), None); // no unit
430+
assert_eq!(parse_grpc_timeout("abc"), None); // non-numeric
431+
assert_eq!(parse_grpc_timeout("5X"), None); // unknown unit
432+
assert_eq!(parse_grpc_timeout("5 S"), None); // space before unit
433+
}
434+
360435
#[test]
361436
fn test_parse_grpc_timeout() {
362437
use std::time::Duration;

0 commit comments

Comments
 (0)