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;
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 role = match authorized_keys.lookup(&public_key_bytes) {
204 Some(role) => role,
205 None => {
206 send_auth_failed(stream);
207 return Err("unknown public key".into());
208 }
209 };
210 if !role.client().is_some_and(|role| role.is_operator()) {
211 send_auth_failed(stream);
212 return Err(format!(
213 "admin endpoint requires an operator key, got a {} key",
214 authorized_keys.token(role)
215 ));
216 }
217
218 let verifying_key = VerifyingKey::from_bytes(&public_key_bytes).map_err(|e| {
219 send_auth_failed(stream);
220 format!("invalid public key: {e}")
221 })?;
222 let signature = ed25519_dalek::Signature::from_bytes(&signature_bytes);
223 verifying_key.verify(&nonce, &signature).map_err(|e| {
224 send_auth_failed(stream);
225 format!("signature verification failed: {e}")
226 })?;
227
228 let written =
229 control_codec::encode_transport_response(&TransportResponse::ServerReady, &mut buf)
230 .map_err(|e| format!("encode ServerReady: {e}"))?;
231 stream
232 .write_all(&buf[..written])
233 .map_err(|e| format!("send ServerReady: {e}"))?;
234 stream
235 .flush()
236 .map_err(|e| format!("flush ServerReady: {e}"))?;
237
238 Ok(())
239}
240
241fn send_auth_failed(stream: &mut TcpStream) {
242 let mut buf = [0u8; 8];
243 if let Ok(written) =
244 control_codec::encode_transport_response(&TransportResponse::AuthFailed, &mut buf)
245 {
246 send_best_effort(stream, &buf[..written]);
250 }
251}
252
253fn send_best_effort(stream: &mut TcpStream, payload: &[u8]) {
259 if let Err(e) = stream.write_all(payload) {
260 debug!(error = %e, "admin write failed");
261 return;
262 }
263 if let Err(e) = stream.flush() {
264 debug!(error = %e, "admin flush failed");
265 }
266}
267
268fn handle_connection(
271 mut stream: TcpStream,
272 promote: Option<&PromotionRequest>,
273 rotate_requested: Option<&AtomicBool>,
274 ack_policy: Option<&AtomicU8>,
275 authorized_keys: &AuthorizedKeys,
276) {
277 stream.set_read_timeout(Some(Duration::from_secs(5))).ok();
278 stream.set_write_timeout(Some(Duration::from_secs(5))).ok();
279
280 if let Err(reason) = authenticate(&mut stream, authorized_keys) {
281 debug!(reason = %reason, "admin auth failed");
282 return;
283 }
284
285 let cloned = match stream.try_clone() {
286 Ok(s) => s,
287 Err(e) => {
288 debug!(error = %e, "failed to clone admin stream");
289 return;
290 }
291 };
292 let mut reader = BufReader::new(cloned);
293
294 let mut line = String::new();
295 if reader.read_line(&mut line).is_err() {
296 debug!("failed to read from admin connection");
297 return;
298 }
299
300 let trimmed = line.trim();
301 match trimmed {
302 "PROMOTE" => match promote {
303 Some(request) => {
304 if request.request(PromotionRequest::MANUAL) {
308 info!("promotion triggered by operator");
309 } else {
310 debug!("PROMOTE received but a promotion is already in flight");
314 }
315 send_best_effort(&mut stream, b"OK\n");
316 }
317 None => {
318 send_best_effort(&mut stream, b"ERR PROMOTE not available on this node\n");
319 debug!("rejected PROMOTE — flag not wired (primary node?)");
320 }
321 },
322 "ROTATE" => match rotate_requested {
323 Some(flag) => {
324 flag.store(true, Ordering::Release);
325 send_best_effort(&mut stream, b"OK\n");
326 info!("rotation requested by operator");
327 }
328 None => {
329 send_best_effort(&mut stream, b"ERR ROTATE not available on this node\n");
330 debug!("rejected ROTATE — flag not wired");
331 }
332 },
333 cmd if cmd.starts_with("ACK-POLICY") => {
334 let mut parts = cmd.splitn(2, char::is_whitespace);
341 let _ = parts.next();
343 let arg = parts.next().map(str::trim).unwrap_or("");
344 handle_ack_policy(&mut stream, ack_policy, arg);
345 }
346 cmd if cmd.starts_with("DURABILITY") => {
347 let mut parts = cmd.splitn(2, char::is_whitespace);
354 let _ = parts.next();
355 let arg = parts.next().map(str::trim).unwrap_or("");
356 let mapped = AckPolicy::parse_legacy(arg).map(AckPolicy::as_str);
357 warn!(
358 received = %cmd,
359 "DURABILITY is deprecated and will be removed in the next minor release; use ACK-POLICY {}",
360 mapped.unwrap_or("<disk|ram|disk+ram|two-disks>")
361 );
362 handle_ack_policy(&mut stream, ack_policy, mapped.unwrap_or(arg));
363 }
364 other => {
365 debug!(received = %other, "unknown admin command");
366 send_best_effort(&mut stream, b"ERR unknown command\n");
367 }
368 }
369}
370
371fn handle_ack_policy(stream: &mut TcpStream, ack_policy: Option<&AtomicU8>, arg: &str) {
378 let Some(atomic) = ack_policy else {
379 send_best_effort(stream, b"ERR ACK-POLICY not available on this node\n");
380 debug!("rejected ACK-POLICY — atomic not wired (replica node?)");
381 return;
382 };
383 if arg.is_empty() {
384 send_best_effort(
385 stream,
386 b"ERR ACK-POLICY requires a policy (disk|ram|disk+ram|two-disks)\n",
387 );
388 debug!("rejected ACK-POLICY — missing argument");
389 return;
390 }
391 let Some(next) = AckPolicy::parse(arg) else {
392 let mut buf = [0u8; 128];
397 let msg = format_unknown_policy(&mut buf, arg);
398 send_best_effort(stream, msg);
399 debug!(received = %arg, "rejected ACK-POLICY — unknown policy");
400 return;
401 };
402 let prev_byte = atomic.swap(next.as_u8(), Ordering::Relaxed);
407 let prev = AckPolicy::from_u8(prev_byte)
408 .map(|p| p.as_str())
409 .unwrap_or("<corrupted>");
410 send_best_effort(stream, b"OK\n");
411 info!(
412 prev = prev,
413 next = next.as_str(),
414 "ack policy changed by operator"
415 );
416}
417
418fn format_unknown_policy<'a>(buf: &'a mut [u8], arg: &str) -> &'a [u8] {
423 use std::io::Write as _;
424 let mut cursor = std::io::Cursor::new(&mut buf[..]);
425 let _ = writeln!(
429 cursor,
430 "ERR ACK-POLICY unknown policy `{arg}` (expected disk|ram|disk+ram|two-disks)"
431 );
432 let n = cursor.position() as usize;
433 &cursor.into_inner()[..n]
434}
435
436#[cfg(test)]
437mod tests {
438 use super::*;
439 use std::io::{BufRead, BufReader, Read, Write};
440
441 use ed25519_dalek::{Signer, SigningKey};
442 use melin_wire_protocol::control_codec::{
443 TAG_AUTH_FAILED, TAG_CHALLENGE, TAG_CHALLENGE_RESPONSE, TAG_SERVER_READY,
444 };
445
446 use crate::test_roles::desk_keys;
447
448 fn operator_keys() -> (SigningKey, Arc<AuthorizedKeys>) {
449 let signing_key = SigningKey::from_bytes(&[0xAD; 32]);
450 let pub_b64 = base64::Engine::encode(
451 &base64::engine::general_purpose::STANDARD,
452 signing_key.verifying_key().to_bytes(),
453 );
454 (signing_key, Arc::new(desk_keys("operator", &pub_b64)))
455 }
456
457 fn trader_keys() -> (SigningKey, Arc<AuthorizedKeys>) {
460 let signing_key = SigningKey::from_bytes(&[0xBD; 32]);
461 let pub_b64 = base64::Engine::encode(
462 &base64::engine::general_purpose::STANDARD,
463 signing_key.verifying_key().to_bytes(),
464 );
465 (signing_key, Arc::new(desk_keys("trader", &pub_b64)))
466 }
467
468 fn client_authenticate(stream: &mut TcpStream, key: &SigningKey) -> u8 {
473 let mut len_buf = [0u8; 4];
475 stream.read_exact(&mut len_buf).expect("read challenge len");
476 let frame_len = u32::from_le_bytes(len_buf) as usize;
477 let mut frame_buf = vec![0u8; frame_len];
478 stream
479 .read_exact(&mut frame_buf)
480 .expect("read challenge payload");
481 assert_eq!(frame_buf[0], TAG_CHALLENGE, "expected Challenge");
482 let nonce = &frame_buf[1..33];
483
484 let signature = key.sign(nonce);
487 let mut frame = Vec::with_capacity(97);
488 frame.push(TAG_CHALLENGE_RESPONSE);
489 frame.extend_from_slice(&signature.to_bytes());
490 frame.extend_from_slice(&key.verifying_key().to_bytes());
491 stream
492 .write_all(&(frame.len() as u32).to_le_bytes())
493 .expect("send result len");
494 stream.write_all(&frame).expect("send");
495 stream.flush().expect("flush");
496
497 let mut len_buf2 = [0u8; 4];
499 stream.read_exact(&mut len_buf2).expect("read result len");
500 let result_len = u32::from_le_bytes(len_buf2) as usize;
501 let mut result_buf = vec![0u8; result_len];
502 stream
503 .read_exact(&mut result_buf)
504 .expect("read result payload");
505 result_buf[0]
506 }
507
508 fn send_command(addr: SocketAddr, key: &SigningKey, command: &[u8]) -> String {
511 let mut stream = TcpStream::connect(addr).unwrap();
512 assert_eq!(client_authenticate(&mut stream, key), TAG_SERVER_READY);
513 stream.write_all(command).unwrap();
514 stream.flush().unwrap();
515 let mut reader = BufReader::new(stream);
516 let mut line = String::new();
517 reader.read_line(&mut line).unwrap();
518 line.trim().to_string()
519 }
520
521 #[test]
526 fn spawn_errs_when_port_taken() {
527 let (_key, auth_keys) = operator_keys();
528 let holder = TcpListener::bind("127.0.0.1:0").unwrap();
529 let addr = holder.local_addr().unwrap();
530 let shutdown = Arc::new(AtomicBool::new(false));
531 let err = spawn(addr, None, None, None, shutdown, auth_keys)
532 .expect_err("bind on a taken port must fail");
533 assert!(
534 err.to_string().contains("failed to bind admin listener"),
535 "{err}"
536 );
537 }
538
539 #[test]
540 fn promote_command_sets_flag_when_wired() {
541 let (key, auth_keys) = operator_keys();
542 let promote = PromotionRequest::new();
543 let rotate = Arc::new(AtomicBool::new(false));
544 let shutdown = Arc::new(AtomicBool::new(false));
545 let (_h, addr) = spawn(
546 "127.0.0.1:0".parse().unwrap(),
547 Some(promote.clone()),
548 Some(Arc::clone(&rotate)),
549 None,
550 Arc::clone(&shutdown),
551 auth_keys,
552 )
553 .expect("spawn admin listener");
554
555 assert_eq!(send_command(addr, &key, b"PROMOTE\n"), "OK");
556 assert_eq!(promote.pending(), Some(PromotionRequest::MANUAL));
557 assert!(!rotate.load(Ordering::Acquire));
558
559 shutdown.store(true, Ordering::Release);
560 }
561
562 #[test]
563 fn rotate_command_sets_flag_when_wired() {
564 let (key, auth_keys) = operator_keys();
565 let promote = PromotionRequest::new();
566 let rotate = Arc::new(AtomicBool::new(false));
567 let shutdown = Arc::new(AtomicBool::new(false));
568 let (_h, addr) = spawn(
569 "127.0.0.1:0".parse().unwrap(),
570 Some(promote.clone()),
571 Some(Arc::clone(&rotate)),
572 None,
573 Arc::clone(&shutdown),
574 auth_keys,
575 )
576 .expect("spawn admin listener");
577
578 assert_eq!(send_command(addr, &key, b"ROTATE\n"), "OK");
579 assert!(rotate.load(Ordering::Acquire));
580 assert!(!promote.is_requested());
581
582 shutdown.store(true, Ordering::Release);
583 }
584
585 #[test]
588 fn promote_rejected_when_not_wired() {
589 let (key, auth_keys) = operator_keys();
590 let rotate = Arc::new(AtomicBool::new(false));
591 let shutdown = Arc::new(AtomicBool::new(false));
592 let (_h, addr) = spawn(
593 "127.0.0.1:0".parse().unwrap(),
594 None,
595 Some(Arc::clone(&rotate)),
596 None,
597 Arc::clone(&shutdown),
598 auth_keys,
599 )
600 .expect("spawn admin listener");
601
602 let resp = send_command(addr, &key, b"PROMOTE\n");
603 assert!(resp.starts_with("ERR"), "expected ERR, got {resp}");
604 assert!(!rotate.load(Ordering::Acquire));
605
606 shutdown.store(true, Ordering::Release);
607 }
608
609 #[test]
611 fn rotate_rejected_when_not_wired() {
612 let (key, auth_keys) = operator_keys();
613 let promote = PromotionRequest::new();
614 let shutdown = Arc::new(AtomicBool::new(false));
615 let (_h, addr) = spawn(
616 "127.0.0.1:0".parse().unwrap(),
617 Some(promote.clone()),
618 None,
619 None,
620 Arc::clone(&shutdown),
621 auth_keys,
622 )
623 .expect("spawn admin listener");
624
625 let resp = send_command(addr, &key, b"ROTATE\n");
626 assert!(resp.starts_with("ERR"), "expected ERR, got {resp}");
627 assert!(!promote.is_requested());
628
629 shutdown.store(true, Ordering::Release);
630 }
631
632 #[test]
636 fn listener_handles_multiple_commands() {
637 let (key, auth_keys) = operator_keys();
638 let promote = PromotionRequest::new();
639 let rotate = Arc::new(AtomicBool::new(false));
640 let shutdown = Arc::new(AtomicBool::new(false));
641 let (_h, addr) = spawn(
642 "127.0.0.1:0".parse().unwrap(),
643 Some(promote.clone()),
644 Some(Arc::clone(&rotate)),
645 None,
646 Arc::clone(&shutdown),
647 auth_keys,
648 )
649 .expect("spawn admin listener");
650
651 for _ in 0..3 {
654 assert_eq!(send_command(addr, &key, b"ROTATE\n"), "OK");
655 assert!(rotate.load(Ordering::Acquire));
656 rotate
657 .compare_exchange(true, false, Ordering::AcqRel, Ordering::Relaxed)
658 .expect("flag should still be set");
659 std::thread::sleep(Duration::from_millis(100));
660 }
661
662 assert_eq!(send_command(addr, &key, b"PROMOTE\n"), "OK");
664 assert_eq!(promote.pending(), Some(PromotionRequest::MANUAL));
665
666 shutdown.store(true, Ordering::Release);
667 }
668
669 #[test]
670 fn unknown_command_rejected() {
671 let (key, auth_keys) = operator_keys();
672 let promote = PromotionRequest::new();
673 let rotate = Arc::new(AtomicBool::new(false));
674 let shutdown = Arc::new(AtomicBool::new(false));
675 let (_h, addr) = spawn(
676 "127.0.0.1:0".parse().unwrap(),
677 Some(promote.clone()),
678 Some(Arc::clone(&rotate)),
679 None,
680 Arc::clone(&shutdown),
681 auth_keys,
682 )
683 .expect("spawn admin listener");
684
685 let resp = send_command(addr, &key, b"INVALID\n");
686 assert!(resp.starts_with("ERR"), "expected ERR, got {resp}");
687 assert!(!promote.is_requested());
688 assert!(!rotate.load(Ordering::Acquire));
689
690 shutdown.store(true, Ordering::Release);
691 }
692
693 #[test]
694 fn non_operator_key_rejected() {
695 let (trader_key, auth_keys) = trader_keys();
696 let promote = PromotionRequest::new();
697 let rotate = Arc::new(AtomicBool::new(false));
698 let shutdown = Arc::new(AtomicBool::new(false));
699 let (_h, addr) = spawn(
700 "127.0.0.1:0".parse().unwrap(),
701 Some(promote.clone()),
702 Some(Arc::clone(&rotate)),
703 None,
704 Arc::clone(&shutdown),
705 auth_keys,
706 )
707 .expect("spawn admin listener");
708
709 let mut stream = TcpStream::connect(addr).unwrap();
710 let result = client_authenticate(&mut stream, &trader_key);
711 assert_eq!(result, TAG_AUTH_FAILED);
712 assert!(!promote.is_requested());
713 assert!(!rotate.load(Ordering::Acquire));
714
715 shutdown.store(true, Ordering::Release);
716 }
717
718 fn run_ack_policy(initial: AckPolicy, cmd: &[u8]) -> (String, Option<AckPolicy>) {
723 let (key, auth_keys) = operator_keys();
724 let policy = Arc::new(AtomicU8::new(initial.as_u8()));
725 let shutdown = Arc::new(AtomicBool::new(false));
726 let (_h, addr) = spawn(
727 "127.0.0.1:0".parse().unwrap(),
728 None,
729 None,
730 Some(Arc::clone(&policy)),
731 Arc::clone(&shutdown),
732 auth_keys,
733 )
734 .expect("spawn admin listener");
735
736 let resp = send_command(addr, &key, cmd);
737 let after = AckPolicy::from_u8(policy.load(Ordering::Relaxed));
738 shutdown.store(true, Ordering::Release);
739 (resp, after)
740 }
741
742 #[test]
743 fn ack_policy_command_swaps_policy() {
744 let (resp, after) = run_ack_policy(AckPolicy::DiskAndRam, b"ACK-POLICY disk\n");
745 assert_eq!(resp, "OK");
746 assert_eq!(after, Some(AckPolicy::Disk));
747 }
748
749 #[test]
750 fn deprecated_durability_alias_maps_old_names() {
751 let (resp, after) = run_ack_policy(AckPolicy::DiskAndRam, b"DURABILITY local\n");
754 assert_eq!(resp, "OK");
755 assert_eq!(after, Some(AckPolicy::Disk));
756
757 let (resp, after) = run_ack_policy(AckPolicy::Disk, b"DURABILITY durably-replicated\n");
758 assert_eq!(resp, "OK");
759 assert_eq!(after, Some(AckPolicy::TwoDisks));
760
761 let (resp, after) = run_ack_policy(AckPolicy::Disk, b"DURABILITY ram\n");
763 assert_eq!(resp, "OK");
764 assert_eq!(after, Some(AckPolicy::Ram));
765 let (resp, after) = run_ack_policy(AckPolicy::Disk, b"DURABILITY fast\n");
766 assert!(resp.starts_with("ERR ACK-POLICY unknown policy"), "{resp}");
767 assert_eq!(after, Some(AckPolicy::Disk));
768 }
769
770 #[test]
771 fn ack_policy_command_accepts_each_policy() {
772 for target in [
773 AckPolicy::Disk,
774 AckPolicy::Ram,
775 AckPolicy::DiskAndRam,
776 AckPolicy::TwoDisks,
777 ] {
778 let cmd = format!("ACK-POLICY {}\n", target.as_str());
779 let (resp, after) = run_ack_policy(AckPolicy::Disk, cmd.as_bytes());
780 assert_eq!(resp, "OK", "policy {target}");
781 assert_eq!(after, Some(target));
782 }
783 }
784
785 #[test]
786 fn ack_policy_command_rejects_unknown_policy() {
787 let (resp, after) = run_ack_policy(AckPolicy::DiskAndRam, b"ACK-POLICY fast\n");
788 assert!(
789 resp.starts_with("ERR ACK-POLICY unknown policy"),
790 "expected unknown-policy ERR, got {resp}"
791 );
792 assert_eq!(after, Some(AckPolicy::DiskAndRam));
794 }
795
796 #[test]
797 fn ack_policy_command_rejects_missing_argument() {
798 let (resp, after) = run_ack_policy(AckPolicy::DiskAndRam, b"ACK-POLICY\n");
799 assert!(
800 resp.starts_with("ERR ACK-POLICY requires a policy"),
801 "expected missing-arg ERR, got {resp}"
802 );
803 assert_eq!(after, Some(AckPolicy::DiskAndRam));
804 }
805
806 #[test]
807 fn ack_policy_command_rejected_when_not_wired() {
808 let (key, auth_keys) = operator_keys();
811 let promote = PromotionRequest::new();
812 let shutdown = Arc::new(AtomicBool::new(false));
813 let (_h, addr) = spawn(
814 "127.0.0.1:0".parse().unwrap(),
815 Some(promote.clone()),
816 None,
817 None,
818 Arc::clone(&shutdown),
819 auth_keys,
820 )
821 .expect("spawn admin listener");
822
823 let resp = send_command(addr, &key, b"ACK-POLICY disk\n");
824 assert!(
825 resp.starts_with("ERR ACK-POLICY not available"),
826 "expected not-available ERR, got {resp}"
827 );
828
829 shutdown.store(true, Ordering::Release);
830 }
831}