1use {
8 super::{ControllerHandle, connection_options},
9 crate::{
10 DelegationTokenMutation, SubmitChangeResult,
11 error::RaftError,
12 network::OutboundDialer,
13 types::{NodeId, controller_endpoint_addr as endpoint_addr_from_endpoints},
14 wire::{
15 API_KEY_DELEGATION_TOKEN_MUTATION, API_KEY_SUBMIT_CHANGE, KrabkaSubmitChangeRequest,
16 KrabkaSubmitChangeResponse, PRIVATE_CLUSTER_AUTHORIZATION_FAILED,
17 PRIVATE_UNSUPPORTED_VERSION, SUBMIT_CHANGE_UNCOMMITTED_TAIL, private_request_version,
18 },
19 },
20 krabka_metadata::{MetadataImage, MetadataRecord},
21 std::sync::Arc,
22};
23
24impl ControllerHandle {
25 pub async fn submit_change_if_image(
32 &self,
33 records: Vec<MetadataRecord>,
34 expected_image: Arc<MetadataImage>,
35 ) -> Result<SubmitChangeResult, RaftError> {
36 self.engine
37 .submit_change_if_image(records, expected_image)
38 .await
39 }
40
41 #[tracing::instrument(
49 level = "debug",
50 skip_all,
51 fields(node = self.self_node_id.0, records = records.len())
52 )]
53 pub async fn submit_change(
54 &self,
55 records: Vec<MetadataRecord>,
56 ) -> Result<SubmitChangeResult, RaftError> {
57 let submission = self.engine.submit_change(records.clone()).await;
58 match submission {
59 Err(RaftError::NotLeader {
60 current_leader: Some(leader),
61 }) => match self.voter_addr(leader) {
62 Some(addr) => self.forward_submit_to(leader, &addr, &records).await,
63 None => Err(RaftError::NotLeader {
64 current_leader: Some(leader),
65 }),
66 },
67 other => other,
68 }
69 }
70
71 pub async fn submit_delegation_token_mutations(
78 &self,
79 mutations: Vec<DelegationTokenMutation>,
80 ) -> Result<SubmitChangeResult, RaftError> {
81 match self
82 .engine
83 .submit_delegation_token_mutations(mutations.clone())
84 .await
85 {
86 Ok(result) => Ok(result),
87 Err(RaftError::NotLeader {
88 current_leader: Some(leader),
89 }) => {
90 if let Some(addr) = self.voter_addr(leader) {
91 self.forward_delegation_token_mutations_to(leader, &addr, &mutations)
92 .await
93 } else {
94 Err(RaftError::NotLeader {
95 current_leader: Some(leader),
96 })
97 }
98 }
99 Err(error) => Err(error),
100 }
101 }
102
103 #[cfg_attr(test, mutants::skip)]
109 async fn forward_delegation_token_mutations_to(
110 &self,
111 leader: NodeId,
112 addr: &str,
113 mutations: &[DelegationTokenMutation],
114 ) -> Result<SubmitChangeResult, RaftError> {
115 let body = encode_delegation_token_mutation_body(mutations)?;
116 let version = private_request_version(
117 krabka_metadata::PrivateRpc::DelegationTokenMutation,
118 Some(&self.current_image()),
119 );
120 let options = connection_options(
121 &self.client_id,
122 self.client_dispatch_queue_capacity,
123 self.client_frame_max,
124 );
125 let connection = self
126 .dialer
127 .dial(leader, addr, options)
128 .await
129 .map_err(RaftError::Network)?;
130 let response = connection
131 .raw_request(
132 API_KEY_DELEGATION_TOKEN_MUTATION,
133 version,
134 bytes::Bytes::from(body),
135 )
136 .await
137 .map_err(RaftError::Network)?;
138 connection.close();
139 translate_submit_change_response(
140 &response,
141 leader,
142 API_KEY_DELEGATION_TOKEN_MUTATION,
143 version,
144 )
145 }
146
147 #[cfg_attr(test, mutants::skip)]
166 pub async fn forward_raw(
167 &self,
168 api_key: i16,
169 version: i16,
170 body: bytes::Bytes,
171 ) -> Result<bytes::Bytes, RaftError> {
172 let Some(leader) = self.engine.quorum_snapshot().leader_id else {
173 return Err(RaftError::NotLeader {
174 current_leader: None,
175 });
176 };
177 let Some(addr) = self.voter_addr(leader) else {
178 return Err(RaftError::NotLeader {
179 current_leader: Some(leader),
180 });
181 };
182 let opts = connection_options(
183 &self.client_id,
184 self.client_dispatch_queue_capacity,
185 self.client_frame_max,
186 );
187 let conn = self
188 .dialer
189 .dial(leader, &addr, opts)
190 .await
191 .map_err(RaftError::Network)?;
192 let resp = conn
193 .raw_request(api_key, version, body)
194 .await
195 .map_err(RaftError::Network)?;
196 conn.close();
197 Ok(resp)
198 }
199
200 fn voter_addr(&self, node_id: NodeId) -> Option<String> {
203 let voters = self.engine.quorum_snapshot().voters;
204 controller_endpoint_addr(&voters, node_id)
205 .or_else(|| controller_endpoint_addr(&self.voters, node_id))
206 }
207
208 #[cfg_attr(test, mutants::skip)]
219 #[tracing::instrument(
220 level = "debug",
221 skip_all,
222 fields(node = self.self_node_id.0, leader = leader.0, addr, records = records.len()),
223 err
224 )]
225 async fn forward_submit_to(
226 &self,
227 leader: NodeId,
228 addr: &str,
229 records: &[krabka_metadata::MetadataRecord],
230 ) -> Result<SubmitChangeResult, RaftError> {
231 let transport = DialerSubmitTransport {
232 dialer: self.dialer.as_ref(),
233 client_id: &self.client_id,
234 client_dispatch_queue_capacity: self.client_dispatch_queue_capacity,
235 client_frame_max: self.client_frame_max,
236 };
237 let version = private_request_version(
238 krabka_metadata::PrivateRpc::SubmitChange,
239 Some(&self.current_image()),
240 );
241 forward_submit_via(&transport, leader, addr, version, records).await
242 }
243}
244
245fn controller_endpoint_addr(voters: &krabka_metadata::VoterSet, node_id: NodeId) -> Option<String> {
256 let voter = voters.get(node_id)?;
257 endpoint_addr_from_endpoints(&voter.endpoints)
258}
259
260#[cfg_attr(test, mockall::automock)]
267#[async_trait::async_trait]
268trait SubmitChangeTransport: Send + Sync {
269 async fn send_submit_change(
272 &self,
273 leader: NodeId,
274 addr: &str,
275 version: i16,
276 body: Vec<u8>,
277 ) -> Result<bytes::Bytes, krabka_client_core::ClientError>;
278}
279
280struct DialerSubmitTransport<'a> {
285 dialer: &'a dyn OutboundDialer,
286 client_id: &'a str,
287 client_dispatch_queue_capacity: krabka_client_core::ConnectionDispatchQueueCapacity,
288 client_frame_max: krabka_client_core::ClientFrameMax,
289}
290
291#[async_trait::async_trait]
292impl SubmitChangeTransport for DialerSubmitTransport<'_> {
293 #[cfg_attr(test, mutants::skip)]
299 async fn send_submit_change(
300 &self,
301 leader: NodeId,
302 addr: &str,
303 version: i16,
304 body: Vec<u8>,
305 ) -> Result<bytes::Bytes, krabka_client_core::ClientError> {
306 let opts = connection_options(
307 self.client_id,
308 self.client_dispatch_queue_capacity,
309 self.client_frame_max,
310 );
311 let conn = self.dialer.dial(leader, addr, opts).await?;
312 let resp_body = conn
313 .raw_request(API_KEY_SUBMIT_CHANGE, version, bytes::Bytes::from(body))
314 .await?;
315 conn.close();
316 Ok(resp_body)
317 }
318}
319
320async fn forward_submit_via(
325 transport: &dyn SubmitChangeTransport,
326 leader: NodeId,
327 addr: &str,
328 version: i16,
329 records: &[krabka_metadata::MetadataRecord],
330) -> Result<SubmitChangeResult, RaftError> {
331 let body = encode_submit_change_body(records)?;
332 let resp_body = transport
333 .send_submit_change(leader, addr, version, body)
334 .await
335 .map_err(RaftError::Network)?;
336 translate_submit_change_response(&resp_body, leader, API_KEY_SUBMIT_CHANGE, version)
337}
338
339fn encode_submit_change_body(
344 records: &[krabka_metadata::MetadataRecord],
345) -> Result<Vec<u8>, RaftError> {
346 let body_bytes = <serde_wincode::SerdeCompat<Vec<krabka_metadata::MetadataRecord>> as wincode::Serialize>::serialize(
347 &records.to_vec(),
348 )
349 .map_err(RaftError::from)?;
350 submit_change_frame(body_bytes)
351}
352
353fn encode_delegation_token_mutation_body(
354 mutations: &[DelegationTokenMutation],
355) -> Result<Vec<u8>, RaftError> {
356 let payload = <serde_wincode::SerdeCompat<Vec<DelegationTokenMutation>> as wincode::Serialize>::serialize(
357 &mutations.to_vec(),
358 )
359 .map_err(RaftError::from)?;
360 submit_change_frame(payload)
361}
362
363fn submit_change_frame(payload: Vec<u8>) -> Result<Vec<u8>, RaftError> {
364 let request = KrabkaSubmitChangeRequest {
365 records: bytes::Bytes::from(payload),
366 };
367 let mut body = Vec::with_capacity(request.records.len() + 4);
368 request.encode_v0(&mut body)?;
369 Ok(body)
370}
371
372fn translate_submit_change_response(
391 resp_body: &[u8],
392 leader: NodeId,
393 api_key: i16,
394 version: i16,
395) -> Result<SubmitChangeResult, RaftError> {
396 let mut cur: &[u8] = resp_body;
397 let resp = KrabkaSubmitChangeResponse::decode_v0(&mut cur)?;
398 match resp.error_code {
399 0 => <serde_wincode::SerdeCompat<SubmitChangeResult> as wincode::Deserialize>::deserialize(
400 &resp.result,
401 )
402 .map_err(RaftError::from),
403 2 => Err(RaftError::Metadata(
404 krabka_metadata::MetadataError::TopicExists(String::new()),
405 )),
406 SUBMIT_CHANGE_UNCOMMITTED_TAIL => Err(RaftError::UncommittedTail),
407 PRIVATE_CLUSTER_AUTHORIZATION_FAILED => Err(RaftError::ClusterAuthorizationFailed),
408 PRIVATE_UNSUPPORTED_VERSION => {
409 Err(RaftError::UnsupportedPrivateVersion { api_key, version })
410 }
411 _ => Err(RaftError::NotLeader {
412 current_leader: (resp.leader_hint >= 0)
413 .then(|| NodeId(u64::try_from(resp.leader_hint).unwrap_or(leader.0))),
414 }),
415 }
416}
417
418#[cfg(test)]
419mod tests {
420 use {
421 super::*,
422 crate::{
423 DelegationTokenMutation, SubmitChangeResult,
424 controller::test_support::{
425 bootstrap_controller, joining_controller, submit_change_response_bytes,
426 topic_record,
427 },
428 wire::{
429 API_KEY_DELEGATION_TOKEN_MUTATION, API_KEY_METADATA_FETCH, API_KEY_SUBMIT_CHANGE,
430 KrabkaSubmitChangeRequest, PRIVATE_CLUSTER_AUTHORIZATION_FAILED,
431 PRIVATE_UNSUPPORTED_VERSION, SUBMIT_CHANGE_UNCOMMITTED_TAIL,
432 },
433 },
434 assert2::check,
435 std::io::{Error, ErrorKind},
436 uuid::Uuid,
437 };
438
439 #[derive(Clone, Copy)]
440 enum SubmitResponseExpectation {
441 Applied,
442 TopicExists,
443 UncommittedTail,
444 ClusterAuthorizationFailed,
445 UnsupportedVersion,
446 NotLeaderWithHint,
447 NotLeaderWithoutHint,
448 }
449
450 #[yare::parameterized(
451 registered_voter = {
452 NodeId(2),
453 Some("demo-broker-2-0.demo-broker-headless.default.svc.cluster.local:9093".to_owned()),
454 },
455 unknown_voter = { NodeId(99), None },
456 )]
457 fn controller_endpoint_addr_keeps_dns_hostname_not_parsed_socketaddr(
458 node_id: NodeId,
459 expected: Option<String>,
460 ) {
461 let host = "demo-broker-2-0.demo-broker-headless.default.svc.cluster.local";
468 let voters = krabka_metadata::VoterSet::from_voters([krabka_metadata::Voter {
469 id: NodeId(2),
470 directory_id: Uuid::nil(),
471 endpoints: vec![krabka_metadata::VoterEndpoint {
472 name: "CONTROLLER".to_owned(),
473 host: host.to_owned(),
474 port: 9093,
475 }],
476 kraft_version: krabka_metadata::KRaftVersionRange::default(),
477 }]);
478 assert2::assert!(controller_endpoint_addr(&voters, node_id) == expected);
479 }
480
481 #[test]
482 fn controller_endpoint_addr_prefers_controller_endpoint_over_others() {
483 let voters = krabka_metadata::VoterSet::from_voters([krabka_metadata::Voter {
491 id: NodeId(7),
492 directory_id: Uuid::nil(),
493 endpoints: vec![
494 krabka_metadata::VoterEndpoint {
495 name: "REPLICATION".to_owned(),
496 host: "replication-host".to_owned(),
497 port: 9092,
498 },
499 krabka_metadata::VoterEndpoint {
500 name: "CONTROLLER".to_owned(),
501 host: "controller-host".to_owned(),
502 port: 9093,
503 },
504 ],
505 kraft_version: krabka_metadata::KRaftVersionRange::default(),
506 }]);
507 assert2::assert!(
508 controller_endpoint_addr(&voters, NodeId(7)) == Some("controller-host:9093".to_owned())
509 );
510 }
511
512 #[test]
513 fn encode_submit_change_body_frames_wincode_records_with_i32_length_prefix() {
514 let records = vec![topic_record("alpha"), topic_record("beta")];
521 let body = encode_submit_change_body(&records).expect("encode");
522
523 let expected_wincode = <serde_wincode::SerdeCompat<
524 Vec<krabka_metadata::MetadataRecord>,
525 > as wincode::Serialize>::serialize(&records)
526 .expect("wincode");
527 assert2::assert!(body.len() == expected_wincode.len() + 4);
528
529 let mut cur: &[u8] = &body;
530 let req = KrabkaSubmitChangeRequest::decode_v0(&mut cur).expect("decode frame");
531 assert2::assert!(req.records.as_ref() == expected_wincode.as_slice());
532 let decoded = <serde_wincode::SerdeCompat<
535 Vec<krabka_metadata::MetadataRecord>,
536 > as wincode::Deserialize>::deserialize(&req.records)
537 .expect("wincode decode");
538 assert2::assert!(decoded == records);
539 }
540
541 const SUBMIT_CHANGE_V0_GOLDEN: &[u8] = &[
543 0, 0, 0, 51, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, b't', 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, ];
551
552 fn delegation_token_mutation_v0_golden() -> Vec<u8> {
554 const USER_ALICE: &[u8] = &[
557 4, 0, 0, 0, 0, 0, 0, 0, b'U', b's', b'e', b'r', 5, 0, 0, 0, 0, 0, 0, 0, b'a', b'l',
558 b'i', b'c', b'e',
559 ];
560 let prefix: &[u8] = &[
561 0, 0, 0, 130, 1, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, b't', b'o', b'k', ];
566 let timestamps_and_renewer_count: &[u8] = &[
567 1, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, ];
572 [
573 prefix,
574 USER_ALICE,
575 USER_ALICE,
576 timestamps_and_renewer_count,
577 USER_ALICE,
578 ]
579 .concat()
580 }
581
582 const SUBMIT_CHANGE_RESULT_V0_GOLDEN: &[u8] = &[
585 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, b't', 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, ];
592
593 fn golden_token() -> krabka_metadata::DelegationTokenRecord {
596 let alice = krabka_security::KafkaPrincipal {
597 principal_type: "User".into(),
598 name: "alice".into(),
599 };
600 krabka_metadata::DelegationTokenRecord {
601 token_id: "tok".into(),
602 owner: alice.clone(),
603 requester: alice.clone(),
604 issue_timestamp_ms: 1,
605 expiry_timestamp_ms: 2,
606 max_timestamp_ms: 3,
607 renewers: vec![alice],
608 }
609 }
610
611 #[test]
617 fn v0_payloads_match_their_golden_bytes() {
618 let delegation_golden = delegation_token_mutation_v0_golden();
619 let records = vec![krabka_metadata::MetadataRecord::V1Topic(
620 krabka_metadata::TopicRecord {
621 name: "t".into(),
622 topic_id: Uuid::from_u128(1),
623 partitions: 1,
624 replication_factor: 1,
625 },
626 )];
627 let mutations = vec![crate::DelegationTokenMutation::Delete {
628 expected: golden_token(),
629 }];
630 let result = SubmitChangeResult {
631 offset_reservations: vec![crate::OffsetReservation {
632 topic: "t".into(),
633 partition: 0,
634 base_offset: 5,
635 count: 2,
636 leader_epoch: 3,
637 }],
638 };
639 let result_bytes =
640 <serde_wincode::SerdeCompat<SubmitChangeResult> as wincode::Serialize>::serialize(
641 &result,
642 )
643 .expect("wincode");
644
645 check!(encode_submit_change_body(&records).unwrap() == SUBMIT_CHANGE_V0_GOLDEN);
646 check!(encode_delegation_token_mutation_body(&mutations).unwrap() == delegation_golden);
647 check!(result_bytes == SUBMIT_CHANGE_RESULT_V0_GOLDEN);
648
649 let mut cur: &[u8] = SUBMIT_CHANGE_V0_GOLDEN;
651 let frame = KrabkaSubmitChangeRequest::decode_v0(&mut cur).unwrap();
652 check!(
653 <serde_wincode::SerdeCompat<Vec<krabka_metadata::MetadataRecord>> as wincode::Deserialize>::deserialize(&frame.records).unwrap()
654 == records
655 );
656 let mut cur: &[u8] = &delegation_golden;
657 let frame = KrabkaSubmitChangeRequest::decode_v0(&mut cur).unwrap();
658 check!(
659 <serde_wincode::SerdeCompat<Vec<crate::DelegationTokenMutation>> as wincode::Deserialize>::deserialize(&frame.records).unwrap()
660 == mutations
661 );
662 check!(
663 <serde_wincode::SerdeCompat<crate::SubmitChangeResult> as wincode::Deserialize>::deserialize(SUBMIT_CHANGE_RESULT_V0_GOLDEN).unwrap()
664 == result
665 );
666 }
667
668 #[yare::parameterized(
669 applied = { 0, -1, API_KEY_SUBMIT_CHANGE, 0, SubmitResponseExpectation::Applied },
670 topic_exists = { 2, -1, API_KEY_SUBMIT_CHANGE, 0, SubmitResponseExpectation::TopicExists },
671 uncommitted_tail = {
672 SUBMIT_CHANGE_UNCOMMITTED_TAIL,
673 -1,
674 API_KEY_SUBMIT_CHANGE,
675 0,
676 SubmitResponseExpectation::UncommittedTail,
677 },
678 cluster_authorization = {
679 PRIVATE_CLUSTER_AUTHORIZATION_FAILED,
680 -1,
681 API_KEY_SUBMIT_CHANGE,
682 0,
683 SubmitResponseExpectation::ClusterAuthorizationFailed,
684 },
685 submit_unsupported_version = {
686 PRIVATE_UNSUPPORTED_VERSION,
687 -1,
688 API_KEY_SUBMIT_CHANGE,
689 2,
690 SubmitResponseExpectation::UnsupportedVersion,
691 },
692 token_unsupported_version = {
693 PRIVATE_UNSUPPORTED_VERSION,
694 -1,
695 API_KEY_DELEGATION_TOKEN_MUTATION,
696 2,
697 SubmitResponseExpectation::UnsupportedVersion,
698 },
699 not_leader_with_hint = {
700 1,
701 9,
702 API_KEY_SUBMIT_CHANGE,
703 0,
704 SubmitResponseExpectation::NotLeaderWithHint,
705 },
706 not_leader_without_hint = {
707 3,
708 -1,
709 API_KEY_SUBMIT_CHANGE,
710 0,
711 SubmitResponseExpectation::NotLeaderWithoutHint,
712 },
713 )]
714 fn translate_submit_change_response_maps_each_error_code(
715 code: i16,
716 leader_hint: i64,
717 api_key: i16,
718 version: i16,
719 expected: SubmitResponseExpectation,
720 ) {
721 let result = translate_submit_change_response(
722 &submit_change_response_bytes(code, leader_hint),
723 NodeId(5),
724 api_key,
725 version,
726 );
727 match expected {
728 SubmitResponseExpectation::Applied => assert2::assert!(result.is_ok()),
729 expected => {
730 let error = result.expect_err("response is an error");
731 let matches = match expected {
732 SubmitResponseExpectation::Applied => unreachable!(),
733 SubmitResponseExpectation::TopicExists => matches!(
734 error,
735 RaftError::Metadata(krabka_metadata::MetadataError::TopicExists(_))
736 ),
737 SubmitResponseExpectation::UncommittedTail => {
738 matches!(error, RaftError::UncommittedTail)
739 }
740 SubmitResponseExpectation::ClusterAuthorizationFailed => {
741 matches!(error, RaftError::ClusterAuthorizationFailed)
742 }
743 SubmitResponseExpectation::UnsupportedVersion => matches!(
744 error,
745 RaftError::UnsupportedPrivateVersion {
746 api_key: got,
747 version: got_version
748 } if got == api_key && got_version == version
749 ),
750 SubmitResponseExpectation::NotLeaderWithHint => {
751 matches!(error, RaftError::NotLeader {
752 current_leader: Some(NodeId(9))
753 })
754 }
755 SubmitResponseExpectation::NotLeaderWithoutHint => {
756 matches!(error, RaftError::NotLeader {
757 current_leader: None
758 })
759 }
760 };
761 assert2::assert!(matches);
762 }
763 }
764 }
765
766 #[test]
767 fn translate_submit_change_response_propagates_decode_error() {
768 let err = translate_submit_change_response(&[0u8; 3], NodeId(5), API_KEY_SUBMIT_CHANGE, 0)
771 .expect_err("truncated decodes err");
772 assert2::assert!(matches!(err, RaftError::Protocol(_)));
773 }
774
775 #[tokio::test]
776 async fn forward_submit_via_sends_encoded_body_and_returns_ok_on_applied() {
777 let records = vec![topic_record("gamma")];
781 let expected_body = encode_submit_change_body(&records).expect("encode");
782
783 let mut transport = MockSubmitChangeTransport::new();
784 transport
785 .expect_send_submit_change()
786 .withf(move |leader, addr, version, body| {
787 *leader == 7
788 && addr == "leader-host:9093"
789 && *version == 0
790 && body == &expected_body
791 })
792 .times(1)
793 .returning(|_, _, _, _| Ok(submit_change_response_bytes(0, -1)));
794
795 forward_submit_via(&transport, NodeId(7), "leader-host:9093", 0, &records)
796 .await
797 .expect("applied");
798 }
799
800 #[tokio::test]
801 async fn forward_submit_via_translates_not_leader_hint() {
802 let mut transport = MockSubmitChangeTransport::new();
805 transport
806 .expect_send_submit_change()
807 .returning(|_, _, _, _| Ok(submit_change_response_bytes(1, 4)));
808
809 let err = forward_submit_via(&transport, NodeId(7), "leader-host:9093", 0, &[
810 topic_record("z"),
811 ])
812 .await
813 .expect_err("not leader");
814 assert2::assert!(matches!(err, RaftError::NotLeader {
815 current_leader: Some(NodeId(4))
816 }));
817 }
818
819 #[tokio::test]
820 async fn forward_submit_via_maps_transport_error_to_network() {
821 let mut transport = MockSubmitChangeTransport::new();
824 transport
825 .expect_send_submit_change()
826 .returning(|_, _, _, _| {
827 Err(krabka_client_core::ClientError::Io(Error::new(
828 ErrorKind::ConnectionRefused,
829 "refused",
830 )))
831 });
832
833 let err = forward_submit_via(&transport, NodeId(7), "leader-host:9093", 0, &[
834 topic_record("z"),
835 ])
836 .await
837 .expect_err("network error");
838 assert2::assert!(matches!(err, RaftError::Network(_)));
839 }
840
841 #[test]
842 fn encode_delegation_token_mutation_body_frames_mutations() {
843 use {krabka_metadata::DelegationTokenRecord, krabka_security::KafkaPrincipal};
844
845 let rec = DelegationTokenRecord {
846 token_id: "tok".into(),
847 owner: KafkaPrincipal {
848 principal_type: "User".into(),
849 name: "alice".into(),
850 },
851 requester: KafkaPrincipal {
852 principal_type: "User".into(),
853 name: "alice".into(),
854 },
855 issue_timestamp_ms: 10,
856 max_timestamp_ms: 20,
857 expiry_timestamp_ms: 15,
858 renewers: vec![],
859 };
860 let mutations = vec![crate::DelegationTokenMutation::Delete { expected: rec }];
861 let body = encode_delegation_token_mutation_body(&mutations).expect("encode");
862 assert2::assert!(body.len() > 10);
863 let mut cur: &[u8] = &body;
864 let req = KrabkaSubmitChangeRequest::decode_v0(&mut cur).expect("decode frame");
865 let decoded = <serde_wincode::SerdeCompat<
866 Vec<DelegationTokenMutation>,
867 > as wincode::Deserialize>::deserialize(&req.records)
868 .expect("wincode decode");
869 assert2::assert!(decoded == mutations);
870 }
871
872 #[tokio::test]
873 async fn controller_handle_voter_addr_resolves_known_voter() {
874 let (_dir, ctrl) = bootstrap_controller("start").await;
875 let addr = ctrl.voter_addr(NodeId(1));
876 assert2::assert!(addr == Some("127.0.0.1:0".to_owned()));
877 assert2::assert!(ctrl.voter_addr(NodeId(999)).is_none());
878 ctrl.shutdown().await;
879 }
880
881 #[tokio::test]
882 async fn forward_raw_with_no_known_leader_rejects_not_leader() {
883 let (_dir, ctrl) = joining_controller("join start").await;
884
885 let err = ctrl
886 .forward_raw(API_KEY_METADATA_FETCH, 0, bytes::Bytes::new())
887 .await
888 .expect_err("no leader is known yet");
889 assert2::assert!(matches!(err, RaftError::NotLeader {
890 current_leader: None
891 }));
892 ctrl.shutdown().await;
893 }
894
895 #[tokio::test]
896 async fn submit_delegation_token_mutations_on_join_node_rejects_not_leader() {
897 let (_dir, ctrl) = joining_controller("join start").await;
898 let res = ctrl.submit_delegation_token_mutations(vec![]).await;
899 assert2::assert!(matches!(
900 res,
901 Err(RaftError::NotLeader {
902 current_leader: None
903 })
904 ));
905 ctrl.shutdown().await;
906 }
907}