From f2ef91672b5937255fbcde644efc003eb4820ecf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tristan=20Dani=C3=ABl=20Maat?= Date: Thu, 13 Nov 2025 05:20:09 +0800 Subject: [PATCH] test(nginx): Add simple tests to assert nginx features work --- checks/default.nix | 2 ++ configuration/nginx/logging.nix | 58 +++++++++++++++++++++++++++++- configuration/nginx/ssl.nix | 64 +++++++++++++++++++++++++++++++++ modules/default.nix | 7 +++- modules/serviceTests/mocks.nix | 27 ++++++++++++++ modules/serviceTests/stub.nix | 20 +++++++++++ 6 files changed, 176 insertions(+), 2 deletions(-) create mode 100644 modules/serviceTests/mocks.nix create mode 100644 modules/serviceTests/stub.nix diff --git a/checks/default.nix b/checks/default.nix index 674b19d..737313d 100644 --- a/checks/default.nix +++ b/checks/default.nix @@ -6,6 +6,8 @@ let in { x86_64-linux = lib.mergeAttrsList [ + flake-inputs.self.nixosConfigurations.hetzner-1.config.serviceTests + { nix = checkLib.mkLint { name = "nix-lints"; diff --git a/configuration/nginx/logging.nix b/configuration/nginx/logging.nix index 0c6a955..e41bfae 100644 --- a/configuration/nginx/logging.nix +++ b/configuration/nginx/logging.nix @@ -1,4 +1,10 @@ -{ config, lib, ... }: +{ + flake-inputs, + pkgs, + config, + lib, + ... +}: let hostNames = lib.attrNames config.services.nginx.virtualHosts; logPath = name: "/var/log/nginx/${name}/access.log"; @@ -80,5 +86,55 @@ in }; }; }; + + serviceTests = + let + testHostConfig = + { config, ... }: + { + imports = [ + ./. + ../../modules/serviceTests/mocks.nix + ]; + + networking.firewall.allowedTCPPorts = [ 80 ]; + + services.nginx = { + domain = "testHost"; + virtualHosts."${config.services.nginx.domain}".locations."/".return = "200 ok"; + }; + }; + in + { + nginxMetricsWork = pkgs.testers.runNixOSTest { + name = "nginx-metrics-work"; + node.specialArgs = { inherit flake-inputs; }; + + nodes = { + testHost = testHostConfig; + + client = + { pkgs, ... }: + { + environment.systemPackages = [ pkgs.curl ]; + }; + }; + + testScript = '' + import time + + start_all() + + testHost.wait_for_unit("nginx.service") + client.succeed("curl --max-time 10 http://testHost") + + # Wait a bit for the prometheus exporter to scrape our logs + time.sleep(5) + + res = testHost.succeed("curl localhost:${builtins.toString config.services.prometheus.exporters.nginxlog.port}/metrics") + assert 'nginxlog_http_response_count_total{method="GET",status="200",vhost="testHost"} 1' in res, res + ''; + }; + }; }; } diff --git a/configuration/nginx/ssl.nix b/configuration/nginx/ssl.nix index 7abc38e..56bfa78 100644 --- a/configuration/nginx/ssl.nix +++ b/configuration/nginx/ssl.nix @@ -1,4 +1,5 @@ { + flake-inputs, pkgs, config, lib, @@ -69,5 +70,68 @@ "porkbun/api-key".owner = "acme"; "porkbun/secret-api-key".owner = "acme"; }; + + serviceTests = + let + testHostConfig = + { config, ... }: + { + imports = [ + ./. + ../../modules/serviceTests/mocks.nix + ]; + + networking.firewall.allowedTCPPorts = [ 443 ]; + + security.acme.certs."tlater.net".extraDomainNames = [ config.services.nginx.domain ]; + + services.nginx = { + domain = "testHost"; + + virtualHosts."${config.services.nginx.domain}" = { + useACMEHost = "tlater.net"; + onlySSL = true; + enableHSTS = true; + locations."/".return = "200 ok"; + }; + }; + }; + in + { + testNginxSSL = pkgs.testers.runNixOSTest { + name = "test-nginx-ssl"; + + node.specialArgs = { inherit flake-inputs; }; + nodes = { + testHost = testHostConfig; + + client = + { pkgs, ... }: + { + environment.systemPackages = [ pkgs.curl ]; + }; + }; + + testScript = '' + start_all() + + testHost.wait_for_unit("nginx.service") + testHost.copy_from_vm("/var/lib/acme/tlater.net/", "certs") + client.copy_from_host(f"{testHost.out_dir}/certs", "/certs") + + res = client.succeed(" ".join([ + "curl", + "--show-error", + "--silent", + "--dump-header -", + "--cacert /certs/tlater.net/fullchain.pem", + "https://testHost", + "-o /dev/null" + ])) + + assert "strict-transport-security: max-age=15552000; includeSubDomains" in res + ''; + }; + }; }; } diff --git a/modules/default.nix b/modules/default.nix index 9483c66..1bf5314 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -1 +1,6 @@ -{ imports = [ ./crowdsec ]; } +{ + imports = [ + ./crowdsec + ./serviceTests/stub.nix + ]; +} diff --git a/modules/serviceTests/mocks.nix b/modules/serviceTests/mocks.nix new file mode 100644 index 0000000..f864253 --- /dev/null +++ b/modules/serviceTests/mocks.nix @@ -0,0 +1,27 @@ +/** + Module containing mock definitions for service test runners. +*/ +{ flake-inputs, lib, ... }: +{ + imports = [ + flake-inputs.sops-nix.nixosModules.sops + ../. + ../../configuration/services/backups.nix + ]; + + sops.defaultSopsFile = ../../keys/staging.yaml; + environment.etc."staging.key" = { + mode = "0400"; + source = ../../keys/hosts/staging.key; + }; + services.openssh = { + enable = true; + hostKeys = lib.mkForce [ + { + type = "rsa"; + bits = 4096; + path = "/etc/staging.key"; + } + ]; + }; +} diff --git a/modules/serviceTests/stub.nix b/modules/serviceTests/stub.nix new file mode 100644 index 0000000..d4a4b78 --- /dev/null +++ b/modules/serviceTests/stub.nix @@ -0,0 +1,20 @@ +/** + Module to make writing service-specific tests easy. +*/ +{ lib, ... }: +let + inherit (lib) mkOption types; +in +{ + options = { + serviceTests = mkOption { + type = types.attrsOf types.package; + + description = '' + NixOS tests to run. + ''; + + default = { }; + }; + }; +}