fix: keep session-only credentials across reconnects so NickServ re-identifies without re-prompting

The NickServ password (and any other "remember for this session" secret) was
resolved through a fresh per-connect cache, so every auto-reconnect prompted
again with a 60 s timeout and an unanswered prompt failed the reconnect.
Session-only secrets now live on the session, survive auto and explicit
reconnects, and IDENTIFY is sent on every registration as before.
This commit is contained in:
Andy Dixon
2026-10-12 07:53:03 +01:00
parent 55a8295de7
commit 17a5b4fbb5
2 changed files with 170 additions and 17 deletions
+78
View File
@@ -125,3 +125,81 @@ fn connect_failure_reports_error_state() {
assert!(err.starts_with(&id) && err.len() > id.len() + 1, "{err}");
let _ = std::fs::remove_dir_all(dir);
}
/// NickServ identification happens on every registration, including auto-reconnects, and a
/// session-only NickServ password is asked once, not again for the reconnect.
#[test]
fn irc_reconnect_identifies_with_nickserv_again() {
use std::io::{BufRead, BufReader};
use std::sync::atomic::{AtomicUsize, Ordering};
let (m, rx, dir) = manager("irc-nickserv");
let l = TcpListener::bind("127.0.0.1:0").unwrap();
let port = l.local_addr().unwrap().port();
let identified = Arc::new(AtomicUsize::new(0));
let seen = identified.clone();
std::thread::spawn(move || {
for (n, s) in l.incoming().flatten().enumerate() {
let mut w = s.try_clone().unwrap();
let mut r = BufReader::new(s);
let mut line = String::new();
// Registration, then welcome.
while r.read_line(&mut line).unwrap_or(0) > 0 {
if line.starts_with("USER ") {
break;
}
line.clear();
}
w.write_all(b":srv 001 nexus :Welcome\r\n").unwrap();
line.clear();
while r.read_line(&mut line).unwrap_or(0) > 0 {
if line == "PRIVMSG NickServ :IDENTIFY acct pw-once\r\n" {
seen.fetch_add(1, Ordering::SeqCst);
break;
}
line.clear();
}
// First connection: hang up so the session auto-reconnects. Second: stay open.
if n == 0 {
drop(w);
} else {
std::thread::sleep(Duration::from_secs(30));
}
}
});
let mut p = Profile::new(Protocol::Irc);
p.host = "127.0.0.1".into();
p.port = port as i64;
let c = p.irc.as_mut().unwrap();
c.nick_serv_identify = true;
c.nick_serv_account = "acct".into();
c.nick_serv_mode = nexus_core::models::SecretMode::SessionOnly;
c.auto_reconnect = true;
let id = m.connect(p);
let prompts = Arc::new(AtomicUsize::new(0));
let answer = |e: &Event| {
if let Event::SecretPrompt { reply, .. } = e {
prompts.fetch_add(1, Ordering::SeqCst);
let _ = reply.send("pw-once".into());
}
};
wait(&rx, |e| {
answer(e);
state(e, SessionState::Connected)
});
wait(&rx, |e| {
answer(e);
matches!(e, Event::State { state: SessionState::Reconnecting, .. }).then_some(())
});
wait(&rx, |e| {
answer(e);
state(e, SessionState::Connected)
});
let deadline = std::time::Instant::now() + Duration::from_secs(5);
while identified.load(Ordering::SeqCst) < 2 && std::time::Instant::now() < deadline {
std::thread::sleep(Duration::from_millis(20));
}
assert_eq!(identified.load(Ordering::SeqCst), 2, "IDENTIFY sent on both registrations");
assert_eq!(prompts.load(Ordering::SeqCst), 1, "session-only password asked once");
m.disconnect(&id).unwrap();
let _ = std::fs::remove_dir_all(dir);
}