From 0e4bbd6fb8883ccebee6a20b9423891c3535dd87 Mon Sep 17 00:00:00 2001 From: feruzm Date: Tue, 22 Sep 2026 15:16:12 +0000 Subject: [PATCH] Stop requiring the window on every window save `open` was mandatory here, so a save about the message or the numbers had to carry it, and sending a stale one could undo another admin's close and reopen applications. Every field on that settings row is optional upstream now, and absent means leave it. A present `open` must still be a boolean. A call carrying none of the four fields is refused rather than forwarded as a no-op, which is what the backend does with it too. --- .../EcencyApi.Tests/CurationDeskPayloadTests.cs | 17 +++++++++++++++-- .../Handlers/PrivateApi.CurationDesk.cs | 13 ++++++++++++- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/dotnet/EcencyApi.Tests/CurationDeskPayloadTests.cs b/dotnet/EcencyApi.Tests/CurationDeskPayloadTests.cs index c7035402..336d4fec 100644 --- a/dotnet/EcencyApi.Tests/CurationDeskPayloadTests.cs +++ b/dotnet/EcencyApi.Tests/CurationDeskPayloadTests.cs @@ -887,16 +887,29 @@ public void TheQueueTakesAStateAndALimitAndNothingElse() } [Theory] - [InlineData("{}", "open must be true or false")] - [InlineData("{\"message\":\"back soon\"}", "open must be true or false")] [InlineData("{\"open\":\"false\"}", "open must be true or false")] [InlineData("{\"open\":0}", "open must be true or false")] [InlineData("{\"open\":null}", "open must be true or false")] + // Nothing at all is a client bug rather than a no-op, and upstream refuses it too. + [InlineData("{}", "nothing to set")] public void AWindowWithoutABooleanIsRefused(string json, string expected) { Assert.Equal(expected, Rejected(CurationDeskWrites.ApplicationWindow, json)); } + [Fact] + public void ASaveAboutOneFieldDoesNotHaveToCarryTheWindow() + { + // `open` decides whether readers can apply. A save about the wording or the + // numbers that carried it too could undo another admin's close, so absent has to + // be allowed here and read upstream as "leave it". + Assert.Equal(new[] { "username", "message" }, + Ok(CurationDeskWrites.ApplicationWindow, "{\"message\":\"back soon\"}") + .Select(kv => kv.Key).ToArray()); + Assert.Equal(new[] { "username", "quorum" }, + Ok(CurationDeskWrites.ApplicationWindow, "{\"quorum\":4}").Select(kv => kv.Key).ToArray()); + } + [Fact] public void AClosedWindowMessageIsCappedAtTheColumn() { diff --git a/dotnet/EcencyApi/Handlers/PrivateApi.CurationDesk.cs b/dotnet/EcencyApi/Handlers/PrivateApi.CurationDesk.cs index ed7bf078..20e76baa 100644 --- a/dotnet/EcencyApi/Handlers/PrivateApi.CurationDesk.cs +++ b/dotnet/EcencyApi/Handlers/PrivateApi.CurationDesk.cs @@ -1283,10 +1283,21 @@ private static void Truncate(JsonObject payload, string key, int max) } if (ReferenceEquals(route, ApplicationWindow)) { - if (body.Field("open")?.GetValueKind() is not (JsonValueKind.True or JsonValueKind.False)) + // Every field here is optional and absent means "leave it" upstream. One + // screen carries four controls that all land on one settings row, so a save + // about the quorum that also sent `open` could undo another admin's close and + // reopen applications to readers who act on it. + if (body.ContainsKey("open") + && body.Field("open")?.GetValueKind() is not (JsonValueKind.True or JsonValueKind.False)) { return "open must be true or false"; } + // A call that sets nothing is a client bug, and upstream refuses it too. + if (!body.ContainsKey("open") && !body.ContainsKey("message") + && !body.ContainsKey("quorum") && !body.ContainsKey("term_days")) + { + return "nothing to set"; + } if (body.TryGetPropertyValue("message", out var message) && message is not null && (body.Str("message") is not { } messageText || messageText.EnumerateRunes().Count() > MaxApplicationMessageLength))