fix: verify success completion signal + cross-session verify_url restore
_on_verify_success() was missing _nickserv_complete() call, causing _go_ready() to hang at _nickserv_done.wait() when registration completed immediately (no email verification needed). get_pending_registration() was not returning verify_url from the DB, so _resume_pending_verification() never restored self._verify_url -- breaking cross-session captcha resume for OFTC-style networks. Four regression tests added. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -244,20 +244,21 @@ class Backlog:
|
||||
|
||||
async def get_pending_registration(
|
||||
self, network: str,
|
||||
) -> tuple[str, str, str, str] | None:
|
||||
) -> tuple[str, str, str, str, str] | None:
|
||||
"""Get a pending (unverified) registration for a network.
|
||||
|
||||
Returns (nick, password, email, host) or None.
|
||||
Returns (nick, password, email, host, verify_url) or None.
|
||||
"""
|
||||
assert self._db is not None
|
||||
cursor = await self._db.execute(
|
||||
"SELECT nick, password, email, host FROM nickserv_creds "
|
||||
"SELECT nick, password, email, host, verify_url "
|
||||
"FROM nickserv_creds "
|
||||
"WHERE network = ? AND status = 'pending' "
|
||||
"ORDER BY registered_at DESC LIMIT 1",
|
||||
(network,),
|
||||
)
|
||||
row = await cursor.fetchone()
|
||||
return (row[0], row[1], row[2], row[3]) if row else None
|
||||
return (row[0], row[1], row[2], row[3], row[4]) if row else None
|
||||
|
||||
async def mark_nickserv_verified(self, network: str, nick: str) -> None:
|
||||
"""Promote a pending registration to verified."""
|
||||
|
||||
@@ -955,6 +955,7 @@ class Network:
|
||||
if self.backlog and self._nickserv_password:
|
||||
await self.backlog.mark_nickserv_verified(self.cfg.name, self.nick)
|
||||
self._nickserv_pending = ""
|
||||
await self._nickserv_complete()
|
||||
|
||||
async def _resume_pending_verification(self) -> bool:
|
||||
"""Check for a pending registration from a previous session and resume.
|
||||
@@ -971,9 +972,9 @@ class Network:
|
||||
if not pending:
|
||||
return False
|
||||
|
||||
p_nick, p_pass, p_email, p_host = pending
|
||||
log.info("[%s] found pending registration: nick=%s email=%s",
|
||||
self.cfg.name, p_nick, p_email)
|
||||
p_nick, p_pass, p_email, p_host, p_url = pending
|
||||
log.info("[%s] found pending registration: nick=%s email=%s url=%s",
|
||||
self.cfg.name, p_nick, p_email, p_url or "(none)")
|
||||
|
||||
# If we're already SASL'd as a different nick, we can't verify
|
||||
# for the pending nick on this connection -- just resume email check
|
||||
@@ -981,6 +982,7 @@ class Network:
|
||||
|
||||
self._nickserv_password = p_pass
|
||||
self._nickserv_email = p_email
|
||||
self._verify_url = p_url
|
||||
self._nickserv_pending = "verify"
|
||||
self._status(f"resuming verification for {p_nick} ({p_email})")
|
||||
|
||||
|
||||
@@ -1058,6 +1058,35 @@ class TestHandleNickserv:
|
||||
assert net._nickserv_pending == ""
|
||||
bl.mark_nickserv_verified.assert_awaited_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_verify_success_signals_completion(self) -> None:
|
||||
"""_on_verify_success must signal _nickserv_done."""
|
||||
bl = _mock_backlog()
|
||||
net = _net(backlog=bl)
|
||||
net.nick = "verified_nick"
|
||||
net._nickserv_pending = "verify"
|
||||
net._nickserv_password = "pass"
|
||||
net._nickserv_done = asyncio.Event()
|
||||
|
||||
await net._handle_nickserv("verified_nick has been verified")
|
||||
assert net._nickserv_pending == ""
|
||||
assert net._nickserv_done.is_set()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_registration_immediate_signals_completion(self) -> None:
|
||||
"""Immediate registration (no email) must signal _nickserv_done."""
|
||||
bl = _mock_backlog()
|
||||
net = _net(backlog=bl)
|
||||
net.nick = "fastnick"
|
||||
net._nickserv_pending = "register"
|
||||
net._nickserv_password = "pass"
|
||||
net._nickserv_done = asyncio.Event()
|
||||
|
||||
await net._handle_nickserv("Nickname registered under your account")
|
||||
assert net._nickserv_pending == ""
|
||||
assert net._nickserv_done.is_set()
|
||||
bl.mark_nickserv_verified.assert_awaited_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_verify_failure(self) -> None:
|
||||
net = _net()
|
||||
@@ -1068,6 +1097,46 @@ class TestHandleNickserv:
|
||||
await net._handle_nickserv("Invalid verification code")
|
||||
assert net._nickserv_pending == ""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resume_pending_restores_verify_url(self) -> None:
|
||||
"""Cross-session resume must restore the verify_url from DB."""
|
||||
bl = _mock_backlog(
|
||||
pending=("oldnick", "pass", "e@mail.tm", "host", "https://oftc/verify/abc"),
|
||||
)
|
||||
net = _net(backlog=bl)
|
||||
net.state = State.READY
|
||||
net._running = True
|
||||
net.nick = "oldnick"
|
||||
writer = MagicMock()
|
||||
writer.is_closing.return_value = False
|
||||
writer.drain = AsyncMock()
|
||||
net._writer = writer
|
||||
|
||||
result = await net._resume_pending_verification()
|
||||
assert result is True
|
||||
assert net._verify_url == "https://oftc/verify/abc"
|
||||
assert net._nickserv_email == "e@mail.tm"
|
||||
assert net._nickserv_password == "pass"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resume_pending_no_url(self) -> None:
|
||||
"""Resume works when verify_url is empty."""
|
||||
bl = _mock_backlog(
|
||||
pending=("nick", "pass", "e@mail.tm", "host", ""),
|
||||
)
|
||||
net = _net(backlog=bl)
|
||||
net.state = State.READY
|
||||
net._running = True
|
||||
net.nick = "nick"
|
||||
writer = MagicMock()
|
||||
writer.is_closing.return_value = False
|
||||
writer.drain = AsyncMock()
|
||||
net._writer = writer
|
||||
|
||||
result = await net._resume_pending_verification()
|
||||
assert result is True
|
||||
assert net._verify_url == ""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_late_registration_confirmation(self) -> None:
|
||||
"""After timeout clears pending state, late confirmation still works."""
|
||||
|
||||
Reference in New Issue
Block a user