Skip to content

Commit

Permalink
add unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
f18m committed Dec 10, 2024
1 parent b93768a commit 59a110e
Show file tree
Hide file tree
Showing 6 changed files with 197 additions and 75 deletions.
6 changes: 2 additions & 4 deletions dhcp-clients-webapp-backend/pkg/uibackend/dnsmasq.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,8 @@ func chaosTXTQueryInteger(server, query string, timeout time.Duration) (int, err
return intVal, nil
}

func getDnsStats() (DnsServerStats, error) {

// this code is meant to be executed on the same machine/container where dnsmasq is running, so:
dnsServer := "localhost"
func getDnsStats(serverHost string, serverPort int) (DnsServerStats, error) {
dnsServer := fmt.Sprintf("%s:%d", serverHost, serverPort)

// since the server is local, the max query duration is expected to be small
dnsTimeout := time.Duration(500 * time.Millisecond)
Expand Down
99 changes: 99 additions & 0 deletions dhcp-clients-webapp-backend/pkg/uibackend/dnsmasq_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package uibackend

import (
"fmt"
"net"
"os"
"os/exec"
"testing"
"time"
)

func TestGetDnsStats_NoUpstreamServers(t *testing.T) {
// Start a temporary dnsmasq instance
dnsmasqCmd := exec.Command("dnsmasq", "--port=12345", "--cache-size=100", "--no-daemon", "--no-resolv") // Adjust arguments as needed
if err := dnsmasqCmd.Start(); err != nil {
t.Fatalf("Failed to start dnsmasq: %v", err)
}
defer func() {
if err := dnsmasqCmd.Process.Kill(); err != nil {
t.Errorf("Failed to kill dnsmasq: %v", err)
}
}()

// Wait for dnsmasq to start listening
for i := 0; i < 10; i++ {
if conn, err := net.DialTimeout("tcp", "localhost:12345", 1*time.Second); err == nil {
conn.Close()
break
}
time.Sleep(100 * time.Millisecond)
}

stats, err := getDnsStats("localhost", 12345)
if err != nil {
t.Fatalf("getDnsStats failed: %v", err)
}

// Assertions
if stats.CacheSize != 100 {
t.Errorf("Unexpected CacheSize: got %d, want %d", stats.CacheSize, 100)
}

// Check for upstream servers. Since we started with --no-resolv, there shouldn't be any upstream servers initially.
if len(stats.UpstreamServers) != 0 {
t.Errorf("Unexpected Upstream Servers found: %v", stats.UpstreamServers)
}

}

func TestGetDnsStats_WithUpstreamServers(t *testing.T) {
// Test with resolv-file, simulating an upstream server
resolvFileContent := "nameserver 8.8.4.4" // Example upstream server
resolvFilePath := "/tmp/resolv.conf" // Choose a temporary file
err := writeTempFile(resolvFilePath, resolvFileContent)
if err != nil {
t.Fatalf("Failed to write temporary resolv file: %v", err)
}

// Restart dnsmasq with the resolv file
dnsmasqCmd := exec.Command("dnsmasq", "--port=12346", "--cache-size=100", "--no-daemon", fmt.Sprintf("--resolv-file=%s", resolvFilePath))
if err := dnsmasqCmd.Start(); err != nil {
t.Fatalf("Failed to restart dnsmasq with resolv-file: %v", err)
}
defer func() {
if err := dnsmasqCmd.Process.Kill(); err != nil {
t.Errorf("Failed to kill dnsmasq: %v", err)
}
}()
for i := 0; i < 10; i++ {
if conn, err := net.DialTimeout("tcp", "localhost:12346", 1*time.Second); err == nil {
conn.Close()
break
}
time.Sleep(100 * time.Millisecond)
}

stats, err := getDnsStats("localhost", 12346)
if err != nil {
t.Fatalf("getDnsStats failed with resolv-file: %v", err)
}
if len(stats.UpstreamServers) != 1 {
t.Errorf("Expected Upstream Servers but found none")
}
if stats.UpstreamServers[0].ServerURL != "8.8.4.4#53" {
t.Errorf("Expected google upstream Servers but found something else")
}
}

// Helper function to write to a temporary file
func writeTempFile(filePath, content string) error { // ... (implementation remains the same)
file, err := os.Create(filePath)
if err != nil {
return err
}
defer file.Close()

_, err = file.WriteString(content)
return err
}
3 changes: 3 additions & 0 deletions dhcp-clients-webapp-backend/pkg/uibackend/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ type AddonConfig struct {
// DNS
dnsEnable bool
dnsDomain string
dnsPort int
}

// UnmarshalJSON reads the configuration of this Home Assistant addon and converts it
Expand Down Expand Up @@ -92,6 +93,7 @@ func (b *AddonConfig) UnmarshalJSON(data []byte) error {
DnsServer struct {
Enable bool `json:"enable"`
DnsDomain string `json:"dns_domain"`
Port int `json:"port"`
} `json:"dns_server"`

WebUI struct {
Expand Down Expand Up @@ -180,6 +182,7 @@ func (b *AddonConfig) UnmarshalJSON(data []byte) error {
b.addressReservationLease = cfg.DhcpServer.AddressReservationLease
b.dnsEnable = cfg.DnsServer.Enable
b.dnsDomain = cfg.DnsServer.DnsDomain
b.dnsPort = cfg.DnsServer.Port

return nil
}
Expand Down
4 changes: 3 additions & 1 deletion dhcp-clients-webapp-backend/pkg/uibackend/uibackend.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,9 @@ func (b *UIBackend) generateWebSocketMessage() WebSocketMessage {
return cmp.Compare(a.PastInfo.LastSeen.Unix(), b.PastInfo.LastSeen.Unix())
})

dnsStats, err := getDnsStats()
// this code is meant to be executed on the same machine/container where dnsmasq is running, so
// that's why we pass "localhost" as DNS server host:
dnsStats, err := getDnsStats("localhost", b.cfg.dnsPort)
if err != nil {
b.logger.Warnf("failed to get updated DNS stats: %s", err.Error())
// keep going
Expand Down
159 changes: 89 additions & 70 deletions dhcp-clients-webapp-backend/templates/dnsmasq-dhcp.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,87 @@ function initAll() {
initTableDarkOrLightTheme()
}

function processWebSocketDHCPCurrentClients(data) {
console.log("Websocket connection: received " + data.current_clients.length + " current DHCP clients from websocket");

// rerender the CURRENT table
tableData = [];
dhcp_addresses_used = 0;
dhcp_static_ip = 0;
data.current_clients.forEach(function (item, index) {
console.log(`CurrentItem ${index + 1}:`, item);

if (item.is_inside_dhcp_pool)
dhcp_addresses_used += 1;

static_ip_str = "NO";
if (item.has_static_ip) {
static_ip_str = "YES";
dhcp_static_ip += 1;
}

external_link_symbol="🡕"
//external_link_symbol="⧉"
if (item.evaluated_link) {
link_str = "<a href=\"" + item.evaluated_link + "\" target=\"_blank\">" + item.evaluated_link + "</a> " + external_link_symbol
} else {
link_str = "N/A"
}

// append new row
tableData.push([index + 1,
item.friendly_name, item.lease.hostname, link_str,
item.lease.ip_addr, item.lease.mac_addr,
formatTimeLeft(item.lease.expires), static_ip_str]);
});
table_current.clear().rows.add(tableData).draw(false /* do not reset page position */);

return dhcp_static_ip, dhcp_addresses_used
}

function processWebSocketDHCPPastClients(data) {
console.log("Websocket connection: received " + data.past_clients.length + " past DHCP clients from websocket");

// rerender the PAST table
tableData = [];
data.past_clients.forEach(function (item, index) {
console.log(`PastItem ${index + 1}:`, item);

static_ip_str = "NO";
if (item.has_static_ip) {
static_ip_str = "YES";
}

// append new row
tableData.push([index + 1,
item.friendly_name, item.past_info.hostname,
item.past_info.mac_addr, static_ip_str,
formatTimeSince(item.past_info.last_seen), item.notes]);
});
table_past.clear().rows.add(tableData).draw(false /* do not reset page position */);
}

function updateDHCPStatus(data, dhcp_static_ip, dhcp_addresses_used, messageElem) {
// compute DHCP pool usage
var usagePerc = 0
if (templated_dhcpPoolSize > 0) {
usagePerc = 100 * dhcp_addresses_used / templated_dhcpPoolSize

// truncate to only 1 digit accuracy
usagePerc = Math.round(usagePerc * 10) / 10
}

// format server uptime
uptime_str = formatTimeSince(templated_dhcpServerStartTime)

// update the message
messageElem.innerHTML = "<span class='boldText'>" + data.current_clients.length + " DHCP current clients</span> hold a DHCP lease.<br/>" +
dhcp_static_ip + " have a static IP address configuration.<br/>" +
dhcp_addresses_used + " are within the DHCP pool. DHCP pool usage is at " + usagePerc + "%.<br/>" +
"<span class='boldText'>" + data.past_clients.length + " DHCP past clients</span> contacted the server some time ago but failed to do so since last DHCP server restart, " +
uptime_str + " hh:mm:ss ago.<br/>";
}

function processWebSocketEvent(event) {

try {
Expand All @@ -187,7 +268,8 @@ function processWebSocketEvent(event) {
message.innerText = "No DHCP clients so far.";

} else if (!("current_clients" in data) ||
!("past_clients" in data)) {
!("past_clients" in data) ||
!("dns_stats" in data)) {
console.error("Websocket connection: expecting a JSON matching the golang WebSocketMessage type, received something else", data);

// clear the table
Expand All @@ -198,77 +280,14 @@ function processWebSocketEvent(event) {

} else {
// console.log("DEBUG:" + JSON.stringify(data))
console.log("Websocket connection: received " + data.current_clients.length + " current clients from websocket");
console.log("Websocket connection: received " + data.past_clients.length + " past clients from websocket");

// rerender the CURRENT table
tableData = [];
dhcp_addresses_used = 0;
dhcp_static_ip = 0;
data.current_clients.forEach(function (item, index) {
console.log(`CurrentItem ${index + 1}:`, item);

if (item.is_inside_dhcp_pool)
dhcp_addresses_used += 1;

static_ip_str = "NO";
if (item.has_static_ip) {
static_ip_str = "YES";
dhcp_static_ip += 1;
}

external_link_symbol="🡕"
//external_link_symbol="⧉"
if (item.evaluated_link) {
link_str = "<a href=\"" + item.evaluated_link + "\" target=\"_blank\">" + item.evaluated_link + "</a> " + external_link_symbol
} else {
link_str = "N/A"
}

// append new row
tableData.push([index + 1,
item.friendly_name, item.lease.hostname, link_str,
item.lease.ip_addr, item.lease.mac_addr,
formatTimeLeft(item.lease.expires), static_ip_str]);
});
table_current.clear().rows.add(tableData).draw(false /* do not reset page position */);

// rerender the PAST table
tableData = [];
data.past_clients.forEach(function (item, index) {
console.log(`PastItem ${index + 1}:`, item);

static_ip_str = "NO";
if (item.has_static_ip) {
static_ip_str = "YES";
}

// append new row
tableData.push([index + 1,
item.friendly_name, item.past_info.hostname,
item.past_info.mac_addr, static_ip_str,
formatTimeSince(item.past_info.last_seen), item.notes]);
});
table_past.clear().rows.add(tableData).draw(false /* do not reset page position */);

// compute DHCP pool usage
var usagePerc = 0
if (templated_dhcpPoolSize > 0) {
usagePerc = 100 * dhcp_addresses_used / templated_dhcpPoolSize

// truncate to only 1 digit accuracy
usagePerc = Math.round(usagePerc * 10) / 10
}

// format server uptime
uptime_str = formatTimeSince(templated_dhcpServerStartTime)
// process DHCP
dhcp_static_ip, dhcp_addresses_used = processWebSocketDHCPCurrentClients(data)
processWebSocketDHCPPastClients(data)
updateDHCPStatus(data, dhcp_static_ip, dhcp_addresses_used, message)

// update the message
message.innerHTML = "<span class='boldText'>" + data.current_clients.length + " DHCP current clients</span> hold a DHCP lease.<br/>" +
dhcp_static_ip + " have a static IP address configuration.<br/>" +
dhcp_addresses_used + " are within the DHCP pool. DHCP pool usage is at " + usagePerc + "%.<br/>" +
"<span class='boldText'>" + data.past_clients.length + " DHCP past clients</span> contacted the server some time ago but failed to do so since last DHCP server restart, " +
uptime_str + " hh:mm:ss ago.<br/>";
// process DNS

}
}

Expand Down
1 change: 1 addition & 0 deletions test-options.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
},
"dns_server": {
"enable": true,
"port": 53,
"dns_domain": "lan",
"cache_size": 10000,
"log_requests": false,
Expand Down

0 comments on commit 59a110e

Please sign in to comment.