about summary refs log tree commit diff
path: root/nixos/tests
diff options
context:
space:
mode:
Diffstat (limited to 'nixos/tests')
-rw-r--r--nixos/tests/acme.nix8
-rw-r--r--nixos/tests/activation/etc-overlay-immutable.nix4
-rw-r--r--nixos/tests/all-tests.nix5
-rw-r--r--nixos/tests/alloy.nix32
-rw-r--r--nixos/tests/aria2.nix43
-rw-r--r--nixos/tests/clatd.nix95
-rw-r--r--nixos/tests/firefly-iii.nix4
-rw-r--r--nixos/tests/greetd-no-shadow.nix49
-rw-r--r--nixos/tests/kerberos/heimdal.nix2
-rw-r--r--nixos/tests/kerberos/mit.nix2
-rw-r--r--nixos/tests/kubo/default.nix4
-rw-r--r--nixos/tests/kubo/kubo-fuse.nix2
-rw-r--r--nixos/tests/ladybird.nix4
-rw-r--r--nixos/tests/mpv.nix2
-rw-r--r--nixos/tests/mycelium/default.nix3
-rw-r--r--nixos/tests/openssh.nix14
-rw-r--r--nixos/tests/pghero.nix63
-rw-r--r--nixos/tests/renovate.nix69
-rw-r--r--nixos/tests/systemd-machinectl.nix24
19 files changed, 386 insertions, 43 deletions
diff --git a/nixos/tests/acme.nix b/nixos/tests/acme.nix
index 511d3c589faef..379496583d25d 100644
--- a/nixos/tests/acme.nix
+++ b/nixos/tests/acme.nix
@@ -392,8 +392,6 @@ in {
   testScript = { nodes, ... }:
     let
       caDomain = nodes.acme.test-support.acme.caDomain;
-      newServerSystem = nodes.webserver.config.system.build.toplevel;
-      switchToNewServer = "${newServerSystem}/bin/switch-to-configuration test";
     in
     # Note, wait_for_unit does not work for oneshot services that do not have RemainAfterExit=true,
     # this is because a oneshot goes from inactive => activating => inactive, and never
@@ -545,6 +543,12 @@ in {
           check_fullchain(webserver, "http.example.test")
           check_issuer(webserver, "http.example.test", "pebble")
 
+      # Perform account hash test
+      with subtest("Assert that account hash didn't unexpected change"):
+          hash = webserver.succeed("ls /var/lib/acme/.lego/accounts/")
+          print("Account hash: " + hash)
+          assert hash.strip() == "d590213ed52603e9128d"
+
       # Perform renewal test
       with subtest("Can renew certificates when they expire"):
           hash = webserver.succeed("sha256sum /var/lib/acme/http.example.test/cert.pem")
diff --git a/nixos/tests/activation/etc-overlay-immutable.nix b/nixos/tests/activation/etc-overlay-immutable.nix
index f347f9cf8efe2..f0abf70d350ff 100644
--- a/nixos/tests/activation/etc-overlay-immutable.nix
+++ b/nixos/tests/activation/etc-overlay-immutable.nix
@@ -13,6 +13,7 @@
     users.mutableUsers = false;
     boot.initrd.systemd.enable = true;
     boot.kernelPackages = pkgs.linuxPackages_latest;
+    time.timeZone = "Utc";
 
     specialisation.new-generation.configuration = {
       environment.etc."newgen".text = "newgen";
@@ -23,6 +24,9 @@
     with subtest("/etc is mounted as an overlay"):
       machine.succeed("findmnt --kernel --type overlay /etc")
 
+    with subtest("direct symlinks point to the target without indirection"):
+      assert machine.succeed("readlink -n /etc/localtime") == "/etc/zoneinfo/Utc"
+
     with subtest("switching to the same generation"):
       machine.succeed("/run/current-system/bin/switch-to-configuration test")
 
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index ddeeeb298c356..a9b6881aab0f8 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -115,6 +115,7 @@ in {
   akkoma = handleTestOn [ "x86_64-linux" "aarch64-linux" ] ./akkoma.nix {};
   akkoma-confined = handleTestOn [ "x86_64-linux" "aarch64-linux" ] ./akkoma.nix { confined = true; };
   alice-lg = handleTest ./alice-lg.nix {};
+  alloy = handleTest ./alloy.nix {};
   allTerminfo = handleTest ./all-terminfo.nix {};
   alps = handleTest ./alps.nix {};
   amazon-init-shell = handleTest ./amazon-init-shell.nix {};
@@ -129,6 +130,7 @@ in {
   appliance-repart-image = runTest ./appliance-repart-image.nix;
   apparmor = handleTest ./apparmor.nix {};
   archi = handleTest ./archi.nix {};
+  aria2 = handleTest ./aria2.nix {};
   armagetronad = handleTest ./armagetronad.nix {};
   artalk = handleTest ./artalk.nix {};
   atd = handleTest ./atd.nix {};
@@ -379,6 +381,7 @@ in {
   grafana-agent = handleTest ./grafana-agent.nix {};
   graphite = handleTest ./graphite.nix {};
   graylog = handleTest ./graylog.nix {};
+  greetd-no-shadow = handleTest ./greetd-no-shadow.nix {};
   grocy = handleTest ./grocy.nix {};
   grow-partition = runTest ./grow-partition.nix;
   grub = handleTest ./grub.nix {};
@@ -720,6 +723,7 @@ in {
   pg_anonymizer = handleTest ./pg_anonymizer.nix {};
   pgadmin4 = handleTest ./pgadmin4.nix {};
   pgbouncer = handleTest ./pgbouncer.nix {};
+  pghero = runTest ./pghero.nix;
   pgjwt = handleTest ./pgjwt.nix {};
   pgmanage = handleTest ./pgmanage.nix {};
   pgvecto-rs = handleTest ./pgvecto-rs.nix {};
@@ -800,6 +804,7 @@ in {
   redis = handleTest ./redis.nix {};
   redlib = handleTest ./redlib.nix {};
   redmine = handleTest ./redmine.nix {};
+  renovate = handleTest ./renovate.nix {};
   restartByActivationScript = handleTest ./restart-by-activation-script.nix {};
   restic-rest-server = handleTest ./restic-rest-server.nix {};
   restic = handleTest ./restic.nix {};
diff --git a/nixos/tests/alloy.nix b/nixos/tests/alloy.nix
new file mode 100644
index 0000000000000..d87492127d5bb
--- /dev/null
+++ b/nixos/tests/alloy.nix
@@ -0,0 +1,32 @@
+import ./make-test-python.nix ({ lib, pkgs, ... }:
+
+  let
+    nodes = {
+      machine = {
+        services.alloy = {
+          enable = true;
+        };
+        environment.etc."alloy/config.alloy".text = "";
+      };
+    };
+  in
+  {
+    name = "alloy";
+
+    meta = with lib.maintainers; {
+      maintainers = [ flokli hbjydev ];
+    };
+
+    inherit nodes;
+
+    testScript = ''
+      start_all()
+
+      machine.wait_for_unit("alloy.service")
+      machine.wait_for_open_port(12345)
+      machine.succeed(
+          "curl -sSfN http://127.0.0.1:12345/-/healthy"
+      )
+      machine.shutdown()
+    '';
+  })
diff --git a/nixos/tests/aria2.nix b/nixos/tests/aria2.nix
new file mode 100644
index 0000000000000..48fe2094b5dcf
--- /dev/null
+++ b/nixos/tests/aria2.nix
@@ -0,0 +1,43 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+let
+  rpcSecret = "supersecret";
+  rpc-listen-port = 6800;
+  curlBody = {
+    jsonrpc = 2.0;
+    id = 1;
+    method = "aria2.getVersion";
+    params = [ "token:${rpcSecret}" ];
+  };
+in
+rec {
+  name = "aria2";
+
+  nodes.machine = {
+    environment.etc."aria2Rpc".text = rpcSecret;
+    services.aria2 = {
+      enable = true;
+      rpcSecretFile = "/etc/aria2Rpc";
+      settings = {
+        inherit rpc-listen-port;
+        allow-overwrite = false;
+        check-integrity = true;
+        console-log-level = "warn";
+        listen-port = [{ from = 20000; to = 20010; } { from = 22222; to = 22222; }];
+        max-concurrent-downloads = 50;
+        seed-ratio = 1.2;
+        summary-interval = 0;
+      };
+    };
+  };
+
+  testScript = ''
+    machine.start()
+    machine.wait_for_unit("aria2.service")
+    curl_cmd = 'curl --fail-with-body -X POST -H "Content-Type: application/json" \
+                -d \'${builtins.toJSON curlBody}\' http://localhost:${toString rpc-listen-port}/jsonrpc'
+    print(machine.wait_until_succeeds(curl_cmd, timeout=10))
+    machine.shutdown()
+  '';
+
+  meta.maintainers = [ pkgs.lib.maintainers.timhae ];
+})
diff --git a/nixos/tests/clatd.nix b/nixos/tests/clatd.nix
index f4d2242ce54f4..d0d504851ce4e 100644
--- a/nixos/tests/clatd.nix
+++ b/nixos/tests/clatd.nix
@@ -6,8 +6,8 @@
 # Client | clat    Address: 192.0.0.1/32  (configured via clatd)
 #        |         Route:   default
 #        |
-#        | eth1    Address: 2001:db8::2/64
-#        |  |      Route:   default via 2001:db8::1
+#        | eth1    Address: Assigned via SLAAC within 2001:db8::/64
+#        |  |      Route:   default via IPv6LL address
 #        +--|---
 #           | VLAN 3
 #        +--|---
@@ -31,7 +31,7 @@ import ./make-test-python.nix ({ pkgs, lib, ... }:
 {
   name = "clatd";
   meta = with pkgs.lib.maintainers; {
-    maintainers = [ hax404 ];
+    maintainers = [ hax404 jmbaur ];
   };
 
   nodes = {
@@ -66,18 +66,19 @@ import ./make-test-python.nix ({ pkgs, lib, ... }:
     };
 
     # The router is configured with static IPv4 addresses towards the server
-    # and IPv6 addresses towards the client. For NAT64, the Well-Known prefix
-    # 64:ff9b::/96 is used. NAT64 is done with TAYGA which provides the
-    # tun-interface nat64 and does the translation over it. The IPv6 packets
-    # are sent to this interfaces and received as IPv4 packets and vice versa.
-    # As TAYGA only translates IPv6 addresses to dedicated IPv4 addresses, it
-    # needs a pool of IPv4 addresses which must be at least as big as the
-    # expected amount of clients. In this test, the packets from the pool are
-    # directly routed towards the client. In normal cases, there would be a
-    # second source NAT44 to map all clients behind one IPv4 address.
+    # and IPv6 addresses towards the client. DNS64 is exposed towards the
+    # client so clatd is able to auto-discover the PLAT prefix. For NAT64, the
+    # Well-Known prefix 64:ff9b::/96 is used. NAT64 is done with TAYGA which
+    # provides the tun-interface nat64 and does the translation over it. The
+    # IPv6 packets are sent to this interfaces and received as IPv4 packets and
+    # vice versa. As TAYGA only translates IPv6 addresses to dedicated IPv4
+    # addresses, it needs a pool of IPv4 addresses which must be at least as
+    # big as the expected amount of clients. In this test, the packets from the
+    # pool are directly routed towards the client. In normal cases, there would
+    # be a second source NAT44 to map all clients behind one IPv4 address.
     router = {
       boot.kernel.sysctl = {
-        "net.ipv4.ip_forward" = 1;
+        "net.ipv4.conf.all.forwarding" = 1;
         "net.ipv6.conf.all.forwarding" = 1;
       };
 
@@ -102,6 +103,36 @@ import ./make-test-python.nix ({ pkgs, lib, ... }:
         };
       };
 
+      systemd.network.networks."40-eth2" = {
+        networkConfig.IPv6SendRA = true;
+        ipv6Prefixes = [ { Prefix = "2001:db8::/64"; } ];
+        ipv6PREF64Prefixes = [ { Prefix = "64:ff9b::/96"; } ];
+        ipv6SendRAConfig = {
+          EmitDNS = true;
+          DNS = "_link_local";
+        };
+      };
+
+      services.resolved.extraConfig = ''
+        DNSStubListener=no
+      '';
+
+      networking.extraHosts = ''
+        192.0.0.171 ipv4only.arpa
+        192.0.0.170 ipv4only.arpa
+      '';
+
+      services.coredns = {
+        enable = true;
+        config = ''
+          .:53 {
+            bind ::
+            hosts /etc/hosts
+            dns64 64:ff9b::/96
+          }
+        '';
+      };
+
       services.tayga = {
         enable = true;
         ipv4 = {
@@ -127,10 +158,10 @@ import ./make-test-python.nix ({ pkgs, lib, ... }:
       };
     };
 
-    # The client is configured with static IPv6 addresses. It has also a static
-    # default route towards the router. To reach the IPv4-only server, the
-    # client starts the clat daemon which starts and configures the local
-    # IPv4 -> IPv6 translation via Tayga.
+    # The client uses SLAAC to assign IPv6 addresses. To reach the IPv4-only
+    # server, the client starts the clat daemon which starts and configures the
+    # local IPv4 -> IPv6 translation via Tayga after discovering the PLAT
+    # prefix via DNS64.
     client = {
       virtualisation.vlans = [
         3 # towards router
@@ -145,25 +176,36 @@ import ./make-test-python.nix ({ pkgs, lib, ... }:
         enable = true;
         networks."vlan1" = {
           matchConfig.Name = "eth1";
-          address = [
-            "2001:db8::2/64"
-          ];
-          routes = [
-            { Destination = "::/0"; Gateway = "2001:db8::1"; }
-          ];
+
+          # NOTE: clatd does not actually use the PREF64 prefix discovered by
+          # systemd-networkd (nor does systemd-networkd do anything with it,
+          # yet), but we set this to confirm it works. See the test script
+          # below.
+          ipv6AcceptRAConfig.UsePREF64 = true;
         };
       };
 
       services.clatd = {
         enable = true;
-        settings.plat-prefix = "64:ff9b::/96";
+        # NOTE: Perl's Net::DNS resolver does not seem to work well querying
+        # for AAAA records to systemd-resolved's default IPv4 bind address
+        # (127.0.0.53), so we add an IPv6 listener address to systemd-resolved
+        # and tell clatd to use that instead.
+        settings.dns64-servers = "::1";
       };
 
+      # Allow clatd to find dns server. See comment above.
+      services.resolved.extraConfig = ''
+        DNSStubListenerExtra=::1
+      '';
+
       environment.systemPackages = [ pkgs.mtr ];
     };
   };
 
   testScript = ''
+    import json
+
     start_all()
 
     # wait for all machines to start up
@@ -178,6 +220,11 @@ import ./make-test-python.nix ({ pkgs, lib, ... }:
         'journalctl -u clatd -e | grep -q "Starting up TAYGA, using config file"'
       )
 
+    with subtest("networkd exports PREF64 prefix"):
+      assert json.loads(client.succeed("networkctl status eth1 --json=short"))[
+          "NDisc"
+      ]["PREF64"][0]["Prefix"] == [0x0, 0x64, 0xFF, 0x9B] + ([0] * 12)
+
     with subtest("Test ICMP"):
       client.wait_until_succeeds("ping -c 3 100.64.0.2 >&2")
 
diff --git a/nixos/tests/firefly-iii.nix b/nixos/tests/firefly-iii.nix
index 2373ba8360264..f8e4ca4bfe2b4 100644
--- a/nixos/tests/firefly-iii.nix
+++ b/nixos/tests/firefly-iii.nix
@@ -39,12 +39,13 @@ in
         DB_DATABASE = "firefly";
         DB_USERNAME = "firefly";
         DB_PASSWORD_FILE = "/etc/postgres-pass";
+        PGSQL_SCHEMA = "firefly";
       };
     };
 
     services.postgresql = {
       enable = true;
-      package = pkgs.postgresql_15;
+      package = pkgs.postgresql_16;
       authentication = ''
         local all postgres peer
         local firefly firefly password
@@ -52,6 +53,7 @@ in
       initialScript = pkgs.writeText "firefly-init.sql" ''
         CREATE USER "firefly" WITH LOGIN PASSWORD '${db-pass}';
         CREATE DATABASE "firefly" WITH OWNER "firefly";
+        \c firefly
         CREATE SCHEMA AUTHORIZATION firefly;
       '';
     };
diff --git a/nixos/tests/greetd-no-shadow.nix b/nixos/tests/greetd-no-shadow.nix
new file mode 100644
index 0000000000000..382218ffa948f
--- /dev/null
+++ b/nixos/tests/greetd-no-shadow.nix
@@ -0,0 +1,49 @@
+import ./make-test-python.nix ({ pkgs, latestKernel ? false, ... }:
+{
+  name = "greetd-no-shadow";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ ];
+  };
+
+  nodes.machine =
+    { pkgs, lib, ... }: {
+
+      users.users.alice = {
+        isNormalUser = true;
+        group = "alice";
+        password = "foobar";
+      };
+      users.groups.alice = {};
+
+      # This means login(1) breaks, so we must use greetd/agreety instead.
+      security.shadow.enable = false;
+
+      services.greetd = {
+        enable = true;
+        settings = {
+          default_session = {
+            command = "${pkgs.greetd.greetd}/bin/agreety --cmd bash";
+          };
+        };
+      };
+    };
+
+  testScript = ''
+      machine.start()
+
+      machine.wait_for_unit("multi-user.target")
+      machine.wait_until_succeeds("pgrep -f 'agretty.*tty1'")
+      machine.screenshot("postboot")
+
+      with subtest("Log in as alice on a virtual console"):
+          machine.wait_until_tty_matches("1", "login: ")
+          machine.send_chars("alice\n")
+          machine.wait_until_tty_matches("1", "login: alice")
+          machine.wait_until_succeeds("pgrep login")
+          machine.wait_until_tty_matches("1", "Password: ")
+          machine.send_chars("foobar\n")
+          machine.wait_until_succeeds("pgrep -u alice bash")
+          machine.send_chars("touch done\n")
+          machine.wait_for_file("/home/alice/done")
+  '';
+})
diff --git a/nixos/tests/kerberos/heimdal.nix b/nixos/tests/kerberos/heimdal.nix
index 393289f7a92ca..098080a84592e 100644
--- a/nixos/tests/kerberos/heimdal.nix
+++ b/nixos/tests/kerberos/heimdal.nix
@@ -4,7 +4,7 @@ import ../make-test-python.nix ({pkgs, ...}: {
   nodes.machine = { config, libs, pkgs, ...}:
   { services.kerberos_server =
     { enable = true;
-      realms = {
+      settings.realms = {
         "FOO.BAR".acl = [{principal = "admin"; access = ["add" "cpw"];}];
       };
     };
diff --git a/nixos/tests/kerberos/mit.nix b/nixos/tests/kerberos/mit.nix
index 1191d047abbf0..172261f95fe6b 100644
--- a/nixos/tests/kerberos/mit.nix
+++ b/nixos/tests/kerberos/mit.nix
@@ -4,7 +4,7 @@ import ../make-test-python.nix ({pkgs, ...}: {
   nodes.machine = { config, libs, pkgs, ...}:
   { services.kerberos_server =
     { enable = true;
-      realms = {
+      settings.realms = {
         "FOO.BAR".acl = [{principal = "admin"; access = ["add" "cpw"];}];
       };
     };
diff --git a/nixos/tests/kubo/default.nix b/nixos/tests/kubo/default.nix
index d8c0c69dc1fbd..629922fc366db 100644
--- a/nixos/tests/kubo/default.nix
+++ b/nixos/tests/kubo/default.nix
@@ -1,7 +1,5 @@
 { recurseIntoAttrs, runTest }:
 recurseIntoAttrs {
   kubo = runTest ./kubo.nix;
-  # The FUSE functionality is completely broken since Kubo v0.24.0
-  # See https://github.com/ipfs/kubo/issues/10242
-  # kubo-fuse = runTest ./kubo-fuse.nix;
+  kubo-fuse = runTest ./kubo-fuse.nix;
 }
diff --git a/nixos/tests/kubo/kubo-fuse.nix b/nixos/tests/kubo/kubo-fuse.nix
index 71a5bf61649f6..c8c273fc0dfc7 100644
--- a/nixos/tests/kubo/kubo-fuse.nix
+++ b/nixos/tests/kubo/kubo-fuse.nix
@@ -23,7 +23,7 @@
 
     with subtest("FUSE mountpoint"):
         machine.fail("echo a | su bob -l -c 'ipfs add --quieter'")
-        # The FUSE mount functionality is broken as of v0.13.0 and v0.17.0.
+        # The FUSE mount functionality is broken as of v0.13.0. This is still the case with v0.29.0.
         # See https://github.com/ipfs/kubo/issues/9044.
         # Workaround: using CID Version 1 avoids that.
         ipfs_hash = machine.succeed(
diff --git a/nixos/tests/ladybird.nix b/nixos/tests/ladybird.nix
index 8ed0f47887c7d..85c23353a668a 100644
--- a/nixos/tests/ladybird.nix
+++ b/nixos/tests/ladybird.nix
@@ -10,9 +10,7 @@ import ./make-test-python.nix ({ pkgs, ... }: {
     ];
 
     services.xserver.enable = true;
-    environment.systemPackages = [
-      pkgs.ladybird
-    ];
+    programs.ladybird.enable = true;
   };
 
   enableOCR = true;
diff --git a/nixos/tests/mpv.nix b/nixos/tests/mpv.nix
index 32a81cbe2495e..c2e151c224760 100644
--- a/nixos/tests/mpv.nix
+++ b/nixos/tests/mpv.nix
@@ -12,7 +12,7 @@ in
     {
       environment.systemPackages = [
         pkgs.curl
-        (pkgs.wrapMpv pkgs.mpv-unwrapped {
+        (pkgs.mpv.override {
           scripts = [ pkgs.mpvScripts.simple-mpv-webui ];
         })
       ];
diff --git a/nixos/tests/mycelium/default.nix b/nixos/tests/mycelium/default.nix
index 9174c49d70869..956a822a21860 100644
--- a/nixos/tests/mycelium/default.nix
+++ b/nixos/tests/mycelium/default.nix
@@ -51,6 +51,9 @@ in
       peer1.wait_for_unit("mycelium.service")
       peer2.wait_for_unit("mycelium.service")
 
+      peer1.succeed("mycelium peers list | grep 192.168.1.12")
+      peer2.succeed("mycelium peers list | grep 192.168.1.11")
+
       peer1.succeed("ping -c5 ${peer2-ip}")
       peer2.succeed("ping -c5 ${peer1-ip}")
     '';
diff --git a/nixos/tests/openssh.nix b/nixos/tests/openssh.nix
index 3929522a39e73..d420c482ca7f2 100644
--- a/nixos/tests/openssh.nix
+++ b/nixos/tests/openssh.nix
@@ -120,12 +120,14 @@ in {
             { type = "ed25519"; path = "/etc/ssh/ssh_host_ed25519_key"; }
           ];
           settings = {
-            # Must not specify the OpenSSL provided algorithms.
-            Ciphers = [ "chacha20-poly1305@openssh.com" ];
-            KexAlgorithms = [
-              "curve25519-sha256"
-              "curve25519-sha256@libssh.org"
-            ];
+            # Since this test is against an OpenSSH-without-OpenSSL,
+            # we have to override NixOS's defaults ciphers (which require OpenSSL)
+            # and instead set these to null, which will mean OpenSSH uses its defaults.
+            # Expectedly, OpenSSH's defaults don't require OpenSSL when it's compiled
+            # without OpenSSL.
+            Ciphers = null;
+            KexAlgorithms = null;
+            Macs = null;
           };
         };
         users.users.root.openssh.authorizedKeys.keys = [
diff --git a/nixos/tests/pghero.nix b/nixos/tests/pghero.nix
new file mode 100644
index 0000000000000..bce32da008862
--- /dev/null
+++ b/nixos/tests/pghero.nix
@@ -0,0 +1,63 @@
+let
+  pgheroPort = 1337;
+  pgheroUser = "pghero";
+  pgheroPass = "pghero";
+in
+{ lib, ... }: {
+  name = "pghero";
+  meta.maintainers = [ lib.maintainers.tie ];
+
+  nodes.machine = { config, ... }: {
+    services.postgresql = {
+      enable = true;
+      # This test uses default peer authentication (socket and its directory is
+      # world-readably by default), so we essentially test that we can connect
+      # with DynamicUser= set.
+      ensureUsers = [{
+        name = "pghero";
+        ensureClauses.superuser = true;
+      }];
+    };
+    services.pghero = {
+      enable = true;
+      listenAddress = "[::]:${toString pgheroPort}";
+      settings = {
+        databases = {
+          postgres.url = "<%= ENV['POSTGRES_DATABASE_URL'] %>";
+          nulldb.url = "nulldb:///";
+        };
+      };
+      environment = {
+        PGHERO_USERNAME = pgheroUser;
+        PGHERO_PASSWORD = pgheroPass;
+        POSTGRES_DATABASE_URL = "postgresql:///postgres?host=/run/postgresql";
+      };
+    };
+  };
+
+  testScript = ''
+    pgheroPort = ${toString pgheroPort}
+    pgheroUser = "${pgheroUser}"
+    pgheroPass = "${pgheroPass}"
+
+    pgheroUnauthorizedURL = f"http://localhost:{pgheroPort}"
+    pgheroBaseURL = f"http://{pgheroUser}:{pgheroPass}@localhost:{pgheroPort}"
+
+    def expect_http_code(node, code, url):
+        http_code = node.succeed(f"curl -s -o /dev/null -w '%{{http_code}}' '{url}'")
+        assert http_code.split("\n")[-1].strip() == code, \
+          f"expected HTTP status code {code} but got {http_code}"
+
+    machine.wait_for_unit("postgresql.service")
+    machine.wait_for_unit("pghero.service")
+
+    with subtest("requires HTTP Basic Auth credentials"):
+      expect_http_code(machine, "401", pgheroUnauthorizedURL)
+
+    with subtest("works with some databases being unavailable"):
+      expect_http_code(machine, "500", pgheroBaseURL + "/nulldb")
+
+    with subtest("connects to the PostgreSQL database"):
+      expect_http_code(machine, "200", pgheroBaseURL + "/postgres")
+  '';
+}
diff --git a/nixos/tests/renovate.nix b/nixos/tests/renovate.nix
new file mode 100644
index 0000000000000..a30b5b3d60b9c
--- /dev/null
+++ b/nixos/tests/renovate.nix
@@ -0,0 +1,69 @@
+import ./make-test-python.nix (
+  { pkgs, ... }:
+  {
+    name = "renovate";
+    meta.maintainers = with pkgs.lib.maintainers; [ marie natsukium ];
+
+    nodes.machine =
+      { config, ... }:
+      {
+        services.renovate = {
+          enable = true;
+          settings = {
+            platform = "gitea";
+            endpoint = "http://localhost:3000";
+            autodiscover = true;
+            gitAuthor = "Renovate <renovate@example.com>";
+          };
+          credentials = {
+            RENOVATE_TOKEN = "/etc/renovate-token";
+          };
+        };
+        environment.systemPackages = [
+          config.services.forgejo.package
+          pkgs.tea
+          pkgs.git
+        ];
+        services.forgejo = {
+          enable = true;
+          settings.server.HTTP_PORT = 3000;
+        };
+      };
+
+    testScript = ''
+      def gitea(command):
+        return machine.succeed(f"cd /var/lib/forgejo && sudo --user=forgejo GITEA_WORK_DIR=/var/lib/forgejo GITEA_CUSTOM=/var/lib/forgejo/custom gitea {command}")
+
+      machine.wait_for_unit("forgejo.service")
+      machine.wait_for_open_port(3000)
+
+      machine.systemctl("stop forgejo.service")
+
+      gitea("admin user create --username meow --email meow@example.com --password meow")
+
+      machine.systemctl("start forgejo.service")
+      machine.wait_for_unit("forgejo.service")
+      machine.wait_for_open_port(3000)
+
+      accessToken = gitea("admin user generate-access-token --raw --username meow --scopes all | tr -d '\n'")
+
+      machine.succeed(f"tea login add --name default --user meow --token '{accessToken}' --password meow --url http://localhost:3000")
+      machine.succeed("tea repo create --name kitty --init")
+      machine.succeed("git config --global user.name Meow")
+      machine.succeed("git config --global user.email meow@example.com")
+      machine.succeed(f"git clone http://meow:{accessToken}@localhost:3000/meow/kitty.git /tmp/kitty")
+      machine.succeed("echo '{ \"name\": \"meow\", \"version\": \"0.1.0\" }' > /tmp/kitty/package.json")
+      machine.succeed("git -C /tmp/kitty add /tmp/kitty/package.json")
+      machine.succeed("git -C /tmp/kitty commit -m 'add package.json'")
+      machine.succeed("git -C /tmp/kitty push origin")
+
+      machine.succeed(f"echo '{accessToken}' > /etc/renovate-token")
+      machine.systemctl("start renovate.service")
+
+      machine.succeed("tea pulls list --repo meow/kitty | grep 'Configure Renovate'")
+      machine.succeed("tea pulls merge --repo meow/kitty 1")
+
+      machine.systemctl("start renovate.service")
+    '';
+  }
+)
diff --git a/nixos/tests/systemd-machinectl.nix b/nixos/tests/systemd-machinectl.nix
index 9d761c6d4d8b8..555a8bb43b30e 100644
--- a/nixos/tests/systemd-machinectl.nix
+++ b/nixos/tests/systemd-machinectl.nix
@@ -76,10 +76,23 @@ in
       };
     };
 
+    systemd.nspawn.${containerName} = {
+      filesConfig = {
+        # workaround to fix kernel namespaces; needed for Nix sandbox
+        # https://github.com/systemd/systemd/issues/27994#issuecomment-1704005670
+        Bind = "/proc:/run/proc";
+      };
+    };
+
     systemd.services."systemd-nspawn@${containerName}" = {
       serviceConfig.Environment = [
         # Disable tmpfs for /tmp
         "SYSTEMD_NSPAWN_TMPFS_TMP=0"
+
+        # force unified cgroup delegation, which would be the default
+        # if systemd could check the capabilities of the installed systemd.
+        # see also: https://github.com/NixOS/nixpkgs/pull/198526
+        "SYSTEMD_NSPAWN_UNIFIED_HIERARCHY=1"
       ];
       overrideStrategy = "asDropin";
     };
@@ -121,6 +134,17 @@ in
     machine.succeed("machinectl start ${containerName}");
     machine.wait_until_succeeds("systemctl -M ${containerName} is-active default.target");
 
+    # Test systemd-nspawn configured unified cgroup delegation
+    # see also:
+    # https://github.com/systemd/systemd/blob/main/docs/CGROUP_DELEGATION.md#three-different-tree-setups-
+    machine.succeed('systemd-run --pty --wait -M ${containerName} /run/current-system/sw/bin/stat --format="%T" --file-system /sys/fs/cgroup > fstype')
+    machine.succeed('test $(tr -d "\\r" < fstype) = cgroup2fs')
+
+    # Test if systemd-nspawn provides a working environment for nix to build derivations
+    # https://nixos.org/guides/nix-pills/07-working-derivation
+    machine.succeed('systemd-run --pty --wait -M ${containerName} /run/current-system/sw/bin/nix-instantiate --expr \'derivation { name = "myname"; builder = "/bin/sh"; args = [ "-c" "echo foo > $out" ]; system = "${pkgs.system}"; }\' --add-root /tmp/drv')
+    machine.succeed('systemd-run --pty --wait -M ${containerName} /run/current-system/sw/bin/nix-store --option substitute false --realize /tmp/drv')
+
     # Test nss_mymachines without nscd
     machine.succeed('LD_LIBRARY_PATH="/run/current-system/sw/lib" getent -s hosts:mymachines hosts ${containerName}');