mirror of
https://github.com/alexhopeoconnor/dnsmasq-webui.git
synced 2026-10-04 04:18:13 +10:00
Refactor settings modal to use ISettingsModalService and SettingsModalSections
- Replace SettingsModalContext with SettingsModalSections - Add SettingsModalService for modal state management - Add StatusSection.razor.css for status-dot styles - Update layout, NavMenu, and modal components - Update tests for config/settings changes
This commit is contained in:
@@ -17,25 +17,30 @@ public class ConfigValueSourceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void GetReadOnlyTooltip_WhenReadOnlyWithoutLineNumber_ReturnsFileNameOnly()
|
public void GetReadOnlyTooltip_WhenReadOnlyWithoutLineNumber_ReturnsFileNameOnly()
|
||||||
{
|
{
|
||||||
var source = new ConfigValueSource("/etc/dnsmasq.d/02.conf", "02.conf", IsManaged: false, LineNumber: null);
|
var fileName = "02.conf";
|
||||||
|
var fullPath = $"/etc/dnsmasq.d/{fileName}";
|
||||||
|
var source = new ConfigValueSource(fullPath, fileName, IsManaged: false, LineNumber: null);
|
||||||
var tooltip = source.GetReadOnlyTooltip();
|
var tooltip = source.GetReadOnlyTooltip();
|
||||||
Assert.NotNull(tooltip);
|
Assert.NotNull(tooltip);
|
||||||
Assert.Contains("02.conf", tooltip);
|
Assert.Contains(fileName, tooltip);
|
||||||
Assert.Contains("readonly", tooltip);
|
Assert.Contains("readonly", tooltip);
|
||||||
Assert.Contains("/etc/dnsmasq.d/02.conf", tooltip);
|
Assert.Contains(fullPath, tooltip);
|
||||||
Assert.DoesNotContain("line", tooltip);
|
Assert.DoesNotContain("line", tooltip);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void GetReadOnlyTooltip_WhenReadOnlyWithLineNumber_IncludesLineNumber()
|
public void GetReadOnlyTooltip_WhenReadOnlyWithLineNumber_IncludesLineNumber()
|
||||||
{
|
{
|
||||||
var source = new ConfigValueSource("/etc/dnsmasq.d/02.conf", "02.conf", IsManaged: false, LineNumber: 3);
|
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();
|
var tooltip = source.GetReadOnlyTooltip();
|
||||||
Assert.NotNull(tooltip);
|
Assert.NotNull(tooltip);
|
||||||
Assert.Contains("02.conf", tooltip);
|
Assert.Contains(fileName, tooltip);
|
||||||
Assert.Contains("line 3", tooltip);
|
Assert.Contains($"line {lineNumber}", tooltip);
|
||||||
Assert.Contains("readonly", tooltip);
|
Assert.Contains("readonly", tooltip);
|
||||||
Assert.Contains("/etc/dnsmasq.d/02.conf", tooltip);
|
Assert.Contains(fullPath, tooltip);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -20,136 +20,219 @@ public class DnsmasqConfDhcpHostLineParserTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void ParseLine_BasicMacIpNameLease_Parses()
|
public void ParseLine_BasicMacIpNameLease_Parses()
|
||||||
{
|
{
|
||||||
var e = DnsmasqConfDhcpHostLineParser.ParseLine("dhcp-host=aa:bb:cc:dd:ee:ff,192.168.1.10,testpc,infinite", 1);
|
string mac = "aa:bb:cc:dd:ee:ff";
|
||||||
|
string address = "192.168.1.10";
|
||||||
|
string name = "testpc";
|
||||||
|
string lease = "infinite";
|
||||||
|
|
||||||
|
var line = $"dhcp-host={mac},{address},{name},{lease}";
|
||||||
|
var e = DnsmasqConfDhcpHostLineParser.ParseLine(line, 1);
|
||||||
|
|
||||||
Assert.NotNull(e);
|
Assert.NotNull(e);
|
||||||
Assert.False(e!.IsComment);
|
Assert.False(e!.IsComment);
|
||||||
Assert.False(e.IsDeleted);
|
Assert.False(e.IsDeleted);
|
||||||
Assert.Single(e.MacAddresses);
|
Assert.Single(e.MacAddresses);
|
||||||
Assert.Equal("aa:bb:cc:dd:ee:ff", e.MacAddresses[0]);
|
Assert.Equal(mac, e.MacAddresses[0]);
|
||||||
Assert.Equal("192.168.1.10", e.Address);
|
Assert.Equal(address, e.Address);
|
||||||
Assert.Equal("testpc", e.Name);
|
Assert.Equal(name, e.Name);
|
||||||
Assert.Equal("infinite", e.Lease);
|
Assert.Equal(lease, e.Lease);
|
||||||
Assert.Empty(e.Extra);
|
Assert.Empty(e.Extra);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ParseLine_CommentedLine_ParsesWithIsComment()
|
public void ParseLine_CommentedLine_ParsesWithIsComment()
|
||||||
{
|
{
|
||||||
var e = DnsmasqConfDhcpHostLineParser.ParseLine("#dhcp-host=aa:bb:cc:dd:ee:ff,192.168.1.10,testpc,infinite", 1);
|
string mac = "aa:bb:cc:dd:ee:ff";
|
||||||
|
string address = "192.168.1.10";
|
||||||
|
string name = "testpc";
|
||||||
|
string lease = "infinite";
|
||||||
|
|
||||||
|
var line = $"#dhcp-host={mac},{address},{name},{lease}";
|
||||||
|
var e = DnsmasqConfDhcpHostLineParser.ParseLine(line, 1);
|
||||||
|
|
||||||
Assert.NotNull(e);
|
Assert.NotNull(e);
|
||||||
Assert.True(e!.IsComment);
|
Assert.True(e!.IsComment);
|
||||||
Assert.False(e.IsDeleted);
|
Assert.False(e.IsDeleted);
|
||||||
Assert.Single(e.MacAddresses);
|
Assert.Single(e.MacAddresses);
|
||||||
Assert.Equal("192.168.1.10", e.Address);
|
Assert.Equal(address, e.Address);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ParseLine_DeletedLine_ParsesWithIsDeleted()
|
public void ParseLine_DeletedLine_ParsesWithIsDeleted()
|
||||||
{
|
{
|
||||||
var e = DnsmasqConfDhcpHostLineParser.ParseLine("##dhcp-host=aa:bb:cc:dd:ee:ff,192.168.1.10,oldpc", 1);
|
string mac = "aa:bb:cc:dd:ee:ff";
|
||||||
|
string address = "192.168.1.10";
|
||||||
|
string name = "oldpc";
|
||||||
|
|
||||||
|
var line = $"##dhcp-host={mac},{address},{name}";
|
||||||
|
var e = DnsmasqConfDhcpHostLineParser.ParseLine(line, 1);
|
||||||
|
|
||||||
Assert.NotNull(e);
|
Assert.NotNull(e);
|
||||||
Assert.True(e!.IsComment);
|
Assert.True(e!.IsComment);
|
||||||
Assert.True(e.IsDeleted);
|
Assert.True(e.IsDeleted);
|
||||||
Assert.Single(e.MacAddresses);
|
Assert.Single(e.MacAddresses);
|
||||||
Assert.Equal("192.168.1.10", e.Address);
|
Assert.Equal(address, e.Address);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ParseLine_TrailingComment_ParsesComment()
|
public void ParseLine_TrailingComment_ParsesComment()
|
||||||
{
|
{
|
||||||
var e = DnsmasqConfDhcpHostLineParser.ParseLine("dhcp-host=11:22:33:44:55:66,192.168.1.11,laptop,infinite # main laptop", 1);
|
string mac = "11:22:33:44:55:66";
|
||||||
|
string address = "192.168.1.11";
|
||||||
|
string name = "laptop";
|
||||||
|
string lease = "infinite";
|
||||||
|
string comment = "main laptop";
|
||||||
|
|
||||||
|
var line = $"dhcp-host={mac},{address},{name},{lease} # {comment}";
|
||||||
|
var e = DnsmasqConfDhcpHostLineParser.ParseLine(line, 1);
|
||||||
|
|
||||||
Assert.NotNull(e);
|
Assert.NotNull(e);
|
||||||
Assert.Equal("main laptop", e!.Comment);
|
Assert.Equal(comment, e!.Comment);
|
||||||
Assert.Single(e.MacAddresses);
|
Assert.Single(e.MacAddresses);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ParseLine_MultipleMacs_SameIp_Parses()
|
public void ParseLine_MultipleMacs_SameIp_Parses()
|
||||||
{
|
{
|
||||||
var e = DnsmasqConfDhcpHostLineParser.ParseLine("dhcp-host=11:22:33:44:55:66,12:34:56:78:90:12,192.168.0.2", 1);
|
string mac1 = "11:22:33:44:55:66";
|
||||||
|
string mac2 = "12:34:56:78:90:12";
|
||||||
|
string address = "192.168.0.2";
|
||||||
|
|
||||||
|
var line = $"dhcp-host={mac1},{mac2},{address}";
|
||||||
|
var e = DnsmasqConfDhcpHostLineParser.ParseLine(line, 1);
|
||||||
|
|
||||||
Assert.NotNull(e);
|
Assert.NotNull(e);
|
||||||
Assert.Equal(2, e!.MacAddresses.Count);
|
Assert.Equal(2, e!.MacAddresses.Count);
|
||||||
Assert.Equal("11:22:33:44:55:66", e.MacAddresses[0]);
|
Assert.Equal(mac1, e.MacAddresses[0]);
|
||||||
Assert.Equal("12:34:56:78:90:12", e.MacAddresses[1]);
|
Assert.Equal(mac2, e.MacAddresses[1]);
|
||||||
Assert.Equal("192.168.0.2", e.Address);
|
Assert.Equal(address, e.Address);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ParseLine_WithSetTag_PutsInExtra()
|
public void ParseLine_WithSetTag_PutsInExtra()
|
||||||
{
|
{
|
||||||
var e = DnsmasqConfDhcpHostLineParser.ParseLine("dhcp-host=AA:BB:CC:DD:CC:BB,redhost1,192.168.1.41,infinite,set:red", 1);
|
string mac = "AA:BB:CC:DD:CC:BB";
|
||||||
|
string name = "redhost1";
|
||||||
|
string address = "192.168.1.41";
|
||||||
|
string lease = "infinite";
|
||||||
|
string extraTag = "set:red";
|
||||||
|
|
||||||
|
var line = $"dhcp-host={mac},{name},{address},{lease},{extraTag}";
|
||||||
|
var e = DnsmasqConfDhcpHostLineParser.ParseLine(line, 1);
|
||||||
|
|
||||||
Assert.NotNull(e);
|
Assert.NotNull(e);
|
||||||
Assert.Equal("192.168.1.41", e!.Address);
|
Assert.Equal(address, e!.Address);
|
||||||
Assert.Equal("redhost1", e.Name);
|
Assert.Equal(name, e.Name);
|
||||||
Assert.Contains(e.Extra, x => x == "set:red");
|
Assert.Contains(e.Extra, x => x == extraTag);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ParseLine_HostnameOnly_Parses()
|
public void ParseLine_HostnameOnly_Parses()
|
||||||
{
|
{
|
||||||
var e = DnsmasqConfDhcpHostLineParser.ParseLine("dhcp-host=lap,192.168.0.199", 1);
|
string name = "lap";
|
||||||
|
string address = "192.168.0.199";
|
||||||
|
|
||||||
|
var line = $"dhcp-host={name},{address}";
|
||||||
|
var e = DnsmasqConfDhcpHostLineParser.ParseLine(line, 1);
|
||||||
|
|
||||||
Assert.NotNull(e);
|
Assert.NotNull(e);
|
||||||
Assert.Equal("lap", e!.Name);
|
Assert.Equal(name, e!.Name);
|
||||||
Assert.Equal("192.168.0.199", e.Address);
|
Assert.Equal(address, e.Address);
|
||||||
Assert.Empty(e.MacAddresses);
|
Assert.Empty(e.MacAddresses);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ParseLine_Ignore_Parses()
|
public void ParseLine_Ignore_Parses()
|
||||||
{
|
{
|
||||||
var e = DnsmasqConfDhcpHostLineParser.ParseLine("dhcp-host=00:20:e0:3b:13:af,ignore", 1);
|
string mac = "00:20:e0:3b:13:af";
|
||||||
|
|
||||||
|
var line = $"dhcp-host={mac},ignore";
|
||||||
|
var e = DnsmasqConfDhcpHostLineParser.ParseLine(line, 1);
|
||||||
|
|
||||||
Assert.NotNull(e);
|
Assert.NotNull(e);
|
||||||
Assert.True(e!.Ignore);
|
Assert.True(e!.Ignore);
|
||||||
Assert.Single(e.MacAddresses);
|
Assert.Single(e.MacAddresses);
|
||||||
Assert.Equal("00:20:e0:3b:13:af", e.MacAddresses[0]);
|
Assert.Equal(mac, e.MacAddresses[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ParseLine_NumericLease_Parses()
|
public void ParseLine_NumericLease_Parses()
|
||||||
{
|
{
|
||||||
var e = DnsmasqConfDhcpHostLineParser.ParseLine("dhcp-host=aa:bb:cc:dd:ee:ff,192.168.1.10,pc,3600", 1);
|
string mac = "aa:bb:cc:dd:ee:ff";
|
||||||
|
string address = "192.168.1.10";
|
||||||
|
string name = "pc";
|
||||||
|
string lease = "3600";
|
||||||
|
|
||||||
|
var line = $"dhcp-host={mac},{address},{name},{lease}";
|
||||||
|
var e = DnsmasqConfDhcpHostLineParser.ParseLine(line, 1);
|
||||||
|
|
||||||
Assert.NotNull(e);
|
Assert.NotNull(e);
|
||||||
Assert.Equal("3600", e!.Lease);
|
Assert.Equal(lease, e!.Lease);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ParseLine_WithIdClient_PutsInExtra()
|
public void ParseLine_WithIdClient_PutsInExtra()
|
||||||
{
|
{
|
||||||
var e = DnsmasqConfDhcpHostLineParser.ParseLine("dhcp-host=id:01:02:03:04,192.168.1.50,myhost", 1);
|
string idPart = "id:01:02:03:04";
|
||||||
|
string address = "192.168.1.50";
|
||||||
|
string name = "myhost";
|
||||||
|
|
||||||
|
var line = $"dhcp-host={idPart},{address},{name}";
|
||||||
|
var e = DnsmasqConfDhcpHostLineParser.ParseLine(line, 1);
|
||||||
|
|
||||||
Assert.NotNull(e);
|
Assert.NotNull(e);
|
||||||
Assert.Contains(e!.Extra, x => x.StartsWith("id:", StringComparison.OrdinalIgnoreCase));
|
Assert.Contains(e!.Extra, x => x.StartsWith("id:", StringComparison.OrdinalIgnoreCase));
|
||||||
Assert.Equal("192.168.1.50", e.Address);
|
Assert.Equal(address, e.Address);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ToLine_Roundtrip_Basic()
|
public void ToLine_Roundtrip_Basic()
|
||||||
{
|
{
|
||||||
var line = "dhcp-host=aa:bb:cc:dd:ee:ff,192.168.1.10,testpc,infinite";
|
string mac = "aa:bb:cc:dd:ee:ff";
|
||||||
|
string address = "192.168.1.10";
|
||||||
|
string name = "testpc";
|
||||||
|
string lease = "infinite";
|
||||||
|
|
||||||
|
var line = $"dhcp-host={mac},{address},{name},{lease}";
|
||||||
var e = DnsmasqConfDhcpHostLineParser.ParseLine(line, 1);
|
var e = DnsmasqConfDhcpHostLineParser.ParseLine(line, 1);
|
||||||
|
|
||||||
Assert.NotNull(e);
|
Assert.NotNull(e);
|
||||||
var back = DnsmasqConfDhcpHostLineParser.ToLine(e!);
|
var back = DnsmasqConfDhcpHostLineParser.ToLine(e!);
|
||||||
Assert.StartsWith("dhcp-host=", back);
|
Assert.StartsWith("dhcp-host=", back);
|
||||||
Assert.Contains("aa:bb:cc:dd:ee:ff", back);
|
Assert.Contains(mac, back);
|
||||||
Assert.Contains("192.168.1.10", back);
|
Assert.Contains(address, back);
|
||||||
Assert.Contains("testpc", back);
|
Assert.Contains(name, back);
|
||||||
Assert.Contains("infinite", back);
|
Assert.Contains(lease, back);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ToLine_Roundtrip_WithComment()
|
public void ToLine_Roundtrip_WithComment()
|
||||||
{
|
{
|
||||||
var line = "dhcp-host=11:22:33:44:55:66,192.168.1.11,laptop,infinite # main laptop";
|
string mac = "11:22:33:44:55:66";
|
||||||
|
string address = "192.168.1.11";
|
||||||
|
string name = "laptop";
|
||||||
|
string lease = "infinite";
|
||||||
|
string comment = "main laptop";
|
||||||
|
|
||||||
|
var line = $"dhcp-host={mac},{address},{name},{lease} # {comment}";
|
||||||
var e = DnsmasqConfDhcpHostLineParser.ParseLine(line, 1);
|
var e = DnsmasqConfDhcpHostLineParser.ParseLine(line, 1);
|
||||||
|
|
||||||
Assert.NotNull(e);
|
Assert.NotNull(e);
|
||||||
var back = DnsmasqConfDhcpHostLineParser.ToLine(e!);
|
var back = DnsmasqConfDhcpHostLineParser.ToLine(e!);
|
||||||
Assert.StartsWith("dhcp-host=", back);
|
Assert.StartsWith("dhcp-host=", back);
|
||||||
Assert.Contains("main laptop", back);
|
Assert.Contains(comment, back);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ToLine_CommentedEntry_Prefix()
|
public void ToLine_CommentedEntry_Prefix()
|
||||||
{
|
{
|
||||||
var e = DnsmasqConfDhcpHostLineParser.ParseLine("#dhcp-host=aa:bb:cc:dd:ee:ff,192.168.1.10,testpc", 1);
|
string mac = "aa:bb:cc:dd:ee:ff";
|
||||||
|
string address = "192.168.1.10";
|
||||||
|
string name = "testpc";
|
||||||
|
|
||||||
|
var line = $"#dhcp-host={mac},{address},{name}";
|
||||||
|
var e = DnsmasqConfDhcpHostLineParser.ParseLine(line, 1);
|
||||||
|
|
||||||
Assert.NotNull(e);
|
Assert.NotNull(e);
|
||||||
var back = DnsmasqConfDhcpHostLineParser.ToLine(e!);
|
var back = DnsmasqConfDhcpHostLineParser.ToLine(e!);
|
||||||
Assert.StartsWith("#dhcp-host=", back);
|
Assert.StartsWith("#dhcp-host=", back);
|
||||||
@@ -158,7 +241,13 @@ public class DnsmasqConfDhcpHostLineParserTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void ToLine_DeletedEntry_DoubleHash()
|
public void ToLine_DeletedEntry_DoubleHash()
|
||||||
{
|
{
|
||||||
var e = DnsmasqConfDhcpHostLineParser.ParseLine("##dhcp-host=aa:bb:cc:dd:ee:ff,192.168.1.10,old", 1);
|
string mac = "aa:bb:cc:dd:ee:ff";
|
||||||
|
string address = "192.168.1.10";
|
||||||
|
string name = "old";
|
||||||
|
|
||||||
|
var line = $"##dhcp-host={mac},{address},{name}";
|
||||||
|
var e = DnsmasqConfDhcpHostLineParser.ParseLine(line, 1);
|
||||||
|
|
||||||
Assert.NotNull(e);
|
Assert.NotNull(e);
|
||||||
var back = DnsmasqConfDhcpHostLineParser.ToLine(e!);
|
var back = DnsmasqConfDhcpHostLineParser.ToLine(e!);
|
||||||
Assert.StartsWith("##dhcp-host=", back);
|
Assert.StartsWith("##dhcp-host=", back);
|
||||||
|
|||||||
@@ -26,10 +26,12 @@ public class DnsmasqConfDirectiveParserTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void TryParseKeyValue_KeyValue_ReturnsKeyAndValue()
|
public void TryParseKeyValue_KeyValue_ReturnsKeyAndValue()
|
||||||
{
|
{
|
||||||
var kv = DnsmasqConfDirectiveParser.TryParseKeyValue("port=53");
|
var value = "53";
|
||||||
|
var line = $"port={value}";
|
||||||
|
var kv = DnsmasqConfDirectiveParser.TryParseKeyValue(line);
|
||||||
Assert.NotNull(kv);
|
Assert.NotNull(kv);
|
||||||
Assert.Equal(DnsmasqConfKeys.Port, kv!.Value.key);
|
Assert.Equal(DnsmasqConfKeys.Port, kv!.Value.key);
|
||||||
Assert.Equal("53", kv.Value.value);
|
Assert.Equal(value, kv.Value.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -50,16 +52,22 @@ public class DnsmasqConfDirectiveParserTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void StripComment_CommentAfterSpace_StripsToEnd()
|
public void StripComment_CommentAfterSpace_StripsToEnd()
|
||||||
{
|
{
|
||||||
Assert.Equal("port=53", DnsmasqConfDirectiveParser.StripComment("port=53 # DNS port"));
|
var before = "port=53";
|
||||||
Assert.Equal("port=53", DnsmasqConfDirectiveParser.StripComment("port=53 # comment"));
|
var comment1 = " DNS port";
|
||||||
|
var comment2 = " comment";
|
||||||
|
Assert.Equal(before, DnsmasqConfDirectiveParser.StripComment(before + " #" + comment1));
|
||||||
|
Assert.Equal(before, DnsmasqConfDirectiveParser.StripComment(before + " #" + comment2));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void TryParseKeyValue_LineWithComment_ValueExcludesComment()
|
public void TryParseKeyValue_LineWithComment_ValueExcludesComment()
|
||||||
{
|
{
|
||||||
var kv = DnsmasqConfDirectiveParser.TryParseKeyValue("port=53 # DNS port");
|
var value = "53";
|
||||||
|
var comment = " DNS port";
|
||||||
|
var line = $"port={value} #{comment}";
|
||||||
|
var kv = DnsmasqConfDirectiveParser.TryParseKeyValue(line);
|
||||||
Assert.NotNull(kv);
|
Assert.NotNull(kv);
|
||||||
Assert.Equal(DnsmasqConfKeys.Port, kv!.Value.key);
|
Assert.Equal(DnsmasqConfKeys.Port, kv!.Value.key);
|
||||||
Assert.Equal("53", kv.Value.value);
|
Assert.Equal(value, kv.Value.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,73 +19,89 @@ public class DnsmasqConfFileLineParserTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void ParseFile_BlankLine_BlankKind()
|
public void ParseFile_BlankLine_BlankKind()
|
||||||
{
|
{
|
||||||
var lines = DnsmasqConfFileLineParser.ParseFile([""]);
|
var raw = "";
|
||||||
|
var lines = DnsmasqConfFileLineParser.ParseFile([raw]);
|
||||||
Assert.Single(lines);
|
Assert.Single(lines);
|
||||||
var blank = Assert.IsType<BlankLine>(lines[0]);
|
var blank = Assert.IsType<BlankLine>(lines[0]);
|
||||||
Assert.Equal(1, blank.LineNumber);
|
Assert.Equal(1, blank.LineNumber);
|
||||||
Assert.Equal("", blank.RawLine);
|
Assert.Equal(raw, blank.RawLine);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ParseFile_BlankLineWithSpaces_BlankKind()
|
public void ParseFile_BlankLineWithSpaces_BlankKind()
|
||||||
{
|
{
|
||||||
var lines = DnsmasqConfFileLineParser.ParseFile([" \t "]);
|
var raw = " \t ";
|
||||||
|
var lines = DnsmasqConfFileLineParser.ParseFile([raw]);
|
||||||
Assert.Single(lines);
|
Assert.Single(lines);
|
||||||
var blank = Assert.IsType<BlankLine>(lines[0]);
|
var blank = Assert.IsType<BlankLine>(lines[0]);
|
||||||
Assert.Equal(" \t ", blank.RawLine);
|
Assert.Equal(raw, blank.RawLine);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ParseFile_CommentLine_CommentKind()
|
public void ParseFile_CommentLine_CommentKind()
|
||||||
{
|
{
|
||||||
var lines = DnsmasqConfFileLineParser.ParseFile(["# Not managed by this app."]);
|
var commentLine = "# Not managed by this app.";
|
||||||
|
var lines = DnsmasqConfFileLineParser.ParseFile([commentLine]);
|
||||||
Assert.Single(lines);
|
Assert.Single(lines);
|
||||||
var comment = Assert.IsType<CommentLine>(lines[0]);
|
var comment = Assert.IsType<CommentLine>(lines[0]);
|
||||||
Assert.Equal(1, comment.LineNumber);
|
Assert.Equal(1, comment.LineNumber);
|
||||||
Assert.Equal("# Not managed by this app.", comment.RawLine);
|
Assert.Equal(commentLine, comment.RawLine);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ParseFile_OtherDirective_OtherKind()
|
public void ParseFile_OtherDirective_OtherKind()
|
||||||
{
|
{
|
||||||
var lines = DnsmasqConfFileLineParser.ParseFile(["domain=local"]);
|
var raw = "domain=local";
|
||||||
|
var lines = DnsmasqConfFileLineParser.ParseFile([raw]);
|
||||||
Assert.Single(lines);
|
Assert.Single(lines);
|
||||||
var other = Assert.IsType<OtherLine>(lines[0]);
|
var other = Assert.IsType<OtherLine>(lines[0]);
|
||||||
Assert.Equal("domain=local", other.RawLine);
|
Assert.Equal(raw, other.RawLine);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ParseFile_AddnHostsLine_AddnHostsKind()
|
public void ParseFile_AddnHostsLine_AddnHostsKind()
|
||||||
{
|
{
|
||||||
var lines = DnsmasqConfFileLineParser.ParseFile(["addn-hosts=/var/lib/dnsmasq/hosts"]);
|
var path = "/var/lib/dnsmasq/hosts";
|
||||||
|
var line = $"addn-hosts={path}";
|
||||||
|
var lines = DnsmasqConfFileLineParser.ParseFile([line]);
|
||||||
Assert.Single(lines);
|
Assert.Single(lines);
|
||||||
var addn = Assert.IsType<AddnHostsLine>(lines[0]);
|
var addn = Assert.IsType<AddnHostsLine>(lines[0]);
|
||||||
Assert.Equal("/var/lib/dnsmasq/hosts", addn.AddnHostsPath);
|
Assert.Equal(path, addn.AddnHostsPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ToLine_AddnHosts_EmitsAddnHostsLine()
|
public void ToLine_AddnHosts_EmitsAddnHostsLine()
|
||||||
{
|
{
|
||||||
var line = new AddnHostsLine { LineNumber = 1, AddnHostsPath = "/etc/hosts" };
|
var path = "/etc/hosts";
|
||||||
Assert.Equal("addn-hosts=/etc/hosts", DnsmasqConfFileLineParser.ToLine(line));
|
var line = new AddnHostsLine { LineNumber = 1, AddnHostsPath = path };
|
||||||
|
Assert.Equal($"addn-hosts={path}", DnsmasqConfFileLineParser.ToLine(line));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ParseFile_DhcpHostLine_DhcpHostKind()
|
public void ParseFile_DhcpHostLine_DhcpHostKind()
|
||||||
{
|
{
|
||||||
var lines = DnsmasqConfFileLineParser.ParseFile(["dhcp-host=aa:bb:cc:dd:ee:ff,192.168.1.10,testpc,infinite"]);
|
string mac = "aa:bb:cc:dd:ee:ff";
|
||||||
|
string address = "192.168.1.10";
|
||||||
|
string name = "testpc";
|
||||||
|
string lease = "infinite";
|
||||||
|
var line = $"dhcp-host={mac},{address},{name},{lease}";
|
||||||
|
var lines = DnsmasqConfFileLineParser.ParseFile([line]);
|
||||||
Assert.Single(lines);
|
Assert.Single(lines);
|
||||||
var dhcpLine = Assert.IsType<DhcpHostLine>(lines[0]);
|
var dhcpLine = Assert.IsType<DhcpHostLine>(lines[0]);
|
||||||
Assert.Single(dhcpLine.DhcpHost.MacAddresses);
|
Assert.Single(dhcpLine.DhcpHost.MacAddresses);
|
||||||
Assert.Equal("aa:bb:cc:dd:ee:ff", dhcpLine.DhcpHost.MacAddresses[0]);
|
Assert.Equal(mac, dhcpLine.DhcpHost.MacAddresses[0]);
|
||||||
Assert.Equal("192.168.1.10", dhcpLine.DhcpHost.Address);
|
Assert.Equal(address, dhcpLine.DhcpHost.Address);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ParseFile_CommentedDhcpHost_TreatedAsComment()
|
public void ParseFile_CommentedDhcpHost_TreatedAsComment()
|
||||||
{
|
{
|
||||||
// Parser treats any line starting with # as Comment (does not parse dhcp-host content)
|
// Parser treats any line starting with # as Comment (does not parse dhcp-host content)
|
||||||
var lines = DnsmasqConfFileLineParser.ParseFile(["#dhcp-host=aa:bb:cc:dd:ee:ff,192.168.1.10,oldpc"]);
|
string mac = "aa:bb:cc:dd:ee:ff";
|
||||||
|
string address = "192.168.1.10";
|
||||||
|
string name = "oldpc";
|
||||||
|
var line = $"#dhcp-host={mac},{address},{name}";
|
||||||
|
var lines = DnsmasqConfFileLineParser.ParseFile([line]);
|
||||||
Assert.Single(lines);
|
Assert.Single(lines);
|
||||||
Assert.Equal(DnsmasqConfLineKind.Comment, lines[0].Kind);
|
Assert.Equal(DnsmasqConfLineKind.Comment, lines[0].Kind);
|
||||||
}
|
}
|
||||||
@@ -93,13 +109,21 @@ public class DnsmasqConfFileLineParserTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void ParseFile_MixedFile_ParsesAll()
|
public void ParseFile_MixedFile_ParsesAll()
|
||||||
{
|
{
|
||||||
|
var domainLine = "domain=local";
|
||||||
|
string dhcpMac = "11:22:33:44:55:66";
|
||||||
|
string dhcpAddress = "192.168.1.11";
|
||||||
|
string dhcpName = "laptop";
|
||||||
|
string dhcpLease = "infinite";
|
||||||
|
var rangeLine = "dhcp-range=192.168.1.100,192.168.1.200";
|
||||||
|
var dhcpLine = $"dhcp-host={dhcpMac},{dhcpAddress},{dhcpName},{dhcpLease}";
|
||||||
|
|
||||||
var input = new[]
|
var input = new[]
|
||||||
{
|
{
|
||||||
"# Sample config",
|
"# Sample config",
|
||||||
"",
|
"",
|
||||||
"domain=local",
|
domainLine,
|
||||||
"dhcp-host=11:22:33:44:55:66,192.168.1.11,laptop,infinite",
|
dhcpLine,
|
||||||
"dhcp-range=192.168.1.100,192.168.1.200"
|
rangeLine
|
||||||
};
|
};
|
||||||
var lines = DnsmasqConfFileLineParser.ParseFile(input);
|
var lines = DnsmasqConfFileLineParser.ParseFile(input);
|
||||||
Assert.Equal(5, lines.Count);
|
Assert.Equal(5, lines.Count);
|
||||||
@@ -107,48 +131,55 @@ public class DnsmasqConfFileLineParserTests
|
|||||||
Assert.IsType<CommentLine>(lines[0]);
|
Assert.IsType<CommentLine>(lines[0]);
|
||||||
Assert.IsType<BlankLine>(lines[1]);
|
Assert.IsType<BlankLine>(lines[1]);
|
||||||
var other2 = Assert.IsType<OtherLine>(lines[2]);
|
var other2 = Assert.IsType<OtherLine>(lines[2]);
|
||||||
Assert.Equal("domain=local", other2.RawLine);
|
Assert.Equal(domainLine, other2.RawLine);
|
||||||
var dhcp3 = Assert.IsType<DhcpHostLine>(lines[3]);
|
var dhcp3 = Assert.IsType<DhcpHostLine>(lines[3]);
|
||||||
Assert.Equal("192.168.1.11", dhcp3.DhcpHost.Address);
|
Assert.Equal(dhcpAddress, dhcp3.DhcpHost.Address);
|
||||||
var other4 = Assert.IsType<OtherLine>(lines[4]);
|
var other4 = Assert.IsType<OtherLine>(lines[4]);
|
||||||
Assert.Equal("dhcp-range=192.168.1.100,192.168.1.200", other4.RawLine);
|
Assert.Equal(rangeLine, other4.RawLine);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ToLine_Blank_PreservesRawLine()
|
public void ToLine_Blank_PreservesRawLine()
|
||||||
{
|
{
|
||||||
var line = new BlankLine { LineNumber = 1, RawLine = " " };
|
var raw = " ";
|
||||||
Assert.Equal(" ", DnsmasqConfFileLineParser.ToLine(line));
|
var line = new BlankLine { LineNumber = 1, RawLine = raw };
|
||||||
|
Assert.Equal(raw, DnsmasqConfFileLineParser.ToLine(line));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ToLine_Comment_PreservesRawLine()
|
public void ToLine_Comment_PreservesRawLine()
|
||||||
{
|
{
|
||||||
var line = new CommentLine { LineNumber = 1, RawLine = "# comment" };
|
var raw = "# comment";
|
||||||
Assert.Equal("# comment", DnsmasqConfFileLineParser.ToLine(line));
|
var line = new CommentLine { LineNumber = 1, RawLine = raw };
|
||||||
|
Assert.Equal(raw, DnsmasqConfFileLineParser.ToLine(line));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ToLine_Other_PreservesRawLine()
|
public void ToLine_Other_PreservesRawLine()
|
||||||
{
|
{
|
||||||
var line = new OtherLine { LineNumber = 1, RawLine = "domain=local" };
|
var raw = "domain=local";
|
||||||
Assert.Equal("domain=local", DnsmasqConfFileLineParser.ToLine(line));
|
var line = new OtherLine { LineNumber = 1, RawLine = raw };
|
||||||
|
Assert.Equal(raw, DnsmasqConfFileLineParser.ToLine(line));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ToLine_DhcpHost_SerializesViaDhcpHostLineParser()
|
public void ToLine_DhcpHost_SerializesViaDhcpHostLineParser()
|
||||||
{
|
{
|
||||||
|
string mac = "aa:bb:cc:dd:ee:ff";
|
||||||
|
string address = "192.168.1.10";
|
||||||
|
string name = "testpc";
|
||||||
|
string lease = "infinite";
|
||||||
var dhcp = new DhcpHostEntry
|
var dhcp = new DhcpHostEntry
|
||||||
{
|
{
|
||||||
LineNumber = 1,
|
LineNumber = 1,
|
||||||
MacAddresses = ["aa:bb:cc:dd:ee:ff"],
|
MacAddresses = [mac],
|
||||||
Address = "192.168.1.10",
|
Address = address,
|
||||||
Name = "testpc",
|
Name = name,
|
||||||
Lease = "infinite"
|
Lease = lease
|
||||||
};
|
};
|
||||||
var line = new DhcpHostLine { LineNumber = 1, DhcpHost = dhcp };
|
var line = new DhcpHostLine { LineNumber = 1, DhcpHost = dhcp };
|
||||||
var back = DnsmasqConfFileLineParser.ToLine(line);
|
var back = DnsmasqConfFileLineParser.ToLine(line);
|
||||||
Assert.Equal("dhcp-host=aa:bb:cc:dd:ee:ff, testpc, 192.168.1.10, infinite", back);
|
Assert.Equal($"dhcp-host={mac}, {name}, {address}, {lease}", back);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -46,10 +46,11 @@ public class DnsmasqConfIncludeParserTests
|
|||||||
Directory.CreateDirectory(dir);
|
Directory.CreateDirectory(dir);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var includePath = Path.Combine(dir, "extra.conf");
|
var includeFileName = "extra.conf";
|
||||||
|
var includePath = Path.Combine(dir, includeFileName);
|
||||||
File.WriteAllText(includePath, "# extra\n");
|
File.WriteAllText(includePath, "# extra\n");
|
||||||
var main = Path.Combine(dir, "dnsmasq.conf");
|
var main = Path.Combine(dir, "dnsmasq.conf");
|
||||||
File.WriteAllText(main, "conf-file=extra.conf\n");
|
File.WriteAllText(main, $"conf-file={includeFileName}\n");
|
||||||
var paths = DnsmasqConfIncludeParser.GetIncludedPaths(main);
|
var paths = DnsmasqConfIncludeParser.GetIncludedPaths(main);
|
||||||
Assert.Equal(2, paths.Count);
|
Assert.Equal(2, paths.Count);
|
||||||
Assert.Equal(Path.GetFullPath(main), paths[0]);
|
Assert.Equal(Path.GetFullPath(main), paths[0]);
|
||||||
@@ -66,19 +67,22 @@ public class DnsmasqConfIncludeParserTests
|
|||||||
{
|
{
|
||||||
var baseDir = Path.Combine(Path.GetTempPath(), "dnsmasq-confdir-" + Guid.NewGuid().ToString("N"));
|
var baseDir = Path.Combine(Path.GetTempPath(), "dnsmasq-confdir-" + Guid.NewGuid().ToString("N"));
|
||||||
Directory.CreateDirectory(baseDir);
|
Directory.CreateDirectory(baseDir);
|
||||||
var subDir = Path.Combine(baseDir, "d");
|
var confDirName = "d";
|
||||||
|
var subDir = Path.Combine(baseDir, confDirName);
|
||||||
Directory.CreateDirectory(subDir);
|
Directory.CreateDirectory(subDir);
|
||||||
|
var file1 = "aa.conf";
|
||||||
|
var file2 = "zz.conf";
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
File.WriteAllText(Path.Combine(subDir, "zz.conf"), "");
|
File.WriteAllText(Path.Combine(subDir, file2), "");
|
||||||
File.WriteAllText(Path.Combine(subDir, "aa.conf"), "");
|
File.WriteAllText(Path.Combine(subDir, file1), "");
|
||||||
var main = Path.Combine(baseDir, "dnsmasq.conf");
|
var main = Path.Combine(baseDir, "dnsmasq.conf");
|
||||||
File.WriteAllText(main, "conf-dir=d\n");
|
File.WriteAllText(main, $"conf-dir={confDirName}\n");
|
||||||
var paths = DnsmasqConfIncludeParser.GetIncludedPaths(main);
|
var paths = DnsmasqConfIncludeParser.GetIncludedPaths(main);
|
||||||
Assert.Equal(3, paths.Count); // main + aa.conf + zz.conf (alphabetical)
|
Assert.Equal(3, paths.Count); // main + aa.conf + zz.conf (alphabetical)
|
||||||
Assert.Equal(Path.GetFullPath(main), paths[0]);
|
Assert.Equal(Path.GetFullPath(main), paths[0]);
|
||||||
Assert.Equal(Path.GetFullPath(Path.Combine(subDir, "aa.conf")), paths[1]);
|
Assert.Equal(Path.GetFullPath(Path.Combine(subDir, file1)), paths[1]);
|
||||||
Assert.Equal(Path.GetFullPath(Path.Combine(subDir, "zz.conf")), paths[2]);
|
Assert.Equal(Path.GetFullPath(Path.Combine(subDir, file2)), paths[2]);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -109,12 +113,13 @@ public class DnsmasqConfIncludeParserTests
|
|||||||
{
|
{
|
||||||
var baseDir = Path.Combine(Path.GetTempPath(), "dnsmasq-firstdir-" + Guid.NewGuid().ToString("N"));
|
var baseDir = Path.Combine(Path.GetTempPath(), "dnsmasq-firstdir-" + Guid.NewGuid().ToString("N"));
|
||||||
Directory.CreateDirectory(baseDir);
|
Directory.CreateDirectory(baseDir);
|
||||||
var subDir = Path.Combine(baseDir, "dnsmasq.d");
|
var confDirName = "dnsmasq.d";
|
||||||
|
var subDir = Path.Combine(baseDir, confDirName);
|
||||||
Directory.CreateDirectory(subDir);
|
Directory.CreateDirectory(subDir);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var main = Path.Combine(baseDir, "dnsmasq.conf");
|
var main = Path.Combine(baseDir, "dnsmasq.conf");
|
||||||
File.WriteAllText(main, "conf-dir=dnsmasq.d\n");
|
File.WriteAllText(main, $"conf-dir={confDirName}\n");
|
||||||
var first = DnsmasqConfIncludeParser.GetFirstConfDir(main);
|
var first = DnsmasqConfIncludeParser.GetFirstConfDir(main);
|
||||||
Assert.NotNull(first);
|
Assert.NotNull(first);
|
||||||
Assert.Equal(Path.GetFullPath(subDir), first);
|
Assert.Equal(Path.GetFullPath(subDir), first);
|
||||||
@@ -240,12 +245,14 @@ public class DnsmasqConfIncludeParserTests
|
|||||||
Directory.CreateDirectory(dir);
|
Directory.CreateDirectory(dir);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
var firstLeasePath = "/var/first.leases";
|
||||||
|
var secondLeasePath = "/var/second.leases";
|
||||||
var first = Path.Combine(dir, "first.conf");
|
var first = Path.Combine(dir, "first.conf");
|
||||||
File.WriteAllText(first, "dhcp-leasefile=/var/first.leases\n");
|
File.WriteAllText(first, $"dhcp-leasefile={firstLeasePath}\n");
|
||||||
var second = Path.Combine(dir, "second.conf");
|
var second = Path.Combine(dir, "second.conf");
|
||||||
File.WriteAllText(second, "dhcp-leasefile=/var/second.leases\n");
|
File.WriteAllText(second, $"dhcp-leasefile={secondLeasePath}\n");
|
||||||
var result = DnsmasqConfIncludeParser.GetDhcpLeaseFilePathFromConfigFiles(new[] { first, second });
|
var result = DnsmasqConfIncludeParser.GetDhcpLeaseFilePathFromConfigFiles(new[] { first, second });
|
||||||
Assert.Equal(Path.GetFullPath("/var/second.leases"), result);
|
Assert.Equal(Path.GetFullPath(secondLeasePath), result);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -260,10 +267,13 @@ public class DnsmasqConfIncludeParserTests
|
|||||||
Directory.CreateDirectory(dir);
|
Directory.CreateDirectory(dir);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
var subdir = "subdir";
|
||||||
|
var leasesFile = "leases";
|
||||||
|
var relativePath = $"{subdir}/{leasesFile}";
|
||||||
var conf = Path.Combine(dir, "dnsmasq.conf");
|
var conf = Path.Combine(dir, "dnsmasq.conf");
|
||||||
File.WriteAllText(conf, "dhcp-leasefile=subdir/leases\n");
|
File.WriteAllText(conf, $"dhcp-leasefile={relativePath}\n");
|
||||||
var result = DnsmasqConfIncludeParser.GetDhcpLeaseFilePathFromConfigFiles(new[] { conf });
|
var result = DnsmasqConfIncludeParser.GetDhcpLeaseFilePathFromConfigFiles(new[] { conf });
|
||||||
Assert.Equal(Path.GetFullPath(Path.Combine(dir, "subdir", "leases")), result);
|
Assert.Equal(Path.GetFullPath(Path.Combine(dir, subdir, leasesFile)), result);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -285,15 +295,18 @@ public class DnsmasqConfIncludeParserTests
|
|||||||
Directory.CreateDirectory(dir);
|
Directory.CreateDirectory(dir);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
var path1 = "/etc/hosts.d/first";
|
||||||
|
var path2 = "/data/hosts";
|
||||||
|
var path3 = "/data/extra.hosts";
|
||||||
var first = Path.Combine(dir, "first.conf");
|
var first = Path.Combine(dir, "first.conf");
|
||||||
File.WriteAllText(first, "addn-hosts=/etc/hosts.d/first\n");
|
File.WriteAllText(first, $"addn-hosts={path1}\n");
|
||||||
var second = Path.Combine(dir, "second.conf");
|
var second = Path.Combine(dir, "second.conf");
|
||||||
File.WriteAllText(second, "addn-hosts=/data/hosts\naddn-hosts=/data/extra.hosts\n");
|
File.WriteAllText(second, $"addn-hosts={path2}\naddn-hosts={path3}\n");
|
||||||
var result = DnsmasqConfIncludeParser.GetAddnHostsPathsFromConfigFiles(new[] { first, second });
|
var result = DnsmasqConfIncludeParser.GetAddnHostsPathsFromConfigFiles(new[] { first, second });
|
||||||
Assert.Equal(3, result.Count);
|
Assert.Equal(3, result.Count);
|
||||||
Assert.Equal(Path.GetFullPath("/etc/hosts.d/first"), result[0]);
|
Assert.Equal(Path.GetFullPath(path1), result[0]);
|
||||||
Assert.Equal(Path.GetFullPath("/data/hosts"), result[1]);
|
Assert.Equal(Path.GetFullPath(path2), result[1]);
|
||||||
Assert.Equal(Path.GetFullPath("/data/extra.hosts"), result[2]);
|
Assert.Equal(Path.GetFullPath(path3), result[2]);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -308,11 +321,14 @@ public class DnsmasqConfIncludeParserTests
|
|||||||
Directory.CreateDirectory(dir);
|
Directory.CreateDirectory(dir);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
var pathPart1 = "hosts.d";
|
||||||
|
var pathPart2 = "app.hosts";
|
||||||
|
var addnRelativePath = $"{pathPart1}/{pathPart2}";
|
||||||
var conf = Path.Combine(dir, "dnsmasq.conf");
|
var conf = Path.Combine(dir, "dnsmasq.conf");
|
||||||
File.WriteAllText(conf, "addn-hosts=hosts.d/app.hosts\n");
|
File.WriteAllText(conf, $"addn-hosts={addnRelativePath}\n");
|
||||||
var result = DnsmasqConfIncludeParser.GetAddnHostsPathsFromConfigFiles(new[] { conf });
|
var result = DnsmasqConfIncludeParser.GetAddnHostsPathsFromConfigFiles(new[] { conf });
|
||||||
Assert.Single(result);
|
Assert.Single(result);
|
||||||
Assert.Equal(Path.GetFullPath(Path.Combine(dir, "hosts.d", "app.hosts")), result[0]);
|
Assert.Equal(Path.GetFullPath(Path.Combine(dir, pathPart1, pathPart2)), result[0]);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -364,12 +380,14 @@ public class DnsmasqConfIncludeParserTests
|
|||||||
Directory.CreateDirectory(dir);
|
Directory.CreateDirectory(dir);
|
||||||
var f1 = Path.Combine(dir, "01.conf");
|
var f1 = Path.Combine(dir, "01.conf");
|
||||||
var f2 = Path.Combine(dir, "02.conf");
|
var f2 = Path.Combine(dir, "02.conf");
|
||||||
|
var firstVal = "100";
|
||||||
|
var secondVal = "200";
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
File.WriteAllText(f1, "cache-size=100\n");
|
File.WriteAllText(f1, $"cache-size={firstVal}\n");
|
||||||
File.WriteAllText(f2, "cache-size=200\n");
|
File.WriteAllText(f2, $"cache-size={secondVal}\n");
|
||||||
var (value, configDir) = DnsmasqConfIncludeParser.GetLastValueFromConfigFiles(new[] { f1, f2 }, DnsmasqConfKeys.CacheSize);
|
var (value, configDir) = DnsmasqConfIncludeParser.GetLastValueFromConfigFiles(new[] { f1, f2 }, DnsmasqConfKeys.CacheSize);
|
||||||
Assert.Equal("200", value);
|
Assert.Equal(secondVal, value);
|
||||||
Assert.Equal(dir, configDir);
|
Assert.Equal(dir, configDir);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@@ -385,10 +403,11 @@ public class DnsmasqConfIncludeParserTests
|
|||||||
Directory.CreateDirectory(dir);
|
Directory.CreateDirectory(dir);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
var logPath = "/data/dnsmasq.log";
|
||||||
var conf = Path.Combine(dir, "dnsmasq.conf");
|
var conf = Path.Combine(dir, "dnsmasq.conf");
|
||||||
File.WriteAllText(conf, "log-facility=/data/dnsmasq.log\n");
|
File.WriteAllText(conf, $"log-facility={logPath}\n");
|
||||||
var (value, configDir) = DnsmasqConfIncludeParser.GetLastValueFromConfigFiles(new[] { conf }, DnsmasqConfKeys.LogFacility);
|
var (value, configDir) = DnsmasqConfIncludeParser.GetLastValueFromConfigFiles(new[] { conf }, DnsmasqConfKeys.LogFacility);
|
||||||
Assert.Equal("/data/dnsmasq.log", value);
|
Assert.Equal(logPath, value);
|
||||||
Assert.Equal(dir, configDir);
|
Assert.Equal(dir, configDir);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@@ -404,12 +423,14 @@ public class DnsmasqConfIncludeParserTests
|
|||||||
Directory.CreateDirectory(dir);
|
Directory.CreateDirectory(dir);
|
||||||
var f1 = Path.Combine(dir, "01.conf");
|
var f1 = Path.Combine(dir, "01.conf");
|
||||||
var f2 = Path.Combine(dir, "02.conf");
|
var f2 = Path.Combine(dir, "02.conf");
|
||||||
|
var firstLogPath = "/var/log/dnsmasq.log";
|
||||||
|
var secondLogPath = "/data/dnsmasq.log";
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
File.WriteAllText(f1, "log-facility=/var/log/dnsmasq.log\n");
|
File.WriteAllText(f1, $"log-facility={firstLogPath}\n");
|
||||||
File.WriteAllText(f2, "log-facility=/data/dnsmasq.log\n");
|
File.WriteAllText(f2, $"log-facility={secondLogPath}\n");
|
||||||
var (value, _) = DnsmasqConfIncludeParser.GetLastValueFromConfigFiles(new[] { f1, f2 }, DnsmasqConfKeys.LogFacility);
|
var (value, _) = DnsmasqConfIncludeParser.GetLastValueFromConfigFiles(new[] { f1, f2 }, DnsmasqConfKeys.LogFacility);
|
||||||
Assert.Equal("/data/dnsmasq.log", value);
|
Assert.Equal(secondLogPath, value);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -421,16 +442,19 @@ public class DnsmasqConfIncludeParserTests
|
|||||||
public void ResolvePath_Relative_ResolvesAgainstDir()
|
public void ResolvePath_Relative_ResolvesAgainstDir()
|
||||||
{
|
{
|
||||||
var dir = Path.GetTempPath();
|
var dir = Path.GetTempPath();
|
||||||
var result = DnsmasqConfIncludeParser.ResolvePath("sub/pid.pid", dir);
|
var subDirName = "sub";
|
||||||
Assert.Equal(Path.GetFullPath(Path.Combine(dir, "sub", "pid.pid")), result);
|
var fileName = "pid.pid";
|
||||||
|
var relativePath = $"{subDirName}/{fileName}";
|
||||||
|
var result = DnsmasqConfIncludeParser.ResolvePath(relativePath, dir);
|
||||||
|
Assert.Equal(Path.GetFullPath(Path.Combine(dir, subDirName, fileName)), result);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ResolvePath_Absolute_ReturnsAsIs()
|
public void ResolvePath_Absolute_ReturnsAsIs()
|
||||||
{
|
{
|
||||||
var abs = Path.Combine(Path.GetTempPath(), "absolute.pid");
|
var absPath = Path.Combine(Path.GetTempPath(), "absolute.pid");
|
||||||
var result = DnsmasqConfIncludeParser.ResolvePath(abs, "/some/dir");
|
var result = DnsmasqConfIncludeParser.ResolvePath(absPath, "/some/dir");
|
||||||
Assert.Equal(Path.GetFullPath(abs), result);
|
Assert.Equal(Path.GetFullPath(absPath), result);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Tests matching dnsmasq option.c semantics (ARG_ONE = last wins, flags = no value allowed) ---
|
// --- Tests matching dnsmasq option.c semantics (ARG_ONE = last wins, flags = no value allowed) ---
|
||||||
@@ -443,8 +467,9 @@ public class DnsmasqConfIncludeParserTests
|
|||||||
Directory.CreateDirectory(dir);
|
Directory.CreateDirectory(dir);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
var invalidValue = "1";
|
||||||
var conf = Path.Combine(dir, "dnsmasq.conf");
|
var conf = Path.Combine(dir, "dnsmasq.conf");
|
||||||
File.WriteAllText(conf, "expand-hosts=1\n");
|
File.WriteAllText(conf, $"expand-hosts={invalidValue}\n");
|
||||||
var result = DnsmasqConfIncludeParser.GetFlagFromConfigFiles(new[] { conf }, DnsmasqConfKeys.ExpandHosts);
|
var result = DnsmasqConfIncludeParser.GetFlagFromConfigFiles(new[] { conf }, DnsmasqConfKeys.ExpandHosts);
|
||||||
Assert.False(result);
|
Assert.False(result);
|
||||||
}
|
}
|
||||||
@@ -462,10 +487,12 @@ public class DnsmasqConfIncludeParserTests
|
|||||||
Directory.CreateDirectory(dir);
|
Directory.CreateDirectory(dir);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
var wrongKey = "Port";
|
||||||
|
var value = "53";
|
||||||
var conf = Path.Combine(dir, "dnsmasq.conf");
|
var conf = Path.Combine(dir, "dnsmasq.conf");
|
||||||
File.WriteAllText(conf, "Port=53\n");
|
File.WriteAllText(conf, $"{wrongKey}={value}\n");
|
||||||
var (value, _) = DnsmasqConfIncludeParser.GetLastValueFromConfigFiles(new[] { conf }, DnsmasqConfKeys.Port);
|
var (parsedValue, _) = DnsmasqConfIncludeParser.GetLastValueFromConfigFiles(new[] { conf }, DnsmasqConfKeys.Port);
|
||||||
Assert.Null(value);
|
Assert.Null(parsedValue);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -481,13 +508,16 @@ public class DnsmasqConfIncludeParserTests
|
|||||||
Directory.CreateDirectory(dir);
|
Directory.CreateDirectory(dir);
|
||||||
var main = Path.Combine(dir, "dnsmasq.conf");
|
var main = Path.Combine(dir, "dnsmasq.conf");
|
||||||
var extra = Path.Combine(dir, "extra.conf");
|
var extra = Path.Combine(dir, "extra.conf");
|
||||||
|
var port1 = "53";
|
||||||
|
var port2 = "5353";
|
||||||
|
var port3 = "54";
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
File.WriteAllText(main, "port=53\nconf-file=extra.conf\nport=5353\n");
|
File.WriteAllText(main, $"port={port1}\nconf-file=extra.conf\nport={port2}\n");
|
||||||
File.WriteAllText(extra, "port=54\n");
|
File.WriteAllText(extra, $"port={port3}\n");
|
||||||
var paths = DnsmasqConfIncludeParser.GetIncludedPaths(main);
|
var paths = DnsmasqConfIncludeParser.GetIncludedPaths(main);
|
||||||
var (value, _) = DnsmasqConfIncludeParser.GetLastValueFromConfigFiles(paths, DnsmasqConfKeys.Port);
|
var (value, _) = DnsmasqConfIncludeParser.GetLastValueFromConfigFiles(paths, DnsmasqConfKeys.Port);
|
||||||
Assert.Equal("54", value);
|
Assert.Equal(port3, value);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -502,18 +532,21 @@ public class DnsmasqConfIncludeParserTests
|
|||||||
var baseDir = Path.Combine(Path.GetTempPath(), "dnsmasq-match-" + Guid.NewGuid().ToString("N"));
|
var baseDir = Path.Combine(Path.GetTempPath(), "dnsmasq-match-" + Guid.NewGuid().ToString("N"));
|
||||||
var subDir = Path.Combine(baseDir, "d");
|
var subDir = Path.Combine(baseDir, "d");
|
||||||
Directory.CreateDirectory(subDir);
|
Directory.CreateDirectory(subDir);
|
||||||
|
var fileConf1 = "a.conf";
|
||||||
|
var fileTxt = "b.txt";
|
||||||
|
var fileConf2 = "c.conf";
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
File.WriteAllText(Path.Combine(subDir, "a.conf"), "");
|
File.WriteAllText(Path.Combine(subDir, fileConf1), "");
|
||||||
File.WriteAllText(Path.Combine(subDir, "b.txt"), "");
|
File.WriteAllText(Path.Combine(subDir, fileTxt), "");
|
||||||
File.WriteAllText(Path.Combine(subDir, "c.conf"), "");
|
File.WriteAllText(Path.Combine(subDir, fileConf2), "");
|
||||||
var main = Path.Combine(baseDir, "dnsmasq.conf");
|
var main = Path.Combine(baseDir, "dnsmasq.conf");
|
||||||
File.WriteAllText(main, "conf-dir=d,*.conf\n");
|
File.WriteAllText(main, "conf-dir=d,*.conf\n");
|
||||||
var paths = DnsmasqConfIncludeParser.GetIncludedPaths(main);
|
var paths = DnsmasqConfIncludeParser.GetIncludedPaths(main);
|
||||||
Assert.Equal(3, paths.Count);
|
Assert.Equal(3, paths.Count);
|
||||||
Assert.Contains(paths, p => p.EndsWith("a.conf", StringComparison.Ordinal));
|
Assert.Contains(paths, p => p.EndsWith(fileConf1, StringComparison.Ordinal));
|
||||||
Assert.Contains(paths, p => p.EndsWith("c.conf", StringComparison.Ordinal));
|
Assert.Contains(paths, p => p.EndsWith(fileConf2, StringComparison.Ordinal));
|
||||||
Assert.DoesNotContain(paths, p => p.EndsWith("b.txt", StringComparison.Ordinal));
|
Assert.DoesNotContain(paths, p => p.EndsWith(fileTxt, StringComparison.Ordinal));
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -528,16 +561,18 @@ public class DnsmasqConfIncludeParserTests
|
|||||||
var baseDir = Path.Combine(Path.GetTempPath(), "dnsmasq-ignore-" + Guid.NewGuid().ToString("N"));
|
var baseDir = Path.Combine(Path.GetTempPath(), "dnsmasq-ignore-" + Guid.NewGuid().ToString("N"));
|
||||||
var subDir = Path.Combine(baseDir, "d");
|
var subDir = Path.Combine(baseDir, "d");
|
||||||
Directory.CreateDirectory(subDir);
|
Directory.CreateDirectory(subDir);
|
||||||
|
var fileIncluded = "a.conf";
|
||||||
|
var fileExcluded = "b.conf.bak";
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
File.WriteAllText(Path.Combine(subDir, "a.conf"), "");
|
File.WriteAllText(Path.Combine(subDir, fileIncluded), "");
|
||||||
File.WriteAllText(Path.Combine(subDir, "b.conf.bak"), "");
|
File.WriteAllText(Path.Combine(subDir, fileExcluded), "");
|
||||||
var main = Path.Combine(baseDir, "dnsmasq.conf");
|
var main = Path.Combine(baseDir, "dnsmasq.conf");
|
||||||
File.WriteAllText(main, "conf-dir=d,.bak\n");
|
File.WriteAllText(main, "conf-dir=d,.bak\n");
|
||||||
var paths = DnsmasqConfIncludeParser.GetIncludedPaths(main);
|
var paths = DnsmasqConfIncludeParser.GetIncludedPaths(main);
|
||||||
Assert.Equal(2, paths.Count);
|
Assert.Equal(2, paths.Count);
|
||||||
Assert.Contains(paths, p => p.EndsWith("a.conf", StringComparison.Ordinal));
|
Assert.Contains(paths, p => p.EndsWith(fileIncluded, StringComparison.Ordinal));
|
||||||
Assert.DoesNotContain(paths, p => p.EndsWith("b.conf.bak", StringComparison.Ordinal));
|
Assert.DoesNotContain(paths, p => p.EndsWith(fileExcluded, StringComparison.Ordinal));
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -553,20 +588,23 @@ public class DnsmasqConfIncludeParserTests
|
|||||||
var f1 = Path.Combine(dir, "01.conf");
|
var f1 = Path.Combine(dir, "01.conf");
|
||||||
var f2 = Path.Combine(dir, "02.conf");
|
var f2 = Path.Combine(dir, "02.conf");
|
||||||
var managedPath = Path.Combine(dir, "managed.conf");
|
var managedPath = Path.Combine(dir, "managed.conf");
|
||||||
|
var path1 = "/etc/hosts.a";
|
||||||
|
var path2 = "/etc/hosts.b";
|
||||||
|
var path3 = "/etc/hosts.c";
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
File.WriteAllText(f1, "addn-hosts=/etc/hosts.a\n");
|
File.WriteAllText(f1, $"addn-hosts={path1}\n");
|
||||||
File.WriteAllText(f2, "addn-hosts=/etc/hosts.b\naddn-hosts=/etc/hosts.c\n");
|
File.WriteAllText(f2, $"addn-hosts={path2}\naddn-hosts={path3}\n");
|
||||||
var result = DnsmasqConfIncludeParser.GetAddnHostsPathsFromConfigFilesWithSource(new[] { f1, f2 }, managedPath);
|
var result = DnsmasqConfIncludeParser.GetAddnHostsPathsFromConfigFilesWithSource(new[] { f1, f2 }, managedPath);
|
||||||
Assert.Equal(3, result.Count);
|
Assert.Equal(3, result.Count);
|
||||||
Assert.Equal(Path.GetFullPath("/etc/hosts.a"), result[0].Path);
|
Assert.Equal(Path.GetFullPath(path1), result[0].Path);
|
||||||
Assert.Equal(Path.GetFileName(f1), result[0].Source.FileName);
|
Assert.Equal(Path.GetFileName(f1), result[0].Source.FileName);
|
||||||
Assert.False(result[0].Source.IsManaged);
|
Assert.False(result[0].Source.IsManaged);
|
||||||
Assert.Equal(1, result[0].Source.LineNumber);
|
Assert.Equal(1, result[0].Source.LineNumber);
|
||||||
Assert.Equal(Path.GetFullPath("/etc/hosts.b"), result[1].Path);
|
Assert.Equal(Path.GetFullPath(path2), result[1].Path);
|
||||||
Assert.Equal(Path.GetFileName(f2), result[1].Source.FileName);
|
Assert.Equal(Path.GetFileName(f2), result[1].Source.FileName);
|
||||||
Assert.Equal(1, result[1].Source.LineNumber);
|
Assert.Equal(1, result[1].Source.LineNumber);
|
||||||
Assert.Equal(Path.GetFullPath("/etc/hosts.c"), result[2].Path);
|
Assert.Equal(Path.GetFullPath(path3), result[2].Path);
|
||||||
Assert.Equal(2, result[2].Source.LineNumber);
|
Assert.Equal(2, result[2].Source.LineNumber);
|
||||||
Assert.True(result[2].Source.IsReadOnly == !result[2].Source.IsManaged);
|
Assert.True(result[2].Source.IsReadOnly == !result[2].Source.IsManaged);
|
||||||
}
|
}
|
||||||
@@ -583,10 +621,12 @@ public class DnsmasqConfIncludeParserTests
|
|||||||
Directory.CreateDirectory(dir);
|
Directory.CreateDirectory(dir);
|
||||||
var mainPath = Path.Combine(dir, "dnsmasq.conf");
|
var mainPath = Path.Combine(dir, "dnsmasq.conf");
|
||||||
var managedPath = Path.Combine(dir, "zz-managed.conf");
|
var managedPath = Path.Combine(dir, "zz-managed.conf");
|
||||||
|
var readOnlyPath = "/other/hosts";
|
||||||
|
var managedAddnPath = "/managed/hosts";
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
File.WriteAllText(mainPath, "addn-hosts=/other/hosts\n");
|
File.WriteAllText(mainPath, $"addn-hosts={readOnlyPath}\n");
|
||||||
File.WriteAllText(managedPath, "addn-hosts=/managed/hosts\n");
|
File.WriteAllText(managedPath, $"addn-hosts={managedAddnPath}\n");
|
||||||
var result = DnsmasqConfIncludeParser.GetAddnHostsPathsFromConfigFilesWithSource(new[] { mainPath, managedPath }, managedPath);
|
var result = DnsmasqConfIncludeParser.GetAddnHostsPathsFromConfigFilesWithSource(new[] { mainPath, managedPath }, managedPath);
|
||||||
Assert.Equal(2, result.Count);
|
Assert.Equal(2, result.Count);
|
||||||
Assert.False(result[0].Source.IsManaged);
|
Assert.False(result[0].Source.IsManaged);
|
||||||
@@ -607,15 +647,18 @@ public class DnsmasqConfIncludeParserTests
|
|||||||
Directory.CreateDirectory(dir);
|
Directory.CreateDirectory(dir);
|
||||||
var f1 = Path.Combine(dir, "01.conf");
|
var f1 = Path.Combine(dir, "01.conf");
|
||||||
var f2 = Path.Combine(dir, "02.conf");
|
var f2 = Path.Combine(dir, "02.conf");
|
||||||
|
var v1 = "1.1.1.1";
|
||||||
|
var v2 = "/local/";
|
||||||
|
var v3 = "/example.com/192.168.1.1";
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
File.WriteAllText(f1, "server=1.1.1.1\nlocal=/local/\n");
|
File.WriteAllText(f1, $"server={v1}\nlocal={v2}\n");
|
||||||
File.WriteAllText(f2, "server=/example.com/192.168.1.1\n");
|
File.WriteAllText(f2, $"server={v3}\n");
|
||||||
var result = DnsmasqConfIncludeParser.GetMultiValueFromConfigFiles(new[] { f1, f2 }, DnsmasqConfKeys.ServerLocalKeys);
|
var result = DnsmasqConfIncludeParser.GetMultiValueFromConfigFiles(new[] { f1, f2 }, DnsmasqConfKeys.ServerLocalKeys);
|
||||||
Assert.Equal(3, result.Count);
|
Assert.Equal(3, result.Count);
|
||||||
Assert.Equal("1.1.1.1", result[0]);
|
Assert.Equal(v1, result[0]);
|
||||||
Assert.Equal("/local/", result[1]);
|
Assert.Equal(v2, result[1]);
|
||||||
Assert.Equal("/example.com/192.168.1.1", result[2]);
|
Assert.Equal(v3, result[2]);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -629,13 +672,15 @@ public class DnsmasqConfIncludeParserTests
|
|||||||
var dir = Path.Combine(Path.GetTempPath(), "dnsmasq-dhcp-" + Guid.NewGuid().ToString("N"));
|
var dir = Path.Combine(Path.GetTempPath(), "dnsmasq-dhcp-" + Guid.NewGuid().ToString("N"));
|
||||||
Directory.CreateDirectory(dir);
|
Directory.CreateDirectory(dir);
|
||||||
var conf = Path.Combine(dir, "dnsmasq.conf");
|
var conf = Path.Combine(dir, "dnsmasq.conf");
|
||||||
|
var range1 = "172.28.0.10,172.28.0.50,12h";
|
||||||
|
var range2 = "192.168.1.10,192.168.1.100,255.255.255.0,24h";
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
File.WriteAllText(conf, "dhcp-range=172.28.0.10,172.28.0.50,12h\ndhcp-range=192.168.1.10,192.168.1.100,255.255.255.0,24h\n");
|
File.WriteAllText(conf, $"dhcp-range={range1}\ndhcp-range={range2}\n");
|
||||||
var result = DnsmasqConfIncludeParser.GetMultiValueFromConfigFiles(new[] { conf }, DnsmasqConfKeys.DhcpRange);
|
var result = DnsmasqConfIncludeParser.GetMultiValueFromConfigFiles(new[] { conf }, DnsmasqConfKeys.DhcpRange);
|
||||||
Assert.Equal(2, result.Count);
|
Assert.Equal(2, result.Count);
|
||||||
Assert.Equal("172.28.0.10,172.28.0.50,12h", result[0]);
|
Assert.Equal(range1, result[0]);
|
||||||
Assert.Equal("192.168.1.10,192.168.1.100,255.255.255.0,24h", result[1]);
|
Assert.Equal(range2, result[1]);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -41,7 +41,10 @@ public class DnsmasqConfigSetServiceTests
|
|||||||
ConfigSetCache? cache = null;
|
ConfigSetCache? cache = null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
File.WriteAllText(mainPath, "port=53\ncache-size=500\naddn-hosts=/etc/hosts.extra\n");
|
int port = 53;
|
||||||
|
int cacheSize = 500;
|
||||||
|
var addnHostsPath = "/etc/hosts.extra";
|
||||||
|
File.WriteAllText(mainPath, $"port={port}\ncache-size={cacheSize}\naddn-hosts={addnHostsPath}\n");
|
||||||
var options = Options.Create(new DnsmasqOptions
|
var options = Options.Create(new DnsmasqOptions
|
||||||
{
|
{
|
||||||
MainConfigPath = mainPath,
|
MainConfigPath = mainPath,
|
||||||
@@ -52,10 +55,10 @@ public class DnsmasqConfigSetServiceTests
|
|||||||
var (config, sources) = service.GetEffectiveConfigWithSources();
|
var (config, sources) = service.GetEffectiveConfigWithSources();
|
||||||
|
|
||||||
Assert.NotNull(config);
|
Assert.NotNull(config);
|
||||||
Assert.Equal(53, config.Port);
|
Assert.Equal(port, config.Port);
|
||||||
Assert.Equal(500, config.CacheSize);
|
Assert.Equal(cacheSize, config.CacheSize);
|
||||||
Assert.Single(config.AddnHostsPaths);
|
Assert.Single(config.AddnHostsPaths);
|
||||||
Assert.Equal(Path.GetFullPath("/etc/hosts.extra"), config.AddnHostsPaths[0]);
|
Assert.Equal(Path.GetFullPath(addnHostsPath), config.AddnHostsPaths[0]);
|
||||||
Assert.Empty(config.ServerLocalValues);
|
Assert.Empty(config.ServerLocalValues);
|
||||||
Assert.Empty(config.DhcpRanges);
|
Assert.Empty(config.DhcpRanges);
|
||||||
|
|
||||||
@@ -64,7 +67,7 @@ public class DnsmasqConfigSetServiceTests
|
|||||||
Assert.Equal(Path.GetFileName(mainPath), sources.Port!.FileName);
|
Assert.Equal(Path.GetFileName(mainPath), sources.Port!.FileName);
|
||||||
Assert.NotNull(sources.CacheSize);
|
Assert.NotNull(sources.CacheSize);
|
||||||
Assert.Single(sources.AddnHostsPaths);
|
Assert.Single(sources.AddnHostsPaths);
|
||||||
Assert.Equal(Path.GetFullPath("/etc/hosts.extra"), sources.AddnHostsPaths[0].Path);
|
Assert.Equal(Path.GetFullPath(addnHostsPath), sources.AddnHostsPaths[0].Path);
|
||||||
Assert.NotNull(sources.AddnHostsPaths[0].Source);
|
Assert.NotNull(sources.AddnHostsPaths[0].Source);
|
||||||
Assert.Equal(Path.GetFileName(mainPath), sources.AddnHostsPaths[0].Source!.FileName);
|
Assert.Equal(Path.GetFileName(mainPath), sources.AddnHostsPaths[0].Source!.FileName);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,63 +19,113 @@ public class DnsmasqLeasesFileLineParserTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void ParseLine_ValidFiveFields_Parses()
|
public void ParseLine_ValidFiveFields_Parses()
|
||||||
{
|
{
|
||||||
var e = DnsmasqLeasesFileLineParser.ParseLine("946689575 00:00:00:00:00:05 192.168.1.155 wdt 01:00:00:00:00:00:05");
|
long epoch = 946689575;
|
||||||
|
string mac = "00:00:00:00:00:05";
|
||||||
|
string address = "192.168.1.155";
|
||||||
|
string name = "wdt";
|
||||||
|
string clientId = "01:00:00:00:00:00:05";
|
||||||
|
|
||||||
|
var line = $"{epoch} {mac} {address} {name} {clientId}";
|
||||||
|
var e = DnsmasqLeasesFileLineParser.ParseLine(line);
|
||||||
|
|
||||||
Assert.NotNull(e);
|
Assert.NotNull(e);
|
||||||
Assert.Equal(946689575, e!.Epoch);
|
Assert.Equal(epoch, e!.Epoch);
|
||||||
Assert.Equal("00:00:00:00:00:05", e.Mac);
|
Assert.Equal(mac, e.Mac);
|
||||||
Assert.Equal("192.168.1.155", e.Address);
|
Assert.Equal(address, e.Address);
|
||||||
Assert.Equal("wdt", e.Name);
|
Assert.Equal(name, e.Name);
|
||||||
Assert.Equal("01:00:00:00:00:00:05", e.ClientId);
|
Assert.Equal(clientId, e.ClientId);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ParseLine_UnknownHostname_Asterisk()
|
public void ParseLine_UnknownHostname_Asterisk()
|
||||||
{
|
{
|
||||||
var e = DnsmasqLeasesFileLineParser.ParseLine("946689522 00:00:00:00:00:04 192.168.1.237 * 01:00:00:00:00:00:04");
|
long epoch = 946689522;
|
||||||
|
string mac = "00:00:00:00:00:04";
|
||||||
|
string address = "192.168.1.237";
|
||||||
|
string name = "*";
|
||||||
|
string clientId = "01:00:00:00:00:00:04";
|
||||||
|
|
||||||
|
var line = $"{epoch} {mac} {address} {name} {clientId}";
|
||||||
|
var e = DnsmasqLeasesFileLineParser.ParseLine(line);
|
||||||
|
|
||||||
Assert.NotNull(e);
|
Assert.NotNull(e);
|
||||||
Assert.Equal("*", e!.Name);
|
Assert.Equal(name, e!.Name);
|
||||||
Assert.Equal("192.168.1.237", e.Address);
|
Assert.Equal(address, e.Address);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ParseLine_UnknownClientId_Asterisk()
|
public void ParseLine_UnknownClientId_Asterisk()
|
||||||
{
|
{
|
||||||
var e = DnsmasqLeasesFileLineParser.ParseLine("946689351 00:0f:b0:3a:b5:0b 192.168.1.208 colinux *");
|
long epoch = 946689351;
|
||||||
|
string mac = "00:0f:b0:3a:b5:0b";
|
||||||
|
string address = "192.168.1.208";
|
||||||
|
string name = "colinux";
|
||||||
|
string clientId = "*";
|
||||||
|
|
||||||
|
var line = $"{epoch} {mac} {address} {name} {clientId}";
|
||||||
|
var e = DnsmasqLeasesFileLineParser.ParseLine(line);
|
||||||
|
|
||||||
Assert.NotNull(e);
|
Assert.NotNull(e);
|
||||||
Assert.Equal("colinux", e!.Name);
|
Assert.Equal(name, e!.Name);
|
||||||
Assert.Equal("*", e.ClientId);
|
Assert.Equal(clientId, e.ClientId);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ParseLine_MultipleSpacesBetweenFields_Parses()
|
public void ParseLine_MultipleSpacesBetweenFields_Parses()
|
||||||
{
|
{
|
||||||
var e = DnsmasqLeasesFileLineParser.ParseLine(" 946689575 00:00:00:00:00:05 192.168.1.155 wdt 01:00:00:00:00:00:05 ");
|
long epoch = 946689575;
|
||||||
|
string mac = "00:00:00:00:00:05";
|
||||||
|
string address = "192.168.1.155";
|
||||||
|
string name = "wdt";
|
||||||
|
string clientId = "01:00:00:00:00:00:05";
|
||||||
|
|
||||||
|
var line = $" {epoch} {mac} {address} {name} {clientId} ";
|
||||||
|
var e = DnsmasqLeasesFileLineParser.ParseLine(line);
|
||||||
|
|
||||||
Assert.NotNull(e);
|
Assert.NotNull(e);
|
||||||
Assert.Equal(946689575, e!.Epoch);
|
Assert.Equal(epoch, e!.Epoch);
|
||||||
Assert.Equal("00:00:00:00:00:05", e.Mac);
|
Assert.Equal(mac, e.Mac);
|
||||||
Assert.Equal("192.168.1.155", e.Address);
|
Assert.Equal(address, e.Address);
|
||||||
Assert.Equal("wdt", e.Name);
|
Assert.Equal(name, e.Name);
|
||||||
Assert.Equal("01:00:00:00:00:00:05", e.ClientId);
|
Assert.Equal(clientId, e.ClientId);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ParseLine_TooFewFields_ReturnsNull()
|
public void ParseLine_TooFewFields_ReturnsNull()
|
||||||
{
|
{
|
||||||
Assert.Null(DnsmasqLeasesFileLineParser.ParseLine("946689575 00:00:00:00:00:05 192.168.1.155"));
|
long epoch = 946689575;
|
||||||
Assert.Null(DnsmasqLeasesFileLineParser.ParseLine("946689575 00:00:00:00:00:05"));
|
string mac = "00:00:00:00:00:05";
|
||||||
|
string address = "192.168.1.155";
|
||||||
|
|
||||||
|
Assert.Null(DnsmasqLeasesFileLineParser.ParseLine($"{epoch} {mac} {address}"));
|
||||||
|
Assert.Null(DnsmasqLeasesFileLineParser.ParseLine($"{epoch} {mac}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ParseLine_TrailingJunk_ReturnsNull()
|
public void ParseLine_TrailingJunk_ReturnsNull()
|
||||||
{
|
{
|
||||||
// Parser uses .End() so extra text after fifth field fails
|
// Parser uses .End() so extra text after fifth field fails
|
||||||
Assert.Null(DnsmasqLeasesFileLineParser.ParseLine("946689575 00:00:00:00:00:05 192.168.1.155 wdt 01:00:00:00:00:00:05 extra"));
|
long epoch = 946689575;
|
||||||
|
string mac = "00:00:00:00:00:05";
|
||||||
|
string address = "192.168.1.155";
|
||||||
|
string name = "wdt";
|
||||||
|
string clientId = "01:00:00:00:00:00:05";
|
||||||
|
|
||||||
|
var line = $"{epoch} {mac} {address} {name} {clientId} extra";
|
||||||
|
Assert.Null(DnsmasqLeasesFileLineParser.ParseLine(line));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ParseLine_InvalidEpoch_ReturnsNull()
|
public void ParseLine_InvalidEpoch_ReturnsNull()
|
||||||
{
|
{
|
||||||
Assert.Null(DnsmasqLeasesFileLineParser.ParseLine("notanum 00:00:00:00:00:05 192.168.1.155 wdt 01:00:00:00:00:00:05"));
|
string invalidEpoch = "notanum";
|
||||||
|
string mac = "00:00:00:00:00:05";
|
||||||
|
string address = "192.168.1.155";
|
||||||
|
string name = "wdt";
|
||||||
|
string clientId = "01:00:00:00:00:00:05";
|
||||||
|
|
||||||
|
var line = $"{invalidEpoch} {mac} {address} {name} {clientId}";
|
||||||
|
Assert.Null(DnsmasqLeasesFileLineParser.ParseLine(line));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -90,9 +140,18 @@ public class DnsmasqLeasesFileLineParserTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void Timestamp_ConvertsEpochToDateTime()
|
public void Timestamp_ConvertsEpochToDateTime()
|
||||||
{
|
{
|
||||||
var e = DnsmasqLeasesFileLineParser.ParseLine("946689575 00:00:00:00:00:05 192.168.1.155 wdt 01:00:00:00:00:00:05");
|
long epoch = 946689575;
|
||||||
|
string mac = "00:00:00:00:00:05";
|
||||||
|
string address = "192.168.1.155";
|
||||||
|
string name = "wdt";
|
||||||
|
string clientId = "01:00:00:00:00:00:05";
|
||||||
|
|
||||||
|
var line = $"{epoch} {mac} {address} {name} {clientId}";
|
||||||
|
var e = DnsmasqLeasesFileLineParser.ParseLine(line);
|
||||||
|
|
||||||
Assert.NotNull(e);
|
Assert.NotNull(e);
|
||||||
var dt = e!.Timestamp;
|
Assert.Equal(epoch, e!.Epoch);
|
||||||
|
var dt = e.Timestamp;
|
||||||
Assert.True(dt.Year >= 1999 && dt.Year <= 2000); // epoch 946689575 is ~1999-12
|
Assert.True(dt.Year >= 1999 && dt.Year <= 2000); // epoch 946689575 is ~1999-12
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -76,35 +76,41 @@ public class EffectiveDnsmasqConfigTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void GetLogsPath_AbsolutePath_ReturnsPath()
|
public void GetLogsPath_AbsolutePath_ReturnsPath()
|
||||||
{
|
{
|
||||||
var config = CreateConfig(logFacility: "/data/dnsmasq.log");
|
var path = "/data/dnsmasq.log";
|
||||||
Assert.Equal("/data/dnsmasq.log", EffectiveDnsmasqConfig.GetLogsPath(config));
|
var config = CreateConfig(logFacility: path);
|
||||||
|
Assert.Equal(path, EffectiveDnsmasqConfig.GetLogsPath(config));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void GetLogsPath_AbsolutePathWithTrim_ReturnsPath()
|
public void GetLogsPath_AbsolutePathWithTrim_ReturnsPath()
|
||||||
{
|
{
|
||||||
var config = CreateConfig(logFacility: " /var/log/dnsmasq.log ");
|
var path = "/var/log/dnsmasq.log";
|
||||||
Assert.Equal("/var/log/dnsmasq.log", EffectiveDnsmasqConfig.GetLogsPath(config));
|
var pathWithSpaces = $" {path} ";
|
||||||
|
var config = CreateConfig(logFacility: pathWithSpaces);
|
||||||
|
Assert.Equal(path, EffectiveDnsmasqConfig.GetLogsPath(config));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void GetLogsPath_RelativePathWithSlash_ReturnsPath()
|
public void GetLogsPath_RelativePathWithSlash_ReturnsPath()
|
||||||
{
|
{
|
||||||
var config = CreateConfig(logFacility: "logs/dnsmasq.log");
|
var path = "logs/dnsmasq.log";
|
||||||
Assert.Equal("logs/dnsmasq.log", EffectiveDnsmasqConfig.GetLogsPath(config));
|
var config = CreateConfig(logFacility: path);
|
||||||
|
Assert.Equal(path, EffectiveDnsmasqConfig.GetLogsPath(config));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void GetLogsPath_SyslogFacilityName_ReturnsNull()
|
public void GetLogsPath_SyslogFacilityName_ReturnsNull()
|
||||||
{
|
{
|
||||||
var config = CreateConfig(logFacility: "local0");
|
var facility = "local0";
|
||||||
|
var config = CreateConfig(logFacility: facility);
|
||||||
Assert.Null(EffectiveDnsmasqConfig.GetLogsPath(config));
|
Assert.Null(EffectiveDnsmasqConfig.GetLogsPath(config));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void GetLogsPath_Stderr_ReturnsNull()
|
public void GetLogsPath_Stderr_ReturnsNull()
|
||||||
{
|
{
|
||||||
var config = CreateConfig(logFacility: "-");
|
var facility = "-";
|
||||||
|
var config = CreateConfig(logFacility: facility);
|
||||||
Assert.Null(EffectiveDnsmasqConfig.GetLogsPath(config));
|
Assert.Null(EffectiveDnsmasqConfig.GetLogsPath(config));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ public class HostsFileLineParserTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void ParseLine_WhitespaceOnly_ReturnsPassthrough()
|
public void ParseLine_WhitespaceOnly_ReturnsPassthrough()
|
||||||
{
|
{
|
||||||
var e = HostsFileLineParser.ParseLine(" \t ", 2);
|
var raw = " \t ";
|
||||||
|
var e = HostsFileLineParser.ParseLine(raw, 2);
|
||||||
Assert.NotNull(e);
|
Assert.NotNull(e);
|
||||||
Assert.True(e!.IsPassthrough);
|
Assert.True(e!.IsPassthrough);
|
||||||
}
|
}
|
||||||
@@ -30,7 +31,8 @@ public class HostsFileLineParserTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void ParseLine_CommentOnly_TreatsWholeLineAsComment()
|
public void ParseLine_CommentOnly_TreatsWholeLineAsComment()
|
||||||
{
|
{
|
||||||
var e = HostsFileLineParser.ParseLine("# This is a comment", 1);
|
var commentLine = "# This is a comment";
|
||||||
|
var e = HostsFileLineParser.ParseLine(commentLine, 1);
|
||||||
Assert.NotNull(e);
|
Assert.NotNull(e);
|
||||||
Assert.True(e!.IsComment);
|
Assert.True(e!.IsComment);
|
||||||
Assert.True(e.IsPassthrough);
|
Assert.True(e.IsPassthrough);
|
||||||
@@ -42,7 +44,10 @@ public class HostsFileLineParserTests
|
|||||||
public void ParseLine_CommentWithDataLikeContent_DoesNotParseAsAddress()
|
public void ParseLine_CommentWithDataLikeContent_DoesNotParseAsAddress()
|
||||||
{
|
{
|
||||||
// Per hosts(5): text from # to EOL is comment - so "# 127.0.0.1 localhost" is entirely comment
|
// Per hosts(5): text from # to EOL is comment - so "# 127.0.0.1 localhost" is entirely comment
|
||||||
var e = HostsFileLineParser.ParseLine("# 127.0.0.1 localhost", 1);
|
string address = "127.0.0.1";
|
||||||
|
string name = "localhost";
|
||||||
|
var line = $"# {address} {name}";
|
||||||
|
var e = HostsFileLineParser.ParseLine(line, 1);
|
||||||
Assert.NotNull(e);
|
Assert.NotNull(e);
|
||||||
Assert.True(e!.IsComment);
|
Assert.True(e!.IsComment);
|
||||||
Assert.Equal("", e.Address);
|
Assert.Equal("", e.Address);
|
||||||
@@ -52,75 +57,99 @@ public class HostsFileLineParserTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void ParseLine_IPv4Localhost_ParsesAddressAndName()
|
public void ParseLine_IPv4Localhost_ParsesAddressAndName()
|
||||||
{
|
{
|
||||||
var e = HostsFileLineParser.ParseLine("127.0.0.1 localhost", 1);
|
string address = "127.0.0.1";
|
||||||
|
string name = "localhost";
|
||||||
|
var line = $"{address} {name}";
|
||||||
|
var e = HostsFileLineParser.ParseLine(line, 1);
|
||||||
Assert.NotNull(e);
|
Assert.NotNull(e);
|
||||||
Assert.False(e!.IsPassthrough);
|
Assert.False(e!.IsPassthrough);
|
||||||
Assert.Equal("127.0.0.1", e.Address);
|
Assert.Equal(address, e.Address);
|
||||||
Assert.Single(e.Names);
|
Assert.Single(e.Names);
|
||||||
Assert.Equal("localhost", e.Names[0]);
|
Assert.Equal(name, e.Names[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ParseLine_IPv4WithCanonicalAndAlias_ParsesAll()
|
public void ParseLine_IPv4WithCanonicalAndAlias_ParsesAll()
|
||||||
{
|
{
|
||||||
var e = HostsFileLineParser.ParseLine("127.0.1.1 thishost.example.org thishost", 1);
|
string address = "127.0.1.1";
|
||||||
|
string canonical = "thishost.example.org";
|
||||||
|
string alias = "thishost";
|
||||||
|
var line = $"{address} {canonical} {alias}";
|
||||||
|
var e = HostsFileLineParser.ParseLine(line, 1);
|
||||||
Assert.NotNull(e);
|
Assert.NotNull(e);
|
||||||
Assert.Equal("127.0.1.1", e.Address);
|
Assert.Equal(address, e.Address);
|
||||||
Assert.True(e.Names.Count >= 1);
|
Assert.True(e.Names.Count >= 1);
|
||||||
Assert.Equal("thishost.example.org", e.Names[0]);
|
Assert.Equal(canonical, e.Names[0]);
|
||||||
if (e.Names.Count >= 2)
|
if (e.Names.Count >= 2)
|
||||||
Assert.Equal("thishost", e.Names[1]);
|
Assert.Equal(alias, e.Names[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ParseLine_IPv4WithMultipleSpaces_ParsesCorrectly()
|
public void ParseLine_IPv4WithMultipleSpaces_ParsesCorrectly()
|
||||||
{
|
{
|
||||||
var e = HostsFileLineParser.ParseLine("192.168.1.10 foo.example.org foo", 1);
|
string address = "192.168.1.10";
|
||||||
|
string name1 = "foo.example.org";
|
||||||
|
string name2 = "foo";
|
||||||
|
var line = $"{address} {name1} {name2}";
|
||||||
|
var e = HostsFileLineParser.ParseLine(line, 1);
|
||||||
Assert.NotNull(e);
|
Assert.NotNull(e);
|
||||||
Assert.Equal("192.168.1.10", e.Address);
|
Assert.Equal(address, e.Address);
|
||||||
Assert.True(e.Names.Count >= 1);
|
Assert.True(e.Names.Count >= 1);
|
||||||
Assert.Equal("foo.example.org", e.Names[0]);
|
Assert.Equal(name1, e.Names[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ParseLine_IPv6_ParsesAddressAndNames()
|
public void ParseLine_IPv6_ParsesAddressAndNames()
|
||||||
{
|
{
|
||||||
var e = HostsFileLineParser.ParseLine("::1 localhost ip6-localhost ip6-loopback", 1);
|
string address = "::1";
|
||||||
|
string name1 = "localhost";
|
||||||
|
var line = $"{address} {name1} ip6-localhost ip6-loopback";
|
||||||
|
var e = HostsFileLineParser.ParseLine(line, 1);
|
||||||
Assert.NotNull(e);
|
Assert.NotNull(e);
|
||||||
Assert.Equal("::1", e.Address);
|
Assert.Equal(address, e.Address);
|
||||||
Assert.True(e.Names.Count >= 1);
|
Assert.True(e.Names.Count >= 1);
|
||||||
Assert.Equal("localhost", e.Names[0]);
|
Assert.Equal(name1, e.Names[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ParseLine_IPv6Multicast_Parses()
|
public void ParseLine_IPv6Multicast_Parses()
|
||||||
{
|
{
|
||||||
var e = HostsFileLineParser.ParseLine("ff02::1 ip6-allnodes", 1);
|
string address = "ff02::1";
|
||||||
|
string name = "ip6-allnodes";
|
||||||
|
var line = $"{address} {name}";
|
||||||
|
var e = HostsFileLineParser.ParseLine(line, 1);
|
||||||
Assert.NotNull(e);
|
Assert.NotNull(e);
|
||||||
Assert.Equal("ff02::1", e.Address);
|
Assert.Equal(address, e.Address);
|
||||||
Assert.Single(e.Names);
|
Assert.Single(e.Names);
|
||||||
Assert.Equal("ip6-allnodes", e.Names[0]);
|
Assert.Equal(name, e.Names[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ParseLine_InlineComment_StopsAtHash()
|
public void ParseLine_InlineComment_StopsAtHash()
|
||||||
{
|
{
|
||||||
// Token stops at #; only address and names before # are parsed
|
// Token stops at #; only address and names before # are parsed
|
||||||
var e = HostsFileLineParser.ParseLine("127.0.0.1 localhost # loopback", 1);
|
string address = "127.0.0.1";
|
||||||
|
string name = "localhost";
|
||||||
|
string comment = "loopback";
|
||||||
|
var line = $"{address} {name} # {comment}";
|
||||||
|
var e = HostsFileLineParser.ParseLine(line, 1);
|
||||||
Assert.NotNull(e);
|
Assert.NotNull(e);
|
||||||
Assert.Equal("127.0.0.1", e.Address);
|
Assert.Equal(address, e.Address);
|
||||||
Assert.Single(e.Names);
|
Assert.Single(e.Names);
|
||||||
Assert.Equal("localhost", e.Names[0]);
|
Assert.Equal(name, e.Names[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ParseLine_LeadingWhitespace_TrimmedAndParsed()
|
public void ParseLine_LeadingWhitespace_TrimmedAndParsed()
|
||||||
{
|
{
|
||||||
var e = HostsFileLineParser.ParseLine(" 192.168.1.13 bar", 1);
|
string address = "192.168.1.13";
|
||||||
|
string name = "bar";
|
||||||
|
var line = $" {address} {name}";
|
||||||
|
var e = HostsFileLineParser.ParseLine(line, 1);
|
||||||
Assert.NotNull(e);
|
Assert.NotNull(e);
|
||||||
Assert.Equal("192.168.1.13", e.Address);
|
Assert.Equal(address, e.Address);
|
||||||
Assert.Single(e.Names);
|
Assert.Single(e.Names);
|
||||||
Assert.Equal("bar", e.Names[0]);
|
Assert.Equal(name, e.Names[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -136,7 +165,8 @@ public class HostsFileLineParserTests
|
|||||||
public void ParseLine_OnlyAddressNoNames_FailsContent_Passthrough()
|
public void ParseLine_OnlyAddressNoNames_FailsContent_Passthrough()
|
||||||
{
|
{
|
||||||
// Content requires address + whitespace + at least one name
|
// Content requires address + whitespace + at least one name
|
||||||
var e = HostsFileLineParser.ParseLine("192.168.1.1", 1);
|
string address = "192.168.1.1";
|
||||||
|
var e = HostsFileLineParser.ParseLine(address, 1);
|
||||||
Assert.NotNull(e);
|
Assert.NotNull(e);
|
||||||
Assert.True(e!.IsPassthrough);
|
Assert.True(e!.IsPassthrough);
|
||||||
}
|
}
|
||||||
@@ -144,7 +174,9 @@ public class HostsFileLineParserTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void ToLine_Entry_Roundtrips()
|
public void ToLine_Entry_Roundtrips()
|
||||||
{
|
{
|
||||||
var line = "127.0.0.1 localhost";
|
string address = "127.0.0.1";
|
||||||
|
string name = "localhost";
|
||||||
|
var line = $"{address} {name}";
|
||||||
var e = HostsFileLineParser.ParseLine(line, 1);
|
var e = HostsFileLineParser.ParseLine(line, 1);
|
||||||
Assert.NotNull(e);
|
Assert.NotNull(e);
|
||||||
var back = HostsFileLineParser.ToLine(e!);
|
var back = HostsFileLineParser.ToLine(e!);
|
||||||
@@ -154,19 +186,23 @@ public class HostsFileLineParserTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void ToLine_CommentedEntry_PrefixPreserved()
|
public void ToLine_CommentedEntry_PrefixPreserved()
|
||||||
{
|
{
|
||||||
var e = HostsFileLineParser.ParseLine("# 127.0.0.1 localhost", 1);
|
string address = "127.0.0.1";
|
||||||
|
string name = "localhost";
|
||||||
|
var commentLine = $"# {address} {name}";
|
||||||
|
var e = HostsFileLineParser.ParseLine(commentLine, 1);
|
||||||
Assert.NotNull(e);
|
Assert.NotNull(e);
|
||||||
// When we parse "# ..." we get isComment=true, address="", names=[]. ToLine for passthrough returns RawLine.
|
// When we parse "# ..." we get isComment=true, address="", names=[]. ToLine for passthrough returns RawLine.
|
||||||
e = new HostEntry { LineNumber = 1, Address = "127.0.0.1", Names = ["localhost"], IsComment = true };
|
e = new HostEntry { LineNumber = 1, Address = address, Names = [name], IsComment = true };
|
||||||
var back = HostsFileLineParser.ToLine(e);
|
var back = HostsFileLineParser.ToLine(e);
|
||||||
Assert.Equal("# 127.0.0.1 localhost", back);
|
Assert.Equal(commentLine, back);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ToLine_Passthrough_ReturnsRawLine()
|
public void ToLine_Passthrough_ReturnsRawLine()
|
||||||
{
|
{
|
||||||
var e = new HostEntry { LineNumber = 1, RawLine = " \t ", IsPassthrough = true };
|
var raw = " \t ";
|
||||||
Assert.Equal(" \t ", HostsFileLineParser.ToLine(e));
|
var e = new HostEntry { LineNumber = 1, RawLine = raw, IsPassthrough = true };
|
||||||
|
Assert.Equal(raw, HostsFileLineParser.ToLine(e));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<Routes />
|
<Routes @rendermode="InteractiveServer" />
|
||||||
<script src="_framework/blazor.web.js"></script>
|
<script src="_framework/blazor.web.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,9 @@
|
|||||||
<strong>Service status</strong>
|
<strong>Service status</strong>
|
||||||
@{
|
@{
|
||||||
var isActive = _status.DnsmasqStatus == "active";
|
var isActive = _status.DnsmasqStatus == "active";
|
||||||
|
var isUpdating = _refreshing || IsReloading;
|
||||||
}
|
}
|
||||||
<span class="status-dot d-inline-block rounded-circle flex-shrink-0 @(isActive ? "bg-success" : "bg-danger")" style="width: 0.5rem; height: 0.5rem;" title="@(isActive ? "Running" : "Not running")" aria-hidden="true"></span>
|
<span class="status-dot d-inline-block rounded-circle flex-shrink-0 @(isActive ? "bg-success" : "bg-danger") @(isUpdating ? "status-dot-updating" : null)" style="width: 0.5rem; height: 0.5rem;" title="@(isUpdating ? "Updating…" : (isActive ? "Running" : "Not running"))" aria-hidden="true"></span>
|
||||||
</span>
|
</span>
|
||||||
<button type="button" class="btn btn-link btn-sm p-0 text-body" aria-label="Service status polling settings" @onclick="() => OnOpenSettings.InvokeAsync()" title="Configure service status polling interval">
|
<button type="button" class="btn btn-link btn-sm p-0 text-body" aria-label="Service status polling settings" @onclick="() => OnOpenSettings.InvokeAsync()" title="Configure service status polling interval">
|
||||||
<i class="bi bi-gear-fill bi-gear-sm" aria-hidden="true"></i>
|
<i class="bi bi-gear-fill bi-gear-sm" aria-hidden="true"></i>
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ public partial class StatusSection : IDisposable
|
|||||||
|
|
||||||
[Parameter] public int RefreshIntervalSeconds { get; set; } = 15;
|
[Parameter] public int RefreshIntervalSeconds { get; set; } = 15;
|
||||||
|
|
||||||
|
[Parameter] public bool IsReloading { get; set; }
|
||||||
|
|
||||||
[Parameter] public EventCallback OnOpenSettings { get; set; }
|
[Parameter] public EventCallback OnOpenSettings { get; set; }
|
||||||
|
|
||||||
[Inject] private IStatusClient StatusClient { get; set; } = null!;
|
[Inject] private IStatusClient StatusClient { get; set; } = null!;
|
||||||
@@ -52,9 +54,13 @@ public partial class StatusSection : IDisposable
|
|||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static readonly TimeSpan MinUpdatingDuration = TimeSpan.FromSeconds(1);
|
||||||
|
|
||||||
private async Task RefreshAsync()
|
private async Task RefreshAsync()
|
||||||
{
|
{
|
||||||
|
var started = DateTime.UtcNow;
|
||||||
_refreshing = true;
|
_refreshing = true;
|
||||||
|
StateHasChanged();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var token = _cts.Token;
|
var token = _cts.Token;
|
||||||
@@ -67,6 +73,13 @@ public partial class StatusSection : IDisposable
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
var elapsed = DateTime.UtcNow - started;
|
||||||
|
var remaining = MinUpdatingDuration - elapsed;
|
||||||
|
if (remaining > TimeSpan.Zero)
|
||||||
|
{
|
||||||
|
try { await Task.Delay(remaining, _cts.Token).ConfigureAwait(false); }
|
||||||
|
catch (OperationCanceledException) { }
|
||||||
|
}
|
||||||
_refreshing = false;
|
_refreshing = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
/* Status dot pulse when fetching/updating status */
|
||||||
|
@keyframes status-dot-pulse {
|
||||||
|
0%, 100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.35;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot.status-dot-updating {
|
||||||
|
animation: status-dot-pulse 0.7s ease-in-out infinite;
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
@inherits LayoutComponentBase
|
@inherits LayoutComponentBase
|
||||||
@namespace DnsmasqWebUI.Components.Layout
|
@namespace DnsmasqWebUI.Components.Layout
|
||||||
|
@inject ISettingsModalService SettingsModalService
|
||||||
|
@implements IDisposable
|
||||||
|
|
||||||
<div class="page">
|
<div class="page">
|
||||||
<div class="sidebar">
|
<div class="sidebar">
|
||||||
@@ -14,8 +16,41 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<SettingsModal IsVisible="_settingsModalOpen" Title="@_settingsModalTitle" SettingsContext="_settingsModalContext" OnClose="CloseSettingsModal" />
|
||||||
|
|
||||||
<div id="blazor-error-ui" data-nosnippet>
|
<div id="blazor-error-ui" data-nosnippet>
|
||||||
An unhandled error has occurred.
|
An unhandled error has occurred.
|
||||||
<a href="." class="reload">Reload</a>
|
<a href="." class="reload">Reload</a>
|
||||||
<span class="dismiss" aria-label="Dismiss">×</span>
|
<span class="dismiss" aria-label="Dismiss">×</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private bool _settingsModalOpen;
|
||||||
|
private string _settingsModalTitle = "Settings";
|
||||||
|
private SettingsModalContext _settingsModalContext = SettingsModalContext.All;
|
||||||
|
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
SettingsModalService.OpenRequested += OnSettingsOpenRequested;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSettingsOpenRequested(SettingsModalContext context, string title)
|
||||||
|
{
|
||||||
|
_settingsModalContext = context;
|
||||||
|
_settingsModalTitle = title;
|
||||||
|
_settingsModalOpen = true;
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CloseSettingsModal()
|
||||||
|
{
|
||||||
|
_settingsModalOpen = false;
|
||||||
|
SettingsModalService.NotifySettingsChanged();
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
SettingsModalService.OpenRequested -= OnSettingsOpenRequested;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
@namespace DnsmasqWebUI.Components.Layout
|
@namespace DnsmasqWebUI.Components.Layout
|
||||||
@inject IOptions<ApplicationOptions> AppOptions
|
@inject IOptions<ApplicationOptions> AppOptions
|
||||||
|
@inject ISettingsModalService SettingsModalService
|
||||||
|
|
||||||
<div class="top-row ps-3 navbar navbar-dark">
|
<div class="top-row ps-3 navbar navbar-dark">
|
||||||
<div class="container-fluid d-flex align-items-center">
|
<div class="container-fluid d-flex align-items-center">
|
||||||
@@ -7,6 +8,9 @@
|
|||||||
<span class="brand-logo-wrap"><img src="logo.webp" alt="" class="brand-logo" /></span>
|
<span class="brand-logo-wrap"><img src="logo.webp" alt="" class="brand-logo" /></span>
|
||||||
<span>@AppTitle</span>
|
<span>@AppTitle</span>
|
||||||
</a>
|
</a>
|
||||||
|
<button type="button" class="nav-settings-btn" aria-label="Settings" title="Settings" @onclick="OpenAllSettings">
|
||||||
|
<i class="bi bi-gear-fill" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<input type="checkbox" title="Navigation menu" class="navbar-toggler" id="nav-toggler" aria-label="Toggle navigation" />
|
<input type="checkbox" title="Navigation menu" class="navbar-toggler" id="nav-toggler" aria-label="Toggle navigation" />
|
||||||
</div>
|
</div>
|
||||||
@@ -33,4 +37,6 @@
|
|||||||
|
|
||||||
@code {
|
@code {
|
||||||
private string? AppTitle => AppOptions?.Value?.EffectiveTitle;
|
private string? AppTitle => AppOptions?.Value?.EffectiveTitle;
|
||||||
|
|
||||||
|
private void OpenAllSettings() => SettingsModalService.Open(SettingsModalContext.All, "Settings");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,38 @@
|
|||||||
background-color: rgba(0,0,0,0.4);
|
background-color: rgba(0,0,0,0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Settings button: right of brand, same visual style as nav. On mobile, leaves space for toggler. */
|
||||||
|
.nav-settings-btn {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
padding: 0.375rem 0.5rem;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: background-color 0.15s ease, color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-settings-btn:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.12);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-settings-btn .bi-gear-fill {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* On mobile: leave space for toggler (right: 1rem, width 3.5rem) so settings doesn't overlap */
|
||||||
|
@media (max-width: 640.98px) {
|
||||||
|
.nav-settings-btn {
|
||||||
|
margin-right: 4rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.navbar-brand {
|
.navbar-brand {
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|||||||
@@ -16,33 +16,36 @@
|
|||||||
{
|
{
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Search settings</label>
|
<label class="form-label">Search settings</label>
|
||||||
<input type="search" class="form-control" placeholder="Search..."
|
<input type="search" class="form-control" placeholder="Search..." autocomplete="off"
|
||||||
@bind="_searchTerm" @bind:event="oninput" />
|
@bind="_searchTerm" @bind:event="oninput" @bind:after="FilterSettings" />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@if (ShouldShowSection("ServiceStatus"))
|
@if (ShouldShowSection(SettingsModalSections.ServiceStatus))
|
||||||
{
|
{
|
||||||
|
var meta = SettingsModalSections.All[SettingsModalSections.ServiceStatus];
|
||||||
<section class="mb-4">
|
<section class="mb-4">
|
||||||
<h6 class="text-muted">Service status</h6>
|
<h6 class="text-muted">@meta.DisplayName</h6>
|
||||||
<label for="status-interval" class="form-label">Polling interval (seconds)</label>
|
<label for="status-interval" class="form-label">@meta.FieldLabel</label>
|
||||||
<input id="status-interval" type="number" class="form-control" min="5" step="1"
|
<input id="status-interval" type="number" class="form-control" min="5" step="1"
|
||||||
@bind="_editingSettings.ServiceStatusPollingIntervalSeconds" />
|
@bind="_editingSettings.ServiceStatusPollingIntervalSeconds" />
|
||||||
</section>
|
</section>
|
||||||
}
|
}
|
||||||
@if (ShouldShowSection("Logs"))
|
@if (ShouldShowSection(SettingsModalSections.Logs))
|
||||||
{
|
{
|
||||||
|
var meta = SettingsModalSections.All[SettingsModalSections.Logs];
|
||||||
<section class="mb-4">
|
<section class="mb-4">
|
||||||
<h6 class="text-muted">Recent logs</h6>
|
<h6 class="text-muted">@meta.DisplayName</h6>
|
||||||
<label for="logs-interval" class="form-label">Polling interval (seconds)</label>
|
<label for="logs-interval" class="form-label">@meta.FieldLabel</label>
|
||||||
<input id="logs-interval" type="number" class="form-control" min="5" step="1"
|
<input id="logs-interval" type="number" class="form-control" min="5" step="1"
|
||||||
@bind="_editingSettings.RecentLogsPollingIntervalSeconds" />
|
@bind="_editingSettings.RecentLogsPollingIntervalSeconds" />
|
||||||
</section>
|
</section>
|
||||||
}
|
}
|
||||||
@if (ShouldShowSection("Leases"))
|
@if (ShouldShowSection(SettingsModalSections.Leases))
|
||||||
{
|
{
|
||||||
|
var meta = SettingsModalSections.All[SettingsModalSections.Leases];
|
||||||
<section class="mb-4">
|
<section class="mb-4">
|
||||||
<h6 class="text-muted">DHCP leases</h6>
|
<h6 class="text-muted">@meta.DisplayName</h6>
|
||||||
<label for="leases-interval" class="form-label">Refresh interval (seconds)</label>
|
<label for="leases-interval" class="form-label">@meta.FieldLabel</label>
|
||||||
<input id="leases-interval" type="number" class="form-control" min="5" step="1"
|
<input id="leases-interval" type="number" class="form-control" min="5" step="1"
|
||||||
@bind="_editingSettings.LeasesPollingIntervalSeconds" />
|
@bind="_editingSettings.LeasesPollingIntervalSeconds" />
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ public partial class SettingsModal : IAsyncDisposable
|
|||||||
// Only load from storage when the modal opens; parent re-renders (e.g. refresh timer) must not overwrite in-progress edits
|
// Only load from storage when the modal opens; parent re-renders (e.g. refresh timer) must not overwrite in-progress edits
|
||||||
if (justOpened)
|
if (justOpened)
|
||||||
{
|
{
|
||||||
|
_searchTerm = string.Empty;
|
||||||
_editingSettings = await ClientSettingsService.LoadSettingsAsync();
|
_editingSettings = await ClientSettingsService.LoadSettingsAsync();
|
||||||
_editingSettings = new ClientSettings
|
_editingSettings = new ClientSettings
|
||||||
{
|
{
|
||||||
@@ -68,27 +69,14 @@ public partial class SettingsModal : IAsyncDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void FilterSettings() => StateHasChanged();
|
||||||
|
|
||||||
private bool ShouldShowSection(string key)
|
private bool ShouldShowSection(string key)
|
||||||
{
|
{
|
||||||
if (SettingsContext == SettingsModalContext.ServicePolling)
|
var focusedKey = SettingsModalSections.GetSectionKeyForContext(SettingsContext);
|
||||||
return key == "ServiceStatus";
|
if (focusedKey != null)
|
||||||
if (SettingsContext == SettingsModalContext.LogsPolling)
|
return string.Equals(key, focusedKey, StringComparison.OrdinalIgnoreCase);
|
||||||
return key == "Logs";
|
return SettingsModalSections.MatchesSearch(key, _searchTerm);
|
||||||
if (SettingsContext == SettingsModalContext.LeasesPolling)
|
|
||||||
return key == "Leases";
|
|
||||||
if (SettingsContext == SettingsModalContext.All)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(_searchTerm)) return true;
|
|
||||||
var term = _searchTerm.Trim();
|
|
||||||
return key switch
|
|
||||||
{
|
|
||||||
"ServiceStatus" => "service status polling".Contains(term, StringComparison.OrdinalIgnoreCase),
|
|
||||||
"Logs" => "recent logs polling".Contains(term, StringComparison.OrdinalIgnoreCase),
|
|
||||||
"Leases" => "dhcp leases refresh polling".Contains(term, StringComparison.OrdinalIgnoreCase),
|
|
||||||
_ => false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task Save()
|
private async Task Save()
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
@inject ILeasesClient LeasesClient
|
@inject ILeasesClient LeasesClient
|
||||||
@inject IReloadClient ReloadClient
|
@inject IReloadClient ReloadClient
|
||||||
@inject IClientSettingsService ClientSettingsService
|
@inject IClientSettingsService ClientSettingsService
|
||||||
|
@inject ISettingsModalService SettingsModalService
|
||||||
@inject IOptions<ApplicationOptions> AppOptions
|
@inject IOptions<ApplicationOptions> AppOptions
|
||||||
@implements IDisposable
|
@implements IDisposable
|
||||||
|
|
||||||
@@ -320,7 +321,6 @@ else
|
|||||||
<button class="btn btn-primary" @onclick="() => LoadLeases(showLoading: true, forceRefresh: true)" disabled="@_leasesLoading">Refresh</button>
|
<button class="btn btn-primary" @onclick="() => LoadLeases(showLoading: true, forceRefresh: true)" disabled="@_leasesLoading">Refresh</button>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<SettingsModal IsVisible="_settingsModalOpen" Title="@_settingsModalTitle" SettingsContext="_settingsModalContext" OnClose="CloseSettingsModal" />
|
|
||||||
</section>
|
</section>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -336,9 +336,6 @@ else
|
|||||||
private Timer? _leasesRefreshTimer;
|
private Timer? _leasesRefreshTimer;
|
||||||
private int _leasesRefreshIntervalSeconds = 5;
|
private int _leasesRefreshIntervalSeconds = 5;
|
||||||
private string? _message;
|
private string? _message;
|
||||||
private bool _settingsModalOpen;
|
|
||||||
private string _settingsModalTitle = "Client settings";
|
|
||||||
private SettingsModalContext _settingsModalContext = SettingsModalContext.All;
|
|
||||||
private bool _saving;
|
private bool _saving;
|
||||||
|
|
||||||
private IEnumerable<DhcpHostEntry> _visibleStaticEntries =>
|
private IEnumerable<DhcpHostEntry> _visibleStaticEntries =>
|
||||||
@@ -404,6 +401,7 @@ else
|
|||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
|
SettingsModalService.SettingsChanged += OnSettingsChanged;
|
||||||
await Load();
|
await Load();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -428,20 +426,18 @@ else
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OpenLeasesSettings()
|
private void OpenLeasesSettings() => SettingsModalService.Open(SettingsModalContext.LeasesPolling, "Leases refresh interval");
|
||||||
{
|
|
||||||
_settingsModalContext = SettingsModalContext.LeasesPolling;
|
|
||||||
_settingsModalTitle = "Leases refresh interval";
|
|
||||||
_settingsModalOpen = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task CloseSettingsModal()
|
private async void OnSettingsChanged()
|
||||||
{
|
{
|
||||||
_settingsModalOpen = false;
|
await InvokeAsync(async () =>
|
||||||
var clientSettings = await ClientSettingsService.LoadSettingsAsync();
|
{
|
||||||
_leasesRefreshIntervalSeconds = Math.Clamp(clientSettings.LeasesPollingIntervalSeconds, 5, 300);
|
var clientSettings = await ClientSettingsService.LoadSettingsAsync();
|
||||||
if (_status != null && _status.LeasesPathConfigured)
|
_leasesRefreshIntervalSeconds = Math.Clamp(clientSettings.LeasesPollingIntervalSeconds, 5, 300);
|
||||||
StartLeasesRefreshTimer();
|
if (_status != null && _status.LeasesPathConfigured)
|
||||||
|
StartLeasesRefreshTimer();
|
||||||
|
StateHasChanged();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void StartLeasesRefreshTimer()
|
private void StartLeasesRefreshTimer()
|
||||||
@@ -640,6 +636,7 @@ else
|
|||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
|
SettingsModalService.SettingsChanged -= OnSettingsChanged;
|
||||||
_leasesRefreshTimer?.Dispose();
|
_leasesRefreshTimer?.Dispose();
|
||||||
_leasesRefreshTimer = null;
|
_leasesRefreshTimer = null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
@inject IConfigSetClient ConfigSetClient
|
@inject IConfigSetClient ConfigSetClient
|
||||||
@inject IReloadClient ReloadClient
|
@inject IReloadClient ReloadClient
|
||||||
@inject IClientSettingsService ClientSettingsService
|
@inject IClientSettingsService ClientSettingsService
|
||||||
|
@inject ISettingsModalService SettingsModalService
|
||||||
@inject IOptions<ApplicationOptions> AppOptions
|
@inject IOptions<ApplicationOptions> AppOptions
|
||||||
@implements IDisposable
|
@implements IDisposable
|
||||||
|
|
||||||
@@ -77,10 +78,8 @@ else if (_status != null)
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<StatusSection RefreshIntervalSeconds="_serviceStatusIntervalSeconds" OnOpenSettings="OpenServiceStatusSettings" />
|
<StatusSection RefreshIntervalSeconds="_serviceStatusIntervalSeconds" IsReloading="_reloading" OnOpenSettings="OpenServiceStatusSettings" />
|
||||||
<LogsSection RefreshIntervalSeconds="_recentLogsIntervalSeconds" OnOpenSettings="OpenRecentLogsSettings" />
|
<LogsSection RefreshIntervalSeconds="_recentLogsIntervalSeconds" OnOpenSettings="OpenRecentLogsSettings" />
|
||||||
|
|
||||||
<SettingsModal IsVisible="_settingsModalOpen" Title="@_settingsModalTitle" SettingsContext="_settingsModalContext" OnClose="CloseSettingsModal" />
|
|
||||||
</section>
|
</section>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,10 +87,6 @@ else if (_status != null)
|
|||||||
private string AppTitle => AppOptions!.Value!.EffectiveTitle;
|
private string AppTitle => AppOptions!.Value!.EffectiveTitle;
|
||||||
private int _serviceStatusIntervalSeconds = 15;
|
private int _serviceStatusIntervalSeconds = 15;
|
||||||
private int _recentLogsIntervalSeconds = 15;
|
private int _recentLogsIntervalSeconds = 15;
|
||||||
private bool _settingsModalOpen;
|
|
||||||
private string _settingsModalTitle = "Client settings";
|
|
||||||
private SettingsModalContext _settingsModalContext = SettingsModalContext.All;
|
|
||||||
private ClientSettings _clientSettings = new();
|
|
||||||
|
|
||||||
private DnsmasqServiceStatus? _status;
|
private DnsmasqServiceStatus? _status;
|
||||||
private DnsmasqConfigSet? _configSet;
|
private DnsmasqConfigSet? _configSet;
|
||||||
@@ -105,14 +100,15 @@ else if (_status != null)
|
|||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
|
SettingsModalService.SettingsChanged += OnSettingsChanged;
|
||||||
_loading = true;
|
_loading = true;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await Task.Delay(1, _cts.Token);
|
await Task.Delay(1, _cts.Token);
|
||||||
var token = _cts.Token;
|
var token = _cts.Token;
|
||||||
_clientSettings = await ClientSettingsService.LoadSettingsAsync();
|
var clientSettings = await ClientSettingsService.LoadSettingsAsync();
|
||||||
_serviceStatusIntervalSeconds = Math.Clamp(_clientSettings.ServiceStatusPollingIntervalSeconds, 5, 300);
|
_serviceStatusIntervalSeconds = Math.Clamp(clientSettings.ServiceStatusPollingIntervalSeconds, 5, 300);
|
||||||
_recentLogsIntervalSeconds = Math.Clamp(_clientSettings.RecentLogsPollingIntervalSeconds, 5, 300);
|
_recentLogsIntervalSeconds = Math.Clamp(clientSettings.RecentLogsPollingIntervalSeconds, 5, 300);
|
||||||
_status = await StatusClient.GetStatusAsync(token);
|
_status = await StatusClient.GetStatusAsync(token);
|
||||||
if (_status != null)
|
if (_status != null)
|
||||||
{
|
{
|
||||||
@@ -140,9 +136,20 @@ else if (_status != null)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OpenServiceStatusSettings() => OpenSettingsModal(SettingsModalContext.ServicePolling, "Service status polling");
|
private void OpenServiceStatusSettings() => SettingsModalService.Open(SettingsModalContext.ServicePolling, "Service status polling");
|
||||||
|
|
||||||
private void OpenRecentLogsSettings() => OpenSettingsModal(SettingsModalContext.LogsPolling, "Recent logs polling");
|
private void OpenRecentLogsSettings() => SettingsModalService.Open(SettingsModalContext.LogsPolling, "Recent logs polling");
|
||||||
|
|
||||||
|
private async void OnSettingsChanged()
|
||||||
|
{
|
||||||
|
await InvokeAsync(async () =>
|
||||||
|
{
|
||||||
|
var clientSettings = await ClientSettingsService.LoadSettingsAsync();
|
||||||
|
_serviceStatusIntervalSeconds = Math.Clamp(clientSettings.ServiceStatusPollingIntervalSeconds, 5, 300);
|
||||||
|
_recentLogsIntervalSeconds = Math.Clamp(clientSettings.RecentLogsPollingIntervalSeconds, 5, 300);
|
||||||
|
StateHasChanged();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private async Task ReloadDnsmasq()
|
private async Task ReloadDnsmasq()
|
||||||
{
|
{
|
||||||
@@ -193,24 +200,9 @@ else if (_status != null)
|
|||||||
return parts.Count > 0 ? string.Join("\n\n", parts) : null;
|
return parts.Count > 0 ? string.Join("\n\n", parts) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OpenSettingsModal(SettingsModalContext context, string title)
|
|
||||||
{
|
|
||||||
_settingsModalContext = context;
|
|
||||||
_settingsModalTitle = title;
|
|
||||||
_settingsModalOpen = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task CloseSettingsModal()
|
|
||||||
{
|
|
||||||
_settingsModalOpen = false;
|
|
||||||
_clientSettings = await ClientSettingsService.LoadSettingsAsync();
|
|
||||||
_serviceStatusIntervalSeconds = Math.Clamp(_clientSettings.ServiceStatusPollingIntervalSeconds, 5, 300);
|
|
||||||
_recentLogsIntervalSeconds = Math.Clamp(_clientSettings.RecentLogsPollingIntervalSeconds, 5, 300);
|
|
||||||
StateHasChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
|
SettingsModalService.SettingsChanged -= OnSettingsChanged;
|
||||||
_cts.Cancel();
|
_cts.Cancel();
|
||||||
_cts.Dispose();
|
_cts.Dispose();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using DnsmasqWebUI.Models.Client;
|
||||||
|
|
||||||
|
namespace DnsmasqWebUI.Infrastructure.Services.Abstractions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Scoped service to open the app-level settings modal from anywhere (e.g. NavMenu, pages).
|
||||||
|
/// MainLayout subscribes to <see cref="OpenRequested"/> and hosts the modal.
|
||||||
|
/// Subscribe to <see cref="SettingsChanged"/> to refresh when settings are saved/closed.
|
||||||
|
/// </summary>
|
||||||
|
public interface ISettingsModalService
|
||||||
|
{
|
||||||
|
/// <summary>Raised when <see cref="Open"/> is called. MainLayout subscribes and shows the modal.</summary>
|
||||||
|
event Action<SettingsModalContext, string>? OpenRequested;
|
||||||
|
|
||||||
|
/// <summary>Raised when the modal is closed (save or cancel). Pages subscribe to reload their intervals.</summary>
|
||||||
|
event Action? SettingsChanged;
|
||||||
|
|
||||||
|
/// <summary>Opens the settings modal with the given context and title.</summary>
|
||||||
|
void Open(SettingsModalContext context, string title);
|
||||||
|
|
||||||
|
/// <summary>Called when the modal closes. Raises <see cref="SettingsChanged"/>.</summary>
|
||||||
|
void NotifySettingsChanged();
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
using DnsmasqWebUI.Infrastructure.Services.Abstractions;
|
||||||
|
using DnsmasqWebUI.Models.Client;
|
||||||
|
|
||||||
|
namespace DnsmasqWebUI.Infrastructure.Services;
|
||||||
|
|
||||||
|
/// <inheritdoc cref="ISettingsModalService" />
|
||||||
|
public sealed class SettingsModalService : ISettingsModalService
|
||||||
|
{
|
||||||
|
public event Action<SettingsModalContext, string>? OpenRequested;
|
||||||
|
public event Action? SettingsChanged;
|
||||||
|
|
||||||
|
public void Open(SettingsModalContext context, string title) => OpenRequested?.Invoke(context, title);
|
||||||
|
|
||||||
|
public void NotifySettingsChanged() => SettingsChanged?.Invoke();
|
||||||
|
}
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
namespace DnsmasqWebUI.Models.Client;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Which subset of client settings to show in the settings modal.
|
|
||||||
/// </summary>
|
|
||||||
public enum SettingsModalContext
|
|
||||||
{
|
|
||||||
/// <summary>Show all settings with optional search.</summary>
|
|
||||||
All,
|
|
||||||
|
|
||||||
/// <summary>Show only service status polling interval.</summary>
|
|
||||||
ServicePolling,
|
|
||||||
|
|
||||||
/// <summary>Show only recent logs polling interval.</summary>
|
|
||||||
LogsPolling,
|
|
||||||
|
|
||||||
/// <summary>Show only DHCP leases refresh interval.</summary>
|
|
||||||
LeasesPolling
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
namespace DnsmasqWebUI.Models.Client;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Which subset of client settings to show in the settings modal.
|
||||||
|
/// </summary>
|
||||||
|
public enum SettingsModalContext
|
||||||
|
{
|
||||||
|
/// <summary>Show all settings with optional search.</summary>
|
||||||
|
All,
|
||||||
|
|
||||||
|
/// <summary>Show only service status polling interval.</summary>
|
||||||
|
ServicePolling,
|
||||||
|
|
||||||
|
/// <summary>Show only recent logs polling interval.</summary>
|
||||||
|
LogsPolling,
|
||||||
|
|
||||||
|
/// <summary>Show only DHCP leases refresh interval.</summary>
|
||||||
|
LeasesPolling
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Central definition of settings modal sections: keys, display names, field labels, and searchable text.
|
||||||
|
/// </summary>
|
||||||
|
public static class SettingsModalSections
|
||||||
|
{
|
||||||
|
public const string ServiceStatus = "ServiceStatus";
|
||||||
|
public const string Logs = "Logs";
|
||||||
|
public const string Leases = "Leases";
|
||||||
|
|
||||||
|
/// <summary>Section metadata: display name (header), field label (for input), and searchable text for filtering.</summary>
|
||||||
|
public sealed record SectionMeta(string DisplayName, string FieldLabel, string SearchableText);
|
||||||
|
|
||||||
|
public static readonly IReadOnlyDictionary<string, SectionMeta> All = new Dictionary<string, SectionMeta>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
[ServiceStatus] = new("Service status", "Polling interval (seconds)", "service status polling interval seconds"),
|
||||||
|
[Logs] = new("Recent logs", "Polling interval (seconds)", "recent logs polling interval seconds"),
|
||||||
|
[Leases] = new("DHCP leases", "Refresh interval (seconds)", "dhcp leases refresh interval seconds"),
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>Maps a focused context (single-section view) to its section key. Returns null for All.</summary>
|
||||||
|
public static string? GetSectionKeyForContext(SettingsModalContext context) => context switch
|
||||||
|
{
|
||||||
|
SettingsModalContext.ServicePolling => ServiceStatus,
|
||||||
|
SettingsModalContext.LogsPolling => Logs,
|
||||||
|
SettingsModalContext.LeasesPolling => Leases,
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
|
||||||
|
public static bool MatchesSearch(string key, string searchTerm)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(searchTerm)) return true;
|
||||||
|
if (!All.TryGetValue(key, out var meta)) return false;
|
||||||
|
return meta.SearchableText.Contains(searchTerm.Trim(), StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@ using DnsmasqWebUI.Components;
|
|||||||
using DnsmasqWebUI.Models.Config;
|
using DnsmasqWebUI.Models.Config;
|
||||||
using DnsmasqWebUI.Extensions;
|
using DnsmasqWebUI.Extensions;
|
||||||
using DnsmasqWebUI.Infrastructure.Helpers.Http;
|
using DnsmasqWebUI.Infrastructure.Helpers.Http;
|
||||||
|
using DnsmasqWebUI.Infrastructure.Services;
|
||||||
|
using DnsmasqWebUI.Infrastructure.Services.Abstractions;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
// When not in Development, use the app's directory (not CWD) so static assets work when run via symlink or from any CWD.
|
// When not in Development, use the app's directory (not CWD) so static assets work when run via symlink or from any CWD.
|
||||||
@@ -22,6 +24,7 @@ builder.Services.AddSingleton<IValidateOptions<DnsmasqOptions>, DnsmasqOptionsVa
|
|||||||
|
|
||||||
// ---- Application services ----
|
// ---- Application services ----
|
||||||
builder.Services.AddApplicationServices();
|
builder.Services.AddApplicationServices();
|
||||||
|
builder.Services.AddScoped<ISettingsModalService, SettingsModalService>();
|
||||||
builder.Services.AddHttpContextAccessor();
|
builder.Services.AddHttpContextAccessor();
|
||||||
builder.Services.AddDnsmasqApiHttpClients();
|
builder.Services.AddDnsmasqApiHttpClients();
|
||||||
|
|
||||||
|
|||||||
Vendored
+1
-3
@@ -1,4 +1,4 @@
|
|||||||
# Config for the dnsmasq test container only (docker-compose.test.yml).
|
# Config for the dnsmasq test container only (docker-compose.test.yml).
|
||||||
# DNS + DHCP: forwards DNS to Cloudflare/Google with caching; hands out DHCP on testnet.
|
# DNS + DHCP: forwards DNS to Cloudflare/Google with caching; hands out DHCP on testnet.
|
||||||
# Uses same testdata layout so app and dnsmasq share hosts, dhcp.conf, leases.
|
# Uses same testdata layout so app and dnsmasq share hosts, dhcp.conf, leases.
|
||||||
# Richer config so UI shows multi-value lists (server, addn-hosts, address, dhcp-range) and readonly sources.
|
# Richer config so UI shows multi-value lists (server, addn-hosts, address, dhcp-range) and readonly sources.
|
||||||
@@ -32,5 +32,3 @@ addn-hosts=/data/hosts.extra
|
|||||||
# Log to file so the harness can show real logs (LogsCommand tails this)
|
# Log to file so the harness can show real logs (LogsCommand tails this)
|
||||||
log-facility=/data/dnsmasq.log
|
log-facility=/data/dnsmasq.log
|
||||||
log-queries
|
log-queries
|
||||||
|
|
||||||
conf-file=zz-dnsmasq-webui.conf
|
|
||||||
|
|||||||
Reference in New Issue
Block a user