Tells apart a slave that is down from one that does not know us

The integrity check mapped four outcomes onto a single null: no response,
a 404, a rejected key, and a genuine answer. Only "rejected key" is
recoverable, and it was being reported as "unreachable" and never repaired —
so an instance registered against the wrong URL stayed broken until someone
edited the database by hand. That is exactly what happened locally.

GetRegisteredMasterUrlAsync now returns an outcome alongside the URL.
Unauthorized triggers registration; 404 is reported as "this host does not
serve the master/slave protocol", which names the actual mistake instead of
hiding it behind a generic contact failure; unreachable and server errors
behave as before.

Registering on a rejected key cannot hijack a slave that belongs to another
master: the slave accepts a registration only when it has none, and refuses
any key that does not match an existing one. So it succeeds exactly in the
case worth recovering and fails harmlessly otherwise. That guarantee lives on
the slave, so the test asserting the refusal now says out loud that the
master depends on it.

Found while diagnosing a status push that failed against a frontend URL.
Small and contained, so fixed here rather than filed as tech debt.

372 tests pass, up from 366.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHoJpxYXzHACSQguHrC5fw
This commit is contained in:
2026-07-28 12:31:00 +02:00
co-authored by Claude Opus 5
parent 980dc80701
commit 6957ec7c60
7 changed files with 251 additions and 20 deletions
@@ -68,6 +68,16 @@ public class MasterAvailabilityServiceTests : IDisposable
await _repo.Received(1).SaveChangesAsync();
}
/// <summary>
/// A slave that already belongs to a master refuses any other key.
/// </summary>
/// <remarks>
/// <b>Do not relax this.</b> The master's integrity check registers automatically when a slave
/// rejects its key (<c>CmsInstanceService.VerifyIntegrityAsync</c>), which is safe only because
/// this refusal holds: registration then succeeds exactly for a slave that has no registration
/// yet, and fails for one that belongs to someone else. Weaken it and that automatic
/// registration becomes a way for one master to take over another master's slave.
/// </remarks>
[Fact]
public async Task RegisterAsync_ReturnsFalse_WhenKeyMismatch()
{