#![allow(clippy::unwrap_used)] #![allow(clippy::cast_possible_truncation)] #![allow(clippy::cast_possible_wrap)]
use bytes::{BufMut, BytesMut};
use fraiseql_wire::protocol::decode::decode_message;
use proptest::prelude::*;
proptest! {
#[test]
fn prop_decode_never_panics(data in proptest::collection::vec(any::<u8>(), 0..1024)) {
let mut buf = BytesMut::from(&data[..]);
let _ = decode_message(&mut buf); }
#[test]
fn prop_decode_with_valid_tag_never_panics(
tag in prop_oneof![
Just(b'R'), Just(b'K'), Just(b'C'), Just(b'D'),
Just(b'E'), Just(b'N'), Just(b'S'), Just(b'Z'), Just(b'T'),
],
body in proptest::collection::vec(any::<u8>(), 0..512),
) {
let mut buf = BytesMut::new();
buf.put_u8(tag);
let len = (body.len() + 4) as i32;
buf.put_i32(len);
buf.extend_from_slice(&body);
let _ = decode_message(&mut buf); }
#[test]
fn prop_short_buffer_returns_eof(len in 0..5usize) {
let data = vec![b'Z'; len];
let mut buf = BytesMut::from(&data[..]);
let result = decode_message(&mut buf);
prop_assert_eq!(result.unwrap_err().kind(), std::io::ErrorKind::UnexpectedEof);
}
#[test]
fn prop_ready_for_query_roundtrip(status in any::<u8>()) {
let mut buf = BytesMut::new();
buf.put_u8(b'Z');
buf.put_i32(5); buf.put_u8(status);
let result = decode_message(&mut buf);
let (msg, consumed) = result.map_err(|e| TestCaseError::fail(format!("expected Ok for ReadyForQuery roundtrip: {e}")))?;
prop_assert_eq!(consumed, 6);
match msg {
fraiseql_wire::protocol::message::BackendMessage::ReadyForQuery { status: s } => {
prop_assert_eq!(s, status);
}
_ => prop_assert!(false, "Expected ReadyForQuery"),
}
}
#[test]
fn prop_backend_key_data_roundtrip(pid in any::<i32>(), key in any::<i32>()) {
let mut buf = BytesMut::new();
buf.put_u8(b'K');
buf.put_i32(12); buf.put_i32(pid);
buf.put_i32(key);
let (msg, _) = decode_message(&mut buf).unwrap();
match msg {
fraiseql_wire::protocol::message::BackendMessage::BackendKeyData { process_id, secret_key } => {
prop_assert_eq!(process_id, pid);
prop_assert_eq!(secret_key, key);
}
_ => prop_assert!(false, "Expected BackendKeyData"),
}
}
#[test]
fn prop_command_complete_roundtrip(tag in "[A-Z]+ [0-9]+") {
let mut buf = BytesMut::new();
buf.put_u8(b'C');
let body_len = tag.len() + 1; buf.put_i32((body_len + 4) as i32);
buf.extend_from_slice(tag.as_bytes());
buf.put_u8(0);
let (msg, _) = decode_message(&mut buf).unwrap();
match msg {
fraiseql_wire::protocol::message::BackendMessage::CommandComplete(t) => {
prop_assert_eq!(t, tag);
}
_ => prop_assert!(false, "Expected CommandComplete"),
}
}
#[test]
fn prop_parameter_status_roundtrip(
name in "[a-z_]{1,30}",
value in "[a-zA-Z0-9_ ]{1,50}",
) {
let mut buf = BytesMut::new();
buf.put_u8(b'S');
let body_len = name.len() + 1 + value.len() + 1;
buf.put_i32((body_len + 4) as i32);
buf.extend_from_slice(name.as_bytes());
buf.put_u8(0);
buf.extend_from_slice(value.as_bytes());
buf.put_u8(0);
let (msg, _) = decode_message(&mut buf).unwrap();
match msg {
fraiseql_wire::protocol::message::BackendMessage::ParameterStatus { name: n, value: v } => {
prop_assert_eq!(n, name);
prop_assert_eq!(v, value);
}
_ => prop_assert!(false, "Expected ParameterStatus"),
}
}
#[test]
fn prop_invalid_tag_returns_error(
tag in any::<u8>().prop_filter("not a valid tag", |t| {
!matches!(t, b'R' | b'K' | b'C' | b'D' | b'E' | b'N' | b'S' | b'Z' | b'T')
})
) {
let mut buf = BytesMut::new();
buf.put_u8(tag);
buf.put_i32(4); let result = decode_message(&mut buf);
prop_assert_eq!(result.unwrap_err().kind(), std::io::ErrorKind::InvalidData);
}
}
proptest! {
#[test]
fn prop_message_length_must_include_length_field(
tag in prop_oneof![
Just(b'R'), Just(b'K'), Just(b'C'), Just(b'D'),
Just(b'E'), Just(b'N'), Just(b'S'), Just(b'Z'), Just(b'T'),
],
body_len in 0usize..512,
) {
let mut buf = BytesMut::new();
buf.put_u8(tag);
let total_len = (body_len + 4) as i32;
buf.put_i32(total_len);
buf.extend_from_slice(&vec![0; body_len]);
let result = decode_message(&mut buf);
if result.is_ok() {
let (_, consumed) = result.unwrap();
prop_assert_eq!(consumed, 1 + body_len + 4);
}
}
#[test]
fn prop_error_message_with_truncation_safe(
code in "[A-Z]{5}",
message in ".*",
position in "[a-z0-9_]{0,20}",
) {
let mut buf = BytesMut::new();
buf.put_u8(b'E');
let body = format!("C{}\x00M{}\x00P{}\x00\x00",
&code[..code.len().min(5)],
message.chars().take(100).collect::<String>(),
position.chars().take(20).collect::<String>()
);
buf.put_i32((body.len() + 4) as i32);
buf.extend_from_slice(body.as_bytes());
let result = decode_message(&mut buf);
let _ = result;
}
#[test]
fn prop_parameter_status_empty_name_rejected(
value in "[a-zA-Z0-9_ ]{1,50}",
) {
let mut buf = BytesMut::new();
buf.put_u8(b'S');
let body_len = 1 + value.len() + 1;
buf.put_i32((body_len + 4) as i32);
buf.put_u8(0); buf.extend_from_slice(value.as_bytes());
buf.put_u8(0);
let result = decode_message(&mut buf);
let _ = result;
}
}