use bgpkit_parser::error::{BgpValidationWarning, ParserError, ParserErrorWithBytes};
use bgpkit_parser::models::*;
use bgpkit_parser::parser::bgp::messages::parse_bgp_update_message;
use bgpkit_parser::parser::mrt::mrt_record::parse_mrt_record;
use bytes::Bytes;
use std::io::Cursor;
fn build_update_body(withdrawn: &[u8], attrs: &[u8], nlri: &[u8]) -> Vec<u8> {
let mut msg = Vec::new();
msg.extend_from_slice(&(withdrawn.len() as u16).to_be_bytes());
msg.extend_from_slice(withdrawn);
msg.extend_from_slice(&(attrs.len() as u16).to_be_bytes());
msg.extend_from_slice(attrs);
msg.extend_from_slice(nlri);
msg
}
fn build_valid_attrs() -> Vec<u8> {
let mut attrs = Vec::new();
attrs.extend_from_slice(&[0x40, 0x01, 0x01, 0x00]);
attrs.extend_from_slice(&[0x40, 0x02, 0x00]);
attrs.extend_from_slice(&[0x40, 0x03, 0x04, 0x01, 0x02, 0x03, 0x04]);
attrs
}
fn valid_nlri_prefix() -> Vec<u8> {
vec![0x18, 0x0A, 0x00, 0x00]
}
fn malformed_nlri() -> Vec<u8> {
vec![0xC8, 0x01]
}
#[test]
fn test_malformed_announced_nlri_produces_warning_not_error() {
let asn_len = AsnLength::Bits32;
let body = build_update_body(&[], &build_valid_attrs(), &malformed_nlri());
let result = parse_bgp_update_message(Bytes::from(body), false, &asn_len);
let update = result.expect("malformed NLRI should not be fatal");
assert!(
update.announced_prefixes.is_empty(),
"prefixes should be empty"
);
assert!(
update.attributes.has_validation_warnings(),
"should have validation warnings"
);
let warnings = update.attributes.validation_warnings();
assert!(
warnings.iter().any(|w| matches!(
w,
BgpValidationWarning::MalformedNlri {
nlri_type: "announced",
..
}
)),
"expected MalformedNlri warning for announced NLRI, got: {:?}",
warnings
);
}
#[test]
fn test_malformed_announced_nlri_preserves_raw_bytes_in_warning() {
let asn_len = AsnLength::Bits32;
let nlri = malformed_nlri();
let body = build_update_body(&[], &build_valid_attrs(), &nlri);
let update = parse_bgp_update_message(Bytes::from(body), false, &asn_len).unwrap();
let warnings = update.attributes.validation_warnings();
let nlri_warning = warnings.iter().find_map(|w| match w {
BgpValidationWarning::MalformedNlri {
nlri_type: "announced",
raw_bytes,
..
} => Some(raw_bytes),
_ => None,
});
assert_eq!(
nlri_warning,
Some(&nlri),
"raw NLRI bytes should be preserved"
);
}
#[test]
fn test_malformed_withdrawn_nlri_produces_warning_not_error() {
let asn_len = AsnLength::Bits32;
let body = build_update_body(&malformed_nlri(), &build_valid_attrs(), &[]);
let result = parse_bgp_update_message(Bytes::from(body), false, &asn_len);
let update = result.expect("malformed withdrawn NLRI should not be fatal");
assert!(
update.withdrawn_prefixes.is_empty(),
"withdrawn prefixes should be empty"
);
let warnings = update.attributes.validation_warnings();
assert!(
warnings.iter().any(|w| matches!(
w,
BgpValidationWarning::MalformedNlri {
nlri_type: "withdrawn",
..
}
)),
"expected MalformedNlri warning for withdrawn NLRI, got: {:?}",
warnings
);
}
#[test]
fn test_attributes_survive_nlri_parse_failure() {
let asn_len = AsnLength::Bits32;
let body = build_update_body(&[], &build_valid_attrs(), &malformed_nlri());
let update = parse_bgp_update_message(Bytes::from(body), false, &asn_len).unwrap();
assert!(update.attributes.has_attr(AttrType::ORIGIN));
assert!(update.attributes.has_attr(AttrType::AS_PATH));
assert!(update.attributes.has_attr(AttrType::NEXT_HOP));
}
#[test]
fn test_valid_withdrawn_survives_malformed_announced_nlri() {
let asn_len = AsnLength::Bits32;
let body = build_update_body(
&valid_nlri_prefix(),
&build_valid_attrs(),
&malformed_nlri(),
);
let update = parse_bgp_update_message(Bytes::from(body), false, &asn_len).unwrap();
assert_eq!(update.withdrawn_prefixes.len(), 1);
assert!(
update.announced_prefixes.is_empty(),
"malformed announced NLRI should produce no prefixes"
);
assert!(
update.attributes.has_validation_warnings(),
"should have validation warnings for the malformed announced NLRI"
);
}
#[test]
fn test_valid_update_still_parses_clean() {
let asn_len = AsnLength::Bits32;
let body = build_update_body(&[], &build_valid_attrs(), &valid_nlri_prefix());
let update = parse_bgp_update_message(Bytes::from(body), false, &asn_len).unwrap();
assert_eq!(update.announced_prefixes.len(), 1);
assert!(
!update.attributes.has_validation_warnings(),
"valid UPDATE should have no validation warnings"
);
}
#[test]
fn test_attribute_length_exceeding_available_bytes_is_fatal() {
let asn_len = AsnLength::Bits32;
let mut msg = Vec::new();
msg.extend_from_slice(&0x0000u16.to_be_bytes()); msg.extend_from_slice(&0x03E8u16.to_be_bytes()); msg.extend_from_slice(&[0x40, 0x01, 0x01, 0x00]);
let result = parse_bgp_update_message(Bytes::from(msg), false, &asn_len);
assert!(result.is_err(), "truncated attribute data should be fatal");
}
#[test]
fn test_parse_mrt_record_preserves_raw_bytes_on_failure() {
let mut data = Vec::new();
data.extend_from_slice(&0x00000000u32.to_be_bytes()); data.extend_from_slice(&0x000Cu16.to_be_bytes()); data.extend_from_slice(&0x0000u16.to_be_bytes()); data.extend_from_slice(&0x00000004u32.to_be_bytes()); data.extend_from_slice(&[0xFF, 0xFF, 0xFF, 0xFF]);
let mut cursor = Cursor::new(data);
let result = parse_mrt_record(&mut cursor);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
err.bytes.is_some(),
"raw bytes should be preserved on parse failure"
);
let bytes = err.bytes.unwrap();
assert!(!bytes.is_empty(), "preserved bytes should not be empty");
}
#[test]
fn test_malformed_otc_attribute_extended_length_mismatch() {
let asn_len = AsnLength::Bits32;
let mut attrs = build_valid_attrs();
attrs.extend_from_slice(&[
0xF0, 0x23, 0x04, 0x00, ]);
attrs.extend_from_slice(&0x0000FE4Cu32.to_be_bytes());
let body = build_update_body(&[], &attrs, &valid_nlri_prefix());
let result = parse_bgp_update_message(Bytes::from(body), false, &asn_len);
match result {
Ok(update) => {
assert_eq!(
update.announced_prefixes.len(),
1,
"NLRI should survive bad optional attribute"
);
assert!(
update.attributes.has_validation_warnings(),
"expected validation warnings for malformed OTC attribute"
);
}
Err(e) => panic!("malformed OTC attribute should not be fatal: {}", e),
}
}
#[test]
fn test_treat_as_withdrawal_emulation() {
let asn_len = AsnLength::Bits32;
let bad_nlri = malformed_nlri();
let bad_body = build_update_body(&[], &build_valid_attrs(), &bad_nlri);
let bad_update = parse_bgp_update_message(Bytes::from(bad_body), false, &asn_len).unwrap();
let needs_taw = bad_update
.attributes
.validation_warnings()
.iter()
.any(|w| matches!(w, BgpValidationWarning::MalformedNlri { .. }));
assert!(
needs_taw,
"malformed NLRI should be detectable via warnings"
);
assert!(bad_update.announced_prefixes.is_empty());
let raw_nlri = bad_update
.attributes
.validation_warnings()
.iter()
.find_map(|w| match w {
BgpValidationWarning::MalformedNlri { raw_bytes, .. } => Some(raw_bytes),
_ => None,
});
assert_eq!(
raw_nlri,
Some(&bad_nlri),
"raw NLRI bytes should be available for extraction"
);
}
#[test]
fn test_malformed_nlri_warning_display() {
let warning = BgpValidationWarning::MalformedNlri {
nlri_type: "announced",
reason: "invalid prefix length".to_string(),
raw_bytes: vec![0xC8, 0x01],
};
let display = format!("{}", warning);
assert!(display.contains("announced"));
assert!(display.contains("invalid prefix length"));
}
#[test]
fn test_parser_error_with_bytes_display() {
let err = ParserErrorWithBytes {
error: ParserError::ParseError("test error".to_string()),
bytes: Some(vec![0x01, 0x02]),
};
let display = format!("{}", err);
assert!(display.contains("test error"));
}
#[test]
fn test_one_byte_nlri_invalid_prefix_produces_warning() {
let asn_len = AsnLength::Bits32;
let invalid_one_byte = vec![0xFF];
let body = build_update_body(&[], &build_valid_attrs(), &invalid_one_byte);
let update = parse_bgp_update_message(Bytes::from(body), false, &asn_len).unwrap();
assert!(update.announced_prefixes.is_empty());
assert!(
update.attributes.has_validation_warnings(),
"1-byte NLRI with invalid prefix length should produce a warning"
);
assert!(update
.attributes
.validation_warnings()
.iter()
.any(|w| matches!(
w,
BgpValidationWarning::MalformedNlri {
nlri_type: "announced",
..
}
)));
}
#[test]
fn test_one_byte_nlri_default_route_is_valid() {
let asn_len = AsnLength::Bits32;
let default_route_nlri = vec![0x00];
let body = build_update_body(&[], &build_valid_attrs(), &default_route_nlri);
let update = parse_bgp_update_message(Bytes::from(body), false, &asn_len).unwrap();
assert_eq!(
update.announced_prefixes.len(),
1,
"default route 0.0.0.0/0 should be parsed"
);
assert!(
!update.attributes.has_validation_warnings(),
"valid default route NLRI must not produce warnings"
);
}
#[test]
fn test_malformed_nlri_still_triggers_mandatory_attr_check() {
let asn_len = AsnLength::Bits32;
let malformed_nlri = vec![0xC8, 0x01];
let body = build_update_body(&[], &[], &malformed_nlri);
let update = parse_bgp_update_message(Bytes::from(body), false, &asn_len).unwrap();
let warnings = update.attributes.validation_warnings();
assert!(
warnings.iter().any(|w| matches!(
w,
BgpValidationWarning::MalformedNlri {
nlri_type: "announced",
..
}
)),
"expected MalformedNlri warning"
);
assert!(
warnings.iter().any(|w| matches!(
w,
BgpValidationWarning::MissingWellKnownAttribute {
attr_type: AttrType::ORIGIN
}
)),
"expected MissingWellKnownAttribute(ORIGIN) — mandatory check must fire even with malformed NLRI, got: {:?}",
warnings
);
assert!(
warnings.iter().any(|w| matches!(
w,
BgpValidationWarning::MissingWellKnownAttribute {
attr_type: AttrType::AS_PATH
}
)),
"expected MissingWellKnownAttribute(AS_PATH)"
);
assert!(
warnings.iter().any(|w| matches!(
w,
BgpValidationWarning::MissingWellKnownAttribute {
attr_type: AttrType::NEXT_HOP
}
)),
"expected MissingWellKnownAttribute(NEXT_HOP)"
);
}