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))