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") } }