From c2783867b63e7654879bbc5e95083daefa00627c Mon Sep 17 00:00:00 2001
From: Fabian Hauser <fabian@fh2.ch>
Date: Mon, 24 Mar 2025 15:35:23 +0200
Subject: [PATCH] Add tests documentation to docs page

---
 SUMMARY.md                                   |  1 +
 checks/default.nix                           |  4 +-
 checks/nixos-modules/default.nix             | 59 ++++++++++++++++++--
 checks/nixos-modules/static-page/default.nix | 45 ---------------
 checks/nixos-modules/static-page/test.py     | 46 ---------------
 lib/default.nix                              | 28 ++++++----
 nixos-modules/static-page/test.nix           | 30 ++++++++++
 nixos-modules/static-page/test.py            | 46 +++++++++++++++
 8 files changed, 152 insertions(+), 107 deletions(-)
 delete mode 100644 checks/nixos-modules/static-page/default.nix
 delete mode 100644 checks/nixos-modules/static-page/test.py
 create mode 100644 nixos-modules/static-page/test.nix
 create mode 100644 nixos-modules/static-page/test.py

diff --git a/SUMMARY.md b/SUMMARY.md
index 11fabb0..fdbf4d1 100644
--- a/SUMMARY.md
+++ b/SUMMARY.md
@@ -1,6 +1,7 @@
 # Summary
 
 - [Repository README](README.md)
+- [Testing](checks/README.md)
 - [Deployment](deploy/README.md)
 
 ---
diff --git a/checks/default.nix b/checks/default.nix
index bf629ba..66d5b0b 100644
--- a/checks/default.nix
+++ b/checks/default.nix
@@ -4,7 +4,7 @@
   pkgs,
   deployPkgs,
   ...
