Files

209 lines
8.6 KiB
Swift

import Foundation
import Testing
@testable import RunnerCore
/// Tests for parsing `/var/db/dhcpd_leases`, including the `1,` hardware-type
/// prefix, non-zero-padded octets, and newest-lease-wins on duplicate MACs.
@Suite("DHCP leases")
struct DHCPLeasesTests {
/// A file shaped like the real thing: a `1,` prefix on every `hw_address`,
/// octets that lack zero padding, one block that is missing its MAC, a
/// decimal `lease=` alongside the usual hex ones, a MAC that appears three
/// times with different expiries, and a truncated trailing block of the kind
/// a mid-write read produces.
static let fixture = """
{
\tname=macos-vm-a
\tip_address=192.168.64.2
\thw_address=1,aa:bb:c:d:ee:ff
\tidentifier=1,aa:bb:c:d:ee:ff
\tlease=0x66b2c0de
}
{
\tname=macos-vm-b
\tip_address=192.168.64.3
\thw_address=1,DE:AD:BE:EF:00:01
\tidentifier=1,de:ad:be:ef:00:01
\tlease=1723000000
}
{
\tname=no-hardware-address
\tip_address=192.168.64.4
\tlease=0x66b2c0de
}
{
\tname=macos-vm-a
\tip_address=192.168.64.9
\thw_address=1,aa:bb:0c:0d:ee:ff
\tlease=0x66b2ffff
}
{
\tname=macos-vm-a
\tip_address=192.168.64.5
\thw_address=1,aa:bb:0c:0d:ee:ff
\tlease=0x66b20000
}
{
\tname=truncated-mid-write
\tip_address=192.168.64.6
"""
@Test("a lease carries the fields it was constructed with")
func leaseIsConstructible() {
let lease = DHCPLease(
name: "guest",
ipAddress: "192.168.64.7",
hwAddress: "aa:bb:0c:dd:ee:ff",
leaseExpiry: nil
)
#expect(lease.ipAddress == "192.168.64.7")
}
@Test("well-formed blocks parse and malformed ones are skipped")
func parsesWellFormedBlocksOnly() {
let leases = DHCPLeaseParser.parse(Self.fixture)
// Six blocks in the fixture: one has no hw_address and one is truncated.
#expect(leases.count == 4)
#expect(leases.map(\.ipAddress) == ["192.168.64.2", "192.168.64.3", "192.168.64.9", "192.168.64.5"])
#expect(!leases.contains { $0.ipAddress == "192.168.64.4" })
#expect(!leases.contains { $0.ipAddress == "192.168.64.6" })
#expect(leases[0].name == "macos-vm-a")
}
@Test("the 1, prefix is stripped and octets are zero-padded")
func normalizesHardwareAddresses() {
let leases = DHCPLeaseParser.parse(Self.fixture)
#expect(leases[0].hwAddress == "aa:bb:0c:0d:ee:ff")
#expect(leases[1].hwAddress == "de:ad:be:ef:00:01")
}
@Test("lease expiry parses from hex and from decimal")
func parsesHexAndDecimalLeaseTimes() {
let leases = DHCPLeaseParser.parse(Self.fixture)
#expect(leases[0].leaseExpiry == Date(timeIntervalSince1970: TimeInterval(0x66b2_c0de)))
#expect(leases[1].leaseExpiry == Date(timeIntervalSince1970: 1_723_000_000))
}
@Test("a duplicate MAC resolves to the newest lease, whatever the file order")
func newestLeaseWinsForDuplicateMACs() {
let leases = DHCPLeaseParser.parse(Self.fixture)
// Three blocks share this MAC: 0x66b2c0de, 0x66b2ffff, then 0x66b20000.
// The newest expiry wins even though it is not the last block.
#expect(DHCPLeaseParser.ipAddress(forMAC: "aa:bb:0c:0d:ee:ff", in: leases) == "192.168.64.9")
}
@Test("the query MAC is normalized the same way as the file's")
func lookupNormalizesTheQuery() {
let leases = DHCPLeaseParser.parse(Self.fixture)
// Every rendering of the same address must find the same lease.
for query in ["aa:bb:c:d:ee:ff", "AA:BB:0C:0D:EE:FF", "aa-bb-0c-0d-ee-ff", "1,aa:bb:c:d:ee:ff"] {
#expect(DHCPLeaseParser.ipAddress(forMAC: query, in: leases) == "192.168.64.9")
}
#expect(DHCPLeaseParser.ipAddress(forMAC: "de:ad:be:ef:00:01", in: leases) == "192.168.64.3")
}
@Test("an unknown or unparseable MAC has no address")
func unknownMACHasNoAddress() {
let leases = DHCPLeaseParser.parse(Self.fixture)
#expect(DHCPLeaseParser.ipAddress(forMAC: "00:11:22:33:44:55", in: leases) == nil)
#expect(DHCPLeaseParser.ipAddress(forMAC: "not-a-mac", in: leases) == nil)
}
@Test("a lease is only newer when bootpd actually rewrote it")
func leaseFreshnessGate() {
// Slot MACs are persistent and leases live 24 h, so the previous guest's
// entry is normally still there when the next clone boots. Only a later
// expiry (or a different address) proves the new guest has leased.
let previous = DHCPLease(
name: nil,
ipAddress: "192.168.64.7",
hwAddress: "aa:bb:0c:dd:ee:ff",
leaseExpiry: Date(timeIntervalSince1970: 1_000)
)
let same = previous
let renewed = DHCPLease(
name: nil,
ipAddress: "192.168.64.7",
hwAddress: "aa:bb:0c:dd:ee:ff",
leaseExpiry: Date(timeIntervalSince1970: 2_000)
)
let reassigned = DHCPLease(
name: nil,
ipAddress: "192.168.64.9",
hwAddress: "aa:bb:0c:dd:ee:ff",
leaseExpiry: Date(timeIntervalSince1970: 1_000)
)
#expect(!DHCPLeaseParser.isNewer(same, than: previous))
#expect(DHCPLeaseParser.isNewer(renewed, than: previous))
#expect(DHCPLeaseParser.isNewer(reassigned, than: previous))
// First boot on this MAC: there is nothing to be stale against.
#expect(DHCPLeaseParser.isNewer(same, than: nil))
}
@Test("lease(forMAC:) returns the same record the address lookup uses")
func leaseLookupAgreesWithAddressLookup() {
let leases = DHCPLeaseParser.parse(Self.fixture)
let lease = DHCPLeaseParser.lease(forMAC: "aa:bb:0c:0d:ee:ff", in: leases)
#expect(lease?.ipAddress == "192.168.64.9")
#expect(lease?.ipAddress == DHCPLeaseParser.ipAddress(forMAC: "aa:bb:0c:0d:ee:ff", in: leases))
#expect(DHCPLeaseParser.lease(forMAC: "00:11:22:33:44:55", in: leases) == nil)
}
@Test("normalizeMAC accepts the renderings bootpd and VZMACAddress produce")
func normalizeMACAcceptsCommonForms() {
#expect(DHCPLeaseParser.normalizeMAC("1,aa:bb:c:dd:ee:ff") == "aa:bb:0c:dd:ee:ff")
#expect(DHCPLeaseParser.normalizeMAC("aa:bb:0c:dd:ee:ff") == "aa:bb:0c:dd:ee:ff")
#expect(DHCPLeaseParser.normalizeMAC("AA-BB-0C-DD-EE-FF") == "aa:bb:0c:dd:ee:ff")
#expect(DHCPLeaseParser.normalizeMAC(" 1,0:0:0:0:0:1 ") == "00:00:00:00:00:01")
}
@Test("normalizeMAC rejects anything that is not six hex octets")
func normalizeMACRejectsGarbage() {
#expect(DHCPLeaseParser.normalizeMAC("") == nil)
#expect(DHCPLeaseParser.normalizeMAC("aa:bb:cc:dd:ee") == nil)
#expect(DHCPLeaseParser.normalizeMAC("aa:bb:cc:dd:ee:ff:00") == nil)
#expect(DHCPLeaseParser.normalizeMAC("aa:bb:cc:dd:ee:gg") == nil)
#expect(DHCPLeaseParser.normalizeMAC("aa:bb:cc:dd:ee:fff") == nil)
#expect(DHCPLeaseParser.normalizeMAC("192.168.64.2") == nil)
}
@Test("parsing never throws on hostile input")
func parsingIsTotal() {
#expect(DHCPLeaseParser.parse("").isEmpty)
#expect(DHCPLeaseParser.parse("}}}{{{\n=\nname=\n").isEmpty)
#expect(DHCPLeaseParser.parse("{\nip_address=1.2.3.4\n").isEmpty)
// A block interrupted by the start of the next one is dropped, not merged.
let interrupted = """
{
ip_address=192.168.64.20
{
ip_address=192.168.64.21
hw_address=1,2:2:2:2:2:2
}
"""
let leases = DHCPLeaseParser.parse(interrupted)
#expect(leases.count == 1)
#expect(leases[0].ipAddress == "192.168.64.21")
#expect(leases[0].leaseExpiry == nil)
}
@Test("a missing lease database reads as no leases")
func missingFileIsEmpty() {
#expect(DHCPLeaseParser.parseFile(at: "/var/db/definitely-not-a-lease-file").isEmpty)
}
@Test("a lease database on disk round-trips through parseFile")
func parsesFromDisk() throws {
let path = FileManager.default.temporaryDirectory
.appendingPathComponent("dhcpd_leases_test_\(UUID().uuidString)")
try Self.fixture.write(to: path, atomically: true, encoding: .utf8)
defer { try? FileManager.default.removeItem(at: path) }
let leases = DHCPLeaseParser.parseFile(at: path.path)
#expect(DHCPLeaseParser.ipAddress(forMAC: "aa:bb:c:d:ee:ff", in: leases) == "192.168.64.9")
}
}