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/all-tests.nix4
-rw-r--r--nixos/tests/flood.nix27
-rw-r--r--nixos/tests/nextcloud/basic.nix104
-rw-r--r--nixos/tests/nextcloud/default.nix121
-rw-r--r--nixos/tests/nextcloud/with-mysql-and-memcached.nix58
-rw-r--r--nixos/tests/nextcloud/with-objectstore.nix96
-rw-r--r--nixos/tests/nextcloud/with-postgresql-and-redis.nix84
-rw-r--r--nixos/tests/odoo.nix12
-rw-r--r--nixos/tests/quickwit.nix72
-rw-r--r--nixos/tests/searx.nix182
-rw-r--r--nixos/tests/snapper.nix5
-rw-r--r--nixos/tests/vaultwarden.nix15
12 files changed, 479 insertions, 301 deletions
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index 746b29fd27258..8d5b865891e4a 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -328,6 +328,7 @@ in {
   firewall-nftables = handleTest ./firewall.nix { nftables = true; };
   fish = handleTest ./fish.nix {};
   flannel = handleTestOn ["x86_64-linux"] ./flannel.nix {};
+  flood = handleTest ./flood.nix {};
   floorp = handleTest ./firefox.nix { firefoxPackage = pkgs.floorp; };
   fluentd = handleTest ./fluentd.nix {};
   fluidd = handleTest ./fluidd.nix {};
@@ -413,6 +414,7 @@ in {
   pyload = handleTest ./pyload.nix {};
   oci-containers = handleTestOn ["aarch64-linux" "x86_64-linux"] ./oci-containers.nix {};
   odoo = handleTest ./odoo.nix {};
+  odoo16 = handleTest ./odoo.nix { package = pkgs.odoo16; };
   odoo15 = handleTest ./odoo.nix { package = pkgs.odoo15; };
   # 9pnet_virtio used to mount /nix partition doesn't support
   # hibernation. This test happens to work on x86_64-linux but
@@ -833,7 +835,7 @@ in {
   scrutiny = handleTest ./scrutiny.nix {};
   sddm = handleTest ./sddm.nix {};
   seafile = handleTest ./seafile.nix {};
-  searx = handleTest ./searx.nix {};
+  searx = runTest ./searx.nix;
   seatd = handleTest ./seatd.nix {};
   service-runner = handleTest ./service-runner.nix {};
   sftpgo = runTest ./sftpgo.nix;
diff --git a/nixos/tests/flood.nix b/nixos/tests/flood.nix
new file mode 100644
index 0000000000000..075d37e62835f
--- /dev/null
+++ b/nixos/tests/flood.nix
@@ -0,0 +1,27 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+let
+  port = 3001;
+in
+{
+  name = "flood";
+  meta = {
+    maintainers = with pkgs.lib.maintainers; [ thiagokokada ];
+  };
+
+  nodes.machine = { pkgs, ... }: {
+    services.flood = {
+      inherit port;
+      enable = true;
+      openFirewall = true;
+      extraArgs = [ "--baseuri=/" ];
+    };
+  };
+
+  testScript = /* python */ ''
+    machine.start()
+    machine.wait_for_unit("flood.service")
+    machine.wait_for_open_port(${toString port})
+
+    machine.succeed("curl --fail http://localhost:${toString port}")
+  '';
+})
diff --git a/nixos/tests/nextcloud/basic.nix b/nixos/tests/nextcloud/basic.nix
index 428fe0aa10db9..2a32f2b4d1992 100644
--- a/nixos/tests/nextcloud/basic.nix
+++ b/nixos/tests/nextcloud/basic.nix
@@ -1,27 +1,27 @@
-args@{ pkgs, nextcloudVersion ? 22, ... }:
+{ name, pkgs, testBase, system,... }:
 
-(import ../make-test-python.nix ({ pkgs, ...}: let
-  adminpass = "notproduction";
-  adminuser = "root";
-in {
-  name = "nextcloud-basic";
+with import ../../lib/testing-python.nix { inherit system pkgs; };
+runTest ({ config, ... }: {
+  inherit name;
   meta = with pkgs.lib.maintainers; {
-    maintainers = [ globin eqyiel ];
+    maintainers = [ globin eqyiel ma27 ];
   };
 
-  nodes = rec {
+  imports = [ testBase ];
+
+  nodes = {
     # The only thing the client needs to do is download a file.
     client = { ... }: {
       services.davfs2.enable = true;
       systemd.tmpfiles.settings.nextcloud = {
         "/tmp/davfs2-secrets"."f+" = {
           mode = "0600";
-          argument = "http://nextcloud/remote.php/dav/files/${adminuser} ${adminuser} ${adminpass}";
+          argument = "http://nextcloud/remote.php/dav/files/${config.adminuser} ${config.adminuser} ${config.adminpass}";
         };
       };
       virtualisation.fileSystems = {
         "/mnt/dav" = {
-          device = "http://nextcloud/remote.php/dav/files/${adminuser}";
+          device = "http://nextcloud/remote.php/dav/files/${config.adminuser}";
           fsType = "davfs";
           options = let
             davfs2Conf = (pkgs.writeText "davfs2.conf" "secrets /tmp/davfs2-secrets");
@@ -30,11 +30,7 @@ in {
       };
     };
 
-    nextcloud = { config, pkgs, ... }: let
-      cfg = config;
-    in {
-      networking.firewall.allowedTCPPorts = [ 80 ];
-
+    nextcloud = { config, pkgs, ... }: {
       systemd.tmpfiles.rules = [
         "d /var/lib/nextcloud-data 0750 nextcloud nginx - -"
       ];
@@ -42,14 +38,7 @@ in {
       services.nextcloud = {
         enable = true;
         datadir = "/var/lib/nextcloud-data";
-        hostName = "nextcloud";
-        database.createLocally = true;
-        config = {
-          # Don't inherit adminuser since "root" is supposed to be the default
-          adminpassFile = "${pkgs.writeText "adminpass" adminpass}"; # Don't try this at home!
-          dbtableprefix = "nixos_";
-        };
-        package = pkgs.${"nextcloud" + (toString nextcloudVersion)};
+        config.dbtableprefix = "nixos_";
         autoUpdateApps = {
           enable = true;
           startAt = "20:00";
@@ -57,64 +46,31 @@ in {
         phpExtraExtensions = all: [ all.bz2 ];
       };
 
-      environment.systemPackages = [ cfg.services.nextcloud.occ ];
+      specialisation.withoutMagick.configuration = {
+        services.nextcloud.enableImagemagick = false;
+      };
     };
-
-    nextcloudWithoutMagick = args@{ config, pkgs, lib, ... }:
-      lib.mkMerge
-      [ (nextcloud args)
-        { services.nextcloud.enableImagemagick = false; } ];
   };
 
-  testScript = { nodes, ... }: let
-    withRcloneEnv = pkgs.writeScript "with-rclone-env" ''
-      #!${pkgs.runtimeShell}
-      export RCLONE_CONFIG_NEXTCLOUD_TYPE=webdav
-      export RCLONE_CONFIG_NEXTCLOUD_URL="http://nextcloud/remote.php/dav/files/${adminuser}"
-      export RCLONE_CONFIG_NEXTCLOUD_VENDOR="nextcloud"
-      export RCLONE_CONFIG_NEXTCLOUD_USER="${adminuser}"
-      export RCLONE_CONFIG_NEXTCLOUD_PASS="$(${pkgs.rclone}/bin/rclone obscure ${adminpass})"
-      "''${@}"
-    '';
-    copySharedFile = pkgs.writeScript "copy-shared-file" ''
-      #!${pkgs.runtimeShell}
-      echo 'hi' | ${withRcloneEnv} ${pkgs.rclone}/bin/rclone rcat nextcloud:test-shared-file
-    '';
-
-    diffSharedFile = pkgs.writeScript "diff-shared-file" ''
-      #!${pkgs.runtimeShell}
-      diff <(echo 'hi') <(${pkgs.rclone}/bin/rclone cat nextcloud:test-shared-file)
-    '';
-
+  test-helpers.extraTests = { nodes, ... }: let
     findInClosure = what: drv: pkgs.runCommand "find-in-closure" { exportReferencesGraph = [ "graph" drv ]; inherit what; } ''
       test -e graph
       grep "$what" graph >$out || true
     '';
-    nextcloudUsesImagick = findInClosure "imagick" nodes.nextcloud.system.build.vm;
-    nextcloudWithoutDoesntUseIt = findInClosure "imagick" nodes.nextcloudWithoutMagick.system.build.vm;
+    nexcloudWithImagick = findInClosure "imagick" nodes.nextcloud.system.build.vm;
+    nextcloudWithoutImagick = findInClosure "imagick" nodes.nextcloud.specialisation.withoutMagick.configuration.system.build.vm;
   in ''
-    assert open("${nextcloudUsesImagick}").read() != ""
-    assert open("${nextcloudWithoutDoesntUseIt}").read() == ""
+    with subtest("File is in proper nextcloud home"):
+        nextcloud.succeed("test -f ${nodes.nextcloud.services.nextcloud.datadir}/data/root/files/test-shared-file")
+
+    with subtest("Closure checks"):
+        assert open("${nexcloudWithImagick}").read() != ""
+        assert open("${nextcloudWithoutImagick}").read() == ""
+
+    with subtest("Davfs2"):
+        assert "hi" in client.succeed("cat /mnt/dav/test-shared-file")
 
-    nextcloud.start()
-    client.start()
-    nextcloud.wait_for_unit("multi-user.target")
-    # This is just to ensure the nextcloud-occ program is working
-    nextcloud.succeed("nextcloud-occ status")
-    nextcloud.succeed("curl -sSf http://nextcloud/login")
-    # Ensure that no OpenSSL 1.1 is used.
-    nextcloud.succeed(
-        "${nodes.nextcloud.services.phpfpm.pools.nextcloud.phpPackage}/bin/php -i | grep 'OpenSSL Library Version' | awk -F'=>' '{ print $2 }' | awk '{ print $2 }' | grep -v 1.1"
-    )
-    nextcloud.succeed(
-        "${withRcloneEnv} ${copySharedFile}"
-    )
-    client.wait_for_unit("multi-user.target")
-    nextcloud.succeed("test -f /var/lib/nextcloud-data/data/root/files/test-shared-file")
-    client.succeed(
-        "${withRcloneEnv} ${diffSharedFile}"
-    )
-    assert "hi" in client.succeed("cat /mnt/dav/test-shared-file")
-    nextcloud.succeed("grep -vE '^HBEGIN:oc_encryption_module' /var/lib/nextcloud-data/data/root/files/test-shared-file")
+    with subtest("Ensure SSE is disabled by default"):
+        nextcloud.succeed("grep -vE '^HBEGIN:oc_encryption_module' /var/lib/nextcloud-data/data/root/files/test-shared-file")
   '';
-})) args
+})
diff --git a/nixos/tests/nextcloud/default.nix b/nixos/tests/nextcloud/default.nix
index d024adffd9f06..33aa227d2b032 100644
--- a/nixos/tests/nextcloud/default.nix
+++ b/nixos/tests/nextcloud/default.nix
@@ -5,21 +5,108 @@
 
 with pkgs.lib;
 
-foldl
-  (matrix: ver: matrix // {
-    "basic${toString ver}" = import ./basic.nix { inherit system pkgs; nextcloudVersion = ver; };
-    "with-postgresql-and-redis${toString ver}" = import ./with-postgresql-and-redis.nix {
-      inherit system pkgs;
-      nextcloudVersion = ver;
-    };
-    "with-mysql-and-memcached${toString ver}" = import ./with-mysql-and-memcached.nix {
-      inherit system pkgs;
-      nextcloudVersion = ver;
-    };
-    "with-declarative-redis-and-secrets${toString ver}" = import ./with-declarative-redis-and-secrets.nix {
-      inherit system pkgs;
-      nextcloudVersion = ver;
+let
+  baseModule = { config, ... }: {
+    imports = [
+      {
+        options.test-helpers = {
+          rclone = mkOption { type = types.str; };
+          upload-sample = mkOption { type = types.str; };
+          check-sample = mkOption { type = types.str; };
+          init = mkOption { type = types.str; default = ""; };
+          extraTests = mkOption { type = types.either types.str (types.functionTo types.str); default = ""; };
+        };
+        options.adminuser = mkOption { type = types.str; };
+        options.adminpass = mkOption { type = types.str; };
+      }
+    ];
+
+    adminuser = "root";
+    adminpass = "hunter2";
+
+    test-helpers.rclone = "${pkgs.writeShellScript "rclone" ''
+      set -euo pipefail
+      export PATH="${pkgs.rclone}/bin:$PATH"
+      export RCLONE_CONFIG_NEXTCLOUD_TYPE=webdav
+      export RCLONE_CONFIG_NEXTCLOUD_URL="http://nextcloud/remote.php/dav/files/${config.adminuser}"
+      export RCLONE_CONFIG_NEXTCLOUD_VENDOR="nextcloud"
+      export RCLONE_CONFIG_NEXTCLOUD_USER="${config.adminuser}"
+      export RCLONE_CONFIG_NEXTCLOUD_PASS="$(rclone obscure ${config.adminpass})"
+      exec "$@"
+    ''}";
+    test-helpers.upload-sample = "${pkgs.writeShellScript "rclone-upload" ''
+      <<<'hi' rclone rcat nextcloud:test-shared-file
+    ''}";
+    test-helpers.check-sample = "${pkgs.writeShellScript "check-sample" ''
+      set -e
+      diff <(echo 'hi') <(rclone cat nextcloud:test-shared-file)
+    ''}";
+
+    nodes = {
+      client = { ... }: {};
+      nextcloud = {
+        networking.firewall.allowedTCPPorts = [ 80 ];
+        services.nextcloud = {
+          enable = true;
+          hostName = "nextcloud";
+          https = false;
+          database.createLocally = true;
+          config = {
+            adminpassFile = "${pkgs.writeText "adminpass" config.adminpass}"; # Don't try this at home!
+          };
+        };
+      };
     };
-  })
-{ }
-  [ 27 28 29 ]
+
+    testScript = args@{ nodes, ... }: let
+      inherit (config) test-helpers;
+    in mkBefore ''
+      nextcloud.start()
+      client.start()
+      nextcloud.wait_for_unit("multi-user.target")
+
+      ${test-helpers.init}
+
+      with subtest("Ensure nextcloud-occ is working"):
+          nextcloud.succeed("nextcloud-occ status")
+          nextcloud.succeed("curl -sSf http://nextcloud/login")
+
+      with subtest("Upload/Download test"):
+          nextcloud.succeed(
+              "${test-helpers.rclone} ${test-helpers.upload-sample}"
+          )
+          client.wait_for_unit("multi-user.target")
+          client.succeed(
+              "${test-helpers.rclone} ${test-helpers.check-sample}"
+          )
+
+      ${if builtins.isFunction test-helpers.extraTests then test-helpers.extraTests args else test-helpers.extraTests}
+    '';
+  };
+
+  genTests = version:
+    let
+      testBase.imports = [
+        baseModule
+        {
+          nodes.nextcloud = { pkgs, ... }: {
+            services.nextcloud.package = pkgs.${"nextcloud${toString version}"};
+          };
+        }
+      ];
+
+      callNextcloudTest = path:
+        let
+          name = "${removeSuffix ".nix" (baseNameOf path)}${toString version}";
+        in nameValuePair name (import path {
+          inherit system pkgs testBase;
+          name = "nextcloud-${name}";
+        });
+    in map callNextcloudTest [
+      ./basic.nix
+      ./with-mysql-and-memcached.nix
+      ./with-postgresql-and-redis.nix
+      ./with-objectstore.nix
+    ];
+in
+listToAttrs (concatMap genTests [ 27 28 29 ])
diff --git a/nixos/tests/nextcloud/with-mysql-and-memcached.nix b/nixos/tests/nextcloud/with-mysql-and-memcached.nix
index 035a7fdcb0c80..07a3e56fae4af 100644
--- a/nixos/tests/nextcloud/with-mysql-and-memcached.nix
+++ b/nixos/tests/nextcloud/with-mysql-and-memcached.nix
@@ -1,79 +1,37 @@
-args@{ pkgs, nextcloudVersion ? 22, ... }:
+{ pkgs, testBase, system, ... }:
 
-(import ../make-test-python.nix ({ pkgs, ...}: let
-  adminpass = "hunter2";
-  adminuser = "root";
-in {
+with import ../../lib/testing-python.nix { inherit system pkgs; };
+runTest ({ config, ... }: {
   name = "nextcloud-with-mysql-and-memcached";
   meta = with pkgs.lib.maintainers; {
     maintainers = [ eqyiel ];
   };
 
-  nodes = {
-    # The only thing the client needs to do is download a file.
-    client = { ... }: {};
+  imports = [ testBase ];
 
+  nodes = {
     nextcloud = { config, pkgs, ... }: {
-      networking.firewall.allowedTCPPorts = [ 80 ];
-
       services.nextcloud = {
-        enable = true;
-        hostName = "nextcloud";
-        https = true;
-        package = pkgs.${"nextcloud" + (toString nextcloudVersion)};
         caching = {
           apcu = true;
           redis = false;
           memcached = true;
         };
-        database.createLocally = true;
-        config = {
-          dbtype = "mysql";
-          # Don't inherit adminuser since "root" is supposed to be the default
-          adminpassFile = "${pkgs.writeText "adminpass" adminpass}"; # Don't try this at home!
-        };
+        config.dbtype = "mysql";
       };
 
       services.memcached.enable = true;
     };
   };
 
-  testScript = let
+  test-helpers.init = let
     configureMemcached = pkgs.writeScript "configure-memcached" ''
-      #!${pkgs.runtimeShell}
       nextcloud-occ config:system:set memcached_servers 0 0 --value 127.0.0.1 --type string
       nextcloud-occ config:system:set memcached_servers 0 1 --value 11211 --type integer
       nextcloud-occ config:system:set memcache.local --value '\OC\Memcache\APCu' --type string
       nextcloud-occ config:system:set memcache.distributed --value '\OC\Memcache\Memcached' --type string
     '';
-    withRcloneEnv = pkgs.writeScript "with-rclone-env" ''
-      #!${pkgs.runtimeShell}
-      export RCLONE_CONFIG_NEXTCLOUD_TYPE=webdav
-      export RCLONE_CONFIG_NEXTCLOUD_URL="http://nextcloud/remote.php/dav/files/${adminuser}"
-      export RCLONE_CONFIG_NEXTCLOUD_VENDOR="nextcloud"
-      export RCLONE_CONFIG_NEXTCLOUD_USER="${adminuser}"
-      export RCLONE_CONFIG_NEXTCLOUD_PASS="$(${pkgs.rclone}/bin/rclone obscure ${adminpass})"
-    '';
-    copySharedFile = pkgs.writeScript "copy-shared-file" ''
-      #!${pkgs.runtimeShell}
-      echo 'hi' | ${pkgs.rclone}/bin/rclone rcat nextcloud:test-shared-file
-    '';
-
-    diffSharedFile = pkgs.writeScript "diff-shared-file" ''
-      #!${pkgs.runtimeShell}
-      diff <(echo 'hi') <(${pkgs.rclone}/bin/rclone cat nextcloud:test-shared-file)
-    '';
   in ''
-    start_all()
-    nextcloud.wait_for_unit("multi-user.target")
     nextcloud.succeed("${configureMemcached}")
-    nextcloud.succeed("curl -sSf http://nextcloud/login")
-    nextcloud.succeed(
-        "${withRcloneEnv} ${copySharedFile}"
-    )
-    client.wait_for_unit("multi-user.target")
-    client.succeed(
-        "${withRcloneEnv} ${diffSharedFile}"
-    )
   '';
-})) args
+})
diff --git a/nixos/tests/nextcloud/with-objectstore.nix b/nixos/tests/nextcloud/with-objectstore.nix
new file mode 100644
index 0000000000000..fc26760b8babd
--- /dev/null
+++ b/nixos/tests/nextcloud/with-objectstore.nix
@@ -0,0 +1,96 @@
+{ name, pkgs, testBase, system, ... }:
+
+with import ../../lib/testing-python.nix { inherit system pkgs; };
+runTest ({ config, lib, ... }: let
+  accessKey = "BKIKJAA5BMMU2RHO6IBB";
+  secretKey = "V7f1CwQqAcwo80UEIJEjc5gVQUSSx5ohQ9GSrr12";
+
+  rootCredentialsFile = pkgs.writeText "minio-credentials-full" ''
+    MINIO_ROOT_USER=${accessKey}
+    MINIO_ROOT_PASSWORD=${secretKey}
+  '';
+in {
+  inherit name;
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ onny ma27 ];
+  };
+
+  imports = [ testBase ];
+
+  nodes = {
+    nextcloud = { config, pkgs, ... }: {
+      networking.firewall.allowedTCPPorts = [ 9000 ];
+      environment.systemPackages = [ pkgs.minio-client ];
+
+      services.nextcloud.config.objectstore.s3 = {
+        enable = true;
+        bucket = "nextcloud";
+        autocreate = true;
+        key = accessKey;
+        secretFile = "${pkgs.writeText "secretKey" secretKey}";
+        hostname = "nextcloud";
+        useSsl = false;
+        port = 9000;
+        usePathStyle = true;
+        region = "us-east-1";
+      };
+
+      services.minio = {
+        enable = true;
+        listenAddress = "0.0.0.0:9000";
+        consoleAddress = "0.0.0.0:9001";
+        inherit rootCredentialsFile;
+      };
+    };
+  };
+
+  test-helpers.init = ''
+    nextcloud.wait_for_open_port(9000)
+  '';
+
+  test-helpers.extraTests = { nodes, ... }: ''
+    with subtest("File is not on the filesystem"):
+        nextcloud.succeed("test ! -e ${nodes.nextcloud.services.nextcloud.home}/data/root/files/test-shared-file")
+
+    with subtest("Check if file is in S3"):
+        nextcloud.succeed(
+            "mc config host add minio http://localhost:9000 ${accessKey} ${secretKey} --api s3v4"
+        )
+        files = nextcloud.succeed('mc ls minio/nextcloud|sort').strip().split('\n')
+
+        # Cannot assert an exact number here, nc27 writes more stuff initially into S3.
+        # For now let's assume it's always the most recently added file.
+        assert len(files) > 0, f"""
+          Expected to have at least one object in minio/nextcloud. But `mc ls` gave output:
+
+          '{files}'
+        """
+
+        import re
+        ptrn = re.compile("^\[[A-Z0-9 :-]+\] +(?P<details>[A-Za-z0-9 :]+)$")
+        match = ptrn.match(files[-1].strip())
+        assert match, "Cannot match mc client output!"
+        size, type_, file = tuple(match.group('details').split(' '))
+
+        assert size == "3B", f"""
+          Expected size of uploaded file to be 3 bytes, got {size}
+        """
+
+        assert type_ == 'STANDARD', f"""
+          Expected type of bucket entry to be a file, i.e. 'STANDARD'. Got {type_}
+        """
+
+        assert file.startswith('urn:oid'), """
+          Expected filename to start with 'urn:oid', instead got '{file}.
+        """
+
+    with subtest("Test download from S3"):
+        client.succeed(
+            "env AWS_ACCESS_KEY_ID=${accessKey} AWS_SECRET_ACCESS_KEY=${secretKey} "
+            + f"${lib.getExe pkgs.awscli2} s3 cp s3://nextcloud/{file} test --endpoint-url http://nextcloud:9000 "
+            + "--region us-east-1"
+        )
+
+        client.succeed("test hi = $(cat test)")
+  '';
+})
diff --git a/nixos/tests/nextcloud/with-postgresql-and-redis.nix b/nixos/tests/nextcloud/with-postgresql-and-redis.nix
index 06afc589403dd..24c17f70932d3 100644
--- a/nixos/tests/nextcloud/with-postgresql-and-redis.nix
+++ b/nixos/tests/nextcloud/with-postgresql-and-redis.nix
@@ -1,45 +1,30 @@
-args@{ pkgs, nextcloudVersion ? 22, ... }:
+{ name, pkgs, testBase, system, ... }:
 
-(import ../make-test-python.nix ({ pkgs, ...}: let
-  adminpass = "hunter2";
-  adminuser = "custom-admin-username";
-in {
-  name = "nextcloud-with-postgresql-and-redis";
+with import ../../lib/testing-python.nix { inherit system pkgs; };
+runTest ({ config, ... }: {
+  inherit name;
   meta = with pkgs.lib.maintainers; {
-    maintainers = [ eqyiel ];
+    maintainers = [ eqyiel ma27 ];
   };
 
-  nodes = {
-    # The only thing the client needs to do is download a file.
-    client = { ... }: {};
+  imports = [ testBase ];
 
+  nodes = {
     nextcloud = { config, pkgs, lib, ... }: {
-      networking.firewall.allowedTCPPorts = [ 80 ];
-
       services.nextcloud = {
-        enable = true;
-        hostName = "nextcloud";
-        package = pkgs.${"nextcloud" + (toString nextcloudVersion)};
         caching = {
           apcu = false;
           redis = true;
           memcached = false;
         };
-        database.createLocally = true;
-        config = {
-          dbtype = "pgsql";
-          inherit adminuser;
-          adminpassFile = toString (pkgs.writeText "admin-pass-file" ''
-            ${adminpass}
-          '');
-        };
+        config.dbtype = "pgsql";
         notify_push = {
           enable = true;
           logLevel = "debug";
         };
         extraAppsEnable = true;
-        extraApps = {
-          inherit (pkgs."nextcloud${lib.versions.major config.services.nextcloud.package.version}Packages".apps) notify_push notes;
+        extraApps = with config.services.nextcloud.package.packages.apps; {
+          inherit notify_push notes;
         };
         settings.trusted_proxies = [ "::1" ];
       };
@@ -49,50 +34,27 @@ in {
     };
   };
 
-  testScript = let
+  test-helpers.init = let
     configureRedis = pkgs.writeScript "configure-redis" ''
-      #!${pkgs.runtimeShell}
       nextcloud-occ config:system:set redis 'host' --value 'localhost' --type string
       nextcloud-occ config:system:set redis 'port' --value 6379 --type integer
       nextcloud-occ config:system:set memcache.local --value '\OC\Memcache\Redis' --type string
       nextcloud-occ config:system:set memcache.locking --value '\OC\Memcache\Redis' --type string
     '';
-    withRcloneEnv = pkgs.writeScript "with-rclone-env" ''
-      #!${pkgs.runtimeShell}
-      export RCLONE_CONFIG_NEXTCLOUD_TYPE=webdav
-      export RCLONE_CONFIG_NEXTCLOUD_URL="http://nextcloud/remote.php/dav/files/${adminuser}"
-      export RCLONE_CONFIG_NEXTCLOUD_VENDOR="nextcloud"
-      export RCLONE_CONFIG_NEXTCLOUD_USER="${adminuser}"
-      export RCLONE_CONFIG_NEXTCLOUD_PASS="$(${pkgs.rclone}/bin/rclone obscure ${adminpass})"
-      "''${@}"
-    '';
-    copySharedFile = pkgs.writeScript "copy-shared-file" ''
-      #!${pkgs.runtimeShell}
-      echo 'hi' | ${pkgs.rclone}/bin/rclone rcat nextcloud:test-shared-file
-    '';
-
-    diffSharedFile = pkgs.writeScript "diff-shared-file" ''
-      #!${pkgs.runtimeShell}
-      diff <(echo 'hi') <(${pkgs.rclone}/bin/rclone cat nextcloud:test-shared-file)
-    '';
   in ''
-    start_all()
-    nextcloud.wait_for_unit("multi-user.target")
     nextcloud.succeed("${configureRedis}")
-    nextcloud.succeed("curl -sSf http://nextcloud/login")
-    nextcloud.succeed(
-        "${withRcloneEnv} ${copySharedFile}"
-    )
-    client.wait_for_unit("multi-user.target")
-    client.execute("${pkgs.lib.getExe pkgs.nextcloud-notify_push.passthru.test_client} http://nextcloud ${adminuser} ${adminpass} >&2 &")
-    client.succeed(
-        "${withRcloneEnv} ${diffSharedFile}"
-    )
-    nextcloud.wait_until_succeeds("journalctl -u nextcloud-notify_push | grep -q \"Sending ping to ${adminuser}\"")
+  '';
+
+  test-helpers.extraTests = ''
+    with subtest("notify-push"):
+        client.execute("${pkgs.lib.getExe pkgs.nextcloud-notify_push.passthru.test_client} http://nextcloud ${config.adminuser} ${config.adminpass} >&2 &")
+        nextcloud.wait_until_succeeds("journalctl -u nextcloud-notify_push | grep -q \"Sending ping to ${config.adminuser}\"")
 
-    # redis cache should not be empty
-    nextcloud.fail('test "[]" = "$(redis-cli --json KEYS "*")"')
+    with subtest("Redis is used for caching"):
+        # redis cache should not be empty
+        nextcloud.fail('test "[]" = "$(redis-cli --json KEYS "*")"')
 
-    nextcloud.fail("curl -f http://nextcloud/nix-apps/notes/lib/AppInfo/Application.php")
+    with subtest("No code is returned when requesting PHP files (regression test)"):
+        nextcloud.fail("curl -f http://nextcloud/nix-apps/notes/lib/AppInfo/Application.php")
   '';
-})) args
+})
diff --git a/nixos/tests/odoo.nix b/nixos/tests/odoo.nix
index 00ae4a2137d10..45ec7b7d7a6b7 100644
--- a/nixos/tests/odoo.nix
+++ b/nixos/tests/odoo.nix
@@ -14,6 +14,18 @@ import ./make-test-python.nix ({ pkgs, lib, package ? pkgs.odoo, ...} : {
         package = package;
         domain = "localhost";
       };
+
+      # odoo does not automatically initialize its database,
+      # even if passing what _should_ be the equivalent of these options:
+      #  settings = {
+      #    options = {
+      #      database = "odoo";
+      #      init = "base";
+      #    };
+      #  };
+      systemd.services.odoo.preStart = ''
+        HOME=$STATE_DIRECTORY ${package}/bin/odoo -d odoo -i base --stop-after-init --without-demo all
+      '';
     };
   };
 
diff --git a/nixos/tests/quickwit.nix b/nixos/tests/quickwit.nix
index 145959f7d3f51..7e617c63d7973 100644
--- a/nixos/tests/quickwit.nix
+++ b/nixos/tests/quickwit.nix
@@ -1,5 +1,54 @@
 import ./make-test-python.nix ({ lib, pkgs, ... }:
 
+let
+  # Define an example Quickwit index schema,
+  # and some `exampleDocs` below, to test if ingesting
+  # and querying works as expected.
+  index_yaml = ''
+    version: 0.7
+    index_id: example_server_logs
+    doc_mapping:
+      mode: dynamic
+      field_mappings:
+        - name: datetime
+          type: datetime
+          fast: true
+          input_formats:
+            - iso8601
+          output_format: iso8601
+          fast_precision: seconds
+          fast: true
+        - name: git
+          type: text
+          tokenizer: raw
+        - name: hostname
+          type: text
+          tokenizer: raw
+        - name: level
+          type: text
+          tokenizer: raw
+        - name: message
+          type: text
+        - name: location
+          type: text
+        - name: source
+          type: text
+      timestamp_field: datetime
+
+    search_settings:
+      default_search_fields: [message]
+
+    indexing_settings:
+      commit_timeout_secs: 10
+  '';
+
+  exampleDocs = ''
+    {"datetime":"2024-05-03T02:36:41.017674444Z","git":"e6e1f087ce12065e44ed3b87b50784e6f9bcc2f9","hostname":"machine-1","level":"Info","message":"Processing request done","location":"path/to/server.c:6442:32","source":""}
+    {"datetime":"2024-05-04T02:36:41.017674444Z","git":"e6e1f087ce12065e44ed3b87b50784e6f9bcc2f9","hostname":"machine-1","level":"Info","message":"Got exception processing request: HTTP 404","location":"path/to/server.c:6444:32","source":""}
+    {"datetime":"2024-05-05T02:36:41.017674444Z","git":"e6e1f087ce12065e44ed3b87b50784e6f9bcc2f9","hostname":"machine-1","level":"Info","message":"Got exception processing request: HTTP 404","location":"path/to/server.c:6444:32","source":""}
+    {"datetime":"2024-05-06T02:36:41.017674444Z","git":"e6e1f087ce12065e44ed3b87b50784e6f9bcc2f9","hostname":"machine-2","level":"Info","message":"Got exception processing request: HTTP 404","location":"path/to/server.c:6444:32","source":""}
+  '';
+in
 {
   name = "quickwit";
   meta.maintainers = [ pkgs.lib.maintainers.happysalada ];
@@ -24,6 +73,29 @@ import ./make-test-python.nix ({ lib, pkgs, ... }:
       "journalctl -o cat -u quickwit.service | grep 'transitioned to ready state'"
     )
 
+    with subtest("verify UI installed"):
+      machine.succeed("curl -sSf http://127.0.0.1:7280/ui/")
+
+    with subtest("injest and query data"):
+      import json
+
+      # Test CLI ingestion
+      print(machine.succeed('${pkgs.quickwit}/bin/quickwit index create --index-config ${pkgs.writeText "index.yaml" index_yaml}'))
+      # Important to use `--wait`, otherwise the queries below race with index processing.
+      print(machine.succeed('${pkgs.quickwit}/bin/quickwit index ingest --index example_server_logs --input-path ${pkgs.writeText "exampleDocs.json" exampleDocs} --wait'))
+
+      # Test CLI query
+      cli_query_output = machine.succeed('${pkgs.quickwit}/bin/quickwit index search --index example_server_logs --query "exception"')
+      print(cli_query_output)
+
+      # Assert query result is as expected.
+      num_hits = len(json.loads(cli_query_output)["hits"])
+      assert num_hits == 3, f"cli_query_output contains unexpected number of results: {num_hits}"
+
+      # Test API query
+      api_query_output = machine.succeed('curl --fail http://127.0.0.1:7280/api/v1/example_server_logs/search?query=exception')
+      print(api_query_output)
+
     quickwit.log(quickwit.succeed(
       "systemd-analyze security quickwit.service | grep -v '✓'"
     ))
diff --git a/nixos/tests/searx.nix b/nixos/tests/searx.nix
index 02a88f690db78..0008424f068b2 100644
--- a/nixos/tests/searx.nix
+++ b/nixos/tests/searx.nix
@@ -1,4 +1,4 @@
-import ./make-test-python.nix ({ pkgs, ...} :
+{ pkgs, ... }:
 
 {
   name = "searx";
@@ -7,108 +7,108 @@ import ./make-test-python.nix ({ pkgs, ...} :
   };
 
   # basic setup: searx running the built-in webserver
-  nodes.base = { ... }: {
-    imports = [ ../modules/profiles/minimal.nix ];
-
-    services.searx = {
-      enable = true;
-      environmentFile = pkgs.writeText "secrets" ''
-        WOLFRAM_API_KEY  = sometoken
-        SEARX_SECRET_KEY = somesecret
-      '';
+  nodes.base =
+    { ... }:
+    {
+      services.searx = {
+        enable = true;
+        environmentFile = pkgs.writeText "secrets" ''
+          WOLFRAM_API_KEY  = sometoken
+          SEARX_SECRET_KEY = somesecret
+        '';
 
-      settings.server =
-        { port = "8080";
+        settings.server = {
+          port = "8080";
           bind_address = "0.0.0.0";
           secret_key = "@SEARX_SECRET_KEY@";
         };
-      settings.engines = [
-        { name = "wolframalpha";
-          api_key = "@WOLFRAM_API_KEY@";
-          engine = "wolframalpha_api";
-        }
-        { name = "startpage";
-          shortcut = "start";
-        }
-      ];
-    };
+        settings.engines = [
+          {
+            name = "wolframalpha";
+            api_key = "@WOLFRAM_API_KEY@";
+            engine = "wolframalpha_api";
+          }
+          {
+            name = "startpage";
+            shortcut = "start";
+          }
+        ];
+      };
 
-  };
+    };
 
   # fancy setup: run in uWSGI and use nginx as proxy
-  nodes.fancy = { config, ... }: {
-    imports = [ ../modules/profiles/minimal.nix ];
-
-    services.searx = {
-      enable = true;
-      # searx refuses to run if unchanged
-      settings.server.secret_key = "somesecret";
-
-      runInUwsgi = true;
-      uwsgiConfig = {
-        # serve using the uwsgi protocol
-        socket = "/run/searx/uwsgi.sock";
-        chmod-socket = "660";
-
-        # use /searx as url "mountpoint"
-        mount = "/searx=searx.webapp:application";
-        module = "";
-        manage-script-name = true;
+  nodes.fancy =
+    { config, ... }:
+    {
+      services.searx = {
+        enable = true;
+        # searx refuses to run if unchanged
+        settings.server.secret_key = "somesecret";
+
+        runInUwsgi = true;
+        uwsgiConfig = {
+          # serve using the uwsgi protocol
+          socket = "/run/searx/uwsgi.sock";
+          chmod-socket = "660";
+
+          # use /searx as url "mountpoint"
+          mount = "/searx=searx.webapp:application";
+          module = "";
+          manage-script-name = true;
+        };
       };
-    };
 
-    # use nginx as reverse proxy
-    services.nginx.enable = true;
-    services.nginx.virtualHosts.localhost = {
-      locations."/searx".extraConfig =
-        ''
+      # use nginx as reverse proxy
+      services.nginx.enable = true;
+      services.nginx.virtualHosts.localhost = {
+        locations."/searx".extraConfig = ''
           include ${pkgs.nginx}/conf/uwsgi_params;
           uwsgi_pass unix:/run/searx/uwsgi.sock;
         '';
-      locations."/searx/static/".alias = "${config.services.searx.package}/share/static/";
-    };
-
-    # allow nginx access to the searx socket
-    users.users.nginx.extraGroups = [ "searx" ];
-
-  };
-
-  testScript =
-    ''
-      base.start()
-
-      with subtest("Settings have been merged"):
-          base.wait_for_unit("searx-init")
-          base.wait_for_file("/run/searx/settings.yml")
-          output = base.succeed(
-              "${pkgs.yq-go}/bin/yq eval"
-              " '.engines[] | select(.name==\"startpage\") | .shortcut'"
-              " /run/searx/settings.yml"
-          ).strip()
-          assert output == "start", "Settings not merged"
+        locations."/searx/static/".alias = "${config.services.searx.package}/share/static/";
+      };
 
-      with subtest("Environment variables have been substituted"):
-          base.succeed("grep -q somesecret /run/searx/settings.yml")
-          base.succeed("grep -q sometoken /run/searx/settings.yml")
-          base.copy_from_vm("/run/searx/settings.yml")
+      # allow nginx access to the searx socket
+      users.users.nginx.extraGroups = [ "searx" ];
 
-      with subtest("Basic setup is working"):
-          base.wait_for_open_port(8080)
-          base.wait_for_unit("searx")
-          base.succeed(
-              "${pkgs.curl}/bin/curl --fail http://localhost:8080"
-          )
-          base.shutdown()
+    };
 
-      with subtest("Nginx+uWSGI setup is working"):
-          fancy.start()
-          fancy.wait_for_open_port(80)
-          fancy.wait_for_unit("uwsgi")
-          fancy.succeed(
-              "${pkgs.curl}/bin/curl --fail http://localhost/searx >&2"
-          )
-          fancy.succeed(
-              "${pkgs.curl}/bin/curl --fail http://localhost/searx/static/themes/simple/js/leaflet.js >&2"
-          )
-    '';
-})
+  testScript = ''
+    base.start()
+
+    with subtest("Settings have been merged"):
+        base.wait_for_unit("searx-init")
+        base.wait_for_file("/run/searx/settings.yml")
+        output = base.succeed(
+            "${pkgs.yq-go}/bin/yq eval"
+            " '.engines[] | select(.name==\"startpage\") | .shortcut'"
+            " /run/searx/settings.yml"
+        ).strip()
+        assert output == "start", "Settings not merged"
+
+    with subtest("Environment variables have been substituted"):
+        base.succeed("grep -q somesecret /run/searx/settings.yml")
+        base.succeed("grep -q sometoken /run/searx/settings.yml")
+        base.copy_from_vm("/run/searx/settings.yml")
+
+    with subtest("Basic setup is working"):
+        base.wait_for_open_port(8080)
+        base.wait_for_unit("searx")
+        base.succeed(
+            "${pkgs.curl}/bin/curl --fail http://localhost:8080"
+        )
+        base.shutdown()
+
+    with subtest("Nginx+uWSGI setup is working"):
+        fancy.start()
+        fancy.wait_for_open_port(80)
+        fancy.wait_for_unit("uwsgi")
+        fancy.succeed(
+            "${pkgs.curl}/bin/curl --fail http://localhost/searx >&2"
+        )
+        fancy.succeed(
+            "${pkgs.curl}/bin/curl --fail http://localhost/searx/static/themes/simple/js/leaflet.js >&2"
+        )
+  '';
+}
diff --git a/nixos/tests/snapper.nix b/nixos/tests/snapper.nix
index 674523584fdaa..0369419930f15 100644
--- a/nixos/tests/snapper.nix
+++ b/nixos/tests/snapper.nix
@@ -19,7 +19,9 @@ import ./make-test-python.nix ({ ... }:
     services.snapper.filters = "/nix";
   };
 
-  testScript = ''
+  testScript = { nodes, ... }: let
+    inherit (nodes.machine.services.snapper) snapshotRootOnBoot;
+  in ''
     machine.succeed("btrfs subvolume create /home/.snapshots")
     machine.succeed("snapper -c home list")
     machine.succeed("snapper -c home create --description empty")
@@ -31,5 +33,6 @@ import ./make-test-python.nix ({ ... }:
     machine.succeed("snapper -c home delete 2")
     machine.succeed("systemctl --wait start snapper-timeline.service")
     machine.succeed("systemctl --wait start snapper-cleanup.service")
+    machine.${if snapshotRootOnBoot then "succeed" else "fail"}("systemctl cat snapper-boot.service")
   '';
 })
diff --git a/nixos/tests/vaultwarden.nix b/nixos/tests/vaultwarden.nix
index 3aba3f6845fa7..baefa67dbf535 100644
--- a/nixos/tests/vaultwarden.nix
+++ b/nixos/tests/vaultwarden.nix
@@ -34,7 +34,7 @@ let
       driver = Firefox(options=options)
 
       driver.implicitly_wait(20)
-      driver.get('http://localhost/#/register')
+      driver.get('http://localhost:8080/#/register')
 
       wait = WebDriverWait(driver, 10)
 
@@ -134,11 +134,11 @@ let
             dbBackend = backend;
             config = {
               rocketAddress = "0.0.0.0";
-              rocketPort = 80;
+              rocketPort = 8080;
             };
           };
 
-          networking.firewall.allowedTCPPorts = [ 80 ];
+          networking.firewall.allowedTCPPorts = [ 8080 ];
 
           environment.systemPackages = [ pkgs.firefox-unwrapped pkgs.geckodriver testRunner ];
         }
@@ -152,10 +152,10 @@ let
     testScript = if testScript != null then testScript else ''
       start_all()
       server.wait_for_unit("vaultwarden.service")
-      server.wait_for_open_port(80)
+      server.wait_for_open_port(8080)
 
       with subtest("configure the cli"):
-          client.succeed("bw --nointeraction config server http://server")
+          client.succeed("bw --nointeraction config server http://server:8080")
 
       with subtest("can't login to nonexistent account"):
           client.fail(
@@ -179,6 +179,9 @@ let
               timeout=60
           )
           assert password.strip() == "${storedPassword}"
+
+      with subtest("Check systemd unit hardening"):
+          server.log(server.succeed("systemd-analyze security vaultwarden.service | grep -v ✓"))
     '';
   });
 in
@@ -193,7 +196,7 @@ builtins.mapAttrs (k: v: makeVaultwardenTest k v) {
     testScript = ''
       start_all()
       server.wait_for_unit("vaultwarden.service")
-      server.wait_for_open_port(80)
+      server.wait_for_open_port(8080)
 
       with subtest("Set up vaultwarden"):
           server.succeed("PYTHONUNBUFFERED=1 test-runner | systemd-cat -t test-runner")