use super::*;
use crate::replication::election::{
quorum_threshold, LastVote, MemoryLastVoteStore, RefusalReason, VotingState,
};
fn identity(subject: &str) -> NodeIdentity {
NodeIdentity::from_certificate_subject(subject).expect("non-empty subject")
}
fn witness(subject: &str) -> WitnessSupervisor<MemoryLastVoteStore> {
WitnessSupervisor::new(identity(subject), MemoryLastVoteStore::new())
}
#[test]
fn data_profile_boots_data_plane_witness_does_not() {
assert!(RuntimeProfile::Data.boots_data_plane());
assert!(!RuntimeProfile::Witness.boots_data_plane());
assert!(RuntimeProfile::Data.boots_supervisor());
assert!(RuntimeProfile::Witness.boots_supervisor());
}
#[test]
fn profile_maps_to_its_member_kind() {
assert_eq!(RuntimeProfile::Data.member_kind(), MemberKind::Data);
assert_eq!(RuntimeProfile::Witness.member_kind(), MemberKind::Witness);
}
#[test]
fn witness_supervisor_boots_no_data_plane() {
let w = witness("CN=witness-1");
assert_eq!(w.profile(), RuntimeProfile::Witness);
assert!(!w.boots_data_plane(), "a witness holds no data plane");
}
#[test]
fn witness_member_is_vote_only_and_not_electable() {
let w = witness("CN=witness-1");
let m = w.member();
assert_eq!(m.kind, MemberKind::Witness);
assert_eq!(m.state, VotingState::Voting, "a witness always votes");
assert!(m.is_voter(), "a witness counts toward quorum");
assert!(
!m.is_electable(),
"a witness holds no data, so it can never be primary",
);
}
#[test]
fn witness_vote_counts_toward_quorum_denominator() {
let members = vec![
Member::data_voting("CN=data-a"),
Member::data_voting("CN=data-b"),
witness("CN=witness-1").member(),
];
assert_eq!(quorum_threshold(&members), 2);
}
#[test]
fn witness_grants_a_covering_candidate() {
let w = witness("CN=witness-1");
let decision = w
.consider_vote(&VoteRequest::real("CN=data-a", 5, 150), 100)
.unwrap();
assert_eq!(decision, VoteDecision::Granted);
assert_eq!(w.current_term().unwrap(), 5);
}
#[test]
fn witness_refuses_a_candidate_below_the_watermark() {
let w = witness("CN=witness-1");
let decision = w
.consider_vote(&VoteRequest::real("CN=data-a", 5, 90), 100)
.unwrap();
assert_eq!(
decision,
VoteDecision::Refused(RefusalReason::WatermarkNotCovered {
candidate_lsn: 90,
watermark: 100,
}),
);
}
#[test]
fn witness_does_not_double_vote_in_a_term() {
let w = witness("CN=witness-1");
assert!(w
.consider_vote(&VoteRequest::real("CN=data-a", 7, 200), 100)
.unwrap()
.is_granted());
assert_eq!(
w.consider_vote(&VoteRequest::real("CN=data-b", 7, 200), 100)
.unwrap(),
VoteDecision::Refused(RefusalReason::AlreadyVoted {
term: 7,
voted_for: "CN=data-a".to_string(),
}),
);
}
#[test]
fn witness_probe_does_not_advance_its_term() {
let w = witness("CN=witness-1");
assert!(w
.consider_vote(&VoteRequest::probe("CN=data-a", 9, 200), 100)
.unwrap()
.is_granted());
assert_eq!(w.current_term().unwrap(), 0);
}
#[test]
fn witness_authenticates_with_the_shared_node_identity() {
let id = identity("CN=node-7,O=reddb");
let w = WitnessSupervisor::new(id.clone(), MemoryLastVoteStore::new());
assert_eq!(w.identity(), &id);
assert_eq!(w.member().id, "CN=node-7,O=reddb");
}
#[test]
fn durable_witness_does_not_double_vote_across_restart() {
let dir = std::env::temp_dir().join(format!(
"reddb-witness-{}-{}",
std::process::id(),
crate::utils::now_unix_nanos()
));
let path = dir.join("witness.lastvote.json");
let id = identity("CN=witness-1");
{
let w = WitnessSupervisor::with_durable_store(id.clone(), &path);
assert!(w
.consider_vote(&VoteRequest::real("CN=data-a", 7, 200), 100)
.unwrap()
.is_granted());
}
{
let w = WitnessSupervisor::with_durable_store(id, &path);
assert_eq!(
w.consider_vote(&VoteRequest::real("CN=data-b", 7, 200), 100)
.unwrap(),
VoteDecision::Refused(RefusalReason::AlreadyVoted {
term: 7,
voted_for: "CN=data-a".to_string(),
}),
);
}
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn seeded_store_reports_its_recorded_term() {
let w = WitnessSupervisor::new(
identity("CN=witness-1"),
MemoryLastVoteStore::seeded(LastVote {
term: 4,
voted_for: Some("CN=data-a".to_string()),
}),
);
assert_eq!(w.current_term().unwrap(), 4);
}