using Mbproxy.Configuration; using Mbproxy.Options; using Xunit; namespace Mbproxy.Tests.Configuration; /// /// Unit tests for . /// Each test covers one specific failure mode or the happy path. /// [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")); } }