fix(adminui): Galaxy editor 500 — read DriverConfig case-insensitively + null-safe FromRecord
v2-ci / build (push) Failing after 39s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped

GalaxyDriverPage deserialized DriverConfig with case-sensitive camelCase opts, but the
persisted/seeded config is PascalCase (the runtime reads it case-insensitively). So all four
nested option records read as null -> FromRecord NRE (HTTP 500) on edit, and the form would
have shown defaults instead of the real config (risking a clobber on save). Fix: add
PropertyNameCaseInsensitive=true (matches the runtime) so real values load, plus null-coalesce
the nested records in FromRecord as defense-in-depth. Regression test asserts the seeded
PascalCase config loads its real values.
This commit is contained in:
Joseph Doherty
2026-05-29 12:45:44 -04:00
parent 32d7fd7cc9
commit e3a27422a1
2 changed files with 172 additions and 19 deletions
@@ -215,6 +215,7 @@ else
private static readonly System.Text.Json.JsonSerializerOptions _jsonOpts = new()
{
PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase,
PropertyNameCaseInsensitive = true,
UnmappedMemberHandling = System.Text.Json.Serialization.JsonUnmappedMemberHandling.Skip,
WriteIndented = false,
};
@@ -408,26 +409,36 @@ else
// GalaxyDriverOptions top-level
public int ProbeTimeoutSeconds { get; set; } = 30;
public static GalaxyFormModel FromRecord(GalaxyDriverOptions r) => new()
public static GalaxyFormModel FromRecord(GalaxyDriverOptions r)
{
GatewayEndpoint = r.Gateway.Endpoint,
GatewayApiKeySecretRef = r.Gateway.ApiKeySecretRef,
GatewayUseTls = r.Gateway.UseTls,
GatewayCaCertificatePath = r.Gateway.CaCertificatePath,
GatewayConnectTimeoutSeconds = r.Gateway.ConnectTimeoutSeconds,
GatewayDefaultCallTimeoutSeconds = r.Gateway.DefaultCallTimeoutSeconds,
GatewayStreamTimeoutSeconds = r.Gateway.StreamTimeoutSeconds,
MxClientName = r.MxAccess.ClientName,
MxPublishingIntervalMs = r.MxAccess.PublishingIntervalMs,
MxWriteUserId = r.MxAccess.WriteUserId,
MxEventPumpChannelCapacity = r.MxAccess.EventPumpChannelCapacity,
RepositoryDiscoverPageSize = r.Repository.DiscoverPageSize,
RepositoryWatchDeployEvents = r.Repository.WatchDeployEvents,
ReconnectInitialBackoffMs = r.Reconnect.InitialBackoffMs,
ReconnectMaxBackoffMs = r.Reconnect.MaxBackoffMs,
ReconnectReplayOnSessionLost = r.Reconnect.ReplayOnSessionLost,
ProbeTimeoutSeconds = r.ProbeTimeoutSeconds,
};
// Null-coalesce each nested record to its default so that persisted configs
// that pre-date a section (e.g. no Reconnect key, or PascalCase keys that
// don't match the camelCase deserializer) don't cause a NullReferenceException.
var gw = r.Gateway ?? new GalaxyGatewayOptions("https://localhost:5001", "env:MX_API_KEY");
var mx = r.MxAccess ?? new GalaxyMxAccessOptions("OtOpcUa");
var repo = r.Repository ?? new GalaxyRepositoryOptions();
var rc = r.Reconnect ?? new GalaxyReconnectOptions();
return new()
{
GatewayEndpoint = gw.Endpoint,
GatewayApiKeySecretRef = gw.ApiKeySecretRef,
GatewayUseTls = gw.UseTls,
GatewayCaCertificatePath = gw.CaCertificatePath,
GatewayConnectTimeoutSeconds = gw.ConnectTimeoutSeconds,
GatewayDefaultCallTimeoutSeconds = gw.DefaultCallTimeoutSeconds,
GatewayStreamTimeoutSeconds = gw.StreamTimeoutSeconds,
MxClientName = mx.ClientName,
MxPublishingIntervalMs = mx.PublishingIntervalMs,
MxWriteUserId = mx.WriteUserId,
MxEventPumpChannelCapacity = mx.EventPumpChannelCapacity,
RepositoryDiscoverPageSize = repo.DiscoverPageSize,
RepositoryWatchDeployEvents = repo.WatchDeployEvents,
ReconnectInitialBackoffMs = rc.InitialBackoffMs,
ReconnectMaxBackoffMs = rc.MaxBackoffMs,
ReconnectReplayOnSessionLost = rc.ReplayOnSessionLost,
ProbeTimeoutSeconds = r.ProbeTimeoutSeconds,
};
}
public GalaxyDriverOptions ToRecord() => new(
Gateway: new GalaxyGatewayOptions(