# Evaluation-time tests: broken configs must fail with exactly the expected # assertion messages, and a valid one with none. { self, pkgs }: let inherit (pkgs) lib; evalRouter = router: (import "${pkgs.path}/nixos/lib/eval-config.nix" { system = "x86_64-linux"; modules = [ self.nixosModules.router { boot.loader.grub.enable = false; fileSystems."/" = { device = "x"; fsType = "ext4"; }; system.stateVersion = "25.05"; router = lib.recursiveUpdate { enable = true; wan.interface = "eth0"; } router; } ]; }).config; lan = overrides: lib.recursiveUpdate { interface = "eth1"; ipv4.address = "192.168.10.1"; dhcp.pool = { start = "192.168.10.100"; end = "192.168.10.199"; }; } overrides; failed = router: map (a: a.message) (lib.filter (a: !a.assertion) (evalRouter router).assertions); cases = { valid = { router.lans.main = lan { }; expect = [ ]; }; noManagement = { router.lans.main = lan { allowManagement = false; }; expect = [ "locked out" ]; }; wanIsLan = { router.lans.main = lan { interface = "eth0"; }; expect = [ "WAN interface `eth0` is also used by a LAN" ]; }; sharedInterface = { router.lans = { a = lan { }; b = lan { ipv4.address = "192.168.20.1"; dhcp.pool = { start = "192.168.20.100"; end = "192.168.20.199"; }; }; }; expect = [ "two LANs share the same interface" ]; }; badAddress = { router.lans.main = lan { ipv4.address = "192.168.10.300"; dhcp.enable = false; }; expect = [ "`192.168.10.300` is not a valid IPv4 address" ]; }; poolOutsideSubnet = { router.lans.main = lan { dhcp.pool = { start = "192.168.11.100"; end = "192.168.11.199"; }; }; expect = [ "pool is not inside 192.168.10.0/24" ]; }; poolReversed = { router.lans.main = lan { dhcp.pool = { start = "192.168.10.199"; end = "192.168.10.100"; }; }; expect = [ "starts after it ends" ]; }; poolContainsRouter = { router.lans.main = lan { dhcp.pool = { start = "192.168.10.1"; end = "192.168.10.50"; }; }; expect = [ "contains the router's own address 192.168.10.1" ]; }; overlappingLans = { router.lans = { a = lan { }; b = lan { interface = "eth2"; ipv4.address = "192.168.10.2"; }; }; expect = [ "`a` (192.168.10.0/24) and `b` (192.168.10.0/24) overlap" ]; }; pdOnOneLan = { router = { wan.ipv6.enable = true; lans.main = lan { ipv6.pd = true; }; }; expect = [ ]; }; pdOnTwoLans = { router = { wan.ipv6.enable = true; lans = { a = lan { ipv6.pd = true; }; b = lan { interface = "eth2"; ipv4.address = "192.168.20.1"; dhcp.pool = { start = "192.168.20.100"; end = "192.168.20.199"; }; ipv6.pd = true; }; }; }; expect = [ "delegated /64 cannot be split" ]; }; pdWithoutWanIpv6 = { router.lans.main = lan { ipv6.pd = true; }; expect = [ "router.wan.ipv6.enable is false" ]; }; pppoeMissingCredentials = { router = { wan.ipv4.type = "pppoe"; lans.main = lan { }; }; expect = [ "pppoe.username must be set" "pppoe.passwordFile must be set" ]; }; pppoeValid = { router = { wan = { ipv4 = { type = "pppoe"; pppoe = { vlan = 7; username = "user@isp"; passwordFile = "/var/lib/secrets/pppoe"; }; }; ipv6.enable = true; }; lans.main = lan { ipv6.pd = true; }; }; expect = [ ]; }; nonOverlappingLans = { router.lans = { a = lan { }; b = lan { interface = "eth2"; ipv4.address = "192.168.20.1"; dhcp.pool = { start = "192.168.20.100"; end = "192.168.20.199"; }; }; }; expect = [ ]; }; }; problems = lib.concatLists ( lib.mapAttrsToList ( name: c: let got = failed c.router; missing = lib.filter (e: !lib.any (m: lib.hasInfix e m) got) c.expect; in lib.optional (missing != [ ] || lib.length got != lib.length c.expect) "${name}: expected ${builtins.toJSON c.expect}, got ${builtins.toJSON got}" ) cases ); # Assertions alone do not force every option (e.g. networkd unit checks), so # fully evaluate the system for the configs that are meant to be valid. fullEval = name: let r = builtins.tryEval (builtins.seq (evalRouter cases.${name}.router).system.build.toplevel.drvPath true); in lib.optional (!r.success) "${name}: full system evaluation failed"; fullEvalProblems = lib.concatMap fullEval [ "valid" "pdOnOneLan" "pppoeValid" ]; subnet = (evalRouter { lans.main = lan { ipv4.prefixLength = 20; ipv4.address = "172.16.37.1"; dhcp.enable = false; }; }).router.lans.main.ipv4.subnet; subnetProblem = lib.optional (subnet != "172.16.32.0/20") "derived subnet: expected 172.16.32.0/20, got ${subnet}"; in pkgs.runCommand "router-assertion-tests" { } ( if problems ++ subnetProblem ++ fullEvalProblems == [ ] then "touch $out" else '' printf '%s\n' ${lib.escapeShellArgs (problems ++ subnetProblem ++ fullEvalProblems)} >&2 exit 1 '' )