1use std::io::{BufRead, BufReader, Write};
37use std::net::{SocketAddr, TcpListener, TcpStream};
38use std::sync::Arc;
39use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
40use std::thread::JoinHandle;
41use std::time::Duration;
42
43use crate::ack_policy::AckPolicy;
44use crate::promotion::PromotionRequest;
45
46use ed25519_dalek::{Verifier, VerifyingKey};
47use tracing::{debug, error, info, warn};
48
49use melin_app::auth::{AuthorizedKeys, Permission};
50use melin_wire_protocol::control::TransportResponse;
51use melin_wire_protocol::control_codec;
52
53pub fn spawn(
69 bind_addr: SocketAddr,
70 promote: Option<PromotionRequest>,
71 rotate_requested: Option<Arc<AtomicBool>>,
72 ack_policy: Option<Arc<AtomicU8>>,
73 shutdown: Arc<AtomicBool>,
74 authorized_keys: Arc<AuthorizedKeys>,
75) -> Result<(JoinHandle<()>, SocketAddr), Box<dyn std::error::Error>> {
76 let listener = TcpListener::bind(bind_addr)
77 .map_err(|e| format!("failed to bind admin listener on {bind_addr}: {e}"))?;
78 listener
79 .set_nonblocking(true)
80 .map_err(|e| format!("failed to set non-blocking on admin listener: {e}"))?;
81 let addr = listener
82 .local_addr()
83 .map_err(|e| format!("failed to read admin listener address: {e}"))?;
84 let handle = std::thread::Builder::new()
85 .name("admin-listener".into())
86 .spawn(move || {
87 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
88 run(
89 listener,
90 addr,
91 promote.as_ref(),
92 rotate_requested.as_deref(),
93 ack_policy.as_deref(),
94 &shutdown,
95 &authorized_keys,
96 )
97 }));
98 if let Err(panic) = result {
99 let msg = panic_message(&panic);
100 error!(addr = %addr, panic = %msg, "admin listener thread panicked");
101 }
102 })
103 .map_err(|e| format!("failed to spawn admin listener thread: {e}"))?;
104 Ok((handle, addr))
105}
106
107fn panic_message(payload: &Box<dyn std::any::Any + Send>) -> String {
108 if let Some(s) = payload.downcast_ref::<&'static str>() {
109 (*s).to_string()
110 } else if let Some(s) = payload.downcast_ref::<String>() {
111 s.clone()
112 } else {
113 "<non-string panic payload>".to_string()
114 }
115}
116
117fn run(
120 listener: TcpListener,
121 addr: SocketAddr,
122 promote: Option<&PromotionRequest>,
123 rotate_requested: Option<&AtomicBool>,
124 ack_policy: Option<&AtomicU8>,
125 shutdown: &AtomicBool,
126 authorized_keys: &AuthorizedKeys,
127) {
128 info!(
129 addr = %addr,
130 promote_enabled = promote.is_some(),
131 rotate_enabled = rotate_requested.is_some(),
132 ack_policy_enabled = ack_policy.is_some(),
133 "admin listener started"
134 );
135
136 loop {
137 if shutdown.load(Ordering::Relaxed) {
138 return;
139 }
140
141 match listener.accept() {
142 Ok((stream, peer)) => {
143 debug!(peer = %peer, "admin connection accepted");
144 handle_connection(
145 stream,
146 promote,
147 rotate_requested,
148 ack_policy,
149 authorized_keys,
150 );
151 }
152 Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {
153 std::thread::sleep(Duration::from_millis(100));
154 }
155 Err(e) => {
156 debug!(error = %e, "admin listener accept error");
157 std::thread::sleep(Duration::from_millis(100));
158 }
159 }
160 }
161}
162
163fn authenticate(stream: &mut TcpStream, authorized_keys: &AuthorizedKeys) -> Result<(), String> {
167 let mut nonce = [0u8; 32];
168 getrandom::fill(&mut nonce).map_err(|e| format!("getrandom failed: {e}"))?;
169
170 let mut buf = [0u8; 128];
171 let written =
172 control_codec::encode_transport_response(&TransportResponse::Challenge { nonce }, &mut buf)
173 .map_err(|e| format!("encode Challenge: {e}"))?;
174 stream
175 .write_all(&buf[..written])
176 .map_err(|e| format!("send Challenge: {e}"))?;
177 stream
178 .flush()
179 .map_err(|e| format!("flush Challenge: {e}"))?;
180
181 let mut len_buf = [0u8; 4];
182 std::io::Read::read_exact(stream, &mut len_buf)
183 .map_err(|e| format!("read auth frame length: {e}"))?;
184 let frame_len = u32::from_le_bytes(len_buf) as usize;
185 if frame_len > 256 {
186 send_auth_failed(stream);
187 return Err(format!("auth frame too large: {frame_len}"));
188 }
189 let mut frame_buf = [0u8; 256];
190 std::io::Read::read_exact(stream, &mut frame_buf[..frame_len])
191 .map_err(|e| format!("read auth frame payload: {e}"))?;
192
193 let cr = match control_codec::decode_challenge_response(&frame_buf[..frame_len]) {
194 Ok(cr) => cr,
195 Err(e) => {
196 send_auth_failed(stream);
197 return Err(format!("decode ChallengeResponse: {e}"));
198 }
199 };
200
201 let (signature_bytes, public_key_bytes) = (cr.signature, cr.public_key);
202
203 let permission = match authorized_keys.lookup(&public_key_bytes) {
204 Some(perm) => perm,
205 None => {
206 send_auth_failed(stream);
207 return Err("unknown public key".into());
208 }
209 };
210 if permission != Permission::Operator {
211 send_auth_failed(stream);
212 return Err(format!(
213 "admin endpoint requires operator key, got {permission:?}"
214 ));
215 }
216
217 let verifying_key = VerifyingKey::from_bytes(&public_key_bytes).map_err(|e| {
218 send_auth_failed(stream);
219 format!("invalid public key: {e}")
220 })?;
221 let signature = ed25519_dalek::Signature::from_bytes(&signature_bytes);
222 verifying_key.verify(&nonce, &signature).map_err(|e| {
223 send_auth_failed(stream);
224 format!("signature verification failed: {e}")
225 })?;
226
227 let written =
228 control_codec::encode_transport_response(&TransportResponse::ServerReady, &mut buf)
229 .map_err(|e| format!("encode ServerReady: {e}"))?;
230 stream
231 .write_all(&buf[..written])
232 .map_err(|e| format!("send ServerReady: {e}"))?;
233 stream
234 .flush()
235 .map_err(|e| format!("flush ServerReady: {e}"))?;
236
237 Ok(())
238}
239
240fn send_auth_failed(stream: &mut TcpStream) {
241 let mut buf = [0u8; 8];
242 if let Ok(written) =
243 control_codec::encode_transport_response(&TransportResponse::AuthFailed, &mut buf)
244 {
245 send_best_effort(stream, &buf[..written]);
249 }
250}
251
252fn send_best_effort(stream: &mut TcpStream, payload: &[u8]) {
258 if let Err(e) = stream.write_all(payload) {
259 debug!(error = %e, "admin write failed");
260 return;
261 }
262 if let Err(e) = stream.flush() {
263 debug!(error = %e, "admin flush failed");
264 }
265}
266
267fn handle_connection(
270 mut stream: TcpStream,
271 promote: Option<&PromotionRequest>,
272 rotate_requested: Option<&AtomicBool>,
273 ack_policy: Option<&AtomicU8>,
274 authorized_keys: &AuthorizedKeys,
275) {
276 stream.set_read_timeout(Some(Duration::from_secs(5))).ok();
277 stream.set_write_timeout(Some(Duration::from_secs(5))).ok();
278
279 if let Err(reason) = authenticate(&mut stream, authorized_keys) {
280 debug!(reason = %reason, "admin auth failed");
281 return;
282 }
283
284 let cloned = match stream.try_clone() {
285 Ok(s) => s,
286 Err(e) => {
287 debug!(error = %e, "failed to clone admin stream");
288 return;
289 }
290 };
291 let mut reader = BufReader::new(cloned);
292
293 let mut line = String::new();
294 if reader.read_line(&mut line).is_err() {
295 debug!("failed to read from admin connection");
296 return;
297 }
298
299 let trimmed = line.trim();
300 match trimmed {
301 "PROMOTE" => match promote {
302 Some(request) => {
303 if request.request(PromotionRequest::MANUAL) {
307 info!("promotion triggered by operator");
308 } else {
309 debug!("PROMOTE received but a promotion is already in flight");
313 }
314 send_best_effort(&mut stream, b"OK\n");
315 }
316 None => {
317 send_best_effort(&mut stream, b"ERR PROMOTE not available on this node\n");
318 debug!("rejected PROMOTE — flag not wired (primary node?)");
319 }
320 },
321 "ROTATE" => match rotate_requested {
322 Some(flag) => {
323 flag.store(true, Ordering::Release);
324 send_best_effort(&mut stream, b"OK\n");
325 info!("rotation requested by operator");
326 }
327 None => {
328 send_best_effort(&mut stream, b"ERR ROTATE not available on this node\n");
329 debug!("rejected ROTATE — flag not wired");
330 }
331 },
332 cmd if cmd.starts_with("ACK-POLICY") => {
333 let mut parts = cmd.splitn(2, char::is_whitespace);
340 let _ = parts.next();
342 let arg = parts.next().map(str::trim).unwrap_or("");
343 handle_ack_policy(&mut stream, ack_policy, arg);
344 }
345 cmd if cmd.starts_with("DURABILITY") => {
346 let mut parts = cmd.splitn(2, char::is_whitespace);
353 let _ = parts.next();
354 let arg = parts.next().map(str::trim).unwrap_or("");
355 let mapped = AckPolicy::parse_legacy(arg).map(AckPolicy::as_str);
356 warn!(
357 received = %cmd,
358 "DURABILITY is deprecated and will be removed in the next minor release; use ACK-POLICY {}",
359 mapped.unwrap_or("<disk|ram|disk+ram|two-disks>")
360 );
361 handle_ack_policy(&mut stream, ack_policy, mapped.unwrap_or(arg));
362 }
363 other => {
364 debug!(received = %other, "unknown admin command");
365 send_best_effort(&mut stream, b"ERR unknown command\n");
366 }
367 }
368}
369
370fn handle_ack_policy(stream: &mut TcpStream, ack_policy: Option<&AtomicU8>, arg: &str) {
377 let Some(atomic) = ack_policy else {
378 send_best_effort(stream, b"ERR ACK-POLICY not available on this node\n");
379 debug!("rejected ACK-POLICY — atomic not wired (replica node?)");
380 return;
381 };
382 if arg.is_empty() {
383 send_best_effort(
384 stream,
385 b"ERR ACK-POLICY requires a policy (disk|ram|disk+ram|two-disks)\n",
386 );
387 debug!("rejected ACK-POLICY — missing argument");
388 return;
389 }
390 let Some(next) = AckPolicy::parse(arg) else {
391 let mut buf = [0u8; 128];
396 let msg = format_unknown_policy(&mut buf, arg);
397 send_best_effort(stream, msg);
398 debug!(received = %arg, "rejected ACK-POLICY — unknown policy");
399 return;
400 };
401 let prev_byte = atomic.swap(next.as_u8(), Ordering::Relaxed);
406 let prev = AckPolicy::from_u8(prev_byte)
407 .map(|p| p.as_str())
408 .unwrap_or("<corrupted>");
409 send_best_effort(stream, b"OK\n");
410 info!(
411 prev = prev,
412 next = next.as_str(),
413 "ack policy changed by operator"
414 );
415}
416
417fn format_unknown_policy<'a>(buf: &'a mut [u8], arg: &str) -> &'a [u8] {
422 use std::io::Write as _;
423 let mut cursor = std::io::Cursor::new(&mut buf[..]);
424 let _ = writeln!(
428 cursor,
429 "ERR ACK-POLICY unknown policy `{arg}` (expected disk|ram|disk+ram|two-disks)"
430 );
431 let n = cursor.position() as usize;
432 &cursor.into_inner()[..n]
433}
434
435#[cfg(test)]
436mod tests {
437 use super::*;
438 use std::io::{BufRead, BufReader, Read, Write};
439
440 use ed25519_dalek::{Signer, SigningKey};
441 use melin_wire_protocol::control_codec::{
442 TAG_AUTH_FAILED, TAG_CHALLENGE, TAG_CHALLENGE_RESPONSE, TAG_SERVER_READY,
443 };
444
445 fn operator_keys() -> (SigningKey, Arc<AuthorizedKeys>) {
446 let signing_key = SigningKey::from_bytes(&[0xAD; 32]);
447 let pub_b64 = base64::Engine::encode(
448 &base64::engine::general_purpose::STANDARD,
449 signing_key.verifying_key().to_bytes(),
450 );
451 let content = format!("operator {pub_b64} test-ops\n");
452 let keys = AuthorizedKeys::parse(&content).expect("parse authorized_keys");
453 (signing_key, Arc::new(keys))
454 }
455
456 fn trader_keys() -> (SigningKey, Arc<AuthorizedKeys>) {
457 let signing_key = SigningKey::from_bytes(&[0xBD; 32]);
458 let pub_b64 = base64::Engine::encode(
459 &base64::engine::general_purpose::STANDARD,
460 signing_key.verifying_key().to_bytes(),
461 );
462 let content = format!("trader {pub_b64} test-trader\n");
463 let keys = AuthorizedKeys::parse(&content).expect("parse authorized_keys");
464 (signing_key, Arc::new(keys))
465 }
466
467 fn client_authenticate(stream: &mut TcpStream, key: &SigningKey) -> u8 {
472 let mut len_buf = [0u8; 4];
474 stream.read_exact(&mut len_buf).expect("read challenge len");
475 let frame_len = u32::from_le_bytes(len_buf) as usize;
476 let mut frame_buf = vec![0u8; frame_len];
477 stream
478 .read_exact(&mut frame_buf)
479 .expect("read challenge payload");
480 assert_eq!(frame_buf[0], TAG_CHALLENGE, "expected Challenge");
481 let nonce = &frame_buf[1..33];
482
483 let signature = key.sign(nonce);
486 let mut frame = Vec::with_capacity(97);
487 frame.push(TAG_CHALLENGE_RESPONSE);
488 frame.extend_from_slice(&signature.to_bytes());
489 frame.extend_from_slice(&key.verifying_key().to_bytes());
490 stream
491 .write_all(&(frame.len() as u32).to_le_bytes())
492 .expect("send result len");
493 stream.write_all(&frame).expect("send");
494 stream.flush().expect("flush");
495
496 let mut len_buf2 = [0u8; 4];
498 stream.read_exact(&mut len_buf2).expect("read result len");
499 let result_len = u32::from_le_bytes(len_buf2) as usize;
500 let mut result_buf = vec![0u8; result_len];
501 stream
502 .read_exact(&mut result_buf)
503 .expect("read result payload");
504 result_buf[0]
505 }
506
507 fn send_command(addr: SocketAddr, key: &SigningKey, command: &[u8]) -> String {
510 let mut stream = TcpStream::connect(addr).unwrap();
511 assert_eq!(client_authenticate(&mut stream, key), TAG_SERVER_READY);
512 stream.write_all(command).unwrap();
513 stream.flush().unwrap();
514 let mut reader = BufReader::new(stream);
515 let mut line = String::new();
516 reader.read_line(&mut line).unwrap();
517 line.trim().to_string()
518 }
519
520 #[test]
525 fn spawn_errs_when_port_taken() {
526 let (_key, auth_keys) = operator_keys();
527 let holder = TcpListener::bind("127.0.0.1:0").unwrap();
528 let addr = holder.local_addr().unwrap();
529 let shutdown = Arc::new(AtomicBool::new(false));
530 let err = spawn(addr, None, None, None, shutdown, auth_keys)
531 .expect_err("bind on a taken port must fail");
532 assert!(
533 err.to_string().contains("failed to bind admin listener"),
534 "{err}"
535 );
536 }
537
538 #[test]
539 fn promote_command_sets_flag_when_wired() {
540 let (key, auth_keys) = operator_keys();
541 let promote = PromotionRequest::new();
542 let rotate = Arc::new(AtomicBool::new(false));
543 let shutdown = Arc::new(AtomicBool::new(false));
544 let (_h, addr) = spawn(
545 "127.0.0.1:0".parse().unwrap(),
546 Some(promote.clone()),
547 Some(Arc::clone(&rotate)),
548 None,
549 Arc::clone(&shutdown),
550 auth_keys,
551 )
552 .expect("spawn admin listener");
553
554 assert_eq!(send_command(addr, &key, b"PROMOTE\n"), "OK");
555 assert_eq!(promote.pending(), Some(PromotionRequest::MANUAL));
556 assert!(!rotate.load(Ordering::Acquire));
557
558 shutdown.store(true, Ordering::Release);
559 }
560
561 #[test]
562 fn rotate_command_sets_flag_when_wired() {
563 let (key, auth_keys) = operator_keys();
564 let promote = PromotionRequest::new();
565 let rotate = Arc::new(AtomicBool::new(false));
566 let shutdown = Arc::new(AtomicBool::new(false));
567 let (_h, addr) = spawn(
568 "127.0.0.1:0".parse().unwrap(),
569 Some(promote.clone()),
570 Some(Arc::clone(&rotate)),
571 None,
572 Arc::clone(&shutdown),
573 auth_keys,
574 )
575 .expect("spawn admin listener");
576
577 assert_eq!(send_command(addr, &key, b"ROTATE\n"), "OK");
578 assert!(rotate.load(Ordering::Acquire));
579 assert!(!promote.is_requested());
580
581 shutdown.store(true, Ordering::Release);
582 }
583
584 #[test]
587 fn promote_rejected_when_not_wired() {
588 let (key, auth_keys) = operator_keys();
589 let rotate = Arc::new(AtomicBool::new(false));
590 let shutdown = Arc::new(AtomicBool::new(false));
591 let (_h, addr) = spawn(
592 "127.0.0.1:0".parse().unwrap(),
593 None,
594 Some(Arc::clone(&rotate)),
595 None,
596 Arc::clone(&shutdown),
597 auth_keys,
598 )
599 .expect("spawn admin listener");
600
601 let resp = send_command(addr, &key, b"PROMOTE\n");
602 assert!(resp.starts_with("ERR"), "expected ERR, got {resp}");
603 assert!(!rotate.load(Ordering::Acquire));
604
605 shutdown.store(true, Ordering::Release);
606 }
607
608 #[test]
610 fn rotate_rejected_when_not_wired() {
611 let (key, auth_keys) = operator_keys();
612 let promote = PromotionRequest::new();
613 let shutdown = Arc::new(AtomicBool::new(false));
614 let (_h, addr) = spawn(
615 "127.0.0.1:0".parse().unwrap(),
616 Some(promote.clone()),
617 None,
618 None,
619 Arc::clone(&shutdown),
620 auth_keys,
621 )
622 .expect("spawn admin listener");
623
624 let resp = send_command(addr, &key, b"ROTATE\n");
625 assert!(resp.starts_with("ERR"), "expected ERR, got {resp}");
626 assert!(!promote.is_requested());
627
628 shutdown.store(true, Ordering::Release);
629 }
630
631 #[test]
635 fn listener_handles_multiple_commands() {
636 let (key, auth_keys) = operator_keys();
637 let promote = PromotionRequest::new();
638 let rotate = Arc::new(AtomicBool::new(false));
639 let shutdown = Arc::new(AtomicBool::new(false));
640 let (_h, addr) = spawn(
641 "127.0.0.1:0".parse().unwrap(),
642 Some(promote.clone()),
643 Some(Arc::clone(&rotate)),
644 None,
645 Arc::clone(&shutdown),
646 auth_keys,
647 )
648 .expect("spawn admin listener");
649
650 for _ in 0..3 {
653 assert_eq!(send_command(addr, &key, b"ROTATE\n"), "OK");
654 assert!(rotate.load(Ordering::Acquire));
655 rotate
656 .compare_exchange(true, false, Ordering::AcqRel, Ordering::Relaxed)
657 .expect("flag should still be set");
658 std::thread::sleep(Duration::from_millis(100));
659 }
660
661 assert_eq!(send_command(addr, &key, b"PROMOTE\n"), "OK");
663 assert_eq!(promote.pending(), Some(PromotionRequest::MANUAL));
664
665 shutdown.store(true, Ordering::Release);
666 }
667
668 #[test]
669 fn unknown_command_rejected() {
670 let (key, auth_keys) = operator_keys();
671 let promote = PromotionRequest::new();
672 let rotate = Arc::new(AtomicBool::new(false));
673 let shutdown = Arc::new(AtomicBool::new(false));
674 let (_h, addr) = spawn(
675 "127.0.0.1:0".parse().unwrap(),
676 Some(promote.clone()),
677 Some(Arc::clone(&rotate)),
678 None,
679 Arc::clone(&shutdown),
680 auth_keys,
681 )
682 .expect("spawn admin listener");
683
684 let resp = send_command(addr, &key, b"INVALID\n");
685 assert!(resp.starts_with("ERR"), "expected ERR, got {resp}");
686 assert!(!promote.is_requested());
687 assert!(!rotate.load(Ordering::Acquire));
688
689 shutdown.store(true, Ordering::Release);
690 }
691
692 #[test]
693 fn non_operator_key_rejected() {
694 let (trader_key, auth_keys) = trader_keys();
695 let promote = PromotionRequest::new();
696 let rotate = Arc::new(AtomicBool::new(false));
697 let shutdown = Arc::new(AtomicBool::new(false));
698 let (_h, addr) = spawn(
699 "127.0.0.1:0".parse().unwrap(),
700 Some(promote.clone()),
701 Some(Arc::clone(&rotate)),
702 None,
703 Arc::clone(&shutdown),
704 auth_keys,
705 )
706 .expect("spawn admin listener");
707
708 let mut stream = TcpStream::connect(addr).unwrap();
709 let result = client_authenticate(&mut stream, &trader_key);
710 assert_eq!(result, TAG_AUTH_FAILED);
711 assert!(!promote.is_requested());
712 assert!(!rotate.load(Ordering::Acquire));
713
714 shutdown.store(true, Ordering::Release);
715 }
716
717 fn run_ack_policy(initial: AckPolicy, cmd: &[u8]) -> (String, Option<AckPolicy>) {
722 let (key, auth_keys) = operator_keys();
723 let policy = Arc::new(AtomicU8::new(initial.as_u8()));
724 let shutdown = Arc::new(AtomicBool::new(false));
725 let (_h, addr) = spawn(
726 "127.0.0.1:0".parse().unwrap(),
727 None,
728 None,
729 Some(Arc::clone(&policy)),
730 Arc::clone(&shutdown),
731 auth_keys,
732 )
733 .expect("spawn admin listener");
734
735 let resp = send_command(addr, &key, cmd);
736 let after = AckPolicy::from_u8(policy.load(Ordering::Relaxed));
737 shutdown.store(true, Ordering::Release);
738 (resp, after)
739 }
740
741 #[test]
742 fn ack_policy_command_swaps_policy() {
743 let (resp, after) = run_ack_policy(AckPolicy::DiskAndRam, b"ACK-POLICY disk\n");
744 assert_eq!(resp, "OK");
745 assert_eq!(after, Some(AckPolicy::Disk));
746 }
747
748 #[test]
749 fn deprecated_durability_alias_maps_old_names() {
750 let (resp, after) = run_ack_policy(AckPolicy::DiskAndRam, b"DURABILITY local\n");
753 assert_eq!(resp, "OK");
754 assert_eq!(after, Some(AckPolicy::Disk));
755
756 let (resp, after) = run_ack_policy(AckPolicy::Disk, b"DURABILITY durably-replicated\n");
757 assert_eq!(resp, "OK");
758 assert_eq!(after, Some(AckPolicy::TwoDisks));
759
760 let (resp, after) = run_ack_policy(AckPolicy::Disk, b"DURABILITY ram\n");
762 assert_eq!(resp, "OK");
763 assert_eq!(after, Some(AckPolicy::Ram));
764 let (resp, after) = run_ack_policy(AckPolicy::Disk, b"DURABILITY fast\n");
765 assert!(resp.starts_with("ERR ACK-POLICY unknown policy"), "{resp}");
766 assert_eq!(after, Some(AckPolicy::Disk));
767 }
768
769 #[test]
770 fn ack_policy_command_accepts_each_policy() {
771 for target in [
772 AckPolicy::Disk,
773 AckPolicy::Ram,
774 AckPolicy::DiskAndRam,
775 AckPolicy::TwoDisks,
776 ] {
777 let cmd = format!("ACK-POLICY {}\n", target.as_str());
778 let (resp, after) = run_ack_policy(AckPolicy::Disk, cmd.as_bytes());
779 assert_eq!(resp, "OK", "policy {target}");
780 assert_eq!(after, Some(target));
781 }
782 }
783
784 #[test]
785 fn ack_policy_command_rejects_unknown_policy() {
786 let (resp, after) = run_ack_policy(AckPolicy::DiskAndRam, b"ACK-POLICY fast\n");
787 assert!(
788 resp.starts_with("ERR ACK-POLICY unknown policy"),
789 "expected unknown-policy ERR, got {resp}"
790 );
791 assert_eq!(after, Some(AckPolicy::DiskAndRam));
793 }
794
795 #[test]
796 fn ack_policy_command_rejects_missing_argument() {
797 let (resp, after) = run_ack_policy(AckPolicy::DiskAndRam, b"ACK-POLICY\n");
798 assert!(
799 resp.starts_with("ERR ACK-POLICY requires a policy"),
800 "expected missing-arg ERR, got {resp}"
801 );
802 assert_eq!(after, Some(AckPolicy::DiskAndRam));
803 }
804
805 #[test]
806 fn ack_policy_command_rejected_when_not_wired() {
807 let (key, auth_keys) = operator_keys();
810 let promote = PromotionRequest::new();
811 let shutdown = Arc::new(AtomicBool::new(false));
812 let (_h, addr) = spawn(
813 "127.0.0.1:0".parse().unwrap(),
814 Some(promote.clone()),
815 None,
816 None,
817 Arc::clone(&shutdown),
818 auth_keys,
819 )
820 .expect("spawn admin listener");
821
822 let resp = send_command(addr, &key, b"ACK-POLICY disk\n");
823 assert!(
824 resp.starts_with("ERR ACK-POLICY not available"),
825 "expected not-available ERR, got {resp}"
826 );
827
828 shutdown.store(true, Ordering::Release);
829 }
830}