use std::sync::Arc;
use d_engine_proto::common::LogId;
use d_engine_proto::server::election::VoteRequest;
use d_engine_proto::server::election::VotedFor;
use crate::MockRaftLog;
use crate::MockTypeConfig;
use crate::election::ElectionCore;
use crate::election::ElectionHandler;
fn create_handler(node_id: u32) -> ElectionHandler<MockTypeConfig> {
ElectionHandler::new(node_id)
}
fn create_vote_request(
term: u64,
candidate_id: u32,
last_log_index: u64,
last_log_term: u64,
) -> VoteRequest {
VoteRequest {
term,
candidate_id,
last_log_index,
last_log_term,
}
}
fn create_mock_raft_log(last_log_id: Option<LogId>) -> MockRaftLog {
let mut raft_log = MockRaftLog::new();
raft_log.expect_last_log_id().returning(move || last_log_id);
raft_log
}
#[tokio::test]
async fn test_handle_vote_request_grant_higher_term() {
let handler = create_handler(2);
let request = create_vote_request(2, 1, 2, 2);
let current_term = 1u64;
let voted_for_option = None;
let last_log_id = Some(LogId { index: 1, term: 1 });
let raft_log = Arc::new(create_mock_raft_log(last_log_id));
let state_update = handler
.handle_vote_request(request, current_term, voted_for_option, &raft_log)
.await
.unwrap();
assert_eq!(
state_update.term_update,
Some(2),
"Term should be updated to 2"
);
assert!(
state_update.new_voted_for.is_some(),
"Vote should be granted"
);
assert_eq!(
state_update.new_voted_for.unwrap().voted_for_id,
1,
"Should vote for candidate 1"
);
assert_eq!(
state_update.new_voted_for.unwrap().voted_for_term,
2,
"Vote should be for term 2"
);
}
#[tokio::test]
async fn test_handle_vote_request_deny_lower_term() {
let handler = create_handler(2);
let request = create_vote_request(2, 1, 5, 2);
let current_term = 3u64;
let voted_for_option = None;
let last_log_id = Some(LogId { index: 5, term: 3 });
let raft_log = Arc::new(create_mock_raft_log(last_log_id));
let state_update = handler
.handle_vote_request(request, current_term, voted_for_option, &raft_log)
.await
.unwrap();
assert_eq!(
state_update.term_update, None,
"Term should not be updated for lower request term"
);
assert_eq!(
state_update.new_voted_for, None,
"Vote should not be granted for lower term"
);
}
#[tokio::test]
async fn test_handle_vote_request_deny_stale_log() {
let handler = create_handler(2);
let request = create_vote_request(1, 1, 5, 1); let current_term = 1u64;
let voted_for_option = None;
let last_log_id = Some(LogId { index: 10, term: 2 }); let raft_log = Arc::new(create_mock_raft_log(last_log_id));
let state_update = handler
.handle_vote_request(request, current_term, voted_for_option, &raft_log)
.await
.unwrap();
assert_eq!(
state_update.new_voted_for, None,
"Vote should be denied for stale log"
);
}
#[tokio::test]
async fn test_handle_vote_request_deny_already_voted_different_candidate() {
let handler = create_handler(2);
let request = create_vote_request(2, 3, 3, 2); let current_term = 2u64;
let voted_for_option = Some(VotedFor {
voted_for_id: 1,
voted_for_term: 2,
committed: false,
}); let last_log_id = Some(LogId { index: 3, term: 2 });
let raft_log = Arc::new(create_mock_raft_log(last_log_id));
let state_update = handler
.handle_vote_request(request, current_term, voted_for_option, &raft_log)
.await
.unwrap();
assert_eq!(
state_update.new_voted_for, None,
"Vote should be denied when already voted for different candidate"
);
}
#[tokio::test]
async fn test_handle_vote_request_grant_revote_same_candidate() {
let handler = create_handler(2);
let request = create_vote_request(2, 1, 3, 2); let current_term = 2u64;
let voted_for_option = Some(VotedFor {
voted_for_id: 1,
voted_for_term: 2,
committed: false,
}); let last_log_id = Some(LogId { index: 3, term: 2 });
let raft_log = Arc::new(create_mock_raft_log(last_log_id));
let state_update = handler
.handle_vote_request(request, current_term, voted_for_option, &raft_log)
.await
.unwrap();
assert!(
state_update.new_voted_for.is_some(),
"Vote should be granted for re-voting"
);
assert_eq!(
state_update.new_voted_for.unwrap().voted_for_id,
1,
"Should vote for the same candidate"
);
}
#[tokio::test]
async fn test_handle_vote_request_grant_higher_term_resets_vote() {
let handler = create_handler(2);
let request = create_vote_request(3, 3, 4, 3); let current_term = 2u64;
let voted_for_option = Some(VotedFor {
voted_for_id: 1,
voted_for_term: 2,
committed: false,
}); let last_log_id = Some(LogId { index: 3, term: 2 });
let raft_log = Arc::new(create_mock_raft_log(last_log_id));
let state_update = handler
.handle_vote_request(request, current_term, voted_for_option, &raft_log)
.await
.unwrap();
assert_eq!(
state_update.term_update,
Some(3),
"Term should be updated to 3"
);
assert!(
state_update.new_voted_for.is_some(),
"Vote should be granted for higher term"
);
assert_eq!(
state_update.new_voted_for.unwrap().voted_for_id,
3,
"Should vote for node 3"
);
}
#[tokio::test]
async fn test_handle_vote_request_grant_higher_log_term() {
let handler = create_handler(2);
let request = create_vote_request(1, 1, 5, 2); let current_term = 1u64;
let voted_for_option = None;
let last_log_id = Some(LogId { index: 10, term: 1 });
let raft_log = Arc::new(create_mock_raft_log(last_log_id));
let state_update = handler
.handle_vote_request(request, current_term, voted_for_option, &raft_log)
.await
.unwrap();
assert!(
state_update.new_voted_for.is_some(),
"Vote should be granted for higher log term"
);
}
#[tokio::test]
async fn test_handle_vote_request_grant_higher_index_same_term() {
let handler = create_handler(2);
let request = create_vote_request(1, 1, 10, 2); let current_term = 1u64;
let voted_for_option = None;
let last_log_id = Some(LogId { index: 5, term: 2 });
let raft_log = Arc::new(create_mock_raft_log(last_log_id));
let state_update = handler
.handle_vote_request(request, current_term, voted_for_option, &raft_log)
.await
.unwrap();
assert!(
state_update.new_voted_for.is_some(),
"Vote should be granted for higher index in same term"
);
}
#[tokio::test]
async fn test_handle_vote_request_empty_local_log() {
let handler = create_handler(2);
let request = create_vote_request(1, 1, 1, 1);
let current_term = 0u64;
let voted_for_option = None;
let last_log_id = None;
let raft_log = Arc::new(create_mock_raft_log(last_log_id));
let state_update = handler
.handle_vote_request(request, current_term, voted_for_option, &raft_log)
.await
.unwrap();
assert!(
state_update.new_voted_for.is_some(),
"Vote should be granted for candidate with valid log when local log is empty"
);
}
#[tokio::test]
async fn test_handle_vote_request_both_empty_logs() {
let handler = create_handler(2);
let request = create_vote_request(1, 1, 0, 0);
let current_term = 0u64;
let voted_for_option = None;
let last_log_id = None;
let raft_log = Arc::new(create_mock_raft_log(last_log_id));
let state_update = handler
.handle_vote_request(request, current_term, voted_for_option, &raft_log)
.await
.unwrap();
assert!(
state_update.new_voted_for.is_some(),
"Vote should be granted when both have empty logs"
);
}
#[tokio::test]
async fn test_check_vote_request_is_legal_lower_term() {
let handler = create_handler(2);
let request = create_vote_request(1, 1, 5, 2);
let current_term = 2u64;
let last_log_index = 5u64;
let last_log_term = 2u64;
let voted_for_option = None;
let is_legal = handler.check_vote_request_is_legal(
&request,
current_term,
last_log_index,
last_log_term,
voted_for_option,
);
assert!(!is_legal, "Request with lower term should be rejected");
}
#[tokio::test]
async fn test_check_vote_request_is_legal_stale_log() {
let handler = create_handler(2);
let request = create_vote_request(2, 1, 3, 1); let current_term = 2u64;
let last_log_index = 5u64;
let last_log_term = 2u64; let voted_for_option = None;
let is_legal = handler.check_vote_request_is_legal(
&request,
current_term,
last_log_index,
last_log_term,
voted_for_option,
);
assert!(!is_legal, "Request with stale log should be rejected");
}
#[tokio::test]
async fn test_check_vote_request_is_legal_already_voted_different() {
let handler = create_handler(2);
let request = create_vote_request(2, 3, 5, 2); let current_term = 2u64;
let last_log_index = 5u64;
let last_log_term = 2u64;
let voted_for_option = Some(VotedFor {
voted_for_id: 1,
voted_for_term: 2,
committed: false,
});
let is_legal = handler.check_vote_request_is_legal(
&request,
current_term,
last_log_index,
last_log_term,
voted_for_option,
);
assert!(
!is_legal,
"Request should be rejected when already voted for different candidate"
);
}
#[tokio::test]
async fn test_check_vote_request_is_legal_valid_request() {
let handler = create_handler(2);
let request = create_vote_request(2, 1, 5, 2); let current_term = 2u64;
let last_log_index = 5u64;
let last_log_term = 2u64;
let voted_for_option = None;
let is_legal = handler.check_vote_request_is_legal(
&request,
current_term,
last_log_index,
last_log_term,
voted_for_option,
);
assert!(is_legal, "Valid request should be accepted");
}
#[tokio::test]
async fn test_handle_vote_request_term_zero() {
let handler = create_handler(2);
let request = create_vote_request(0, 1, 0, 0);
let current_term = 0u64;
let voted_for_option = None;
let last_log_id = None;
let raft_log = Arc::new(create_mock_raft_log(last_log_id));
let state_update = handler
.handle_vote_request(request, current_term, voted_for_option, &raft_log)
.await
.unwrap();
assert_eq!(state_update.term_update, None);
}
#[tokio::test]
async fn test_handle_vote_request_large_term_numbers() {
let handler = create_handler(2);
let large_term = u64::MAX;
let request = create_vote_request(large_term, 1, 100, large_term);
let current_term = large_term - 1;
let voted_for_option = None;
let last_log_id = Some(LogId {
index: 100,
term: large_term,
});
let raft_log = Arc::new(create_mock_raft_log(last_log_id));
let state_update = handler
.handle_vote_request(request, current_term, voted_for_option, &raft_log)
.await
.unwrap();
assert_eq!(state_update.term_update, Some(large_term));
}
#[cfg(test)]
mod single_node_election_tests {
use std::collections::HashSet;
use d_engine_proto::server::cluster::NodeMeta;
use d_engine_proto::server::election::VoteResponse;
use super::*;
use crate::ConsensusError;
use crate::ElectionError;
use crate::Error;
use crate::MockMembership;
use crate::MockTransport;
use crate::RaftNodeConfig;
use crate::VoteResult;
#[tokio::test]
async fn test_single_node_auto_wins_election() {
let handler = ElectionHandler::<MockTypeConfig>::new(1);
let mut mock_membership = MockMembership::new();
mock_membership.expect_is_single_node_cluster().times(1).returning(|| true);
mock_membership.expect_voters().times(0);
let membership = Arc::new(mock_membership);
let raft_log = Arc::new(create_mock_raft_log(None));
let mock_transport = MockTransport::new();
let transport = Arc::new(mock_transport);
let settings = Arc::new(RaftNodeConfig::default());
let result = handler
.broadcast_vote_requests(1, membership, &raft_log, &transport, &settings)
.await;
assert!(
result.is_ok(),
"Single-node should automatically win election"
);
}
#[tokio::test]
async fn test_three_node_cluster_goes_through_normal_election() {
let handler = ElectionHandler::<MockTypeConfig>::new(1);
let mut mock_membership = MockMembership::new();
mock_membership.expect_is_single_node_cluster().times(1).returning(|| false);
mock_membership.expect_voters().times(1).returning(|| {
vec![
NodeMeta {
id: 2,
address: "127.0.0.1:9082".to_string(),
role: 0,
status: 2,
},
NodeMeta {
id: 3,
address: "127.0.0.1:9083".to_string(),
role: 0,
status: 2,
},
]
});
let membership = Arc::new(mock_membership);
let raft_log = Arc::new(create_mock_raft_log(None));
let mut mock_transport = MockTransport::new();
mock_transport.expect_send_vote_requests().times(1).returning(
|_req, _retry, _membership| {
Ok(VoteResult {
peer_ids: HashSet::from([2, 3]),
responses: vec![
Ok(VoteResponse {
term: 1,
vote_granted: true,
last_log_index: 0,
last_log_term: 0,
}),
Ok(VoteResponse {
term: 1,
vote_granted: true,
last_log_index: 0,
last_log_term: 0,
}),
],
})
},
);
let transport = Arc::new(mock_transport);
let settings = Arc::new(RaftNodeConfig::default());
let result = handler
.broadcast_vote_requests(1, membership, &raft_log, &transport, &settings)
.await;
assert!(
result.is_ok(),
"Three-node cluster should complete normal election"
);
}
#[tokio::test]
async fn test_network_partition_with_empty_voters_still_reports_error() {
let handler = ElectionHandler::<MockTypeConfig>::new(1);
let mut mock_membership = MockMembership::new();
mock_membership.expect_is_single_node_cluster().times(1).returning(|| false);
mock_membership.expect_voters().times(1).returning(Vec::new);
let membership = Arc::new(mock_membership);
let raft_log = Arc::new(create_mock_raft_log(None));
let mock_transport = MockTransport::new();
let transport = Arc::new(mock_transport);
let settings = Arc::new(RaftNodeConfig::default());
let result = handler
.broadcast_vote_requests(1, membership, &raft_log, &transport, &settings)
.await;
assert!(result.is_err(), "Network partition should return error");
assert!(
matches!(
result.unwrap_err(),
Error::Consensus(crate::ConsensusError::Election(
ElectionError::NoVotingMemberFound { .. }
))
),
"Should return NoVotingMemberFound error"
);
}
#[tokio::test]
async fn test_broadcast_vote_requests_returns_error_when_no_voting_members() {
let election_handler = ElectionHandler::<MockTypeConfig>::new(1);
let term = 1;
let mut raft_log_mock = MockRaftLog::new();
raft_log_mock
.expect_last_log_id()
.times(0)
.returning(|| Some(LogId { index: 1, term: 1 }));
let mut transport_mock = MockTransport::new();
transport_mock.expect_send_vote_requests().times(0).returning(|_, _, _| {
Ok(VoteResult {
peer_ids: vec![2].into_iter().collect(),
responses: vec![Ok(VoteResponse {
term: 1,
vote_granted: false,
last_log_index: 1,
last_log_term: 1,
})],
})
});
let mut membership = MockMembership::new();
membership.expect_voters().returning(Vec::new);
membership.expect_is_single_node_cluster().returning(|| false);
let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
let mut node_config = RaftNodeConfig::new().expect("Should create default config");
node_config.cluster.db_root_dir = temp_dir.path().to_path_buf();
let node_config = node_config.validate().expect("Should validate config");
let result = election_handler
.broadcast_vote_requests(
term,
Arc::new(membership),
&Arc::new(raft_log_mock),
&Arc::new(transport_mock),
&Arc::new(node_config),
)
.await;
assert!(
result.is_err(),
"Should return error when no voting members"
);
assert!(
matches!(
result.unwrap_err(),
Error::Consensus(ConsensusError::Election(
ElectionError::NoVotingMemberFound { candidate_id: 1 }
))
),
"Expected NoVotingMemberFound error with candidate_id=1"
);
}
#[tokio::test]
#[ignore = "False positive test - needs fix before enabling (see FIXME above)"]
async fn test_broadcast_vote_requests_majority_reject_due_to_log_conflict() {
let election_handler = ElectionHandler::<MockTypeConfig>::new(1);
let term = 1;
let mut raft_log_mock = MockRaftLog::new();
raft_log_mock
.expect_last_log_id()
.times(0)
.returning(|| Some(LogId { index: 1, term: 1 }));
let mut transport_mock = MockTransport::new();
transport_mock.expect_send_vote_requests().times(0).returning(|_, _, _| {
Ok(VoteResult {
peer_ids: vec![2].into_iter().collect(),
responses: vec![Ok(VoteResponse {
term: 1,
vote_granted: false,
last_log_index: 1,
last_log_term: 1,
})],
})
});
let mut membership = MockMembership::new();
membership.expect_voters().returning(Vec::new);
membership.expect_is_single_node_cluster().returning(|| false);
let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
let mut node_config = RaftNodeConfig::new().expect("Should create default config");
node_config.cluster.db_root_dir = temp_dir.path().to_path_buf();
let node_config = node_config.validate().expect("Should validate config");
let e = election_handler
.broadcast_vote_requests(
term,
Arc::new(membership),
&Arc::new(raft_log_mock),
&Arc::new(transport_mock),
&Arc::new(node_config),
)
.await
.unwrap_err();
if let Error::Consensus(ConsensusError::Election(ElectionError::LogConflict {
index,
expected_term,
actual_term,
})) = e
{
assert_eq!(index, 1);
assert_eq!(actual_term, 1);
assert_eq!(expected_term, 1);
}
}
#[tokio::test]
async fn test_broadcast_vote_requests_wins_election_with_majority_votes() {
let election_handler = ElectionHandler::<MockTypeConfig>::new(1);
let term = 1;
let mut raft_log_mock = MockRaftLog::new();
raft_log_mock
.expect_last_log_id()
.times(1)
.returning(|| Some(LogId { index: 1, term: 1 }));
let mut transport_mock = MockTransport::new();
transport_mock.expect_send_vote_requests().times(1).returning(|_, _, _| {
Ok(VoteResult {
peer_ids: vec![2].into_iter().collect(),
responses: vec![Ok(VoteResponse {
term: 1,
vote_granted: true,
last_log_index: 1,
last_log_term: 1,
})],
})
});
let mut membership = MockMembership::new();
membership.expect_is_single_node_cluster().returning(|| false);
membership.expect_initial_cluster_size().returning(|| 2);
membership.expect_voters().returning(move || {
use d_engine_proto::common::NodeRole::Follower;
use d_engine_proto::common::NodeStatus;
use d_engine_proto::server::cluster::NodeMeta;
vec![NodeMeta {
id: 2,
address: "http://127.0.0.1:55001".to_string(),
role: Follower.into(),
status: NodeStatus::Active.into(),
}]
});
let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
let mut node_config = RaftNodeConfig::new().expect("Should create default config");
node_config.cluster.db_root_dir = temp_dir.path().to_path_buf();
let node_config = node_config.validate().expect("Should validate config");
let result = election_handler
.broadcast_vote_requests(
term,
Arc::new(membership),
&Arc::new(raft_log_mock),
&Arc::new(transport_mock),
&Arc::new(node_config),
)
.await;
assert!(
result.is_ok(),
"Expected successful election with majority votes, got: {result:?}"
);
}
#[tokio::test]
#[ignore = "False positive test - needs fix before enabling (see FIXME above)"]
async fn test_broadcast_vote_requests_peer_has_higher_term() {
let election_handler = ElectionHandler::<MockTypeConfig>::new(1);
let my_last_log_term = 3;
let transport_mock = MockTransport::new();
let raft_log_mock = MockRaftLog::new();
let mut membership = MockMembership::new();
membership.expect_voters().returning(Vec::new);
membership.expect_is_single_node_cluster().returning(|| false);
let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
let mut node_config = RaftNodeConfig::new().expect("Should create default config");
node_config.cluster.db_root_dir = temp_dir.path().to_path_buf();
let node_config = node_config.validate().expect("Should validate config");
let e = election_handler
.broadcast_vote_requests(
my_last_log_term,
Arc::new(membership),
&Arc::new(raft_log_mock),
&Arc::new(transport_mock),
&Arc::new(node_config),
)
.await
.unwrap_err();
if let Error::Consensus(ConsensusError::Election(ElectionError::HigherTerm(higher_term))) =
e
{
assert_eq!(higher_term, my_last_log_term + 1);
}
}
#[tokio::test]
#[ignore = "False positive test - needs fix before enabling (see FIXME above)"]
async fn test_broadcast_vote_requests_peer_has_higher_log_index() {
let election_handler = ElectionHandler::<MockTypeConfig>::new(1);
let my_last_log_index = 1;
let my_last_log_term = 3;
let transport_mock = MockTransport::new();
let raft_log_mock = MockRaftLog::new();
let mut membership = MockMembership::new();
membership.expect_voters().returning(Vec::new);
membership.expect_is_single_node_cluster().returning(|| false);
let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
let mut node_config = RaftNodeConfig::new().expect("Should create default config");
node_config.cluster.db_root_dir = temp_dir.path().to_path_buf();
let node_config = node_config.validate().expect("Should validate config");
let e = election_handler
.broadcast_vote_requests(
my_last_log_term,
Arc::new(membership),
&Arc::new(raft_log_mock),
&Arc::new(transport_mock),
&Arc::new(node_config),
)
.await
.unwrap_err();
if let Error::Consensus(ConsensusError::Election(ElectionError::LogConflict {
index,
expected_term,
actual_term,
})) = e
{
assert_eq!(index, my_last_log_index);
assert_eq!(expected_term, my_last_log_term);
assert_eq!(actual_term, my_last_log_term);
}
}
#[tokio::test]
async fn test_handle_vote_request_grants_vote_for_valid_higher_term() {
let election_handler = ElectionHandler::<MockTypeConfig>::new(1);
let mut raft_log_mock = MockRaftLog::new();
raft_log_mock
.expect_last_log_id()
.times(1)
.returning(|| Some(LogId { index: 1, term: 1 }));
let current_term = 1;
let request_term = current_term + 1;
let vote_request = VoteRequest {
term: request_term,
candidate_id: 1,
last_log_index: 2, last_log_term: 1,
};
let voted_for_option = None;
let result = election_handler
.handle_vote_request(
vote_request,
current_term,
voted_for_option,
&Arc::new(raft_log_mock),
)
.await;
assert!(
result.is_ok(),
"Should grant vote for valid higher term request"
);
let state_update = result.unwrap();
assert!(
state_update.new_voted_for.is_some(),
"Should update voted_for"
);
assert_eq!(
state_update.term_update,
Some(request_term),
"Should advance term to request term"
);
}
#[tokio::test]
async fn test_handle_vote_request_rejects_vote_for_lower_term() {
let election_handler = ElectionHandler::<MockTypeConfig>::new(1);
let mut raft_log_mock = MockRaftLog::new();
raft_log_mock
.expect_last_log_id()
.times(1)
.returning(|| Some(LogId { index: 1, term: 1 }));
let current_term = 10;
let request_term = current_term - 1;
let vote_request = VoteRequest {
term: request_term,
candidate_id: 1,
last_log_index: 2,
last_log_term: 1,
};
let voted_for_option = None;
let result = election_handler
.handle_vote_request(
vote_request,
current_term,
voted_for_option,
&Arc::new(raft_log_mock),
)
.await;
assert!(result.is_ok(), "Should not error on stale request");
let state_update = result.unwrap();
assert!(
state_update.new_voted_for.is_none(),
"Should NOT grant vote for lower term"
);
assert_eq!(
state_update.term_update, None,
"Should NOT update term for stale request"
);
}
#[tokio::test]
async fn test_check_vote_request_is_legal_rejects_when_current_term_not_lower() {
let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
let mut node_config = RaftNodeConfig::new().expect("Should create default config");
node_config.cluster.db_root_dir = temp_dir.path().to_path_buf();
let _node_config = node_config.validate().expect("Should validate config");
let election_handler = ElectionHandler::<MockTypeConfig>::new(1);
let vote_request = VoteRequest {
term: 1,
candidate_id: 1,
last_log_index: 1,
last_log_term: 1,
};
let last_log_index = 1;
let last_log_term = 1;
let voted_for_id = 1;
let voted_for_term = 1;
let current_term = 1;
assert!(
!election_handler.check_vote_request_is_legal(
&vote_request,
current_term,
last_log_index,
last_log_term,
Some(VotedFor {
voted_for_id,
voted_for_term,
committed: false
})
),
"Should reject when current_term equals request term"
);
let current_term = 2;
assert!(
!election_handler.check_vote_request_is_legal(
&vote_request,
current_term,
last_log_index,
last_log_term,
Some(VotedFor {
voted_for_id,
voted_for_term,
committed: false
})
),
"Should reject when current_term is higher than request term"
);
}
#[tokio::test]
async fn test_check_vote_request_is_legal_rejects_when_request_log_not_more_recent() {
let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
let mut node_config = RaftNodeConfig::new().expect("Should create default config");
node_config.cluster.db_root_dir = temp_dir.path().to_path_buf();
let _node_config = node_config.validate().expect("Should validate config");
let election_handler = ElectionHandler::<MockTypeConfig>::new(1);
let current_term = 1;
let vote_request = VoteRequest {
term: current_term,
candidate_id: 1,
last_log_index: 1,
last_log_term: 1,
};
let last_log_index = 1;
let voted_for_id = 1;
let voted_for_term = 1;
let last_log_term = 2;
assert!(
!election_handler.check_vote_request_is_legal(
&vote_request,
current_term,
last_log_index,
last_log_term,
Some(VotedFor {
voted_for_id,
voted_for_term,
committed: false
})
),
"Should reject when local log term is higher"
);
let last_log_term = 1;
assert!(
!election_handler.check_vote_request_is_legal(
&vote_request,
current_term,
last_log_index,
last_log_term,
Some(VotedFor {
voted_for_id,
voted_for_term,
committed: false
})
),
"Should reject when log terms are equal but already voted"
);
}
#[tokio::test]
async fn test_check_vote_request_is_legal_accepts_when_request_log_more_recent() {
let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
let mut node_config = RaftNodeConfig::new().expect("Should create default config");
node_config.cluster.db_root_dir = temp_dir.path().to_path_buf();
let _node_config = node_config.validate().expect("Should validate config");
let election_handler = ElectionHandler::<MockTypeConfig>::new(1);
let current_term = 1;
let last_log_index = 1;
let last_log_term = 1;
let vote_request = VoteRequest {
term: current_term,
candidate_id: 1,
last_log_index: last_log_index + 1, last_log_term,
};
assert!(
election_handler.check_vote_request_is_legal(
&vote_request,
current_term,
last_log_index,
last_log_term,
None, ),
"Should accept when request has higher log index (same term)"
);
}
#[tokio::test]
async fn test_check_vote_request_is_legal_rejects_when_local_log_more_recent() {
let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
let mut node_config = RaftNodeConfig::new().expect("Should create default config");
node_config.cluster.db_root_dir = temp_dir.path().to_path_buf();
let _node_config = node_config.validate().expect("Should validate config");
let election_handler = ElectionHandler::<MockTypeConfig>::new(1);
let current_term = 1;
let vote_request = VoteRequest {
term: current_term,
candidate_id: 1,
last_log_index: 1,
last_log_term: 1,
};
let last_log_index = 2; let last_log_term = 1;
let voted_for_id = 1;
let voted_for_term = 1;
assert!(
!election_handler.check_vote_request_is_legal(
&vote_request,
current_term,
last_log_index,
last_log_term,
Some(VotedFor {
voted_for_id,
voted_for_term,
committed: false
})
),
"Should reject when local log is more up-to-date"
);
}
#[tokio::test]
async fn test_check_vote_request_is_legal_rejects_when_already_voted_for_different_candidate() {
let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
let mut node_config = RaftNodeConfig::new().expect("Should create default config");
node_config.cluster.db_root_dir = temp_dir.path().to_path_buf();
let _node_config = node_config.validate().expect("Should validate config");
let election_handler = ElectionHandler::<MockTypeConfig>::new(1);
let current_term = 1;
let vote_request = VoteRequest {
term: current_term,
candidate_id: 1,
last_log_index: 3,
last_log_term: 1,
};
let last_log_index = 2;
let last_log_term = 1;
let voted_for_id = 3; let voted_for_term = 1;
assert!(
!election_handler.check_vote_request_is_legal(
&vote_request,
current_term,
last_log_index,
last_log_term,
Some(VotedFor {
voted_for_id,
voted_for_term,
committed: false
})
),
"Should reject when already voted for different candidate in same term"
);
}
#[tokio::test]
async fn test_check_vote_request_is_legal_rejects_when_voted_in_higher_term() {
let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
let mut node_config = RaftNodeConfig::new().expect("Should create default config");
node_config.cluster.db_root_dir = temp_dir.path().to_path_buf();
let _node_config = node_config.validate().expect("Should validate config");
let election_handler = ElectionHandler::<MockTypeConfig>::new(1);
let current_term = 1;
let vote_request = VoteRequest {
term: current_term,
candidate_id: 1,
last_log_index: 3,
last_log_term: 1,
};
let last_log_index = 2;
let last_log_term = 1;
let voted_for_id = 1;
let voted_for_term = 10;
assert!(
!election_handler.check_vote_request_is_legal(
&vote_request,
current_term,
last_log_index,
last_log_term,
Some(VotedFor {
voted_for_id,
voted_for_term,
committed: false
})
),
"Should reject when already voted in higher term"
);
}
#[tokio::test]
async fn test_check_vote_request_is_legal_accepts_revote_for_same_candidate_new_term() {
let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
let mut node_config = RaftNodeConfig::new().expect("Should create default config");
node_config.cluster.db_root_dir = temp_dir.path().to_path_buf();
let _node_config = node_config.validate().expect("Should validate config");
let election_handler = ElectionHandler::<MockTypeConfig>::new(1);
let current_term = 10; let vote_request = VoteRequest {
term: current_term,
candidate_id: 1,
last_log_index: 3,
last_log_term: 1,
};
let last_log_index = 2;
let last_log_term = 1;
let voted_for_id = 1; let voted_for_term = 1;
assert!(
election_handler.check_vote_request_is_legal(
&vote_request,
current_term,
last_log_index,
last_log_term,
Some(VotedFor {
voted_for_id,
voted_for_term,
committed: false
})
),
"Should accept re-vote for same candidate in new term"
);
}
}