using FrameProcessor.Configuration; namespace FrameProcessor.Tests; public class FramesOptionsValidatorTests { private readonly FramesOptionsValidator _validator = new(); [Fact] public void NoFrames_IsValid() { var result = _validator.Validate(null, new FramesOptions()); Assert.True(result.Succeeded); } [Fact] public void ValidFrame_Succeeds() { var options = new FramesOptions { Frames = { ValidFrame() } }; var result = _validator.Validate(null, options); Assert.True(result.Succeeded, JoinFailures(result)); } [Theory] [InlineData(null)] [InlineData("")] [InlineData(" ")] public void Name_MissingOrBlank_Fails(string? badName) { var frame = ValidFrame(); frame.Name = badName; var result = _validator.Validate(null, new FramesOptions { Frames = { frame } }); Assert.False(result.Succeeded); Assert.Contains(result.Failures!, f => f.Contains("Name is required")); } [Theory] [InlineData("living room")] [InlineData("living/room")] [InlineData("li:vingroom")] public void Name_NotUrlSafe_Fails(string badName) { var frame = ValidFrame(); frame.Name = badName; var result = _validator.Validate(null, new FramesOptions { Frames = { frame } }); Assert.False(result.Succeeded); Assert.Contains(result.Failures!, f => f.Contains("is not a valid frame name")); } [Theory] [InlineData(null)] [InlineData("")] public void Mac_Missing_Fails(string? badMac) { var frame = ValidFrame(); frame.Mac = badMac; var result = _validator.Validate(null, new FramesOptions { Frames = { frame } }); Assert.False(result.Succeeded); Assert.Contains(result.Failures!, f => f.Contains("Mac is required")); } [Theory] [InlineData("ZZ:ZZ:ZZ:ZZ:ZZ:ZZ")] [InlineData("AABBCC")] [InlineData("not-a-mac")] public void Mac_Unparseable_Fails(string badMac) { var frame = ValidFrame(); frame.Mac = badMac; var result = _validator.Validate(null, new FramesOptions { Frames = { frame } }); Assert.False(result.Succeeded); Assert.Contains(result.Failures!, f => f.Contains("is not a valid MAC address")); } [Fact] public void Resolution_Missing_Fails() { var frame = ValidFrame(); frame.Resolution = null; var result = _validator.Validate(null, new FramesOptions { Frames = { frame } }); Assert.False(result.Succeeded); Assert.Contains(result.Failures!, f => f.Contains("Resolution is required")); } [Theory] [InlineData(0, 1200)] [InlineData(1600, 0)] [InlineData(-1, 1200)] [InlineData(1600, -1)] public void Resolution_NonPositive_Fails(int width, int height) { var frame = ValidFrame(); frame.Resolution = new ResolutionOptions { Width = width, Height = height }; var result = _validator.Validate(null, new FramesOptions { Frames = { frame } }); Assert.False(result.Succeeded); Assert.Contains(result.Failures!, f => f.Contains("must be positive")); } [Theory] [InlineData(null)] [InlineData("")] [InlineData("LANDSCAPE")] [InlineData("sideways")] public void Orientation_Invalid_Fails(string? badOrientation) { var frame = ValidFrame(); frame.Orientation = badOrientation; var result = _validator.Validate(null, new FramesOptions { Frames = { frame } }); Assert.False(result.Succeeded); Assert.Contains(result.Failures!, f => f.Contains("Orientation")); } [Theory] [InlineData(null)] [InlineData("")] [InlineData("FloydSteinberg")] [InlineData("ordered")] public void Dithering_Invalid_Fails(string? badDithering) { var frame = ValidFrame(); frame.Dithering = badDithering; var result = _validator.Validate(null, new FramesOptions { Frames = { frame } }); Assert.False(result.Succeeded); Assert.Contains(result.Failures!, f => f.Contains("Dithering")); } [Fact] public void Palette_FewerThanTwoEntries_Fails() { var frame = ValidFrame(); frame.Palette = new List { new() { Name = "black", Color = "#000000", DeviceColor = "#000000" }, }; var result = _validator.Validate(null, new FramesOptions { Frames = { frame } }); Assert.False(result.Succeeded); Assert.Contains(result.Failures!, f => f.Contains("at least 2 entries")); } [Fact] public void Palette_EntryWithBadHex_Fails() { var frame = ValidFrame(); frame.Palette[0].Color = "not-a-color"; var result = _validator.Validate(null, new FramesOptions { Frames = { frame } }); Assert.False(result.Succeeded); Assert.Contains(result.Failures!, f => f.Contains("Color 'not-a-color' is not a valid hex color")); } [Fact] public void Palette_EntryWithBadDeviceHex_Fails() { var frame = ValidFrame(); frame.Palette[1].DeviceColor = "zzzzzz"; var result = _validator.Validate(null, new FramesOptions { Frames = { frame } }); Assert.False(result.Succeeded); Assert.Contains(result.Failures!, f => f.Contains("DeviceColor 'zzzzzz' is not a valid hex color")); } [Fact] public void Palette_EntryMissingName_Fails() { var frame = ValidFrame(); frame.Palette[0].Name = ""; var result = _validator.Validate(null, new FramesOptions { Frames = { frame } }); Assert.False(result.Succeeded); Assert.Contains(result.Failures!, f => f.Contains("Palette[0].Name is required")); } [Fact] public void DuplicateName_Fails() { var a = ValidFrame(); var b = ValidFrame(); b.Mac = "11:22:33:44:55:66"; var result = _validator.Validate(null, new FramesOptions { Frames = { a, b } }); Assert.False(result.Succeeded); Assert.Contains(result.Failures!, f => f.Contains("Name 'living-room' is a duplicate")); } [Fact] public void DuplicateMac_DetectedAfterCanonicalization() { var a = ValidFrame(); a.Mac = "AA:BB:CC:DD:EE:FF"; var b = ValidFrame(); b.Name = "kitchen"; b.Mac = "aabbccddeeff"; var result = _validator.Validate(null, new FramesOptions { Frames = { a, b } }); Assert.False(result.Succeeded); Assert.Contains(result.Failures!, f => f.Contains("is a duplicate")); } [Fact] public void AllErrors_AreReportedTogether() { var frame = new FrameOptions { Name = "bad name", Mac = "not-a-mac", Resolution = null, Orientation = "diagonal", Dithering = "ordered", Palette = new List(), }; var result = _validator.Validate(null, new FramesOptions { Frames = { frame } }); Assert.False(result.Succeeded); var failures = result.Failures!.ToList(); Assert.Contains(failures, f => f.Contains("Name")); Assert.Contains(failures, f => f.Contains("Mac")); Assert.Contains(failures, f => f.Contains("Resolution is required")); Assert.Contains(failures, f => f.Contains("Orientation")); Assert.Contains(failures, f => f.Contains("Dithering")); Assert.Contains(failures, f => f.Contains("at least 2 entries")); } private static FrameOptions ValidFrame() => new() { Name = "living-room", Mac = "AA:BB:CC:DD:EE:FF", Resolution = new ResolutionOptions { Width = 1600, Height = 1200 }, Orientation = "landscape", Dithering = "floyd-steinberg", Palette = new List { new() { Name = "black", Color = "#1F2226", DeviceColor = "#000000" }, new() { Name = "white", Color = "#B9C7C9", DeviceColor = "#FFFFFF" }, }, }; private static string JoinFailures(Microsoft.Extensions.Options.ValidateOptionsResult result) => result.Failures is null ? string.Empty : string.Join("; ", result.Failures); }