pub use krabka_ids::LeaderEpoch;
pub use krabka_voters::NodeId;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TopicRecord {
pub name: String,
pub topic_id: Uuid,
pub partitions: i32,
pub replication_factor: i16,
}
fn default_partition_epoch() -> i32 {
-1
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct PartitionRecord {
pub topic: String,
pub partition: i32,
pub leader: NodeId,
pub replicas: Vec<NodeId>,
pub isr: Vec<NodeId>,
pub leader_epoch: LeaderEpoch,
pub adding_replicas: Vec<NodeId>,
pub removing_replicas: Vec<NodeId>,
pub directories: Vec<Uuid>,
#[serde(default = "default_partition_epoch")]
pub partition_epoch: i32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PartitionDirAssignmentRecord {
pub topic: String,
pub partition: i32,
pub replica: NodeId,
pub directory: Uuid,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PartitionOffsetAdvanceRecord {
pub topic: String,
pub partition: i32,
pub count: i64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PartitionElrRecord {
pub topic: String,
pub partition: i32,
pub eligible_leader_replicas: Vec<NodeId>,
pub last_known_elr: Vec<NodeId>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[repr(i8)]
pub enum LeaderRecoveryState {
#[default]
Recovered = 0,
Recovering = 1,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PartitionRecoveryRecord {
pub topic: String,
pub partition: i32,
pub state: LeaderRecoveryState,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PartitionUpdateRecord {
pub partition: PartitionRecord,
pub eligible_leader_replicas: Option<Vec<NodeId>>,
pub last_known_elr: Option<Vec<NodeId>>,
pub recovery_state: Option<LeaderRecoveryState>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BrokerEndpoint {
pub name: String,
pub host: String,
pub port: u16,
pub protocol: krabka_security::ListenerProtocol,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BrokerRegistrationRecord {
pub node_id: NodeId,
pub broker_epoch: i64,
#[serde(default)]
pub incarnation_id: uuid::Uuid,
pub host: String,
pub port: u16,
pub rack: Option<String>,
pub endpoints: Vec<BrokerEndpoint>,
#[serde(default)]
pub log_dirs: Vec<uuid::Uuid>,
pub fenced: bool,
pub in_controlled_shutdown: bool,
pub cordoned_log_dirs: Option<Vec<uuid::Uuid>>,
#[serde(default)]
pub features: std::collections::BTreeMap<String, (i16, i16)>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ControllerRegistrationRecord {
pub node_id: NodeId,
pub incarnation_id: Uuid,
pub zk_migration_ready: bool,
pub endpoints: Vec<BrokerEndpoint>,
pub features: std::collections::BTreeMap<String, (i16, i16)>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DeleteTopicRecord {
pub name: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct UnregisterBrokerRecord {
pub node_id: NodeId,
pub broker_epoch: i64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum FencingChange {
Unfence,
None,
Fence,
}
impl FencingChange {
#[must_use]
pub fn wire_value(self) -> i8 {
match self {
Self::Unfence => -1,
Self::None => 0,
Self::Fence => 1,
}
}
#[must_use]
pub fn from_wire(value: i8) -> Option<Self> {
match value {
-1 => Some(Self::Unfence),
0 => Some(Self::None),
1 => Some(Self::Fence),
_ => None,
}
}
#[must_use]
pub fn apply(self, fenced: bool) -> bool {
match self {
Self::Unfence => false,
Self::None => fenced,
Self::Fence => true,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BrokerRegistrationChangeRecord {
pub node_id: NodeId,
pub broker_epoch: i64,
pub fenced: FencingChange,
pub in_controlled_shutdown: bool,
pub log_dirs: Vec<Uuid>,
pub cordoned_log_dirs: Option<Vec<Uuid>>,
}
impl BrokerRegistrationChangeRecord {
#[must_use]
pub fn no_change(node_id: NodeId, broker_epoch: i64) -> Self {
Self {
node_id,
broker_epoch,
fenced: FencingChange::None,
in_controlled_shutdown: false,
log_dirs: Vec::new(),
cordoned_log_dirs: None,
}
}
#[must_use]
pub fn applied_to(&self, current: &BrokerRegistrationRecord) -> BrokerRegistrationRecord {
BrokerRegistrationRecord {
fenced: self.fenced.apply(current.fenced),
in_controlled_shutdown: current.in_controlled_shutdown || self.in_controlled_shutdown,
log_dirs: if self.log_dirs.is_empty() {
current.log_dirs.clone()
} else {
self.log_dirs.clone()
},
cordoned_log_dirs: self
.cordoned_log_dirs
.clone()
.or_else(|| current.cordoned_log_dirs.clone()),
..current.clone()
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct UnregisterControllerRecord {
pub node_id: NodeId,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TopicConfigRecord {
pub topic: String,
pub overrides: std::collections::BTreeMap<String, String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BrokerConfigRecord {
pub node_id: NodeId,
pub config_name: String,
pub config_value: Option<String>,
}
pub const DEFAULT_BROKER_CONFIG_NODE_ID: NodeId = NodeId(u64::MAX);
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ClientMetricsConfigRecord {
pub name: String,
pub configs: std::collections::BTreeMap<String, String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GroupConfigRecord {
pub group_id: String,
pub configs: std::collections::BTreeMap<String, String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct QuotaEntity {
pub entity_type: String,
pub entity_name: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ClientQuotaRecord {
pub entity: Vec<QuotaEntity>,
pub config_key: String,
pub config_value: Option<f64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProducerIdsRecord {
pub broker_id: NodeId,
pub broker_epoch: i64,
pub next_producer_id: i64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ScramCredentialRecord {
pub user: String,
pub mechanism: krabka_security::SaslMechanism,
pub salt: Vec<u8>,
pub stored_key: Vec<u8>,
pub server_key: Vec<u8>,
pub iterations: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DeleteScramCredentialRecord {
pub user: String,
pub mechanism: krabka_security::SaslMechanism,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DelegationTokenRecord {
pub token_id: String,
pub owner: krabka_security::KafkaPrincipal,
pub requester: krabka_security::KafkaPrincipal,
pub issue_timestamp_ms: i64,
pub expiry_timestamp_ms: i64,
pub max_timestamp_ms: i64,
pub renewers: Vec<krabka_security::KafkaPrincipal>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DeleteDelegationTokenRecord {
pub token_id: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct KRaftVersionRecord {
pub kraft_version: u16,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct VotersRecord {
pub voters: crate::voters::VoterSet,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FeatureLevelRecord {
pub name: String,
pub level: i16,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct FeaturesEpochRecord {
pub epoch: i64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum MetadataRecord {
V1Topic(TopicRecord),
V1Partition(PartitionRecord),
V1BrokerRegistration(BrokerRegistrationRecord),
V1DeleteTopic(DeleteTopicRecord),
V1TopicConfig(TopicConfigRecord),
V1ScramCredential(ScramCredentialRecord),
V1DeleteScramCredential(DeleteScramCredentialRecord),
V1AccessControlEntry(crate::AclEntry),
V1DeleteAccessControlEntry(crate::AclEntryFilter),
V1BrokerConfig(BrokerConfigRecord),
V1ClientQuota(ClientQuotaRecord),
V1ProducerIds(ProducerIdsRecord),
V1DelegationToken(DelegationTokenRecord),
V1DeleteDelegationToken(DeleteDelegationTokenRecord),
V1UnregisterBroker(UnregisterBrokerRecord),
V1KRaftVersion(KRaftVersionRecord),
V1Voters(VotersRecord),
V1FeatureLevel(FeatureLevelRecord),
V1ClientMetricsConfig(ClientMetricsConfigRecord),
V1FeaturesEpoch(FeaturesEpochRecord),
V1PartitionDirAssignment(PartitionDirAssignmentRecord),
V1PartitionOffsetAdvance(PartitionOffsetAdvanceRecord),
V1GroupConfig(GroupConfigRecord),
V1ControllerRegistration(ControllerRegistrationRecord),
V1TopicFreeze(crate::write_freeze::TopicFreezeRecord),
V1BreakGlassProposal(crate::break_glass::BreakGlassProposalRecord),
V1DeleteBreakGlassProposal(Uuid),
V1PartitionElr(PartitionElrRecord),
V1PartitionRecovery(PartitionRecoveryRecord),
V1PartitionUpdate(PartitionUpdateRecord),
V1BrokerRegistrationChange(BrokerRegistrationChangeRecord),
V1UnregisterController(UnregisterControllerRecord),
}
#[cfg(test)]
mod tests {
use serde_wincode::SerdeCompat;
use wincode::{Deserialize as _, Serialize as _};
use super::*;
fn round_trip(r: &MetadataRecord) -> MetadataRecord {
let bytes = <SerdeCompat<MetadataRecord>>::serialize(r).unwrap();
<SerdeCompat<MetadataRecord>>::deserialize(&bytes).unwrap()
}
#[test]
fn feature_level_round_trip() {
let r = MetadataRecord::V1FeatureLevel(FeatureLevelRecord {
name: "metadata.version".into(),
level: 1,
});
assert2::assert!(round_trip(&r) == r);
}
#[test]
fn group_config_round_trip() {
let r = MetadataRecord::V1GroupConfig(GroupConfigRecord {
group_id: "streams-app".into(),
configs: std::collections::BTreeMap::from([(
"streams.num.standby.replicas".into(),
"1".into(),
)]),
});
assert2::assert!(round_trip(&r) == r);
}
#[test]
fn controller_registration_round_trip() {
let r = MetadataRecord::V1ControllerRegistration(ControllerRegistrationRecord {
node_id: NodeId(3),
incarnation_id: Uuid::from_u128(7),
zk_migration_ready: false,
endpoints: vec![BrokerEndpoint {
name: "CONTROLLER".into(),
host: "controller-3".into(),
port: 9093,
protocol: krabka_security::ListenerProtocol::Plaintext,
}],
features: std::collections::BTreeMap::from([("metadata.version".into(), (7, 25))]),
});
assert2::assert!(round_trip(&r) == r);
}
#[test]
fn producer_ids_round_trip() {
let r = MetadataRecord::V1ProducerIds(ProducerIdsRecord {
broker_id: NodeId(3),
broker_epoch: 9,
next_producer_id: 2_000,
});
assert2::assert!(round_trip(&r) == r);
}
#[test]
fn features_epoch_round_trip() {
let r = MetadataRecord::V1FeaturesEpoch(FeaturesEpochRecord { epoch: 7 });
assert2::assert!(round_trip(&r) == r);
}
#[test]
fn topic_record_round_trip() {
let r = MetadataRecord::V1Topic(TopicRecord {
name: "t".into(),
topic_id: Uuid::new_v4(),
partitions: 3,
replication_factor: 1,
});
assert2::assert!(round_trip(&r) == r);
}
#[test]
fn partition_record_round_trip() {
let r = MetadataRecord::V1Partition(PartitionRecord {
topic: "t".into(),
partition: 0,
leader: NodeId(1),
replicas: vec![NodeId(1), NodeId(2), NodeId(3)],
isr: vec![NodeId(1), NodeId(2)],
leader_epoch: LeaderEpoch(0),
adding_replicas: vec![],
removing_replicas: vec![],
directories: vec![Uuid::from_u128(1), Uuid::from_u128(2), Uuid::nil()],
partition_epoch: 0,
});
assert2::assert!(round_trip(&r) == r);
}
#[test]
fn partition_dir_assignment_round_trip() {
let r = MetadataRecord::V1PartitionDirAssignment(PartitionDirAssignmentRecord {
topic: "t".into(),
partition: 2,
replica: NodeId(3),
directory: Uuid::from_u128(0xAB),
});
assert2::assert!(round_trip(&r) == r);
}
#[test]
fn broker_registration_round_trip() {
let r = MetadataRecord::V1BrokerRegistration(BrokerRegistrationRecord {
node_id: NodeId(7),
broker_epoch: 0,
incarnation_id: Uuid::from_u128(0xdeadbeef_cafe_babe_0123_456789abcdef),
host: "192.168.1.10".into(),
port: 9092,
rack: Some("us-east-1a".into()),
log_dirs: vec![],
fenced: false,
in_controlled_shutdown: false,
cordoned_log_dirs: None,
endpoints: vec![],
features: std::collections::BTreeMap::new(),
});
assert2::assert!(round_trip(&r) == r);
}
#[test]
fn broker_registration_with_endpoints_round_trip() {
let r = MetadataRecord::V1BrokerRegistration(BrokerRegistrationRecord {
node_id: NodeId(1),
broker_epoch: 0,
incarnation_id: Uuid::from_u128(0xfeedface_0000_0000_0000_000000000001),
host: "h".into(),
port: 9092,
rack: None,
log_dirs: vec![],
fenced: false,
in_controlled_shutdown: false,
cordoned_log_dirs: None,
endpoints: vec![BrokerEndpoint {
name: "EXTERNAL".into(),
host: "ext.example.com".into(),
port: 9092,
protocol: krabka_security::ListenerProtocol::SaslSsl,
}],
features: std::collections::BTreeMap::new(),
});
assert2::assert!(round_trip(&r) == r);
}
#[test]
fn delete_topic_round_trip() {
let r = MetadataRecord::V1DeleteTopic(DeleteTopicRecord {
name: "doomed".into(),
});
assert2::assert!(round_trip(&r) == r);
}
#[test]
fn unregister_broker_round_trip() {
let r = MetadataRecord::V1UnregisterBroker(UnregisterBrokerRecord {
node_id: NodeId(42),
broker_epoch: 17,
});
assert2::assert!(round_trip(&r) == r);
}
#[test]
fn broker_registration_change_and_unregister_controller_round_trip() {
for r in [
MetadataRecord::V1BrokerRegistrationChange(BrokerRegistrationChangeRecord {
node_id: NodeId(4),
broker_epoch: 12,
fenced: FencingChange::Unfence,
in_controlled_shutdown: true,
log_dirs: vec![Uuid::from_u128(0xD1)],
cordoned_log_dirs: Some(vec![]),
}),
MetadataRecord::V1UnregisterController(UnregisterControllerRecord {
node_id: NodeId(3000),
}),
] {
assert2::assert!(round_trip(&r) == r);
}
}
#[test]
fn fencing_change_matches_kafka() {
for (change, wire, from_false, from_true) in [
(FencingChange::Unfence, -1, false, false),
(FencingChange::None, 0, false, true),
(FencingChange::Fence, 1, true, true),
] {
assert2::check!(
(
change.wire_value(),
FencingChange::from_wire(wire),
change.apply(false),
change.apply(true),
) == (wire, Some(change), from_false, from_true)
);
}
for undefined in [-2, 2, i8::MIN, i8::MAX] {
assert2::check!(FencingChange::from_wire(undefined) == None);
}
}
#[test]
fn registration_change_applies_like_clone_with() {
let (d1, d2) = (Uuid::from_u128(0xD1), Uuid::from_u128(0xD2));
let current = BrokerRegistrationRecord {
node_id: NodeId(4),
broker_epoch: 12,
incarnation_id: Uuid::from_u128(4),
host: "broker-4".into(),
port: 9092,
rack: None,
endpoints: vec![],
log_dirs: vec![d1],
fenced: true,
in_controlled_shutdown: false,
cordoned_log_dirs: None,
features: std::collections::BTreeMap::new(),
};
let base = BrokerRegistrationChangeRecord::no_change(NodeId(4), 12);
let with = |edit: fn(&mut BrokerRegistrationRecord)| {
let mut want = current.clone();
edit(&mut want);
want
};
for (case, change, want) in [
("no change", base.clone(), current.clone()),
(
"unfence",
BrokerRegistrationChangeRecord {
fenced: FencingChange::Unfence,
..base.clone()
},
with(|b| b.fenced = false),
),
(
"controlled shutdown",
BrokerRegistrationChangeRecord {
in_controlled_shutdown: true,
..base.clone()
},
with(|b| b.in_controlled_shutdown = true),
),
(
"new log dirs",
BrokerRegistrationChangeRecord {
log_dirs: vec![d2],
..base.clone()
},
with(|b| b.log_dirs = vec![Uuid::from_u128(0xD2)]),
),
(
"uncordon every dir",
BrokerRegistrationChangeRecord {
cordoned_log_dirs: Some(vec![]),
..base.clone()
},
with(|b| b.cordoned_log_dirs = Some(vec![])),
),
] {
assert2::check!(change.applied_to(¤t) == want, "{case}");
}
let shutting_down = BrokerRegistrationRecord {
in_controlled_shutdown: true,
..current.clone()
};
assert2::check!(base.applied_to(&shutting_down) == shutting_down);
}
#[test]
fn topic_config_record_round_trip() {
let mut overrides = std::collections::BTreeMap::new();
overrides.insert("retention.ms".to_string(), "60000".to_string());
overrides.insert("segment.bytes".to_string(), "1048576".to_string());
let r = MetadataRecord::V1TopicConfig(TopicConfigRecord {
topic: "t".into(),
overrides,
});
assert2::assert!(round_trip(&r) == r);
}
#[test]
fn scram_credential_round_trip() {
let r = MetadataRecord::V1ScramCredential(ScramCredentialRecord {
user: "alice".into(),
mechanism: krabka_security::SaslMechanism::ScramSha512,
salt: vec![1u8; 16],
stored_key: vec![2u8; 64],
server_key: vec![3u8; 64],
iterations: 4096,
});
assert2::assert!(round_trip(&r) == r);
}
#[test]
fn delete_scram_credential_round_trip() {
let r = MetadataRecord::V1DeleteScramCredential(DeleteScramCredentialRecord {
user: "alice".into(),
mechanism: krabka_security::SaslMechanism::ScramSha512,
});
assert2::assert!(round_trip(&r) == r);
}
#[test]
fn v1_access_control_entry_round_trip() {
let entry = crate::AclEntry {
resource_type: crate::ResourceType::Topic,
resource_name: "foo".into(),
pattern_type: crate::PatternType::Literal,
principal: "User:alice".into(),
host: "*".into(),
operation: crate::AclOperation::Read,
permission_type: crate::PermissionType::Allow,
};
let r = MetadataRecord::V1AccessControlEntry(entry);
assert2::assert!(round_trip(&r) == r);
}
#[test]
fn v1_delete_access_control_entry_round_trip() {
let filter = crate::AclEntryFilter {
resource_type: Some(crate::ResourceType::Group),
resource_name: Some("cg-foo".into()),
pattern_type: Some(crate::PatternType::Literal),
principal: None,
host: None,
operation: None,
permission_type: None,
};
let r = MetadataRecord::V1DeleteAccessControlEntry(filter);
assert2::assert!(round_trip(&r) == r);
}
#[test]
fn broker_config_record_round_trip() {
let r = MetadataRecord::V1BrokerConfig(BrokerConfigRecord {
node_id: NodeId(7),
config_name: "leader.replication.throttled.rate".into(),
config_value: Some("2048".into()),
});
assert2::assert!(round_trip(&r) == r);
}
#[test]
fn client_quota_record_round_trip() {
let r = MetadataRecord::V1ClientQuota(ClientQuotaRecord {
entity: vec![
QuotaEntity {
entity_type: "client-id".into(),
entity_name: Some("app1".into()),
},
QuotaEntity {
entity_type: "user".into(),
entity_name: Some("alice".into()),
},
],
config_key: "producer_byte_rate".into(),
config_value: Some(1024.0),
});
assert2::assert!(round_trip(&r) == r);
}
#[test]
fn delegation_token_record_round_trip() {
let r = MetadataRecord::V1DelegationToken(DelegationTokenRecord {
token_id: "tok-abc".into(),
owner: krabka_security::KafkaPrincipal {
principal_type: "User".into(),
name: "alice".into(),
},
requester: krabka_security::KafkaPrincipal {
principal_type: "User".into(),
name: "admin".into(),
},
issue_timestamp_ms: 1_700_000_000_000,
expiry_timestamp_ms: 1_700_000_600_000,
max_timestamp_ms: 1_700_604_800_000,
renewers: vec![krabka_security::KafkaPrincipal {
principal_type: "User".into(),
name: "bob".into(),
}],
});
assert2::assert!(round_trip(&r) == r);
}
#[test]
fn delete_delegation_token_record_round_trip() {
let r = MetadataRecord::V1DeleteDelegationToken(DeleteDelegationTokenRecord {
token_id: "tok-abc".into(),
});
assert2::assert!(round_trip(&r) == r);
}
#[test]
fn voters_record_round_trips() {
let rec = MetadataRecord::V1Voters(VotersRecord {
voters: crate::voters::VoterSet::from_voters([crate::voters::Voter {
id: NodeId(7),
directory_id: uuid::Uuid::from_u128(7),
endpoints: vec![crate::voters::VoterEndpoint {
name: "CONTROLLER".into(),
host: "h".into(),
port: 1,
}],
kraft_version: crate::voters::KRaftVersionRange::default(),
}]),
});
assert2::assert!(round_trip(&rec) == rec);
}
#[test]
fn kraft_version_record_round_trips() {
let rec = MetadataRecord::V1KRaftVersion(KRaftVersionRecord { kraft_version: 1 });
assert2::assert!(round_trip(&rec) == rec);
}
#[test]
fn client_metrics_config_round_trip() {
let mut overrides = std::collections::BTreeMap::new();
overrides.insert("interval.ms".to_string(), "60000".to_string());
overrides.insert(
"metrics".to_string(),
"org.apache.kafka.consumer.".to_string(),
);
let r = MetadataRecord::V1ClientMetricsConfig(ClientMetricsConfigRecord {
name: "sub-a".into(),
configs: overrides,
});
assert2::assert!(round_trip(&r) == r);
}
#[test]
fn topic_freeze_round_trip() {
let r = MetadataRecord::V1TopicFreeze(crate::write_freeze::TopicFreezeRecord {
scope: "tenant-a.".into(),
pattern_type: crate::PatternType::Prefixed,
frozen: true,
reason: "DR cutover".into(),
set_by: "User:alice".into(),
set_at_ms: 1_700_000_000_000,
proposal_id: Uuid::from_u128(0xB1),
key_id: "alice-yubi".into(),
signature: vec![0xAB; 64],
});
assert2::assert!(round_trip(&r) == r);
}
#[test]
fn break_glass_proposal_round_trip() {
let r =
MetadataRecord::V1BreakGlassProposal(crate::break_glass::BreakGlassProposalRecord {
proposal_id: Uuid::from_u128(0xB1),
action: crate::break_glass::BreakGlassAction::ThawTopicFreeze,
target: "literal:orders".into(),
proposer: "User:alice".into(),
reason: "incident 4711 closed".into(),
created_at_ms: 1_700_000_000_000,
expires_at_ms: 1_700_000_600_000,
approvals: vec![crate::break_glass::BreakGlassApproval {
principal: "User:bob".into(),
approved_at_ms: 1_700_000_060_000,
key_id: "bob-yubi".into(),
signature: vec![0xCD; 64],
}],
consumed_at_ms: 0,
withdrawn: false,
});
assert2::assert!(round_trip(&r) == r);
}
#[test]
fn delete_break_glass_proposal_round_trip() {
let r = MetadataRecord::V1DeleteBreakGlassProposal(Uuid::from_u128(0xB1));
assert2::assert!(round_trip(&r) == r);
}
#[test]
fn partition_epoch_serde_default_is_minus_one() {
assert2::assert!(default_partition_epoch() == -1);
}
}