Skip to content

Commit c3c6bb0

Browse files
feat: Enhance boolean handling in Game Server configuration and UI for better compatibility
1 parent 40a5666 commit c3c6bb0

4 files changed

Lines changed: 86 additions & 3 deletions

File tree

.github/copilot-instructions.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,13 @@ The `policy-resource` attribute passes a resource (typically `GameType`) to hand
109109
- PRs trigger dev Terraform plans automatically; prod plans require the `run-prd-plan` label.
110110
- Copilot and Dependabot PRs skip Terraform plans unless explicitly labeled.
111111

112+
## Config JSON Boolean Standard
113+
114+
- For Game Server configuration namespaces stored as JSON (`agent`, `moderation`, `events`, `broadcasts`, etc.), always serialize boolean fields as JSON booleans (`true`/`false`), never strings (`"true"`/`"false"`).
115+
- When reading existing configuration JSON in controllers, support legacy string booleans defensively, then preserve canonical output by re-saving as JSON booleans.
116+
- For Razor boolean form fields that intentionally post both hidden and checkbox values for the same property name, render the hidden `false` input before the checkbox input to ensure checked values bind correctly.
117+
- Add or update controller tests whenever introducing or changing bool-backed config fields to verify both canonical bool handling and legacy-string compatibility on read.
118+
112119
## UI Conventions (Razor Views)
113120

114121
**Always read [docs/ui-standards-guide.md](docs/ui-standards-guide.md) before creating or modifying views.** Key rules:

src/XtremeIdiots.Portal.Web.Tests/Controllers/GameServersControllerTests.cs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,82 @@ public void PopulateConfigFromNamespace_BroadcastsNamespace_MapsAllFields()
140140
Assert.False(model.BroadcastMessages[1].Enabled);
141141
}
142142

143+
[Fact]
144+
public void PopulateConfigFromNamespace_BroadcastsNamespace_ParsesStringBooleans()
145+
{
146+
// Arrange
147+
var sut = CreateSut();
148+
var method = GetPrivateInstanceMethod("PopulateConfigFromNamespace");
149+
var model = new GameServerEditViewModel();
150+
var config = JsonConvert.DeserializeObject<ConfigurationDto>(JsonConvert.SerializeObject(new
151+
{
152+
Namespace = "broadcasts",
153+
Configuration = """
154+
{
155+
"enabled": "true",
156+
"intervalSeconds": 600,
157+
"messages": [
158+
{ "message": "^1Message A", "enabled": "true" },
159+
{ "message": "^2Message B", "enabled": "false" }
160+
]
161+
}
162+
"""
163+
}));
164+
165+
// Act
166+
method.Invoke(sut, [model, config]);
167+
168+
// Assert
169+
Assert.True(model.BroadcastsEnabled);
170+
Assert.Equal(600, model.BroadcastsIntervalSeconds);
171+
Assert.Equal(2, model.BroadcastMessages.Count);
172+
Assert.True(model.BroadcastMessages[0].Enabled);
173+
Assert.False(model.BroadcastMessages[1].Enabled);
174+
}
175+
176+
[Fact]
177+
public void PopulateConfigFromNamespace_ParsesStringBooleans_ForOtherConfigNamespaces()
178+
{
179+
// Arrange
180+
var sut = CreateSut();
181+
var method = GetPrivateInstanceMethod("PopulateConfigFromNamespace");
182+
var model = new GameServerEditViewModel
183+
{
184+
GameServer = new GameServerViewModel
185+
{
186+
FileTransportType = FileTransportType.Ftp
187+
}
188+
};
189+
190+
var agentConfig = JsonConvert.DeserializeObject<ConfigurationDto>(JsonConvert.SerializeObject(new
191+
{
192+
Namespace = "agent",
193+
Configuration = """
194+
{
195+
"rconSyncEnabled": "false"
196+
}
197+
"""
198+
}));
199+
200+
var moderationConfig = JsonConvert.DeserializeObject<ConfigurationDto>(JsonConvert.SerializeObject(new
201+
{
202+
Namespace = "moderation",
203+
Configuration = """
204+
{
205+
"protectedNameEnforcementEnabled": "false"
206+
}
207+
"""
208+
}));
209+
210+
// Act
211+
method.Invoke(sut, [model, agentConfig]);
212+
method.Invoke(sut, [model, moderationConfig]);
213+
214+
// Assert
215+
Assert.False(model.AgentConfigRconSyncEnabled);
216+
Assert.False(model.ModerationProtectedNameEnforcementEnabled);
217+
}
218+
143219
[Fact]
144220
public async Task SaveConfigNamespacesAsync_AgentEnabled_UpsertsBroadcastsContract()
145221
{

src/XtremeIdiots.Portal.Web/Controllers/GameServersController.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -704,10 +704,10 @@ private static bool GetBoolProperty(JsonElement root, string propertyName, bool
704704
{
705705
JsonValueKind.True => true,
706706
JsonValueKind.False => false,
707+
JsonValueKind.String when bool.TryParse(prop.GetString(), out var parsedBool) => parsedBool,
707708
JsonValueKind.Undefined => defaultValue,
708709
JsonValueKind.Object => defaultValue,
709710
JsonValueKind.Array => defaultValue,
710-
JsonValueKind.String => defaultValue,
711711
JsonValueKind.Number => defaultValue,
712712
JsonValueKind.Null => defaultValue,
713713
_ => defaultValue

src/XtremeIdiots.Portal.Web/Views/GameServers/ConfigurationSections/_BroadcastsConfiguration.cshtml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
<div class="border rounded p-3 mb-3" data-broadcast-row>
4343
<div class="d-flex justify-content-between align-items-center mb-2">
4444
<div class="form-check form-switch mb-0">
45+
<input type="hidden" data-field="enabled-hidden" name="BroadcastMessages[@i].Enabled" value="false" />
4546
@if (broadcastMessages[i].Enabled)
4647
{
4748
<input class="form-check-input" data-field="enabled" type="checkbox" name="BroadcastMessages[@i].Enabled" value="true" id="BroadcastMessages_@(i)__Enabled" checked="checked" />
@@ -50,7 +51,6 @@
5051
{
5152
<input class="form-check-input" data-field="enabled" type="checkbox" name="BroadcastMessages[@i].Enabled" value="true" id="BroadcastMessages_@(i)__Enabled" />
5253
}
53-
<input type="hidden" data-field="enabled-hidden" name="BroadcastMessages[@i].Enabled" value="false" />
5454
<label class="form-check-label" data-field="enabled-label" for="BroadcastMessages_@(i)__Enabled">Enabled</label>
5555
</div>
5656
<div class="btn-group btn-group-sm" role="group" aria-label="Broadcast message ordering">
@@ -79,8 +79,8 @@
7979
<div class="border rounded p-3 mb-3" data-broadcast-row>
8080
<div class="d-flex justify-content-between align-items-center mb-2">
8181
<div class="form-check form-switch mb-0">
82-
<input class="form-check-input" data-field="enabled" type="checkbox" value="true" checked="checked" />
8382
<input type="hidden" data-field="enabled-hidden" value="false" />
83+
<input class="form-check-input" data-field="enabled" type="checkbox" value="true" checked="checked" />
8484
<label class="form-check-label" data-field="enabled-label">Enabled</label>
8585
</div>
8686
<div class="btn-group btn-group-sm" role="group" aria-label="Broadcast message ordering">

0 commit comments

Comments
 (0)