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 (_seq, cr) = match control_codec::decode_challenge_response(&frame_buf[..frame_len]) {
194 Ok(pair) => pair,
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(105);
487 frame.extend_from_slice(&0u64.to_le_bytes()); 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}