#![allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::indexing_slicing
)]
use std::collections::HashSet;
use kafka_meta::{Cluster, ClusterConfig, ErrorCode, KNOWN_ERROR_CODES};
#[test]
fn every_known_code_round_trips_through_its_wire_value() {
for code in KNOWN_ERROR_CODES {
let wire = code.code();
assert_ne!(wire, 0, "{code} claims the success code");
assert_eq!(
ErrorCode::from_code(wire),
Some(code),
"{code} does not round-trip"
);
assert!(code.name().is_some(), "{code} has no name");
assert!(code.description().is_some(), "{code} has no description");
}
}
#[test]
fn wire_values_are_unique() {
let mut seen = HashSet::new();
for code in KNOWN_ERROR_CODES {
assert!(seen.insert(code.code()), "duplicate wire value for {code}");
}
}
#[test]
fn zero_is_success_and_has_no_error_code() {
assert_eq!(ErrorCode::from_code(0), None);
}
#[test]
fn a_code_no_kafka_release_defines_lands_in_unknown_and_still_renders() {
let code = ErrorCode::from_code(30_000).expect("non-zero is always an error");
assert_eq!(code, ErrorCode::Unknown(30_000));
assert_eq!(code.code(), 30_000);
assert_eq!(code.name(), None);
assert_eq!(code.to_string(), "UNKNOWN(30000)");
assert!(!code.retriable());
assert!(!code.needs_metadata_refresh());
assert!(!code.needs_coordinator_refresh());
assert!(!code.is_authentication());
assert!(!code.is_authorization());
}
#[test]
fn negative_codes_are_errors_too() {
assert_eq!(
ErrorCode::from_code(-1),
Some(ErrorCode::UnknownServerError)
);
assert_eq!(
ErrorCode::from_code(-30_000),
Some(ErrorCode::Unknown(-30_000))
);
}
#[test]
fn the_three_axes_are_independent() {
assert!(ErrorCode::RequestTimedOut.retriable());
assert!(!ErrorCode::RequestTimedOut.needs_metadata_refresh());
assert!(!ErrorCode::RequestTimedOut.needs_coordinator_refresh());
assert!(ErrorCode::NotLeaderOrFollower.retriable());
assert!(ErrorCode::NotLeaderOrFollower.needs_metadata_refresh());
assert!(!ErrorCode::NotLeaderOrFollower.needs_coordinator_refresh());
assert!(ErrorCode::NotCoordinator.needs_coordinator_refresh());
assert!(!ErrorCode::NotCoordinator.needs_metadata_refresh());
assert!(!ErrorCode::InvalidTopicException.retriable());
assert!(!ErrorCode::InvalidTopicException.needs_metadata_refresh());
assert!(!ErrorCode::InvalidTopicException.needs_coordinator_refresh());
}
#[test]
fn retriability_comes_from_the_protocol_not_from_us() {
for (code, retriable) in [
(ErrorCode::UnknownServerError, false),
(ErrorCode::CorruptMessage, true),
(ErrorCode::UnknownTopicOrPartition, true),
(ErrorCode::OffsetOutOfRange, false),
(ErrorCode::NotController, true),
(ErrorCode::KafkaStorageError, true),
(ErrorCode::PolicyViolation, false),
(ErrorCode::ThrottlingQuotaExceeded, true),
(ErrorCode::UnsupportedVersion, false),
] {
assert_eq!(code.retriable(), retriable, "{code}");
}
}
#[test]
fn a_named_resource_that_does_not_exist_is_not_worth_retrying() {
for code in [
ErrorCode::UnknownTopicOrPartition,
ErrorCode::UnknownTopicId,
ErrorCode::GroupIdNotFound,
ErrorCode::TransactionalIdNotFound,
] {
assert!(!code.retriable_for_named_resource(), "{code}");
}
assert!(ErrorCode::RequestTimedOut.retriable_for_named_resource());
assert!(ErrorCode::NotLeaderOrFollower.retriable_for_named_resource());
assert!(!ErrorCode::InvalidTopicException.retriable_for_named_resource());
}
#[test]
fn authentication_and_authorization_are_distinguishable() {
for code in [
ErrorCode::SaslAuthenticationFailed,
ErrorCode::UnsupportedSaslMechanism,
ErrorCode::IllegalSaslState,
] {
assert!(code.is_authentication(), "{code}");
assert!(!code.is_authorization(), "{code}");
}
for code in [
ErrorCode::TopicAuthorizationFailed,
ErrorCode::GroupAuthorizationFailed,
ErrorCode::ClusterAuthorizationFailed,
ErrorCode::TransactionalIdAuthorizationFailed,
ErrorCode::DelegationTokenAuthorizationFailed,
] {
assert!(code.is_authorization(), "{code}");
assert!(!code.is_authentication(), "{code}");
assert!(!code.retriable(), "{code}");
}
}
#[test]
fn every_known_code_renders_with_its_number() {
for code in KNOWN_ERROR_CODES {
let rendered = code.to_string();
assert!(
rendered.contains(&code.code().to_string()),
"{rendered} does not show its wire value"
);
}
}
#[testkit::integration_test]
async fn describing_a_nonexistent_topic_is_a_non_retriable_unknown_topic() {
use kafka_conn::protocol::messages::MetadataRequest;
use kafka_conn::protocol::messages::metadata_request::MetadataRequestTopic;
use kafka_conn::protocol::{StrBytes, messages::TopicName};
use testkit::Cluster as _;
let broker = testkit::single_broker().await.unwrap();
let cluster = Cluster::connect(broker.bootstrap().to_vec(), ClusterConfig::default())
.await
.unwrap();
let response = cluster
.send_any(
MetadataRequest::default()
.with_allow_auto_topic_creation(false)
.with_topics(Some(vec![MetadataRequestTopic::default().with_name(Some(
TopicName(StrBytes::from_static_str("definitely-not-a-topic")),
))])),
)
.await
.expect("the request itself succeeds — the *topic* is what is missing");
let topic = response.topics.first().expect("one topic entry");
let code = ErrorCode::from_code(topic.error_code).expect("an error code");
assert_eq!(code, ErrorCode::UnknownTopicOrPartition);
assert!(code.retriable(), "the protocol's answer");
assert!(
!code.retriable_for_named_resource(),
"a named topic that does not exist will not start existing"
);
assert!(code.needs_metadata_refresh());
}