-}@inputs:
+}:
 {
   ${system} = {
 
@@ -17,7 +17,7 @@
     '';
 
     nixos-modules = pkgs.callPackage ./nixos-modules {
-      inherit (self.lib) loadSubmodulesFrom;
+      inherit (self.lib) getSubDirs isFolderWithFile;
     };
 
     #TODO(#29): Integration/System tests
diff --git a/checks/nixos-modules/default.nix b/checks/nixos-modules/default.nix
index 8ddddcf..74e8a25 100644
--- a/checks/nixos-modules/default.nix
+++ b/checks/nixos-modules/default.nix
@@ -1,9 +1,60 @@
 {
   linkFarmFromDrvs,
-  callPackage,
-  loadSubmodulesFrom,
+  isFolderWithFile,
+  getSubDirs,
+  lib,
+  testers,
 }:
 let
-  tests = map (test: callPackage test { }) (loadSubmodulesFrom ./.);
+  inherit (lib)
+    filter
+    path
+    mkDefault
+    readFile
+    attrNames
+    concatStringsSep
+    pipe
+    ;
+  modulesBaseDir = ../../nixos-modules;
+  mkTest =
+    name:
+    let
+      getFilePath = file: path.append modulesBaseDir "./${name}/${file}";
+    in
+    testers.runNixOSTest {
+      inherit name;
+      imports = [
+        (import (getFilePath "test.nix") {
+          inherit name;
+          inherit lib;
+        })
+      ];
+
+      defaults.imports = [ (getFilePath "default.nix") ];
+
+      # Calls a `test(...)` python function in the test's python file with the list of nodes and helper functions.
+      # Helper symbols may be added as function args when needed and can be found in:
+      #   https://github.com/NixOS/nixpkgs/blob/master/nixos/lib/test-driver/src/test_driver/driver.py#L121
+      testScript = mkDefault (
+        { nodes, ... }:
+        let
+          script = readFile (getFilePath "test.py");
+          nodeArgs = pipe nodes [
+            attrNames
+            (map (val: "${val}=${val}"))
+            (concatStringsSep ", ")
+          ];
+        in
+        ''
+          ${script}
+          test(${nodeArgs}, subtest=subtest)
+        ''
+      );
+    };
 in
-linkFarmFromDrvs "nixos-modules" tests
+pipe modulesBaseDir [
+  getSubDirs
+  (filter (isFolderWithFile "test.nix" modulesBaseDir))
+  (map mkTest)
+  (linkFarmFromDrvs "nixos-modules")
+]
diff --git a/checks/nixos-modules/static-page/default.nix b/checks/nixos-modules/static-page/default.nix
deleted file mode 100644
index 1d90105..0000000
--- a/checks/nixos-modules/static-page/default.nix
+++ /dev/null
@@ -1,45 +0,0 @@
-{
-  testers,
-  curl,
-  lib,
-  gnugrep,
-  ...
-}:
-testers.runNixOSTest {
-  name = "static-page";
-
-  nodes.webserver =
-    { ... }:
-    {
-      # Service under test
-      imports = [ ../../../nixos-modules/static-page ];
-      qois.static-page = {
-        enable = true;
-        pages = lib.mkForce {
-          "localhost" = {
-            domainAliases = [ "example.com" ];
-          };
-        };
-      };
-
-      # Test environment
-      environment.systemPackages = [
-        curl
-        gnugrep
-      ];
-      # Disable TLS services
-      services.nginx.virtualHosts =
-        let
-          tlsOff = {
-            forceSSL = lib.mkForce false;
-            enableACME = lib.mkForce false;
-          };
-        in
-        {
-          "localhost" = tlsOff;
-          "example.com" = tlsOff;
-        };
-    };
-
-  testScript = lib.readFile ./test.py;
-}
diff --git a/checks/nixos-modules/static-page/test.py b/checks/nixos-modules/static-page/test.py
deleted file mode 100644
index d084c5e..0000000
--- a/checks/nixos-modules/static-page/test.py
+++ /dev/null
@@ -1,46 +0,0 @@
-webserver.wait_for_unit("nginx")
-webserver.wait_for_open_port(80)
-
-# Preparations
-webserverRoot = "/var/lib/nginx-localhost/root"
-indexContent = "It works!"
-webserver.succeed(f"mkdir {webserverRoot}")
-webserver.succeed(f"echo '{indexContent}' > {webserverRoot}/index.html")
-webserver.succeed(f"chown -R nginx-localhost\: {webserverRoot}")
-
-# Helpers
-
-
-def expect_http_code(node, code, url):
-    http_code = node.succeed(
-        f"curl -s --no-location -o /dev/null -w '%{{http_code}}' '{url}'")
-    assert http_code == code, \
-        f"expected {code} but got following response:\n{http_code}"
-
-
-def expect_http_location(node, location, url):
-    redirect_url = node.succeed(
-        f"curl -s --no-location -o /dev/null -w '%{{redirect_url}}' '{url}'")
-    assert redirect_url == location, \
-        f"expected redirect to {location} but got:\n{redirect_url}"
-
-
-def expect_http_content(node, expectedContent, url):
-    content = node.succeed(f"curl --no-location --silent '{url}'")
-    assert content.strip() == expectedContent.strip(), \
-        f"expected:\n{expectedContent}\n at {
-            url} but got following content:\n'{content}'"
-
-
-# Tests
-with subtest("website is successfully served on localhost"):
-    expect_http_code(webserver, "200", "http://localhost/index.html")
-    expect_http_content(webserver, indexContent, "http://localhost/index.html")
-
-with subtest("example.com is a hosts alias and redirects to localhost"):
-    webserver.succeed("grep example.com /etc/hosts")
-
-    url = "http://example.com/index.html"
-    expect_http_code(webserver, "301", url)
-    expect_http_location(
-        webserver, "http://localhost/index.html", url)
diff --git a/lib/default.nix b/lib/default.nix
index 404d93e..e4cd3ee 100644
--- a/lib/default.nix
+++ b/lib/default.nix
@@ -1,18 +1,26 @@
 { pkgs, ... }:
 let
-  lib = pkgs.lib;
-  foldersWithNix =
-    path:
-    let
-      folders = lib.attrNames (lib.filterAttrs (n: t: t == "directory") (builtins.readDir path));
-      isFolderWithDefaultNix = folder: lib.pathExists (lib.path.append path "./${folder}/default.nix");
-    in
-    lib.filter isFolderWithDefaultNix folders;
+  inherit (pkgs.lib)
+    attrNames
+    filterAttrs
+    filter
+    pathExists
+    path
+    ;
+  # Get a list of all subdirectories of a directory.
+  getSubDirs = base: attrNames (filterAttrs (n: t: t == "directory") (builtins.readDir base));
+  # Check if a folder with a base path and folder name contains a file with a specific name
+  isFolderWithFile =
+    fileName: basePath: folderName:
+    (pathExists (path.append basePath "./${folderName}/${fileName}"));
+  # Get a list of subfolders that contain a default.nix file.
+  foldersWithNix = base: filter (isFolderWithFile "default.nix" base) (getSubDirs base);
 
 in
 {
-  inherit foldersWithNix;
+  inherit getSubDirs isFolderWithFile foldersWithNix;
 
+  # Get a list of default.nix files that are nix submodules of the current folder.
   loadSubmodulesFrom =
-    path: map (folder: lib.path.append path "./${folder}/default.nix") (foldersWithNix path);
+    basePath: map (folder: path.append basePath "./${folder}/default.nix") (foldersWithNix basePath);
 }
diff --git a/nixos-modules/static-page/test.nix b/nixos-modules/static-page/test.nix
new file mode 100644
index 0000000..7d8f9f6
--- /dev/null
+++ b/nixos-modules/static-page/test.nix
@@ -0,0 +1,30 @@
+{
+  ...
+}:
+{
+  nodes.webserver =
+    { pkgs, lib, ... }:
+    let
+      inherit (pkgs) curl gnugrep;
+      inherit (lib) mkForce genAttrs const;
+    in
+    {
+      # Setup simple localhost page with an example.com redirect
+      qois.static-page = {
+        enable = true;
+        pages."localhost".domainAliases = [ "example.com" ];
+      };
+
+      # Disable TLS services
+      services.nginx.virtualHosts = genAttrs [ "localhost" "example.com" ] (const {
+        forceSSL = mkForce false;
+        enableACME = mkForce false;
+      });
+
+      # Test environment
+      environment.systemPackages = [
+        curl
+        gnugrep
+      ];
+    };
+}
diff --git a/nixos-modules/static-page/test.py b/nixos-modules/static-page/test.py
new file mode 100644
index 0000000..5c273d5
--- /dev/null
+++ b/nixos-modules/static-page/test.py
@@ -0,0 +1,46 @@
+def test(subtest, webserver):
+    webserver.wait_for_unit("nginx")
+    webserver.wait_for_open_port(80)
+
+    # Preparations
+    webserverRoot = "/var/lib/nginx-localhost/root"
+    indexContent = "It works!"
+    webserver.succeed(f"mkdir {webserverRoot}")
+    webserver.succeed(f"echo '{indexContent}' > {webserverRoot}/index.html")
+    webserver.succeed(f"chown -R nginx-localhost\: {webserverRoot}")
+
+    # Helpers
+    def curl_variable_test(node, variable, expected, url):
+        value = node.succeed(
+            f"curl -s --no-location -o /dev/null -w '%{{{variable}}}' '{url}'")
+        assert value == expected, \
+            f"expected {variable} to be '{expected}' but got '{value}'"
+
+    def expect_http_code(node, code, url):
+        curl_variable_test(node, "http_code", code, url)
+
+    def expect_http_location(node, location, url):
+        curl_variable_test(node, "redirect_url", location, url)
+
+    def expect_http_content(node, expectedContent, url):
+        content = node.succeed(f"curl --no-location --silent '{url}'")
+        assert content.strip() == expectedContent.strip(), f'''
+                expected content:
+                  {expectedContent}
+                at {url} but got following content:
+                  {content}
+            '''
+
+    # Tests
+    with subtest("website is successfully served on localhost"):
+        expect_http_code(webserver, "200", "http://localhost/index.html")
+        expect_http_content(webserver, indexContent,
+                            "http://localhost/index.html")
+
+    with subtest("example.com is a hosts alias and redirect"):
+        webserver.succeed("grep example.com /etc/hosts")
+
+        url = "http://example.com/index.html"
+        expect_http_code(webserver, "301", url)
+        expect_http_location(
+            webserver, "http://localhost/index.html", url)