Files
wwtools/mbproxy/tests/Mbproxy.Tests/Configuration/ReloadValidatorTests.cs
T
Joseph Doherty 1a2856526a mbproxy: strip historical phase/wave/plan references from source comments
Comments described the *history* of how the code arrived (phase numbers,
wave IDs, review IDs, dated TODOs) instead of what it does today. That
scaffolding rotted as the codebase evolved. Cleaned 60 source files +
.gitignore; behaviour unchanged (387/387 tests still pass).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 13:04:30 -04:00

268 lines
9.7 KiB
C#

using Mbproxy.Configuration;
using Mbproxy.Options;
using Xunit;
namespace Mbproxy.Tests.Configuration;
/// <summary>
/// Unit tests for <see cref="ReloadValidator.Validate"/>.
/// Each test covers one specific failure mode or the happy path.
/// </summary>
[Trait("Category", "Unit")]
public sealed class ReloadValidatorTests
{
// ── Helpers ───────────────────────────────────────────────────────────────────────────
private static PlcOptions MakePlc(string name, int listenPort, string host = "127.0.0.1")
=> new() { Name = name, ListenPort = listenPort, Host = host, Port = 502 };
private static MbproxyOptions MakeOptions(
PlcOptions[] plcs,
int adminPort = 8080,
BcdTagListOptions? global = null)
=> new()
{
Plcs = plcs,
AdminPort = adminPort,
BcdTags = global ?? new BcdTagListOptions(),
};
// ── 1. Duplicate PLC name → fails ────────────────────────────────────────────────────
[Fact]
public void Validate_DuplicatePlcName_Fails()
{
var opts = MakeOptions([
MakePlc("PLC-A", 5020),
MakePlc("PLC-A", 5021), // same name
]);
bool valid = ReloadValidator.Validate(opts, out var errors);
Assert.False(valid);
Assert.Contains(errors, e => e.Contains("PLC-A") && e.Contains("uplicate"));
}
// ── 2. Duplicate ListenPort → fails ──────────────────────────────────────────────────
[Fact]
public void Validate_DuplicateListenPort_Fails()
{
var opts = MakeOptions([
MakePlc("PLC-A", 5020),
MakePlc("PLC-B", 5020), // same port
]);
bool valid = ReloadValidator.Validate(opts, out var errors);
Assert.False(valid);
Assert.Contains(errors, e => e.Contains("5020") && e.Contains("uplicate"));
}
// ── 3. AdminPort collides with a PLC's ListenPort → fails ────────────────────────────
[Fact]
public void Validate_AdminPortCollidesWith_PlcListenPort_Fails()
{
var opts = MakeOptions(
plcs: [MakePlc("PLC-A", 5020)],
adminPort: 5020); // collides with PLC-A
bool valid = ReloadValidator.Validate(opts, out var errors);
Assert.False(valid);
Assert.Contains(errors, e => e.Contains("AdminPort") && e.Contains("5020"));
}
// ── 4. Per-PLC BCD map build error → fails ────────────────────────────────────────────
[Fact]
public void Validate_PerPlc_BcdMapBuildError_Fails()
{
// A 32-bit tag at address 100 and a 16-bit tag at 101 collide on high register.
var global = new BcdTagListOptions
{
Global =
[
new BcdTagOptions { Address = 100, Width = 32 },
new BcdTagOptions { Address = 101, Width = 16 }, // overlaps 100's high register
],
};
var opts = MakeOptions([MakePlc("PLC-A", 5020)], global: global);
bool valid = ReloadValidator.Validate(opts, out var errors);
Assert.False(valid);
Assert.Contains(errors, e => e.Contains("PLC-A"));
}
// ── 5. Port out of range → fails ─────────────────────────────────────────────────────
[Fact]
public void Validate_PortOutOfRange_Fails()
{
// ListenPort 0 is below the valid [1, 65535] range.
var opts = MakeOptions([MakePlc("PLC-A", 0)]);
bool valid = ReloadValidator.Validate(opts, out var errors);
Assert.False(valid);
Assert.Contains(errors, e => e.Contains("0") && e.Contains("range"));
}
// ── 5b. AdminPort out of range → fails ───────────────────────────────────────────────
[Fact]
public void Validate_AdminPortOutOfRange_Fails()
{
var opts = MakeOptions([MakePlc("PLC-A", 5020)], adminPort: 70000);
bool valid = ReloadValidator.Validate(opts, out var errors);
Assert.False(valid);
Assert.Contains(errors, e => e.Contains("70000") && e.Contains("range"));
}
// ── 6. Happy path → passes ───────────────────────────────────────────────────────────
[Fact]
public void Validate_HappyPath_Passes()
{
var global = new BcdTagListOptions
{
Global = [new BcdTagOptions { Address = 1072, Width = 16 }],
};
var opts = MakeOptions(
plcs: [MakePlc("PLC-A", 5020), MakePlc("PLC-B", 5021)],
adminPort: 8080,
global: global);
bool valid = ReloadValidator.Validate(opts, out var errors);
Assert.True(valid);
Assert.Empty(errors);
}
// ── 7. Empty PLC name → fails ────────────────────────────────────────────────────────
[Fact]
public void Validate_EmptyPlcName_Fails()
{
var opts = MakeOptions([MakePlc("", 5020)]);
bool valid = ReloadValidator.Validate(opts, out var errors);
Assert.False(valid);
Assert.Contains(errors, e => e.Contains("non-empty"));
}
// ── Cache.AllowLongTtl gate ─────────────────────────────────────────────────────────
/// <summary>
/// Per-tag CacheTtlMs > 60_000 without Cache.AllowLongTtl is rejected.
/// </summary>
[Fact]
public void Validate_PerTagCacheTtl_Above60s_Without_AllowLongTtl_Fails()
{
var opts = new MbproxyOptions
{
Plcs = [MakePlc("PLC-A", 5020)],
BcdTags = new BcdTagListOptions
{
Global = [ new BcdTagOptions { Address = 1024, Width = 16, CacheTtlMs = 120_000 } ],
},
Cache = new CacheOptions { AllowLongTtl = false },
};
bool valid = ReloadValidator.Validate(opts, out var errors);
Assert.False(valid);
Assert.Contains(errors, e => e.Contains("AllowLongTtl") && e.Contains("60_000"));
}
/// <summary>
/// Same value passes when AllowLongTtl is true (operator opt-in).
/// </summary>
[Fact]
public void Validate_PerTagCacheTtl_Above60s_With_AllowLongTtl_Passes()
{
var opts = new MbproxyOptions
{
Plcs = [MakePlc("PLC-A", 5020)],
BcdTags = new BcdTagListOptions
{
Global = [ new BcdTagOptions { Address = 1024, Width = 16, CacheTtlMs = 120_000 } ],
},
Cache = new CacheOptions { AllowLongTtl = true },
};
bool valid = ReloadValidator.Validate(opts, out var errors);
Assert.True(valid);
Assert.Empty(errors);
}
/// <summary>
/// Per-PLC DefaultCacheTtlMs > 60_000 inherited by a tag with null CacheTtlMs is
/// caught by the resolved-value check even if the per-PLC default check itself
/// passes (it doesn't, but this validates the defensive resolved re-check).
/// </summary>
[Fact]
public void Validate_ResolvedTtl_FromPerPlcDefault_AboveCap_Fails()
{
var opts = new MbproxyOptions
{
Plcs = [
new PlcOptions
{
Name = "PLC-A", ListenPort = 5020, Host = "127.0.0.1", Port = 502,
DefaultCacheTtlMs = 90_000,
},
],
BcdTags = new BcdTagListOptions
{
// Tag with no explicit CacheTtlMs — inherits the per-PLC 90_000.
Global = [ new BcdTagOptions { Address = 1024, Width = 16 } ],
},
Cache = new CacheOptions { AllowLongTtl = false },
};
bool valid = ReloadValidator.Validate(opts, out var errors);
Assert.False(valid);
Assert.Contains(errors, e => e.Contains("60_000"));
}
// ── ConnectionOptions validation ────────────────────────────────────────────────────
[Fact]
public void Validate_ZeroBackendConnectTimeoutMs_Fails()
{
var opts = new MbproxyOptions
{
Plcs = [MakePlc("PLC-A", 5020)],
Connection = new ConnectionOptions { BackendConnectTimeoutMs = 0 },
};
bool valid = ReloadValidator.Validate(opts, out var errors);
Assert.False(valid);
Assert.Contains(errors, e => e.Contains("BackendConnectTimeoutMs"));
}
[Fact]
public void Validate_NegativeGracefulShutdownTimeoutMs_Fails()
{
var opts = new MbproxyOptions
{
Plcs = [MakePlc("PLC-A", 5020)],
Connection = new ConnectionOptions { GracefulShutdownTimeoutMs = -1 },
};
bool valid = ReloadValidator.Validate(opts, out var errors);
Assert.False(valid);
Assert.Contains(errors, e => e.Contains("GracefulShutdownTimeoutMs"));
}
}