mirror of
https://github.com/alexhopeoconnor/dnsmasq-webui.git
synced 2026-10-03 22:12:00 +10:00
Tests: add real-world config coverage and prune brittle cases
Reduce presentation-focused test churn while adding corpus-based parser, validator, capability-guard, and round-trip save coverage to better protect future refactors.
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
using DnsmasqWebUI.Infrastructure.Services.EffectiveConfig.Metadata;
|
||||
|
||||
namespace DnsmasqWebUI.Tests.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Sample config line per option for consistent parser coverage. When adding new options to the app,
|
||||
/// add an entry here so the option-coverage test continues to exercise every option.
|
||||
/// </summary>
|
||||
public static class OptionCoverageData
|
||||
{
|
||||
/// <summary>Option key and a minimal valid config line (without newline).</summary>
|
||||
public static IReadOnlyList<(string OptionKey, string ConfigLine)> GetParserCoverageEntries()
|
||||
{
|
||||
return new (string, string)[]
|
||||
{
|
||||
// LastWins
|
||||
(DnsmasqConfKeys.Port, "port=53"),
|
||||
(DnsmasqConfKeys.CacheSize, "cache-size=500"),
|
||||
(DnsmasqConfKeys.LocalTtl, "local-ttl=300"),
|
||||
(DnsmasqConfKeys.MxTarget, "mx-target=mail.example.com"),
|
||||
(DnsmasqConfKeys.DhcpLeasefile, "dhcp-leasefile=/var/lib/dnsmasq.leases"),
|
||||
(DnsmasqConfKeys.DhcpLeaseMax, "dhcp-lease-max=1000"),
|
||||
(DnsmasqConfKeys.Hostsdir, "hostsdir=/etc/dnsmasq.d"),
|
||||
(DnsmasqConfKeys.PidFile, "pid-file=/run/dnsmasq.pid"),
|
||||
(DnsmasqConfKeys.User, "user=nobody"),
|
||||
(DnsmasqConfKeys.TftpRoot, "tftp-root=/var/lib/tftpboot"),
|
||||
// Flag
|
||||
(DnsmasqConfKeys.NoHosts, "no-hosts"),
|
||||
(DnsmasqConfKeys.ExpandHosts, "expand-hosts"),
|
||||
(DnsmasqConfKeys.BogusPriv, "bogus-priv"),
|
||||
(DnsmasqConfKeys.NoResolv, "no-resolv"),
|
||||
(DnsmasqConfKeys.DomainNeeded, "domain-needed"),
|
||||
(DnsmasqConfKeys.StrictOrder, "strict-order"),
|
||||
(DnsmasqConfKeys.AllServers, "all-servers"),
|
||||
(DnsmasqConfKeys.BindInterfaces, "bind-interfaces"),
|
||||
(DnsmasqConfKeys.DhcpAuthoritative, "dhcp-authoritative"),
|
||||
(DnsmasqConfKeys.EnableTftp, "enable-tftp"),
|
||||
(DnsmasqConfKeys.Dnssec, "dnssec"),
|
||||
(DnsmasqConfKeys.Conntrack, "conntrack"),
|
||||
(DnsmasqConfKeys.Do0x20Encode, "do-0x20-encode"),
|
||||
(DnsmasqConfKeys.No0x20Encode, "no-0x20-encode"),
|
||||
// Multi - resolver / DNS
|
||||
(DnsmasqConfKeys.Server, "server=1.1.1.1"),
|
||||
(DnsmasqConfKeys.Local, "local=/lan/"),
|
||||
(DnsmasqConfKeys.RevServer, "rev-server=192.168.1.0/24,10.0.0.1"),
|
||||
(DnsmasqConfKeys.Address, "address=/example.com/127.0.0.1"),
|
||||
(DnsmasqConfKeys.Domain, "domain=home.lan,192.168.1.0/24"),
|
||||
(DnsmasqConfKeys.Cname, "cname=www.example.com,example.com"),
|
||||
(DnsmasqConfKeys.MxHost, "mx-host=example.com,mail.example.com,10"),
|
||||
(DnsmasqConfKeys.Srv, "srv-host=_http._tcp.example.com,host.example.com,80"),
|
||||
(DnsmasqConfKeys.PtrRecord, "ptr-record=1.168.192.in-addr.arpa,router.lan"),
|
||||
(DnsmasqConfKeys.TxtRecord, "txt-record=example.com,text"),
|
||||
(DnsmasqConfKeys.NaptrRecord, "naptr-record=example.com,0,0,a,s,r"),
|
||||
(DnsmasqConfKeys.DnsRr, "dns-rr=example.com,16,01:02"),
|
||||
(DnsmasqConfKeys.HostRecord, "host-record=host.example.com,1.2.3.4"),
|
||||
(DnsmasqConfKeys.DynamicHost, "dynamic-host=example.com"),
|
||||
(DnsmasqConfKeys.CaaRecord, "caa-record=example.com,0,issue,letsencrypt.org"),
|
||||
(DnsmasqConfKeys.RebindDomainOk, "rebind-domain-ok=example.com"),
|
||||
(DnsmasqConfKeys.BogusNxdomain, "bogus-nxdomain=192.168.1.1"),
|
||||
(DnsmasqConfKeys.IgnoreAddress, "ignore-address=192.168.1.1"),
|
||||
(DnsmasqConfKeys.Alias, "alias=0.0.0.0,example.com"),
|
||||
(DnsmasqConfKeys.FilterRr, "filter-rr=0.0.0.0,A"),
|
||||
(DnsmasqConfKeys.CacheRr, "cache-rr=0.0.0.0,A"),
|
||||
(DnsmasqConfKeys.Ipset, "ipset=/example.com/set1"),
|
||||
(DnsmasqConfKeys.Nftset, "nftset=/example.com/table/set"),
|
||||
(DnsmasqConfKeys.ConnmarkAllowlist, "connmark-allowlist=0xff,example.com"),
|
||||
// Multi - auth / synth
|
||||
(DnsmasqConfKeys.AuthServer, "auth-server=zone.example.com,eth0"),
|
||||
(DnsmasqConfKeys.SynthDomain, "synth-domain=dynamic.example.com,192.168.2.1,192.168.2.100"),
|
||||
(DnsmasqConfKeys.AuthZone, "auth-zone=example.com"),
|
||||
(DnsmasqConfKeys.TrustAnchor, "trust-anchor=.,20326,8,2,E4F1769B8A0E3142E125223716F878F0A3B2C3D4"),
|
||||
// Multi - DHCP
|
||||
(DnsmasqConfKeys.DhcpRange, "dhcp-range=192.168.1.50,192.168.1.150,12h"),
|
||||
(DnsmasqConfKeys.DhcpHost, "dhcp-host=aa:bb:cc:dd:ee:ff,192.168.1.10"),
|
||||
(DnsmasqConfKeys.DhcpOption, "dhcp-option=3,192.168.1.1"),
|
||||
(DnsmasqConfKeys.DhcpMatch, "dhcp-match=set:efi,option:client-arch,6"),
|
||||
(DnsmasqConfKeys.DhcpMac, "dhcp-mac=set:vendor,aa:bb:cc:dd:ee:ff"),
|
||||
(DnsmasqConfKeys.DhcpNameMatch, "dhcp-name-match=set:name,hostname*"),
|
||||
(DnsmasqConfKeys.DhcpIgnoreNames, "dhcp-ignore-names=tag:guest"),
|
||||
(DnsmasqConfKeys.DhcpBoot, "dhcp-boot=pxelinux.0"),
|
||||
(DnsmasqConfKeys.Leasequery, "leasequery=10.0.0.0/24"),
|
||||
(DnsmasqConfKeys.RaParam, "ra-param=eth0,high"),
|
||||
(DnsmasqConfKeys.Slaac, "slaac=eth0"),
|
||||
(DnsmasqConfKeys.PxeService, "pxe-service=x86PC,Install Linux"),
|
||||
(DnsmasqConfKeys.DhcpRelay, "dhcp-relay=192.168.1.1,eth0"),
|
||||
(DnsmasqConfKeys.DhcpCircuitid, "dhcp-circuitid=eth0,010203"),
|
||||
(DnsmasqConfKeys.DhcpRemoteid, "dhcp-remoteid=010203"),
|
||||
(DnsmasqConfKeys.DhcpSubscrid, "dhcp-subscrid=010203"),
|
||||
(DnsmasqConfKeys.TagIf, "tag-if=set:tag,eth0"),
|
||||
(DnsmasqConfKeys.SharedNetwork, "shared-network=name,192.168.1.0/24"),
|
||||
(DnsmasqConfKeys.DhcpOptionPxe, "dhcp-option-pxe=set:known,1"),
|
||||
(DnsmasqConfKeys.DhcpVendorclass, "dhcp-vendorclass=set:pxe,PXEClient"),
|
||||
(DnsmasqConfKeys.DhcpUserclass, "dhcp-userclass=set:efi,EFI"),
|
||||
(DnsmasqConfKeys.AddnHosts, "addn-hosts=/etc/hosts.extra"),
|
||||
(DnsmasqConfKeys.Interface, "interface=eth0"),
|
||||
(DnsmasqConfKeys.ListenAddress, "listen-address=127.0.0.1"),
|
||||
(DnsmasqConfKeys.ResolvFile, "resolv-file=/etc/resolv.dnsmasq"),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace DnsmasqWebUI.Tests.Helpers;
|
||||
|
||||
/// <summary>Expected parser/effective state for a real-world corpus case. Counts and booleans only.</summary>
|
||||
public sealed class CorpusExpected
|
||||
{
|
||||
public int? ServerCountMin { get; set; }
|
||||
public int? LocalCountMin { get; set; }
|
||||
public int? AddressCountMin { get; set; }
|
||||
public int? CacheSize { get; set; }
|
||||
public int? AddnHostsCountMin { get; set; }
|
||||
public int? DomainCountMin { get; set; }
|
||||
public bool? NoHosts { get; set; }
|
||||
public bool? NoResolv { get; set; }
|
||||
public bool? BogusPriv { get; set; }
|
||||
public string? Do0x20State { get; set; }
|
||||
public int? Port { get; set; }
|
||||
public bool? ExpandHosts { get; set; }
|
||||
public int? CnameCountMin { get; set; }
|
||||
public int? TxtRecordCountMin { get; set; }
|
||||
public int? SrvCountMin { get; set; }
|
||||
public int? PtrRecordCountMin { get; set; }
|
||||
public int? MxHostCountMin { get; set; }
|
||||
public int? AuthZoneCountMin { get; set; }
|
||||
public int? SynthDomainCountMin { get; set; }
|
||||
public int? AuthServerCountMin { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>Single real-world corpus case from dnsmasq-real-cases.json.</summary>
|
||||
public sealed class CorpusCase
|
||||
{
|
||||
public string File { get; set; } = "";
|
||||
public CorpusExpected Expected { get; set; } = new();
|
||||
public List<string> SemanticInvalidOptions { get; set; } = [];
|
||||
}
|
||||
|
||||
/// <summary>Loads real-world corpus index and resolves paths via TestDataHelper.</summary>
|
||||
public static class RealWorldCasesHelper
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNameCaseInsensitive = true };
|
||||
|
||||
public static IReadOnlyList<CorpusCase> LoadCases()
|
||||
{
|
||||
var path = TestDataHelper.GetPath("real-world/dnsmasq-real-cases.json");
|
||||
var json = File.ReadAllText(path);
|
||||
return JsonSerializer.Deserialize<List<CorpusCase>>(json, JsonOptions) ?? [];
|
||||
}
|
||||
|
||||
public static string Resolve(CorpusCase c) => TestDataHelper.GetPath(c.File);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using DnsmasqWebUI.Infrastructure.Services.EffectiveConfig.Metadata;
|
||||
using DnsmasqWebUI.Infrastructure.Serialization.Parsers.DnsmasqConfig;
|
||||
|
||||
namespace DnsmasqWebUI.Tests.Helpers;
|
||||
|
||||
/// <summary>Ensures corpus index and files stay in sync; fails fast when testdata drifts.</summary>
|
||||
public class RealWorldCorpusIntegrityTests
|
||||
{
|
||||
[Fact]
|
||||
public void EveryCorpusCase_PointsToExistingFile()
|
||||
{
|
||||
var cases = RealWorldCasesHelper.LoadCases();
|
||||
Assert.NotEmpty(cases);
|
||||
foreach (var c in cases)
|
||||
{
|
||||
var path = RealWorldCasesHelper.Resolve(c);
|
||||
Assert.True(File.Exists(path), $"Corpus file missing: {c.File}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EveryBadCase_DeclaresSemanticInvalidOptions()
|
||||
{
|
||||
var cases = RealWorldCasesHelper.LoadCases();
|
||||
var badFiles = new[] { "bad/", "real-world/bad/" };
|
||||
foreach (var c in cases)
|
||||
{
|
||||
var isBad = badFiles.Any(p => c.File.Contains(p, StringComparison.Ordinal));
|
||||
if (isBad)
|
||||
Assert.NotEmpty(c.SemanticInvalidOptions);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SemanticInvalidOptions_ArePresentInParsedValues()
|
||||
{
|
||||
foreach (var c in RealWorldCasesHelper.LoadCases())
|
||||
{
|
||||
if (c.SemanticInvalidOptions.Count == 0)
|
||||
continue;
|
||||
var mainPath = RealWorldCasesHelper.Resolve(c);
|
||||
var paths = DnsmasqConfIncludeParser.GetIncludedPaths(mainPath);
|
||||
foreach (var option in c.SemanticInvalidOptions)
|
||||
{
|
||||
var values = DnsmasqConfIncludeParser.GetMultiValueFromConfigFiles(paths, option);
|
||||
Assert.NotEmpty(values);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
|
||||
|
||||
namespace DnsmasqWebUI.Tests.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="ConfigValueSource"/> and <see cref="ConfigValueSource.GetReadOnlyTooltip"/>.
|
||||
/// </summary>
|
||||
public class ConfigValueSourceTests
|
||||
{
|
||||
[Fact]
|
||||
public void GetReadOnlyTooltip_WhenManaged_ReturnsNull()
|
||||
{
|
||||
var source = new ConfigValueSource("/data/zz-managed.conf", "zz-managed.conf", IsManaged: true, LineNumber: 5);
|
||||
Assert.Null(source.GetReadOnlyTooltip());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetReadOnlyTooltip_WhenReadOnlyWithoutLineNumber_ReturnsFileNameOnly()
|
||||
{
|
||||
var fileName = "02.conf";
|
||||
var fullPath = $"/etc/dnsmasq.d/{fileName}";
|
||||
var source = new ConfigValueSource(fullPath, fileName, IsManaged: false, LineNumber: null);
|
||||
var tooltip = source.GetReadOnlyTooltip();
|
||||
Assert.NotNull(tooltip);
|
||||
Assert.Contains(fileName, tooltip);
|
||||
Assert.Contains("readonly", tooltip);
|
||||
Assert.Contains(fullPath, tooltip);
|
||||
Assert.DoesNotContain("line", tooltip);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetReadOnlyTooltip_WhenReadOnlyWithLineNumber_IncludesLineNumber()
|
||||
{
|
||||
var fileName = "02.conf";
|
||||
var fullPath = $"/etc/dnsmasq.d/{fileName}";
|
||||
int? lineNumber = 3;
|
||||
var source = new ConfigValueSource(fullPath, fileName, IsManaged: false, LineNumber: lineNumber);
|
||||
var tooltip = source.GetReadOnlyTooltip();
|
||||
Assert.NotNull(tooltip);
|
||||
Assert.Contains(fileName, tooltip);
|
||||
Assert.Contains($"line {lineNumber}", tooltip);
|
||||
Assert.Contains("readonly", tooltip);
|
||||
Assert.Contains(fullPath, tooltip);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsReadOnly_WhenManaged_IsFalse()
|
||||
{
|
||||
var source = new ConfigValueSource("/data/zz.conf", "zz.conf", IsManaged: true);
|
||||
Assert.False(source.IsReadOnly);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsReadOnly_WhenNotManaged_IsTrue()
|
||||
{
|
||||
var source = new ConfigValueSource("/etc/dnsmasq.conf", "dnsmasq.conf", IsManaged: false);
|
||||
Assert.True(source.IsReadOnly);
|
||||
}
|
||||
}
|
||||
+19
-26
@@ -80,32 +80,6 @@ public class DnsmasqConfIncludeParserOfficialExampleTests
|
||||
Assert.False(DnsmasqConfIncludeParser.GetFlagFromConfigFiles(paths, DnsmasqConfKeys.LogDhcp));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OfficialExample_FileContainsExpectedOptionNames()
|
||||
{
|
||||
var path = GetOfficialExamplePath();
|
||||
var content = File.ReadAllText(path);
|
||||
// Sanity: official example documents the options we support (as commented lines).
|
||||
Assert.Contains("read-ethers", content);
|
||||
Assert.Contains("dhcp-option-force", content);
|
||||
Assert.Contains("dhcp-rapid-commit", content);
|
||||
Assert.Contains("dhcp-script", content);
|
||||
Assert.Contains("tftp-no-fail", content);
|
||||
Assert.Contains("tftp-no-blocksize", content);
|
||||
Assert.Contains("mx-target", content);
|
||||
Assert.Contains("localmx", content);
|
||||
Assert.Contains("selfmx", content);
|
||||
Assert.Contains("enable-ra", content);
|
||||
Assert.Contains("log-dhcp", content);
|
||||
Assert.Contains("ipset=", content);
|
||||
Assert.Contains("nftset=", content);
|
||||
Assert.Contains("dhcp-mac", content);
|
||||
Assert.Contains("dhcp-name-match", content);
|
||||
Assert.Contains("dhcp-ignore-names", content);
|
||||
// SRV records use option name srv-host (not "srv")
|
||||
Assert.Contains("srv-host", content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetAddnHostsPathsFromConfigFiles_OfficialExample_AllCommented_ReturnsEmpty()
|
||||
{
|
||||
@@ -124,4 +98,23 @@ public class DnsmasqConfIncludeParserOfficialExampleTests
|
||||
var noHosts = DnsmasqConfIncludeParser.GetNoHostsFromConfigFiles(paths);
|
||||
Assert.False(noHosts);
|
||||
}
|
||||
|
||||
/// <summary>Official-like sample with commented and active lines: commented ignored, active parsed, state reflects active only.</summary>
|
||||
[Fact]
|
||||
public void OfficialLikeActive_CommentedLinesIgnored_ActiveLinesParsed()
|
||||
{
|
||||
var mainPath = TestDataHelper.GetPath("real-world/edge/dnsmasq-real-official-like-active.conf");
|
||||
Assert.True(File.Exists(mainPath));
|
||||
var paths = DnsmasqConfIncludeParser.GetIncludedPaths(mainPath);
|
||||
|
||||
var (portVal, _) = DnsmasqConfIncludeParser.GetLastValueFromConfigFiles(paths, DnsmasqConfKeys.Port);
|
||||
Assert.NotNull(portVal);
|
||||
Assert.True(int.TryParse(portVal, out var port) && port == 5353);
|
||||
|
||||
var servers = DnsmasqConfIncludeParser.GetMultiValueFromConfigFiles(paths, DnsmasqConfKeys.Server);
|
||||
Assert.Single(servers);
|
||||
Assert.Equal("1.1.1.1", servers[0]);
|
||||
|
||||
Assert.True(DnsmasqConfIncludeParser.GetFlagFromConfigFiles(paths, DnsmasqConfKeys.ExpandHosts));
|
||||
}
|
||||
}
|
||||
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
using DnsmasqWebUI.Infrastructure.Services.EffectiveConfig.Metadata;
|
||||
using DnsmasqWebUI.Infrastructure.Serialization.Parsers.DnsmasqConfig;
|
||||
using DnsmasqWebUI.Tests.Helpers;
|
||||
|
||||
namespace DnsmasqWebUI.Tests.Serialization.Parsers.DnsmasqConfig;
|
||||
|
||||
/// <summary>
|
||||
/// Ensures every option in the coverage list is parsed consistently: one config line per option,
|
||||
/// dispatch by parser behavior (Flag / LastWins / Multi), assert we get a value back.
|
||||
/// Add new options to <see cref="OptionCoverageData.GetParserCoverageEntries"/> when adding to the app.
|
||||
/// </summary>
|
||||
public class DnsmasqConfIncludeParserOptionCoverageTests
|
||||
{
|
||||
public static IEnumerable<object[]> GetCoverageEntries()
|
||||
{
|
||||
foreach (var (optionKey, configLine) in OptionCoverageData.GetParserCoverageEntries())
|
||||
yield return new object[] { optionKey, configLine };
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(GetCoverageEntries))]
|
||||
public void EveryOption_ParseSingleLine_ReturnsExpectedResult(string optionKey, string configLine)
|
||||
{
|
||||
var dir = Path.Combine(Path.GetTempPath(), "dnsmasq-opt-" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(dir);
|
||||
var conf = Path.Combine(dir, "dnsmasq.conf");
|
||||
try
|
||||
{
|
||||
File.WriteAllText(conf, configLine + "\n");
|
||||
var paths = new[] { conf };
|
||||
|
||||
if (optionKey == DnsmasqConfKeys.AddnHosts)
|
||||
{
|
||||
var addn = DnsmasqConfIncludeParser.GetAddnHostsPathsFromConfigFiles(paths);
|
||||
Assert.True(addn.Count >= 1, $"addn-hosts should parse from: {configLine}");
|
||||
return;
|
||||
}
|
||||
|
||||
var behavior = EffectiveConfigParserBehaviorMap.GetBehavior(optionKey);
|
||||
|
||||
switch (behavior)
|
||||
{
|
||||
case EffectiveConfigParserBehavior.Flag:
|
||||
{
|
||||
var flag = DnsmasqConfIncludeParser.GetFlagFromConfigFiles(paths, optionKey);
|
||||
Assert.True(flag, $"Flag option {optionKey} should be true when set.");
|
||||
break;
|
||||
}
|
||||
case EffectiveConfigParserBehavior.LastWins:
|
||||
{
|
||||
var (value, _) = DnsmasqConfIncludeParser.GetLastValueFromConfigFiles(paths, optionKey);
|
||||
Assert.NotNull(value);
|
||||
Assert.True(value!.Length > 0, $"LastWins option {optionKey} should return non-empty value.");
|
||||
break;
|
||||
}
|
||||
case EffectiveConfigParserBehavior.Multi:
|
||||
{
|
||||
var list = DnsmasqConfIncludeParser.GetMultiValueFromConfigFiles(paths, optionKey);
|
||||
Assert.True(list.Count >= 1, $"Multi option {optionKey} should return at least one value.");
|
||||
break;
|
||||
}
|
||||
default:
|
||||
Assert.Fail($"Unknown behavior for {optionKey}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (Directory.Exists(dir))
|
||||
Directory.Delete(dir, recursive: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
using DnsmasqWebUI.Infrastructure.Services.EffectiveConfig.Metadata;
|
||||
using DnsmasqWebUI.Infrastructure.Serialization.Parsers.DnsmasqConfig;
|
||||
using DnsmasqWebUI.Tests.Helpers;
|
||||
|
||||
namespace DnsmasqWebUI.Tests.Serialization.Parsers.DnsmasqConfig;
|
||||
|
||||
/// <summary>
|
||||
/// Corpus-driven parser tests using testdata/real-world. Asserts counts, booleans, and effective state only.
|
||||
/// </summary>
|
||||
public class DnsmasqConfIncludeParserRealWorldCorpusTests
|
||||
{
|
||||
public static IEnumerable<object[]> GetCases()
|
||||
{
|
||||
foreach (var c in RealWorldCasesHelper.LoadCases())
|
||||
yield return new object[] { c };
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(GetCases))]
|
||||
public void GetIncludedPaths_RealWorldCorpus_ReturnsAtLeastMain(CorpusCase c)
|
||||
{
|
||||
var mainPath = RealWorldCasesHelper.Resolve(c);
|
||||
Assert.True(File.Exists(mainPath), $"Corpus file missing: {c.File}");
|
||||
|
||||
var paths = DnsmasqConfIncludeParser.GetIncludedPaths(mainPath);
|
||||
Assert.NotNull(paths);
|
||||
Assert.NotEmpty(paths);
|
||||
Assert.Contains(Path.GetFullPath(mainPath), paths);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(GetCases))]
|
||||
public void ParseEffectiveSignals_FromRealWorldCorpus(CorpusCase c)
|
||||
{
|
||||
var e = c.Expected;
|
||||
var hasAny = e.ServerCountMin is not null || e.NoHosts is not null || e.Do0x20State is not null ||
|
||||
e.Port is not null || e.ExpandHosts is not null || e.LocalCountMin is not null ||
|
||||
e.AddressCountMin is not null || e.CacheSize is not null || e.AddnHostsCountMin is not null ||
|
||||
e.DomainCountMin is not null || e.NoResolv is not null || e.BogusPriv is not null ||
|
||||
e.CnameCountMin is not null || e.TxtRecordCountMin is not null || e.SrvCountMin is not null ||
|
||||
e.PtrRecordCountMin is not null || e.MxHostCountMin is not null || e.AuthZoneCountMin is not null ||
|
||||
e.SynthDomainCountMin is not null || e.AuthServerCountMin is not null;
|
||||
if (!hasAny)
|
||||
return;
|
||||
|
||||
var mainPath = RealWorldCasesHelper.Resolve(c);
|
||||
var paths = DnsmasqConfIncludeParser.GetIncludedPaths(mainPath);
|
||||
|
||||
if (e.ServerCountMin is int minServers)
|
||||
{
|
||||
var servers = DnsmasqConfIncludeParser.GetMultiValueFromConfigFiles(paths, DnsmasqConfKeys.Server);
|
||||
Assert.True(servers.Count >= minServers, $"Expected at least {minServers} server(s) for {c.File}");
|
||||
}
|
||||
if (e.LocalCountMin is int minLocal)
|
||||
{
|
||||
var locals = DnsmasqConfIncludeParser.GetMultiValueFromConfigFiles(paths, DnsmasqConfKeys.Local);
|
||||
Assert.True(locals.Count >= minLocal, $"Expected at least {minLocal} local(s) for {c.File}");
|
||||
}
|
||||
if (e.AddressCountMin is int minAddr)
|
||||
{
|
||||
var addrs = DnsmasqConfIncludeParser.GetMultiValueFromConfigFiles(paths, DnsmasqConfKeys.Address);
|
||||
Assert.True(addrs.Count >= minAddr, $"Expected at least {minAddr} address(es) for {c.File}");
|
||||
}
|
||||
if (e.CacheSize is int cacheSize)
|
||||
{
|
||||
var (val, _) = DnsmasqConfIncludeParser.GetLastValueFromConfigFiles(paths, DnsmasqConfKeys.CacheSize);
|
||||
Assert.NotNull(val);
|
||||
Assert.True(int.TryParse(val, out var n) && n == cacheSize);
|
||||
}
|
||||
if (e.AddnHostsCountMin is int minAddn)
|
||||
{
|
||||
var addn = DnsmasqConfIncludeParser.GetAddnHostsPathsFromConfigFiles(paths);
|
||||
Assert.True(addn.Count >= minAddn);
|
||||
}
|
||||
if (e.DomainCountMin is int minDomain)
|
||||
{
|
||||
var domains = DnsmasqConfIncludeParser.GetMultiValueFromConfigFiles(paths, DnsmasqConfKeys.Domain);
|
||||
Assert.True(domains.Count >= minDomain);
|
||||
}
|
||||
if (e.NoHosts is bool noHostsExpected)
|
||||
{
|
||||
var noHosts = DnsmasqConfIncludeParser.GetNoHostsFromConfigFiles(paths);
|
||||
Assert.Equal(noHostsExpected, noHosts);
|
||||
}
|
||||
if (e.NoResolv is true)
|
||||
{
|
||||
Assert.True(DnsmasqConfIncludeParser.GetFlagFromConfigFiles(paths, DnsmasqConfKeys.NoResolv));
|
||||
}
|
||||
if (e.BogusPriv is true)
|
||||
{
|
||||
Assert.True(DnsmasqConfIncludeParser.GetFlagFromConfigFiles(paths, DnsmasqConfKeys.BogusPriv));
|
||||
}
|
||||
if (e.Do0x20State == "Disabled")
|
||||
{
|
||||
Assert.True(DnsmasqConfIncludeParser.GetFlagFromConfigFiles(paths, DnsmasqConfKeys.No0x20Encode));
|
||||
}
|
||||
if (e.Port is int portExpected)
|
||||
{
|
||||
var (portVal, _) = DnsmasqConfIncludeParser.GetLastValueFromConfigFiles(paths, DnsmasqConfKeys.Port);
|
||||
Assert.NotNull(portVal);
|
||||
Assert.True(int.TryParse(portVal, out var port) && port == portExpected);
|
||||
}
|
||||
if (e.ExpandHosts is true)
|
||||
{
|
||||
Assert.True(DnsmasqConfIncludeParser.GetFlagFromConfigFiles(paths, DnsmasqConfKeys.ExpandHosts));
|
||||
}
|
||||
if (e.CnameCountMin is int minCname)
|
||||
{
|
||||
var list = DnsmasqConfIncludeParser.GetMultiValueFromConfigFiles(paths, DnsmasqConfKeys.Cname);
|
||||
Assert.True(list.Count >= minCname);
|
||||
}
|
||||
if (e.TxtRecordCountMin is int minTxt)
|
||||
{
|
||||
var list = DnsmasqConfIncludeParser.GetMultiValueFromConfigFiles(paths, DnsmasqConfKeys.TxtRecord);
|
||||
Assert.True(list.Count >= minTxt);
|
||||
}
|
||||
if (e.SrvCountMin is int minSrv)
|
||||
{
|
||||
var list = DnsmasqConfIncludeParser.GetMultiValueFromConfigFiles(paths, DnsmasqConfKeys.Srv);
|
||||
Assert.True(list.Count >= minSrv);
|
||||
}
|
||||
if (e.PtrRecordCountMin is int minPtr)
|
||||
{
|
||||
var list = DnsmasqConfIncludeParser.GetMultiValueFromConfigFiles(paths, DnsmasqConfKeys.PtrRecord);
|
||||
Assert.True(list.Count >= minPtr);
|
||||
}
|
||||
if (e.MxHostCountMin is int minMx)
|
||||
{
|
||||
var list = DnsmasqConfIncludeParser.GetMultiValueFromConfigFiles(paths, DnsmasqConfKeys.MxHost);
|
||||
Assert.True(list.Count >= minMx);
|
||||
}
|
||||
if (e.AuthZoneCountMin is int minAz)
|
||||
{
|
||||
var list = DnsmasqConfIncludeParser.GetMultiValueFromConfigFiles(paths, DnsmasqConfKeys.AuthZone);
|
||||
Assert.True(list.Count >= minAz);
|
||||
}
|
||||
if (e.SynthDomainCountMin is int minSd)
|
||||
{
|
||||
var list = DnsmasqConfIncludeParser.GetMultiValueFromConfigFiles(paths, DnsmasqConfKeys.SynthDomain);
|
||||
Assert.True(list.Count >= minSd);
|
||||
}
|
||||
if (e.AuthServerCountMin is int minAs)
|
||||
{
|
||||
var list = DnsmasqConfIncludeParser.GetMultiValueFromConfigFiles(paths, DnsmasqConfKeys.AuthServer);
|
||||
Assert.True(list.Count >= minAs);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ using DnsmasqWebUI.Models.Config;
|
||||
using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using DnsmasqWebUI.Tests.Helpers;
|
||||
|
||||
namespace DnsmasqWebUI.Tests.Services.Dnsmasq.Config;
|
||||
|
||||
@@ -134,4 +135,52 @@ public class ConfigSetCacheEffectiveConfigTests
|
||||
if (Directory.Exists(dir)) Directory.Delete(dir, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RealWorldCorpus_EdgeCommentedActiveMix_Do0x20StateAndSource()
|
||||
{
|
||||
var mainPath = TestDataHelper.GetPath("real-world/edge/dnsmasq-real-edge-commented-active-mix.conf");
|
||||
Assert.True(File.Exists(mainPath));
|
||||
var dir = Path.GetDirectoryName(mainPath)!;
|
||||
var managedName = "zz-managed.conf";
|
||||
var options = Options.Create(new DnsmasqOptions { MainConfigPath = mainPath, ManagedFileName = managedName });
|
||||
using var cache = new ConfigSetCache(options, NullLogger<ConfigSetCache>.Instance);
|
||||
cache.Invalidate();
|
||||
var snapshot = await cache.GetSnapshotAsync();
|
||||
Assert.Equal(ExplicitToggleState.Disabled, snapshot.Config.Do0x20EncodeState);
|
||||
Assert.NotNull(snapshot.Sources.Do0x20Encode);
|
||||
Assert.Single(snapshot.Config.ServerValues);
|
||||
Assert.Equal("1.1.1.1", snapshot.Config.ServerValues[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RealWorldCorpus_GoodHomeBasic_ServerCountAndSource()
|
||||
{
|
||||
var mainPath = TestDataHelper.GetPath("real-world/good/dnsmasq-real-home-basic.conf");
|
||||
Assert.True(File.Exists(mainPath));
|
||||
var options = Options.Create(new DnsmasqOptions { MainConfigPath = mainPath, ManagedFileName = "zz-managed.conf" });
|
||||
using var cache = new ConfigSetCache(options, NullLogger<ConfigSetCache>.Instance);
|
||||
cache.Invalidate();
|
||||
var snapshot = await cache.GetSnapshotAsync();
|
||||
Assert.True(snapshot.Config.ServerValues.Count >= 2);
|
||||
Assert.Equal(2, snapshot.Sources.ServerValues.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RealWorldCorpus_GoodResolverRich_LocalAddressCacheSizeFlags()
|
||||
{
|
||||
var mainPath = TestDataHelper.GetPath("real-world/good/dnsmasq-real-resolver-rich.conf");
|
||||
Assert.True(File.Exists(mainPath));
|
||||
var options = Options.Create(new DnsmasqOptions { MainConfigPath = mainPath, ManagedFileName = "zz-managed.conf" });
|
||||
using var cache = new ConfigSetCache(options, NullLogger<ConfigSetCache>.Instance);
|
||||
cache.Invalidate();
|
||||
var snapshot = await cache.GetSnapshotAsync();
|
||||
Assert.True(snapshot.Config.ServerValues.Count >= 2);
|
||||
Assert.True(snapshot.Config.LocalValues.Count >= 2);
|
||||
Assert.True(snapshot.Config.AddressValues.Count >= 2);
|
||||
Assert.Equal(1000, snapshot.Config.CacheSize);
|
||||
Assert.True(snapshot.Config.NoResolv);
|
||||
Assert.True(snapshot.Config.BogusPriv);
|
||||
Assert.True(snapshot.Config.ExpandHosts);
|
||||
}
|
||||
}
|
||||
|
||||
+134
@@ -303,6 +303,140 @@ public class DnsmasqConfigServiceApplyChangesTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ApplyChanges_Do0x20Enabled_RoundTripsToEffectiveState()
|
||||
{
|
||||
var dir = Path.Combine(Path.GetTempPath(), "dnsmasq-roundtrip-" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(dir);
|
||||
var mainPath = Path.Combine(dir, "dnsmasq.conf");
|
||||
var managedName = "zz-managed.conf";
|
||||
ConfigSetCache? cache = null;
|
||||
try
|
||||
{
|
||||
File.WriteAllText(mainPath, "port=53\n");
|
||||
var options = Options.Create(new DnsmasqOptions { MainConfigPath = mainPath, ManagedFileName = managedName });
|
||||
cache = new ConfigSetCache(options, NullLogger<ConfigSetCache>.Instance);
|
||||
var setService = new DnsmasqConfigSetService(cache);
|
||||
var configService = new DnsmasqConfigService(setService, cache, NullLogger<DnsmasqConfigService>.Instance);
|
||||
var changes = new List<PendingEffectiveConfigChange>
|
||||
{
|
||||
new(EffectiveConfigSections.SectionResolver, DnsmasqConfKeys.Do0x20Encode, ExplicitToggleState.Default, ExplicitToggleState.Enabled, null)
|
||||
};
|
||||
await configService.ApplyEffectiveConfigChangesAsync(changes);
|
||||
cache.Invalidate();
|
||||
var snapshot = await cache.GetSnapshotAsync();
|
||||
Assert.Equal(ExplicitToggleState.Enabled, snapshot.Config.Do0x20EncodeState);
|
||||
Assert.NotNull(snapshot.Sources.Do0x20Encode);
|
||||
}
|
||||
finally
|
||||
{
|
||||
cache?.Dispose();
|
||||
if (Directory.Exists(dir))
|
||||
Directory.Delete(dir, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ApplyChanges_UseStaleCache_RoundTripsToEffectiveState()
|
||||
{
|
||||
var dir = Path.Combine(Path.GetTempPath(), "dnsmasq-roundtrip-" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(dir);
|
||||
var mainPath = Path.Combine(dir, "dnsmasq.conf");
|
||||
var managedName = "zz-managed.conf";
|
||||
ConfigSetCache? cache = null;
|
||||
try
|
||||
{
|
||||
File.WriteAllText(mainPath, "port=53\n");
|
||||
var options = Options.Create(new DnsmasqOptions { MainConfigPath = mainPath, ManagedFileName = managedName });
|
||||
cache = new ConfigSetCache(options, NullLogger<ConfigSetCache>.Instance);
|
||||
var setService = new DnsmasqConfigSetService(cache);
|
||||
var configService = new DnsmasqConfigService(setService, cache, NullLogger<DnsmasqConfigService>.Instance);
|
||||
var changes = new List<PendingEffectiveConfigChange>
|
||||
{
|
||||
new(EffectiveConfigSections.SectionCache, DnsmasqConfKeys.UseStaleCache, null, "60", null)
|
||||
};
|
||||
await configService.ApplyEffectiveConfigChangesAsync(changes);
|
||||
cache.Invalidate();
|
||||
var snapshot = await cache.GetSnapshotAsync();
|
||||
Assert.Equal("60", snapshot.Config.UseStaleCache);
|
||||
Assert.NotNull(snapshot.Sources.UseStaleCache);
|
||||
}
|
||||
finally
|
||||
{
|
||||
cache?.Dispose();
|
||||
if (Directory.Exists(dir))
|
||||
Directory.Delete(dir, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ApplyChanges_Server_RoundTripsToEffectiveState()
|
||||
{
|
||||
var dir = Path.Combine(Path.GetTempPath(), "dnsmasq-roundtrip-" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(dir);
|
||||
var mainPath = Path.Combine(dir, "dnsmasq.conf");
|
||||
var managedName = "zz-managed.conf";
|
||||
ConfigSetCache? cache = null;
|
||||
try
|
||||
{
|
||||
File.WriteAllText(mainPath, "port=53\n");
|
||||
var options = Options.Create(new DnsmasqOptions { MainConfigPath = mainPath, ManagedFileName = managedName });
|
||||
cache = new ConfigSetCache(options, NullLogger<ConfigSetCache>.Instance);
|
||||
var setService = new DnsmasqConfigSetService(cache);
|
||||
var configService = new DnsmasqConfigService(setService, cache, NullLogger<DnsmasqConfigService>.Instance);
|
||||
var changes = new List<PendingEffectiveConfigChange>
|
||||
{
|
||||
new(EffectiveConfigSections.SectionResolver, DnsmasqConfKeys.Server, null, new List<string> { "1.1.1.1", "8.8.8.8" }, null)
|
||||
};
|
||||
await configService.ApplyEffectiveConfigChangesAsync(changes);
|
||||
cache.Invalidate();
|
||||
var snapshot = await cache.GetSnapshotAsync();
|
||||
Assert.Equal(2, snapshot.Config.ServerValues.Count);
|
||||
Assert.Contains("1.1.1.1", snapshot.Config.ServerValues);
|
||||
Assert.Contains("8.8.8.8", snapshot.Config.ServerValues);
|
||||
Assert.Equal(2, snapshot.Sources.ServerValues.Count);
|
||||
}
|
||||
finally
|
||||
{
|
||||
cache?.Dispose();
|
||||
if (Directory.Exists(dir))
|
||||
Directory.Delete(dir, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ApplyChanges_Conntrack_RoundTripsToEffectiveState()
|
||||
{
|
||||
var dir = Path.Combine(Path.GetTempPath(), "dnsmasq-roundtrip-" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(dir);
|
||||
var mainPath = Path.Combine(dir, "dnsmasq.conf");
|
||||
var managedName = "zz-managed.conf";
|
||||
ConfigSetCache? cache = null;
|
||||
try
|
||||
{
|
||||
File.WriteAllText(mainPath, "port=53\n");
|
||||
var options = Options.Create(new DnsmasqOptions { MainConfigPath = mainPath, ManagedFileName = managedName });
|
||||
cache = new ConfigSetCache(options, NullLogger<ConfigSetCache>.Instance);
|
||||
var setService = new DnsmasqConfigSetService(cache);
|
||||
var configService = new DnsmasqConfigService(setService, cache, NullLogger<DnsmasqConfigService>.Instance);
|
||||
var changes = new List<PendingEffectiveConfigChange>
|
||||
{
|
||||
new(EffectiveConfigSections.SectionResolver, DnsmasqConfKeys.Conntrack, false, true, null)
|
||||
};
|
||||
await configService.ApplyEffectiveConfigChangesAsync(changes);
|
||||
cache.Invalidate();
|
||||
var snapshot = await cache.GetSnapshotAsync();
|
||||
Assert.True(snapshot.Config.Conntrack);
|
||||
Assert.NotNull(snapshot.Sources.Conntrack);
|
||||
}
|
||||
finally
|
||||
{
|
||||
cache?.Dispose();
|
||||
if (Directory.Exists(dir))
|
||||
Directory.Delete(dir, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ApplyChanges_Do0x20_Disabled_WritesNo0x20Encode()
|
||||
{
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
|
||||
|
||||
namespace DnsmasqWebUI.Tests.Services.EffectiveConfig;
|
||||
|
||||
/// <summary>
|
||||
/// Documents and guards the Do0x20 display label rule: Default and Disabled both show "Disabled" (product choice).
|
||||
/// Do not "fix" the display to show "Default" for unset without updating this test and the component comment.
|
||||
/// </summary>
|
||||
public class Do0x20EncodeDisplayLabelTests
|
||||
{
|
||||
/// <summary>Same logic as Do0x20EncodeDisplay.razor view mode: Enabled → "Enabled", else "Disabled".</summary>
|
||||
private static string GetDisplayLabel(ExplicitToggleState state) =>
|
||||
state == ExplicitToggleState.Enabled ? "Enabled" : "Disabled";
|
||||
|
||||
[Fact]
|
||||
public void Do0x20_Default_ShowsDisabled()
|
||||
{
|
||||
Assert.Equal("Disabled", GetDisplayLabel(ExplicitToggleState.Default));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Do0x20_Disabled_ShowsDisabled()
|
||||
{
|
||||
Assert.Equal("Disabled", GetDisplayLabel(ExplicitToggleState.Disabled));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Do0x20_Enabled_ShowsEnabled()
|
||||
{
|
||||
Assert.Equal("Enabled", GetDisplayLabel(ExplicitToggleState.Enabled));
|
||||
}
|
||||
}
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
using DnsmasqWebUI.Infrastructure.Services.EffectiveConfig.Metadata;
|
||||
|
||||
namespace DnsmasqWebUI.Tests.Services.EffectiveConfig;
|
||||
|
||||
/// <summary>
|
||||
/// Ensures effective-config editable options list includes supported cache flags.
|
||||
/// </summary>
|
||||
public class EffectiveConfigEditableOptionsTests
|
||||
{
|
||||
/// <summary>
|
||||
/// strip-mac and strip-subnet are supported in the effective-config UI and should be listed in cache options.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void EditableOptions_Include_StripMac_And_StripSubnet()
|
||||
{
|
||||
var allOptionNames = EffectiveConfigSections.GetSectionsInOrder()
|
||||
.SelectMany(t => EffectiveConfigSections.GetOptionsInSection(t.SectionId))
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
Assert.Contains(DnsmasqConfKeys.StripMac, allOptionNames);
|
||||
Assert.Contains(DnsmasqConfKeys.StripSubnet, allOptionNames);
|
||||
}
|
||||
}
|
||||
+1
-3
@@ -72,7 +72,6 @@ public class EffectiveConfigFeatureRequirementsTests
|
||||
var (isDisabled, reason) = EffectiveConfigFeatureRequirements.GetCapabilityDisabled(DnsmasqConfKeys.DnssecCheckUnsigned, status);
|
||||
Assert.True(isDisabled);
|
||||
Assert.NotNull(reason);
|
||||
Assert.Contains("DNSSEC", reason, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -92,8 +91,7 @@ public class EffectiveConfigFeatureRequirementsTests
|
||||
var noDnssecCaps = new DnsmasqCompileCapabilities(true, true, false, false,
|
||||
new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "DHCP", "TFTP" });
|
||||
Assert.False(EffectiveConfigFeatureRequirements.IsSupportedByCapabilities(DnsmasqConfKeys.DnssecCheckUnsigned, noDnssecCaps));
|
||||
var reason = EffectiveConfigFeatureRequirements.GetUnsupportedReason(DnsmasqFeature.Dnssec);
|
||||
Assert.Contains("DNSSEC", reason, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.NotNull(EffectiveConfigFeatureRequirements.GetUnsupportedReason(DnsmasqFeature.Dnssec));
|
||||
}
|
||||
|
||||
private static DnsmasqServiceStatus CreateStatusWithCapabilities(bool dhcp = true, bool tftp = true, bool dnssec = true, bool dbus = false)
|
||||
|
||||
-1
@@ -23,7 +23,6 @@ public class EffectiveConfigFieldBuilderCapabilityTests
|
||||
|
||||
Assert.True(descriptor.IsCapabilityDisabled);
|
||||
Assert.NotNull(descriptor.CapabilityDisabledReason);
|
||||
Assert.Contains("DNSSEC", descriptor.CapabilityDisabledReason, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
using DnsmasqWebUI.Infrastructure.Services.Dnsmasq.Config;
|
||||
using DnsmasqWebUI.Infrastructure.Services.Dnsmasq.Reload.Abstractions;
|
||||
using DnsmasqWebUI.Infrastructure.Services.Dnsmasq.Validation.Abstractions;
|
||||
using DnsmasqWebUI.Infrastructure.Services.Dnsmasq.Version.Abstractions;
|
||||
using DnsmasqWebUI.Infrastructure.Services.EffectiveConfig;
|
||||
using DnsmasqWebUI.Infrastructure.Services.EffectiveConfig.Metadata;
|
||||
using DnsmasqWebUI.Infrastructure.Services.EffectiveConfig.Validation;
|
||||
using DnsmasqWebUI.Infrastructure.Services.EffectiveConfig.Validation.Abstractions;
|
||||
using DnsmasqWebUI.Models.Config;
|
||||
using DnsmasqWebUI.Models.Dnsmasq;
|
||||
using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace DnsmasqWebUI.Tests.Services.EffectiveConfig;
|
||||
|
||||
/// <summary>
|
||||
/// Save path capability guard: unsupported-option changes return UnsupportedCapabilities
|
||||
/// before validation or reload can run.
|
||||
/// </summary>
|
||||
public class EffectiveConfigSaveServiceCapabilityTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task SaveAsync_WhenCapabilitiesMissingDnssec_ReturnsUnsupportedCapabilitiesErrorCode()
|
||||
{
|
||||
var dir = Path.Combine(Path.GetTempPath(), "dnsmasq-cap-" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(dir);
|
||||
var mainPath = Path.Combine(dir, "dnsmasq.conf");
|
||||
var managedName = "zz-managed.conf";
|
||||
ConfigSetCache? cache = null;
|
||||
try
|
||||
{
|
||||
File.WriteAllText(mainPath, "port=53\n");
|
||||
var options = Options.Create(new DnsmasqOptions { MainConfigPath = mainPath, ManagedFileName = managedName });
|
||||
cache = new ConfigSetCache(options, NullLogger<ConfigSetCache>.Instance);
|
||||
var setService = new DnsmasqConfigSetService(cache);
|
||||
var configService = new DnsmasqConfigService(setService, cache, NullLogger<DnsmasqConfigService>.Instance);
|
||||
|
||||
var versionService = new StubVersionService(CreateSupportedVersionInfo(dnssec: false));
|
||||
var semanticValidationService = new EffectiveConfigSemanticValidationService(new OptionSemanticValidator(Array.Empty<IOptionSemanticHandler>()));
|
||||
|
||||
var saveService = new EffectiveConfigSaveService(
|
||||
setService,
|
||||
configService,
|
||||
cache,
|
||||
new UnexpectedValidationService(),
|
||||
semanticValidationService,
|
||||
new UnexpectedReloadService(),
|
||||
versionService,
|
||||
NullLogger<EffectiveConfigSaveService>.Instance);
|
||||
|
||||
var changes = new List<PendingEffectiveConfigChange>
|
||||
{
|
||||
new(EffectiveConfigSections.SectionDnssec, DnsmasqConfKeys.DnssecCheckUnsigned, null, "no", null)
|
||||
};
|
||||
|
||||
var result = await saveService.SaveAsync(changes);
|
||||
|
||||
Assert.Equal(EffectiveConfigSaveResult.ErrorCodes.UnsupportedCapabilities, result.ErrorCode);
|
||||
Assert.False(File.Exists(Path.Combine(dir, managedName)));
|
||||
}
|
||||
finally
|
||||
{
|
||||
cache?.Dispose();
|
||||
if (Directory.Exists(dir))
|
||||
Directory.Delete(dir, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class StubVersionService : IDnsmasqVersionService
|
||||
{
|
||||
private readonly DnsmasqVersionInfo _info;
|
||||
|
||||
public StubVersionService(DnsmasqVersionInfo info) => _info = info;
|
||||
|
||||
public Task<DnsmasqVersionInfo> GetVersionInfoAsync(CancellationToken ct = default) =>
|
||||
Task.FromResult(_info);
|
||||
}
|
||||
|
||||
private static DnsmasqVersionInfo CreateSupportedVersionInfo(bool dnssec)
|
||||
{
|
||||
var capabilities = new DnsmasqCompileCapabilities(
|
||||
Dhcp: true,
|
||||
Tftp: true,
|
||||
Dnssec: dnssec,
|
||||
Dbus: false,
|
||||
new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "DHCP", "TFTP" });
|
||||
|
||||
return new DnsmasqVersionInfo(
|
||||
new Version(2, 91),
|
||||
new Version(2, 91),
|
||||
ProbeSucceeded: true,
|
||||
IsSupported: true,
|
||||
"dnsmasq --version",
|
||||
null,
|
||||
capabilities);
|
||||
}
|
||||
|
||||
private sealed class UnexpectedValidationService : IConfigValidationService
|
||||
{
|
||||
public Task<ConfigValidationResult> ValidateAsync(CancellationToken ct = default) =>
|
||||
throw new InvalidOperationException("ValidateAsync should not be called when capabilities are unsupported.");
|
||||
}
|
||||
|
||||
private sealed class UnexpectedReloadService : IReloadService
|
||||
{
|
||||
public Task<ReloadResult> ReloadAsync(CancellationToken ct = default) =>
|
||||
throw new InvalidOperationException("ReloadAsync should not be called when capabilities are unsupported.");
|
||||
}
|
||||
}
|
||||
+8
-6
@@ -13,13 +13,15 @@ namespace DnsmasqWebUI.Tests.Services.EffectiveConfig;
|
||||
public class EffectiveConfigSemanticsWiringTests
|
||||
{
|
||||
[Fact]
|
||||
public void ParserBehaviorMap_UsesSpecialSemantics_ForAllSpecialOptions()
|
||||
public void Registry_WiresSemanticValidator_ForDnsRr()
|
||||
{
|
||||
foreach (var option in EffectiveConfigSpecialOptionSemantics.GetAllOptionNames())
|
||||
{
|
||||
var semantics = EffectiveConfigSpecialOptionSemantics.TryGetSemantics(option)!;
|
||||
Assert.Equal(semantics.ParserBehavior, EffectiveConfigParserBehaviorMap.GetBehavior(option));
|
||||
}
|
||||
var registry = new EffectiveConfigRenderFragmentRegistry(new OptionSemanticValidator([new DnsRrSemanticHandler()]));
|
||||
var factory = registry.GetMultiDescriptorFactory(EffectiveConfigFieldBuilder.SectionDnsRecords, DnsmasqConfKeys.DnsRr);
|
||||
Assert.NotNull(factory);
|
||||
var descriptor = factory!(EffectiveConfigFieldBuilder.SectionDnsRecords, DnsmasqConfKeys.DnsRr, status: null, getItems: _ => null);
|
||||
Assert.NotNull(descriptor.Validator);
|
||||
Assert.Null(descriptor.Validator!.ValidateItem("example.com,16,01:02", Array.Empty<string>()));
|
||||
Assert.NotNull(descriptor.Validator.ValidateItem("example.com,not-a-number", Array.Empty<string>()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
using DnsmasqWebUI.Infrastructure.Services.EffectiveConfig.Metadata;
|
||||
using DnsmasqWebUI.Infrastructure.Services.EffectiveConfig.Validation;
|
||||
using DnsmasqWebUI.Infrastructure.Services.EffectiveConfig.Validation.Abstractions;
|
||||
using DnsmasqWebUI.Infrastructure.Services.EffectiveConfig.Validation.Handlers;
|
||||
using DnsmasqWebUI.Infrastructure.Serialization.Parsers.DnsmasqConfig;
|
||||
using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
|
||||
using DnsmasqWebUI.Tests.Helpers;
|
||||
|
||||
namespace DnsmasqWebUI.Tests.Services.EffectiveConfig;
|
||||
|
||||
/// <summary>Corpus-driven semantic validation: bad samples produce errors for expected option keys only.</summary>
|
||||
public class OptionSemanticValidatorRealWorldCorpusTests
|
||||
{
|
||||
private readonly IOptionSemanticValidator _validator = new OptionSemanticValidator([
|
||||
new DomainSemanticHandler(),
|
||||
new DnsRrSemanticHandler(),
|
||||
new SynthDomainSemanticHandler(),
|
||||
new AuthServerSemanticHandler(),
|
||||
new SrvSemanticHandler(),
|
||||
]);
|
||||
|
||||
public static IEnumerable<object[]> GetCasesWithInvalidOptions()
|
||||
{
|
||||
foreach (var c in RealWorldCasesHelper.LoadCases())
|
||||
{
|
||||
if (c.SemanticInvalidOptions.Count > 0)
|
||||
yield return new object[] { c };
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(GetCasesWithInvalidOptions))]
|
||||
public void SemanticInvalidOptions_FromCorpus_AreRejected(CorpusCase c)
|
||||
{
|
||||
var mainPath = RealWorldCasesHelper.Resolve(c);
|
||||
var paths = DnsmasqConfIncludeParser.GetIncludedPaths(mainPath);
|
||||
var semantics = new OptionValidationSemantics(OptionValidationKind.Complex, allowEmpty: true);
|
||||
|
||||
foreach (var option in c.SemanticInvalidOptions)
|
||||
{
|
||||
var values = DnsmasqConfIncludeParser.GetMultiValueFromConfigFiles(paths, option);
|
||||
Assert.NotEmpty(values);
|
||||
|
||||
var anyInvalid = values.Any(v =>
|
||||
_validator.ValidateMultiItem(option, v, semantics) is not null);
|
||||
|
||||
Assert.True(anyInvalid, $"Expected at least one invalid value for option '{option}' in {c.File}");
|
||||
}
|
||||
}
|
||||
}
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
using DnsmasqWebUI.Infrastructure.Services.EffectiveConfig.Metadata;
|
||||
using DnsmasqWebUI.Infrastructure.Services.EffectiveConfig.Validation;
|
||||
using DnsmasqWebUI.Infrastructure.Services.EffectiveConfig.Validation.Abstractions;
|
||||
using DnsmasqWebUI.Infrastructure.Services.EffectiveConfig.Validation.Handlers;
|
||||
using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
|
||||
|
||||
namespace DnsmasqWebUI.Tests.Services.EffectiveConfig;
|
||||
|
||||
/// <summary>
|
||||
/// Ensures every specialized option (one with a semantic handler) has at least one valid and one invalid
|
||||
/// test case; asserts only Null/NotNull, not error message text.
|
||||
/// </summary>
|
||||
public class OptionSemanticValidatorSpecializedTests
|
||||
{
|
||||
private static readonly OptionValidationSemantics ComplexAllowEmpty = new(OptionValidationKind.Complex, allowEmpty: true);
|
||||
|
||||
private readonly IOptionSemanticValidator _validator = new OptionSemanticValidator([
|
||||
new LeasequerySemanticHandler(),
|
||||
new ServerSemanticHandler(),
|
||||
new LocalSemanticHandler(),
|
||||
new RevServerSemanticHandler(),
|
||||
new AddressSemanticHandler(),
|
||||
new TrustAnchorSemanticHandler(),
|
||||
new AliasSemanticHandler(),
|
||||
new IpsetSemanticHandler(),
|
||||
new NftsetSemanticHandler(),
|
||||
new IgnoreAddressSemanticHandler(),
|
||||
new ConnmarkAllowlistSemanticHandler(),
|
||||
new DhcpRangeSemanticHandler(),
|
||||
new DhcpHostSemanticHandler(),
|
||||
new DhcpOptionSemanticHandler(),
|
||||
new DhcpMatchSemanticHandler(),
|
||||
new DhcpMacSemanticHandler(),
|
||||
new DhcpRelaySemanticHandler(),
|
||||
new DhcpProxySemanticHandler(),
|
||||
new RaParamSemanticHandler(),
|
||||
new DhcpNameMatchSemanticHandler(),
|
||||
new DhcpIgnoreSemanticHandler(),
|
||||
new DhcpVendorclassSemanticHandler(),
|
||||
new DhcpUserclassSemanticHandler(),
|
||||
new TagIfSemanticHandler(),
|
||||
new BridgeInterfaceSemanticHandler(),
|
||||
new SharedNetworkSemanticHandler(),
|
||||
new DhcpOptionPxeSemanticHandler(),
|
||||
new RebindDomainOkSemanticHandler(),
|
||||
new BogusNxdomainSemanticHandler(),
|
||||
new DhcpIgnoreNamesSemanticHandler(),
|
||||
new DhcpBootSemanticHandler(),
|
||||
new SlaacSemanticHandler(),
|
||||
new PxeServiceSemanticHandler(),
|
||||
new DhcpCircuitidSemanticHandler(),
|
||||
new DhcpRemoteidSemanticHandler(),
|
||||
new DhcpSubscrIdSemanticHandler(),
|
||||
new FilterRrSemanticHandler(),
|
||||
new CacheRrSemanticHandler(),
|
||||
new InterfaceNameSemanticHandler(),
|
||||
new AuthServerSemanticHandler(),
|
||||
new CnameSemanticHandler(),
|
||||
new MxHostSemanticHandler(),
|
||||
new PtrRecordSemanticHandler(),
|
||||
new InterfaceNameRecordSemanticHandler(),
|
||||
new CaaRecordSemanticHandler(),
|
||||
new SrvSemanticHandler(),
|
||||
new NaptrRecordSemanticHandler(),
|
||||
new DnsRrSemanticHandler(),
|
||||
new DynamicHostSemanticHandler(),
|
||||
new AuthSoaSemanticHandler(),
|
||||
new AuthSecServersSemanticHandler(),
|
||||
new AuthPeerSemanticHandler(),
|
||||
new HostRecordSemanticHandler(),
|
||||
new TxtRecordSemanticHandler(),
|
||||
new DomainSemanticHandler(),
|
||||
new SynthDomainSemanticHandler(),
|
||||
new AuthZoneSemanticHandler(),
|
||||
]);
|
||||
|
||||
/// <summary>Option key, one valid value (validator returns null), one invalid value (validator returns non-null).</summary>
|
||||
public static IEnumerable<object[]> GetSpecializedValidInvalidPairs()
|
||||
{
|
||||
yield return new object[] { DnsmasqConfKeys.Leasequery, "10.0.0.0/24", "not-an-ip" };
|
||||
yield return new object[] { DnsmasqConfKeys.Server, "1.1.1.1", "" };
|
||||
yield return new object[] { DnsmasqConfKeys.Local, "/lan/", "" };
|
||||
yield return new object[] { DnsmasqConfKeys.Address, "/example.com/127.0.0.1", "/bad..name/1.2.3.4" };
|
||||
yield return new object[] { DnsmasqConfKeys.Domain, "home.lan,192.168.1.0/24", "example.com,192.168.1.0/24,local,extra" };
|
||||
yield return new object[] { DnsmasqConfKeys.AuthServer, "zone.example.com,eth0", ",eth0" };
|
||||
yield return new object[] { DnsmasqConfKeys.SynthDomain, "dynamic.example.com,192.168.2.1,192.168.2.100", "example.com," };
|
||||
yield return new object[] { DnsmasqConfKeys.DnsRr, "example.com,16,01:02", "example.com,not-a-number" };
|
||||
yield return new object[] { DnsmasqConfKeys.Srv, "_http._tcp.example.com,host.example.com,80", "_sip._tcp,target,99999" };
|
||||
yield return new object[] { DnsmasqConfKeys.NaptrRecord, "example.com,0,0,a,s,r", "example.com,not-num,0,a,s,r" };
|
||||
yield return new object[] { DnsmasqConfKeys.TxtRecord, "example.com,text", "" };
|
||||
yield return new object[] { DnsmasqConfKeys.Cname, "www.example.com,example.com", "bad..name,example.com" };
|
||||
yield return new object[] { DnsmasqConfKeys.MxHost, "example.com,mail.example.com,10", "bad..name,mail.example.com,10" };
|
||||
yield return new object[] { DnsmasqConfKeys.PtrRecord, "1.168.192.in-addr.arpa,router.lan", "bad..arpa,host" };
|
||||
yield return new object[] { DnsmasqConfKeys.RebindDomainOk, "example.com", "bad..name" };
|
||||
yield return new object[] { DnsmasqConfKeys.BogusNxdomain, "192.168.1.1", "not-an-ip" };
|
||||
yield return new object[] { DnsmasqConfKeys.ConnmarkAllowlist, "0xff,example.com", "not-a-mark,example.com" };
|
||||
yield return new object[] { DnsmasqConfKeys.DhcpRange, "192.168.1.50,192.168.1.150,12h", "not-an-ip,192.168.1.150" };
|
||||
yield return new object[] { DnsmasqConfKeys.DhcpHost, "aa:bb:cc:dd:ee:ff,192.168.1.10", "ignore" };
|
||||
yield return new object[] { DnsmasqConfKeys.DhcpOption, "3,192.168.1.1", "option:," };
|
||||
yield return new object[] { DnsmasqConfKeys.DhcpMatch, "set:efi,option:client-arch,6", "option:client-arch,6" };
|
||||
yield return new object[] { DnsmasqConfKeys.DhcpMac, "set:vendor,aa:bb:cc:dd:ee:ff", "aa:bb:cc:dd:ee:ff" };
|
||||
yield return new object[] { DnsmasqConfKeys.DhcpNameMatch, "set:tag,hostname*", "hostname*" };
|
||||
yield return new object[] { DnsmasqConfKeys.DhcpIgnoreNames, "tag:guest", "guest" };
|
||||
yield return new object[] { DnsmasqConfKeys.AuthZone, "example.com", "bad..name" };
|
||||
yield return new object[] { DnsmasqConfKeys.FilterRr, "A,AAAA", "" };
|
||||
yield return new object[] { DnsmasqConfKeys.CacheRr, "A,TXT", "" };
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(GetSpecializedValidInvalidPairs))]
|
||||
public void SpecializedOption_ValidAccepted_InvalidRejected(string optionKey, string validValue, string invalidValue)
|
||||
{
|
||||
var validErr = _validator.ValidateMultiItem(optionKey, validValue, ComplexAllowEmpty);
|
||||
Assert.Null(validErr);
|
||||
|
||||
var invalidErr = _validator.ValidateMultiItem(optionKey, invalidValue, ComplexAllowEmpty);
|
||||
Assert.NotNull(invalidErr);
|
||||
}
|
||||
}
|
||||
@@ -937,4 +937,72 @@ public class OptionSemanticValidatorTests
|
||||
Assert.NotNull(_validator.ValidateMultiItem(DnsmasqConfKeys.AuthZone, "", semantics));
|
||||
Assert.NotNull(_validator.ValidateMultiItem(DnsmasqConfKeys.AuthZone, "bad..name", semantics));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateMultiItem_AuthServer_InvalidGlueDomain_IsRejected()
|
||||
{
|
||||
var semantics = new OptionValidationSemantics(OptionValidationKind.Complex, allowEmpty: true);
|
||||
var err = _validator.ValidateMultiItem(DnsmasqConfKeys.AuthServer, ",eth0", semantics);
|
||||
Assert.NotNull(err);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateMultiItem_SynthDomain_InvalidAddressRange_IsRejected()
|
||||
{
|
||||
var semantics = new OptionValidationSemantics(OptionValidationKind.Complex, allowEmpty: true);
|
||||
var err = _validator.ValidateMultiItem(DnsmasqConfKeys.SynthDomain, "example.com,", semantics);
|
||||
Assert.NotNull(err);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateMultiItem_Domain_ExtraFields_AreRejected()
|
||||
{
|
||||
var semantics = new OptionValidationSemantics(OptionValidationKind.Complex, allowEmpty: true);
|
||||
var err = _validator.ValidateMultiItem(DnsmasqConfKeys.Domain, "example.com,192.168.1.0/24,local,extra", semantics);
|
||||
Assert.NotNull(err);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("example.com,not-a-number")]
|
||||
[InlineData(",16,01:02")]
|
||||
[InlineData("bad..name,16,01:02")]
|
||||
public void ValidateMultiItem_DnsRr_Malformed_IsRejected(string value)
|
||||
{
|
||||
var semantics = new OptionValidationSemantics(OptionValidationKind.Complex, allowEmpty: true);
|
||||
var err = _validator.ValidateMultiItem(DnsmasqConfKeys.DnsRr, value, semantics);
|
||||
Assert.NotNull(err);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("_sip._tcp,host,not-a-port")]
|
||||
[InlineData("")]
|
||||
[InlineData("_sip._tcp,target,99999")] // port out of range
|
||||
public void ValidateMultiItem_SrvHost_Malformed_IsRejected(string value)
|
||||
{
|
||||
var semantics = new OptionValidationSemantics(OptionValidationKind.Complex, allowEmpty: true);
|
||||
var err = _validator.ValidateMultiItem(DnsmasqConfKeys.Srv, value, semantics);
|
||||
Assert.NotNull(err);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("example.com,not-num,0,a,s,r")]
|
||||
[InlineData("bad..name,0,0,a,s,r")]
|
||||
[InlineData("short,0,0,a,s")] // fewer than 6 fields
|
||||
public void ValidateMultiItem_NaptrRecord_Malformed_IsRejected(string value)
|
||||
{
|
||||
var semantics = new OptionValidationSemantics(OptionValidationKind.Complex, allowEmpty: true);
|
||||
var err = _validator.ValidateMultiItem(DnsmasqConfKeys.NaptrRecord, value, semantics);
|
||||
Assert.NotNull(err);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData("bad..name,text")]
|
||||
[InlineData(",only-text")]
|
||||
public void ValidateMultiItem_TxtRecord_Malformed_IsRejected(string value)
|
||||
{
|
||||
var semantics = new OptionValidationSemantics(OptionValidationKind.Complex, allowEmpty: true);
|
||||
var err = _validator.ValidateMultiItem(DnsmasqConfKeys.TxtRecord, value, semantics);
|
||||
Assert.NotNull(err);
|
||||
}
|
||||
}
|
||||
|
||||
+2
@@ -26,6 +26,8 @@ public sealed class DomainSemanticHandler : IOptionSemanticHandler
|
||||
var domain = tokens[0];
|
||||
if (domain != "#" && !DnsmasqScopedDomainSyntax.IsValidDnsName(domain))
|
||||
return "domain must be a valid DNS name or #.";
|
||||
if (tokens.Length > 3)
|
||||
return "domain has at most 3 fields: domain[,address range[,local]] or domain,interface.";
|
||||
if (tokens.Length >= 2 && tokens[1].Length == 0)
|
||||
return "domain cannot have empty fields.";
|
||||
if (tokens.Length >= 2 && !IsValidSecondToken(tokens[1]))
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
auth-server=,eth0
|
||||
@@ -0,0 +1 @@
|
||||
dns-rr=example.com,not-a-number,01:02
|
||||
@@ -0,0 +1 @@
|
||||
domain=example.com,192.168.1.0/24,local,extra
|
||||
@@ -0,0 +1 @@
|
||||
srv-host=_sip._tcp.example.com,host,99999
|
||||
@@ -0,0 +1 @@
|
||||
synth-domain=example.com,
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
[
|
||||
{
|
||||
"file": "real-world/good/dnsmasq-real-home-basic.conf",
|
||||
"expected": {
|
||||
"serverCountMin": 2,
|
||||
"noHosts": false,
|
||||
"cacheSize": 500,
|
||||
"domainCountMin": 1,
|
||||
"expandHosts": true
|
||||
},
|
||||
"semanticInvalidOptions": []
|
||||
},
|
||||
{
|
||||
"file": "real-world/good/dnsmasq-real-dhcp-pxe.conf",
|
||||
"expected": {},
|
||||
"semanticInvalidOptions": []
|
||||
},
|
||||
{
|
||||
"file": "real-world/good/dnsmasq-real-resolver-rich.conf",
|
||||
"expected": {
|
||||
"serverCountMin": 2,
|
||||
"localCountMin": 2,
|
||||
"addressCountMin": 2,
|
||||
"cacheSize": 1000,
|
||||
"domainCountMin": 1,
|
||||
"noResolv": true,
|
||||
"bogusPriv": true,
|
||||
"expandHosts": true
|
||||
},
|
||||
"semanticInvalidOptions": []
|
||||
},
|
||||
{
|
||||
"file": "real-world/good/dnsmasq-real-dns-records.conf",
|
||||
"expected": {
|
||||
"cnameCountMin": 1,
|
||||
"txtRecordCountMin": 1,
|
||||
"srvCountMin": 1,
|
||||
"ptrRecordCountMin": 1,
|
||||
"mxHostCountMin": 1
|
||||
},
|
||||
"semanticInvalidOptions": []
|
||||
},
|
||||
{
|
||||
"file": "real-world/good/dnsmasq-real-auth-synth.conf",
|
||||
"expected": {
|
||||
"authZoneCountMin": 1,
|
||||
"synthDomainCountMin": 1,
|
||||
"authServerCountMin": 1
|
||||
},
|
||||
"semanticInvalidOptions": []
|
||||
},
|
||||
{
|
||||
"file": "real-world/bad/dnsmasq-real-bad-domain-extra-fields.conf",
|
||||
"expected": {},
|
||||
"semanticInvalidOptions": ["domain"]
|
||||
},
|
||||
{
|
||||
"file": "real-world/bad/dnsmasq-real-bad-dns-rr-type.conf",
|
||||
"expected": {},
|
||||
"semanticInvalidOptions": ["dns-rr"]
|
||||
},
|
||||
{
|
||||
"file": "real-world/bad/dnsmasq-real-bad-synth-domain-missing-range.conf",
|
||||
"expected": {},
|
||||
"semanticInvalidOptions": ["synth-domain"]
|
||||
},
|
||||
{
|
||||
"file": "real-world/bad/dnsmasq-real-bad-auth-server.conf",
|
||||
"expected": {},
|
||||
"semanticInvalidOptions": ["auth-server"]
|
||||
},
|
||||
{
|
||||
"file": "real-world/bad/dnsmasq-real-bad-srv-host.conf",
|
||||
"expected": {},
|
||||
"semanticInvalidOptions": ["srv-host"]
|
||||
},
|
||||
{
|
||||
"file": "real-world/edge/dnsmasq-real-edge-commented-active-mix.conf",
|
||||
"expected": {
|
||||
"serverCountMin": 1,
|
||||
"do0x20State": "Disabled"
|
||||
},
|
||||
"semanticInvalidOptions": []
|
||||
},
|
||||
{
|
||||
"file": "real-world/edge/dnsmasq-real-edge-flags-mix.conf",
|
||||
"expected": {
|
||||
"noResolv": true,
|
||||
"bogusPriv": true,
|
||||
"expandHosts": true,
|
||||
"cacheSize": 200
|
||||
},
|
||||
"semanticInvalidOptions": []
|
||||
},
|
||||
{
|
||||
"file": "real-world/edge/dnsmasq-real-official-like-active.conf",
|
||||
"expected": {
|
||||
"serverCountMin": 1,
|
||||
"port": 5353,
|
||||
"expandHosts": true
|
||||
},
|
||||
"semanticInvalidOptions": []
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,4 @@
|
||||
# server=9.9.9.9
|
||||
server=1.1.1.1
|
||||
# do-0x20-encode
|
||||
no-0x20-encode
|
||||
@@ -0,0 +1,6 @@
|
||||
# no-resolv
|
||||
no-resolv
|
||||
expand-hosts
|
||||
# bogus-priv
|
||||
bogus-priv
|
||||
cache-size=200
|
||||
@@ -0,0 +1,6 @@
|
||||
# port=53
|
||||
port=5353
|
||||
# server=8.8.8.8
|
||||
server=1.1.1.1
|
||||
# expand-hosts
|
||||
expand-hosts
|
||||
@@ -0,0 +1,3 @@
|
||||
auth-zone=example.com
|
||||
synth-domain=dynamic.example.com,192.168.2.1,192.168.2.100
|
||||
auth-server=zone.example.com,eth0
|
||||
@@ -0,0 +1,5 @@
|
||||
port=53
|
||||
dhcp-range=192.168.1.50,192.168.1.150,12h
|
||||
enable-tftp
|
||||
tftp-root=/var/lib/tftpboot
|
||||
dhcp-boot=pxelinux.0
|
||||
@@ -0,0 +1,5 @@
|
||||
cname=www.example.com,example.com
|
||||
txt-record=example.com,"v=spf1 -all"
|
||||
srv-host=_http._tcp.example.com,host.example.com,80,10,10
|
||||
ptr-record=1.168.192.in-addr.arpa,router.home.lan
|
||||
mx-host=example.com,mail.example.com,5
|
||||
@@ -0,0 +1,6 @@
|
||||
port=53
|
||||
cache-size=500
|
||||
server=1.1.1.1
|
||||
server=8.8.8.8
|
||||
expand-hosts
|
||||
domain=home.arpa,192.168.1.0/24
|
||||
@@ -0,0 +1,13 @@
|
||||
port=53
|
||||
cache-size=1000
|
||||
no-resolv
|
||||
domain-needed
|
||||
bogus-priv
|
||||
server=1.1.1.1
|
||||
server=8.8.8.8
|
||||
local=/lan/
|
||||
local=/localdomain/
|
||||
address=/home.lan/192.168.1.1
|
||||
address=/router.local/192.168.1.1
|
||||
domain=home.lan,192.168.1.0/24
|
||||
expand-hosts
|
||||
Reference in New Issue
Block a user