@@ -374,8 +374,15 @@ async def remove_guild(guild_id: str, by: str) -> dict:
|
||||
link = await fstore.doc_get(GUILDS, guild_id) or {}
|
||||
removed = []
|
||||
for s in await fstore.collection_list(STREAMS, guild_id=guild_id):
|
||||
if s.get("stream_id") and await delete_stream(s["stream_id"], "server_removed"):
|
||||
removed.append(s["stream_id"])
|
||||
try:
|
||||
if s.get("stream_id") and await delete_stream(s["stream_id"], "server_removed"):
|
||||
removed.append(s["stream_id"])
|
||||
except StreamError as e:
|
||||
# A channel restarted mid-removal: stop here, leave the server
|
||||
# linked, and tell the caller which channels are already gone so
|
||||
# the partial removal is still audited.
|
||||
e.removed = removed
|
||||
raise
|
||||
now = _now_iso()
|
||||
await fstore.doc_set(GUILDS, guild_id, {
|
||||
"guild_id": guild_id, "org_id": None,
|
||||
|
||||
@@ -222,6 +222,11 @@ async def remove_guild(guild_id: str, leave_radio_bots: bool = False,
|
||||
try:
|
||||
out = await streams.remove_guild(guild_id, uid)
|
||||
except streams.StreamError as e:
|
||||
# Channels deleted before the race are gone for good — audit them.
|
||||
await write_audit(uid, email, "discord.guild.remove", details={
|
||||
"guild_id": guild_id, "acting_org_id": acting_org, "outcome": "incomplete", "error": e.code,
|
||||
"streams_removed": getattr(e, "removed", []), "leave_radio_bots": leave_radio_bots,
|
||||
})
|
||||
return _error(e)
|
||||
left: list = []
|
||||
if leave_radio_bots:
|
||||
|
||||
@@ -278,6 +278,27 @@ async def test_remove_guild_deletes_streams_and_unlinks_without_fallback(fs, mon
|
||||
assert [_lease(fs, t) for t in toks] == [None, None]
|
||||
|
||||
|
||||
def test_remove_guild_interrupted_by_a_restart_is_still_audited(fs, monkeypatch):
|
||||
import asyncio
|
||||
a = asyncio.run(_start("1001"))
|
||||
b = asyncio.run(_start("1002", node_id="n2"))
|
||||
real_stop = streams.stop_stream
|
||||
|
||||
async def _stop(stream_id, reason="stopped"):
|
||||
await real_stop(stream_id, reason)
|
||||
if stream_id == b["stream_id"]:
|
||||
await _start("1002", node_id="n2") # restarted between stop and delete
|
||||
monkeypatch.setattr(streams, "stop_stream", _stop)
|
||||
app.dependency_overrides[require_admin_token] = lambda: ADMIN
|
||||
|
||||
r = client.delete("/discord/guilds/100")
|
||||
assert r.status_code == 409 and r.json()["code"] == "restarted"
|
||||
(entry,) = _audit(fs, "discord.guild.remove")
|
||||
assert entry["details"]["outcome"] == "incomplete"
|
||||
assert entry["details"]["streams_removed"] == [a["stream_id"]]
|
||||
assert fs.get("discord_guilds", "100")["org_id"] == ORG # left linked: a live channel remains
|
||||
|
||||
|
||||
def test_remove_guild_route_hides_server_and_relink_restores_it(fs):
|
||||
import asyncio
|
||||
asyncio.run(_start("1001"))
|
||||
|
||||
Reference in New Issue
Block a user