about summary refs log tree commit diff stats
path: root/tests/by-name
diff options
context:
space:
mode:
Diffstat (limited to 'tests/by-name')
-rw-r--r--tests/by-name/at/atuin-sync/test.nix191
-rw-r--r--tests/by-name/ba/back/test.nix147
-rw-r--r--tests/by-name/dn/dns/test.nix129
-rw-r--r--tests/by-name/em/email-dns/nodes/mail_server.nix52
-rw-r--r--tests/by-name/em/email-dns/nodes/name_server.nix292
-rw-r--r--tests/by-name/em/email-dns/nodes/user.nix70
-rw-r--r--tests/by-name/em/email-dns/test.nix167
-rw-r--r--tests/by-name/em/email-http/nodes/mail_server.nix57
-rw-r--r--tests/by-name/em/email-http/nodes/user.nix26
-rw-r--r--tests/by-name/em/email-http/test.nix100
-rw-r--r--tests/by-name/em/email-ip/test.nix174
-rw-r--r--tests/by-name/ru/rust-motd/test.nix62
-rw-r--r--tests/by-name/sh/sharkey-cpu/test.nix81
-rw-r--r--tests/by-name/sh/sharkey/test.nix102
-rw-r--r--tests/by-name/ta/taskchampion-sync/test.nix151
15 files changed, 1767 insertions, 34 deletions
diff --git a/tests/by-name/at/atuin-sync/test.nix b/tests/by-name/at/atuin-sync/test.nix
new file mode 100644
index 0000000..d29c031
--- /dev/null
+++ b/tests/by-name/at/atuin-sync/test.nix
@@ -0,0 +1,191 @@
+{
+  nixos-lib,
+  pkgsUnstable,
+  nixpkgs-unstable,
+  vhackPackages,
+  pkgs,
+  extraModules,
+  nixLib,
+  ...
+}:
+nixos-lib.runTest {
+  hostPkgs = pkgs;
+  name = "atuin-sync";
+
+  node = {
+    specialArgs = {inherit pkgsUnstable vhackPackages nixpkgs-unstable nixLib;};
+
+    # Use the nixpkgs as constructed by the `nixpkgs.*` options
+    pkgs = null;
+  };
+
+  nodes = let
+    atuinSession = "01969ec6b8d07e30a9d2df0911fbfe2a";
+  in {
+    acme = {
+      imports = [
+        ../../../common/acme/server.nix
+        ../../../common/dns/client.nix
+      ];
+    };
+    name_server = {nodes, ...}: {
+      imports =
+        extraModules
+        ++ [
+          ../../../common/acme/client.nix
+          ../../../common/dns/server.nix
+        ];
+
+      vhack.dns.zones = {
+        "atuin-sync.server" = {
+          SOA = {
+            nameServer = "ns";
+            adminEmail = "admin@server.com";
+            serial = 2025012301;
+          };
+          useOrigin = false;
+
+          A = [
+            nodes.server.networking.primaryIPAddress
+          ];
+          AAAA = [
+            nodes.server.networking.primaryIPv6Address
+          ];
+        };
+      };
+    };
+
+    server = {config, ...}: {
+      imports =
+        extraModules
+        ++ [
+          ../../../../modules
+          ../../../common/acme/client.nix
+          ../../../common/dns/client.nix
+        ];
+
+      vhack = {
+        persist.enable = true;
+        nginx.enable = true;
+        atuin-sync = {
+          enable = true;
+          fqdn = "atuin-sync.server";
+        };
+      };
+    };
+
+    client1 = {config, ...}: {
+      imports = [
+        ../../../common/acme/client.nix
+        ../../../common/dns/client.nix
+      ];
+
+      environment.sessionVariables.ATUIN_SESSION = atuinSession;
+
+      environment.systemPackages = [
+        pkgs.atuin
+        pkgs.sqlite-interactive
+      ];
+    };
+    client2 = {config, ...}: {
+      imports = [
+        ../../../common/acme/client.nix
+        ../../../common/dns/client.nix
+      ];
+
+      environment.sessionVariables.ATUIN_SESSION = atuinSession;
+
+      environment.systemPackages = [
+        pkgs.atuin
+        pkgs.sqlite-interactive
+      ];
+    };
+  };
+
+  testScript = {nodes, ...}: let
+    syncLogin = pkgs.writeShellScript "login-atuin-sync-account" ''
+      atuin login --username syncy --password password1234 --key "$1"
+    '';
+
+    syncRegister = pkgs.writeShellScript "register-atuin-sync-account" ''
+      atuin register --username syncy --email syncy@email.com --password password1234
+    '';
+
+    mkSyncConfig = pkgs.writeShellScript "register-atuin-sync-account" ''
+      mkdir --parents ~/.config/atuin/
+      cat << EOF > ~/.config/atuin/config.toml
+      sync_address = "https://atuin-sync.server"
+
+      # Use the v2 sync
+      [sync]
+      records = true
+      EOF
+    '';
+
+    runCommandAndRecordInAtuin = pkgs.writeShellScript "run-command-and-record-in-atuin" ''
+      # SPDX-SnippetBegin
+      # SPDX-SnippetCopyrightText: 2023 mentalisttraceur (https://github.com/mentalisttraceur)
+      # Source: https://github.com/atuinsh/atuin/issues/1188#issuecomment-1698354107
+      run_and_record_in_atuin()
+      {
+          local id
+          local status
+          local escaped_command="$(printf '%q ' "$@")"
+          id="$(atuin history start -- "$escaped_command")"
+          "$@"
+          status=$?
+          atuin history end --exit $status "$id"
+          return $status
+      }
+      # SPDX-SnippetEnd
+
+      run_and_record_in_atuin "$@"
+    '';
+
+    acme = import ../../../common/acme {inherit pkgs;};
+  in
+    acme.prepare ["server" "client1" "client2"]
+    # Python
+    ''
+      server.wait_for_unit("atuin.service")
+      server.wait_for_open_port(443)
+
+      # Wait for the server to acquire the acme certificate
+      client1.wait_until_succeeds("curl https://atuin-sync.server")
+
+      with subtest("Setup client syncing"):
+          for client in [client1, client2]:
+            client.succeed("${mkSyncConfig}")
+
+          client1.succeed("${syncRegister}")
+
+          for client in [client1, client2]:
+            # See https://docs.atuin.sh/guide/sync/
+            client.succeed(f"${syncLogin} '{client1.succeed("atuin key")}'")
+
+      with subtest("Can import shell history"):
+          client1.succeed("${runCommandAndRecordInAtuin} echo hi - client 1")
+          client2.succeed("${runCommandAndRecordInAtuin} echo hi - client 2")
+
+      with subtest("Can sync tasks"):
+          for client in [client1, client2]:
+            client.succeed("atuin sync --force")
+          client1.succeed("atuin sync --force")
+
+
+      with subtest("Have correct tasks"):
+          hist1 = client1.succeed("atuin history list --session --format '{command}'").strip().split('\n')
+          hist2 = client2.succeed("atuin history list --session --format '{command}'").strip().split('\n')
+
+          hist1.sort()
+          hist2.sort()
+
+          canonicalHistory = [
+            "echo hi - client 1",
+            "echo hi - client 2"
+          ]
+
+          assert hist1 == hist2, f"The clients don't have the same amount of history items, client1: '{hist1}', client2: '{hist2}'"
+          assert hist1 == canonicalHistory, f"The history is not correct: '{hist1}' vs. '{canonicalHistory}'"
+    '';
+}
diff --git a/tests/by-name/ba/back/test.nix b/tests/by-name/ba/back/test.nix
index 63f2837..cce5ede 100644
--- a/tests/by-name/ba/back/test.nix
+++ b/tests/by-name/ba/back/test.nix
@@ -8,9 +8,19 @@
   nixLib,
   ...
 }: let
-  gitRepoPath = "/srv/test/repo";
-
   domain = "server";
+
+  sshKeys =
+    import ../../gi/git-server/ssh_keys.nix {inherit pkgs;};
+
+  gitoliteAdminConfSnippet = pkgs.writeText "gitolite-admin-conf-snippet" ''
+    repo CREATOR/[a-zA-Z0-9].*
+      C     = @all
+      RW+   = CREATOR
+      RW    = WRITERS
+      R     = READERS
+      option user-configs = cgit\.owner cgit\.desc cgit\.section cgit\.homepage
+  '';
 in
   nixos-lib.runTest {
     hostPkgs = pkgs; # the Nixpkgs package set used outside the VMs
@@ -26,7 +36,7 @@ in
 
     nodes = {
       server = {config, ...}: {
-        environment.systemPackages = [pkgs.git pkgs.git-bug pkgs.gawk];
+        environment.systemPackages = [pkgs.git];
 
         imports =
           extraModules
@@ -35,28 +45,41 @@ in
           ];
 
         vhack = {
+          persist.enable = true;
+          openssh.enable = true;
           nginx = {
             enable = true;
             selfsign = true;
           };
           git-server = {
             enable = true;
+            domain = "git.${domain}";
+            gitolite.adminPubkey = sshKeys.admin.pub;
           };
-          back = {
+          git-back = {
             enable = true;
-            repositories = {
-              "${gitRepoPath}" = {
-                enable = true;
-                domain = "${domain}";
-                port = 9220;
-              };
-            };
+            domain = "issues.${domain}";
           };
         };
       };
 
-      client = {...}: {
-        environment.systemPackages = [pkgs.curl];
+      client = {nodes, ...}: {
+        environment.systemPackages = [pkgs.git pkgs.curl pkgs.git-bug pkgs.gawk];
+        programs.ssh.extraConfig = ''
+          Host *
+            UserKnownHostsFile /dev/null
+            StrictHostKeyChecking no
+            # there's nobody around that can input password
+            PreferredAuthentications publickey
+        '';
+        users.users.alice = {isNormalUser = true;};
+        networking.hosts = {
+          "${nodes.server.networking.primaryIPAddress}" = [
+            "git.${domain}"
+            "issues.${domain}"
+            "${domain}"
+          ];
+        };
       };
     };
 
@@ -67,55 +90,111 @@ in
     ''
       start_all()
 
-      with subtest("can setup git-bug issues on server"):
-        server.succeed("${pkgs.writeShellScript "setup-git-repo" ''
-        set -ex
+      with subtest("can setup ssh keys on client"):
+        client.succeed(
+            "mkdir -p ~root/.ssh",
+            "cp ${sshKeys.admin.priv} ~root/.ssh/id_ed25519",
+            "chmod 600 ~root/.ssh/id_ed25519",
+        )
+        client.succeed(
+            "sudo -u alice mkdir -p ~alice/.ssh",
+            "sudo -u alice cp ${sshKeys.alice.priv} ~alice/.ssh/id_ed25519",
+            "sudo -u alice chmod 600 ~alice/.ssh/id_ed25519",
+        )
+
+      with subtest("gitolite server starts"):
+        server.wait_for_unit("gitolite-init.service")
+        server.wait_for_unit("sshd.service")
+        client.succeed("ssh -n git@git.${domain} info")
+
+
+      with subtest("admin can clone and configure gitolite-admin.git"):
+        client.succeed("${pkgs.writeShellScript "setup-gitolite-admin.git" ''
+        set -xe
+
+        git clone git@git.${domain}:gitolite-admin.git
+        git config --global user.name 'System Administrator'
+        git config --global user.email root\@domain.example
+
+        cp ${sshKeys.alice.pub} gitolite-admin/keydir/alice.pub
+
+        (cd gitolite-admin && git switch -c master && git branch -D main)
+
+        (cd gitolite-admin && git add . && git commit -m 'Add keys for alice' && git push -u origin master)
+        cat ${gitoliteAdminConfSnippet} >> gitolite-admin/conf/gitolite.conf
+        (cd gitolite-admin && git add . && git commit -m 'Add support for wild repos' && git push)
+        (cd gitolite-admin && git push -d origin main)
+      ''}")
 
-        mkdir --parents "${gitRepoPath}"
-        cd "${gitRepoPath}"
+      with subtest("alice can create a repo"):
+        client.succeed("sudo -u alice ${pkgs.writeShellScript "alice-create-repo" ''
+        set -xe
+
+        mkdir --parents ./alice/repo1 && cd alice/repo1;
+
+        git init --initial-branch main
+        echo "# Alice's Repo" > README.md
+        git add README.md
+        git -c user.name=Alice -c user.email=alice@domain.example commit -m 'Add readme'
 
-        git init
+        git remote add origin git@git.${domain}:alice/repo1.git
+        git push --set-upstream origin main
+      ''}")
+
+      with subtest("can setup git-bug issues in alice/repo1"):
+        client.succeed("sudo -u alice ${pkgs.writeShellScript "setup-git-repo" ''
+        set -ex
 
-        git bug user create --avatar "" --email "test@email.org" --name "test user" --non-interactive
+        cd alice/repo1
 
-        git bug add \
+        git bug user new --avatar "" --email "alice@server.org" --name "alice" --non-interactive
+
+        git bug bug new \
         --title "Some bug title" \
         --message "A long description of the bug. Probably has some code segments, maybe even *markdown* mark_up_ or other things" \
         --non-interactive
 
-        git bug add \
+        git bug bug new \
         --title "Second bug title" \
         --message "" \
         --non-interactive
 
-        git bug add \
+        git bug bug new \
         --title "Third bug title" \
         --message "" \
         --non-interactive
 
-        git bug select "$(git bug ls --format plain | awk '{print $1}' | head -n 1)"
+        git bug bug select "$(git bug bug --format plain | awk '{print $1}' | head -n 1)"
+
+        git bug bug comment new --message "Some comment message" --non-interactive
+        git bug bug comment new --message "Second comment message" --non-interactive
 
-        git bug comment add --message "Some comment message" --non-interactive
-        git bug comment add --message "Second comment message" --non-interactive
+        # TODO: This should use `git bug push`, but their ssh implementation is just
+        # too special to work in a VM test <2025-03-08>
+        git push origin +refs/bugs/*
+        git push origin +refs/identities/*
 
-        # NOTE(@bpeetz): Currently, the `back` module assumes that the git user can write
-        # to the repository, as such we need to provide write access here <2024-12-24>
-        chown --recursive git:git "${gitRepoPath}"
+        ssh git@git.${domain} -- config alice/repo1 --add cgit.owner Alice
+        ssh git@git.${domain} -- perms alice/repo1 + READERS @all
       ''}")
 
       with subtest("back server starts"):
-        server.wait_for_unit("${builtins.replaceStrings ["/"] ["_"] "back-${domain}.service"}")
+        server.wait_for_unit("back.service")
 
       with subtest("client can access the server"):
         client.succeed("${pkgs.writeShellScript "curl-back" ''
         set -xe
 
-        curl --insecure --silent --fail --show-error "https://${domain}/issues/open" --output /root/issues.html
-
-        grep -- '- 2 comments' /root/issues.html
+        curl --insecure --fail --show-error "https://issues.${domain}/alice/repo1/issues/?query=status:open" --output /root/issues.html
         grep -- 'Second bug title' /root/issues.html
-      ''}")
+
+        curl --insecure --fail --show-error "https://issues.${domain}/" --output /root/repos.html
+        grep -- 'repo' /root/repos.html
+        grep -- "&lt;No description&gt;" /root/repos.html
+        grep -- '<span class="user-name">Alice</span>' /root/repos.html
+      ''} >&2")
 
       client.copy_from_vm("/root/issues.html", "");
+      client.copy_from_vm("/root/repos.html", "");
     '';
   }
diff --git a/tests/by-name/dn/dns/test.nix b/tests/by-name/dn/dns/test.nix
new file mode 100644
index 0000000..01d8833
--- /dev/null
+++ b/tests/by-name/dn/dns/test.nix
@@ -0,0 +1,129 @@
+# Inspired by this file: /nixpkgs/nixos/tests/nsd.nix
+{
+  nixos-lib,
+  pkgsUnstable,
+  nixpkgs-unstable,
+  vhackPackages,
+  pkgs,
+  extraModules,
+  nixLib,
+  ...
+}: let
+  common = {...}: {
+    networking.firewall.enable = false;
+    networking.dhcpcd.enable = false;
+  };
+
+  mkClient = version: {
+    lib,
+    nodes,
+    ...
+  }: {
+    environment.systemPackages = [pkgs.dig pkgs.dig.dnsutils];
+
+    imports = [common];
+    networking.nameservers = lib.mkForce [
+      (lib.head nodes.server.networking.interfaces.eth1."${version}".addresses).address
+    ];
+  };
+in
+  nixos-lib.runTest {
+    hostPkgs = pkgs; # the Nixpkgs package set used outside the VMs
+
+    name = "dns";
+
+    node = {
+      specialArgs = {inherit pkgsUnstable vhackPackages nixpkgs-unstable nixLib;};
+
+      # Use the nixpkgs as constructed by the `nixpkgs.*` options
+      pkgs = null;
+    };
+
+    nodes = {
+      server = {
+        config,
+        lib,
+        ...
+      }: {
+        imports =
+          extraModules
+          ++ [
+            ../../../../modules
+            common
+          ];
+
+        vhack = {
+          dns = {
+            enable = true;
+            interfaces = [
+              (lib.head config.networking.interfaces.eth1.ipv4.addresses).address
+              (lib.head config.networking.interfaces.eth1.ipv6.addresses).address
+            ];
+            zones = {
+              "example.com" = {
+                SOA = {
+                  nameServer = "ns";
+                  adminEmail = "admin@example.com";
+                  serial = 2024012301;
+                };
+
+                useOrigin = false;
+                NS = [
+                  "ns.example.com."
+                ];
+
+                subdomains = {
+                  ns = {
+                    A = ["192.168.1.3"];
+                  };
+                  ipv4 = {
+                    A = ["1.2.3.4"];
+                  };
+                  ipv6 = {
+                    AAAA = ["dead:beef::1"];
+                  };
+                  openpgpkey = {
+                    TXT = ["Hi!"];
+                  };
+                };
+              };
+            };
+          };
+        };
+      };
+
+      clientV4 = mkClient "ipv4";
+      clientV6 = mkClient "ipv6";
+    };
+
+    testScript = {nodes, ...}:
+    /*
+    python
+    */
+    ''
+      start_all()
+
+      clientV4.wait_for_unit("network.target")
+      clientV6.wait_for_unit("network.target")
+      server.wait_for_unit("nsd.service")
+
+      def assert_host(ipVersion, dnsRecordType, dnsQuery, expected):
+          self = clientV4 if ipVersion == 4 else clientV6
+          out = self.succeed(f"host -{ipVersion} -t {dnsRecordType} {dnsQuery}").rstrip()
+          self.log(f"output: {out}")
+          import re
+          assert re.search(expected, out), f"DNS IPv{ipVersion} dnsQuery on {dnsQuery} gave '{out}' instead of '{expected}'"
+
+
+      for ipv in 4, 6:
+          with subtest(f"IPv{ipv}"):
+              assert_host(ipv, "a", "example.com", "has no [^ ]+ record")
+              assert_host(ipv, "aaaa", "example.com", "has no [^ ]+ record")
+
+              assert_host(ipv, "soa", "example.com", "SOA.*?admin\\.example\\.com")
+              assert_host(ipv, "a", "ipv4.example.com", "address 1.2.3.4$")
+              assert_host(ipv, "aaaa", "ipv6.example.com", "address dead:beef::1$")
+
+              assert_host(ipv, "txt", "openpgpkey.example.com", "descriptive text \"Hi!\"$")
+    '';
+  }
diff --git a/tests/by-name/em/email-dns/nodes/mail_server.nix b/tests/by-name/em/email-dns/nodes/mail_server.nix
new file mode 100644
index 0000000..279d289
--- /dev/null
+++ b/tests/by-name/em/email-dns/nodes/mail_server.nix
@@ -0,0 +1,52 @@
+{
+  extraModules,
+  pkgs,
+  vhackPackages,
+}: {
+  mkMailServer = serverName: principal: {
+    config,
+    lib,
+    nodes,
+    ...
+  }: {
+    imports =
+      extraModules
+      ++ [
+        ../../../../../modules
+        ../../../../common/acme/client.nix
+        ../../../../common/dns/client.nix
+      ];
+
+    environment.systemPackages = [
+      pkgs.bind
+      pkgs.openssl
+    ];
+
+    age.identityPaths = ["${../../../../common/email/hostKey}"];
+
+    vhack = {
+      stalwart-mail = {
+        enable = true;
+        fqdn = "${serverName}.server.com";
+        admin = "admin@${serverName}.server.com";
+        security = {
+          dkimKeys = let
+            loadKey = name: {
+              dkimPublicKey = builtins.readFile (../../../../common/email/dkim + "/${name}/public");
+              dkimPrivateKeyPath = ../../../../common/email/dkim + "/${name}/private.age";
+              keyAlgorithm = "ed25519-sha256";
+            };
+          in {
+            "mail1.server.com" = loadKey "mail1.server.com";
+            "mail2.server.com" = loadKey "mail2.server.com";
+            "alice.com" = loadKey "alice.com";
+            "bob.com" = loadKey "bob.com";
+          };
+          verificationMode = "strict";
+        };
+        openFirewall = true;
+        principals = [principal];
+      };
+    };
+  };
+}
diff --git a/tests/by-name/em/email-dns/nodes/name_server.nix b/tests/by-name/em/email-dns/nodes/name_server.nix
new file mode 100644
index 0000000..bde1a16
--- /dev/null
+++ b/tests/by-name/em/email-dns/nodes/name_server.nix
@@ -0,0 +1,292 @@
+{extraModules}: {
+  config,
+  lib,
+  nodes,
+  pkgs,
+  ...
+}: let
+  keyAlgoToKeyType = keyAlgo:
+    if keyAlgo == "ed25519-sha256"
+    then "ed25519"
+    else if keyAlgo == "rsa-sha-256" || keyAlgo == "rsa-sha-1"
+    then "rsa"
+    else builtins.throw "Impossible";
+
+  mkZone = user: nodes: lib: cfg: {
+    SOA = {
+      nameServer = "ns.server.com";
+      adminEmail = "${user}@${user}.com";
+      serial = 2024012301;
+    };
+
+    MX = [
+      {
+        preference = 10;
+        exchange = "${cfg.fqdn}.";
+      }
+    ];
+
+    # https://www.rfc-editor.org/rfc/rfc8461.html#section-3.1
+    # Also see the policy in the hmtl part.
+    MTA-STS = [
+      {
+        id = "20250228Z";
+      }
+    ];
+
+    # https://www.rfc-editor.org/rfc/rfc7208.html
+    # https://en.wikipedia.org/wiki/Sender_Policy_Framework
+    TXT = [
+      (builtins.concatStringsSep " "
+        [
+          "v=spf1" # The version.
+          "+mx" # Allow mail from this domain MX record.
+          "-all" # Reject all other emails if the previous mechanism did not match.
+        ])
+    ];
+
+    # https://www.rfc-editor.org/rfc/rfc6376.html#section-3.6.1
+    # https://www.rfc-editor.org/rfc/rfc6376.html#section-7.5
+    DKIM = [
+      {
+        selector = "mail";
+        k = keyAlgoToKeyType cfg.security.dkimKeys."${user}.com".keyAlgorithm;
+        p = cfg.security.dkimKeys."${user}.com".dkimPublicKey;
+        s = ["email"];
+        t = ["s"];
+      }
+    ];
+
+    # https://www.rfc-editor.org/rfc/rfc7489.html#section-6.3
+    DMARC = [
+      {
+        adkim = "strict";
+        aspf = "strict";
+        fo = ["0" "1" "d" "s"];
+        p = "reject";
+        rua = cfg.admin;
+        ruf = [cfg.admin];
+      }
+    ];
+
+    A = [
+      nodes.${user}.networking.primaryIPAddress
+    ];
+    AAAA = [
+      nodes.${user}.networking.primaryIPv6Address
+    ];
+  };
+  mkServerZone = serverName: nodes: lib: let
+    cfg = nodes."${serverName}_server".vhack.stalwart-mail;
+  in {
+    SOA = {
+      nameServer = "ns.server.com";
+      adminEmail = "admin@server.com";
+      serial = 2024012301;
+    };
+    MX = [
+      {
+        preference = 10;
+        exchange = "${serverName}.server.com.";
+      }
+    ];
+
+    # https://www.rfc-editor.org/rfc/rfc6376.html#section-3.6.1
+    # https://www.rfc-editor.org/rfc/rfc6376.html#section-7.5
+    DKIM = [
+      {
+        selector = "mail";
+        k = keyAlgoToKeyType cfg.security.dkimKeys."${serverName}.server.com".keyAlgorithm;
+        p = cfg.security.dkimKeys."${serverName}.server.com".dkimPublicKey;
+        s = ["email"];
+        t = ["s"];
+      }
+    ];
+
+    # https://www.rfc-editor.org/rfc/rfc7489.html#section-6.3
+    DMARC = [
+      {
+        adkim = "strict";
+        aspf = "strict";
+        fo = ["0" "1" "d" "s"];
+        p = "quarantine";
+        rua = cfg.admin;
+        ruf = [cfg.admin];
+      }
+    ];
+
+    # https://www.rfc-editor.org/rfc/rfc7208.html
+    # NOTE(@bpeetz): This server might not be directly sending mail, but it is still required for
+    # the SMTP EHLO check. <2025-02-25>
+    TXT = [
+      (builtins.concatStringsSep " "
+        [
+          "v=spf1" # The version.
+          "+mx" # Allow mail from this domain MX record.
+          "-all" # Reject all other emails if the previous mechanism did not match.
+        ])
+    ];
+
+    A = [
+      nodes."${serverName}_server".networking.primaryIPAddress
+    ];
+    AAAA = [
+      nodes."${serverName}_server".networking.primaryIPv6Address
+    ];
+  };
+in {
+  imports =
+    extraModules
+    ++ [
+      ../../../../../modules
+      ../../../../common/acme/client.nix
+      ../../../../common/dns/server.nix
+    ];
+
+  services.nginx = {
+    logError = "stderr debug";
+    virtualHosts = let
+      mkStsHost = mx: {
+        forceSSL = true;
+        enableACME = true;
+        root = pkgs.runCommandLocal "mkPolicy" {} ''
+          mkdir --parents $out/.well-known/
+
+          # https://www.rfc-editor.org/rfc/rfc8461.html#section-3.2
+          cat << EOF > $out/.well-known/mta-sts.txt
+          version: STSv1
+          mode: enforce
+          mx: ${mx}
+          max_age: 604800
+          EOF
+        '';
+      };
+    in {
+      "mta-sts.alice.com" = mkStsHost "mail2.server.com";
+      "mta-sts.bob.com" = mkStsHost "mail1.server.com";
+    };
+  };
+
+  vhack = {
+    nginx = {
+      enable = true;
+    };
+    dns.zones = let
+      stsZone = {
+        SOA = {
+          nameServer = "ns";
+          adminEmail = "admin@server.com";
+          serial = 2025012301;
+        };
+
+        useOrigin = false;
+
+        A = [
+          nodes.name_server.networking.primaryIPAddress
+        ];
+        AAAA = [
+          nodes.name_server.networking.primaryIPv6Address
+        ];
+      };
+    in {
+      "arpa" = {
+        SOA = {
+          nameServer = "ns";
+          adminEmail = "admin@server.com";
+          serial = 2025012301;
+        };
+        useOrigin = false;
+
+        PTR = [
+          {
+            name = "acme.test";
+            ip.v4 = nodes.acme.networking.primaryIPAddress;
+          }
+          {
+            name = "acme.test";
+            ip.v6 = nodes.acme.networking.primaryIPv6Address;
+          }
+
+          {
+            name = "alice.com";
+            ip.v4 = nodes.alice.networking.primaryIPAddress;
+          }
+          {
+            name = "alice.com";
+            ip.v6 = nodes.alice.networking.primaryIPv6Address;
+          }
+
+          {
+            name = "bob";
+            ip.v4 = nodes.bob.networking.primaryIPAddress;
+          }
+          {
+            name = "bob";
+            ip.v6 = nodes.bob.networking.primaryIPv6Address;
+          }
+
+          {
+            name = "mail1.server.com";
+            ip.v4 = nodes.mail1_server.networking.primaryIPAddress;
+          }
+          {
+            name = "mail1.server.com";
+            ip.v6 = nodes.mail1_server.networking.primaryIPv6Address;
+          }
+
+          {
+            name = "mail2.server.com";
+            ip.v4 = nodes.mail2_server.networking.primaryIPAddress;
+          }
+          {
+            name = "mail2.server.com";
+            ip.v6 = nodes.mail2_server.networking.primaryIPv6Address;
+          }
+
+          {
+            name = "ns.server.com";
+            ip.v4 = nodes.name_server.networking.primaryIPAddress;
+          }
+          {
+            name = "ns.server.com";
+            ip.v6 = nodes.name_server.networking.primaryIPv6Address;
+          }
+        ];
+      };
+
+      "alice.com" = mkZone "alice" nodes lib nodes.mail2_server.vhack.stalwart-mail;
+      "mta-sts.alice.com" = stsZone;
+      "bob.com" = mkZone "bob" nodes lib nodes.mail1_server.vhack.stalwart-mail;
+      "mta-sts.bob.com" = stsZone;
+      "mail1.server.com" = mkServerZone "mail1" nodes lib;
+      "mail2.server.com" = mkServerZone "mail2" nodes lib;
+      "ns.server.com" = {
+        SOA = {
+          nameServer = "ns";
+          adminEmail = "admin@server.com";
+          serial = 2025012301;
+        };
+        useOrigin = false;
+
+        A = [
+          nodes.name_server.networking.primaryIPAddress
+        ];
+        AAAA = [
+          nodes.name_server.networking.primaryIPv6Address
+        ];
+      };
+      "server.com" = {
+        SOA = {
+          nameServer = "ns";
+          adminEmail = "admin@server.com";
+          serial = 2025012301;
+        };
+
+        useOrigin = false;
+        NS = [
+          "ns.server.com."
+        ];
+      };
+    };
+  };
+}
diff --git a/tests/by-name/em/email-dns/nodes/user.nix b/tests/by-name/em/email-dns/nodes/user.nix
new file mode 100644
index 0000000..fba02ce
--- /dev/null
+++ b/tests/by-name/em/email-dns/nodes/user.nix
@@ -0,0 +1,70 @@
+{
+  pkgs,
+  vhackPackages,
+}: {
+  mkUser = user: serverName: {
+    nodes,
+    lib,
+    ...
+  }: {
+    imports = [
+      ../../../../common/acme/client.nix
+      ../../../../common/dns/client.nix
+    ];
+
+    environment.systemPackages = [
+      vhackPackages.fetchmail-common-name
+      pkgs.msmtp
+      pkgs.procmail
+
+      pkgs.bind
+      pkgs.openssl
+    ];
+
+    users.users."${user}" = {isNormalUser = true;};
+
+    systemd.tmpfiles.rules = [
+      "d /home/${user}/mail         0700 ${user} users - -"
+      "L /home/${user}/.fetchmailrc -    -       -     - /etc/homeSetup/.fetchmailrc"
+      "L /home/${user}/.procmailrc  -    -       -     - /etc/homeSetup/.procmailrc"
+      "L /home/${user}/.msmtprc     -    -       -     - /etc/homeSetup/.msmtprc"
+    ];
+
+    environment.etc = {
+      "homeSetup/.fetchmailrc" = {
+        text = ''
+          poll "${serverName}.server.com" protocol IMAP
+            username "${user}"
+            password "${user}-password"
+            ssl
+            mda procmail;
+        '';
+        mode = "0600";
+        inherit user;
+      };
+      "homeSetup/.procmailrc" = {
+        text = ''
+          DEFAULT=$HOME/mail
+        '';
+        mode = "0600";
+        inherit user;
+      };
+      "homeSetup/.msmtprc" = {
+        text = ''
+          account        ${user}
+          host           ${serverName}.server.com
+          domain         ${user}.com
+          port           465
+          from           ${user}@${user}.com
+          user           ${user}
+          password       ${user}-password
+          auth           on
+          tls            on
+          tls_starttls   off
+        '';
+        mode = "0600";
+        inherit user;
+      };
+    };
+  };
+}
diff --git a/tests/by-name/em/email-dns/test.nix b/tests/by-name/em/email-dns/test.nix
new file mode 100644
index 0000000..c7ba3b3
--- /dev/null
+++ b/tests/by-name/em/email-dns/test.nix
@@ -0,0 +1,167 @@
+{
+  nixos-lib,
+  pkgsUnstable,
+  nixpkgs-unstable,
+  vhackPackages,
+  pkgs,
+  extraModules,
+  nixLib,
+  ...
+}: let
+  mail_server = import ./nodes/mail_server.nix {inherit extraModules pkgs vhackPackages;};
+  inherit (mail_server) mkMailServer;
+  user = import ./nodes/user.nix {inherit pkgs vhackPackages;};
+  inherit (user) mkUser;
+in
+  nixos-lib.runTest {
+    hostPkgs = pkgs; # the Nixpkgs package set used outside the VMs
+
+    name = "email-dns";
+
+    node = {
+      specialArgs = {inherit pkgsUnstable vhackPackages nixpkgs-unstable nixLib;};
+
+      # Use the nixpkgs as constructed by the `nixpkgs.*` options
+      pkgs = null;
+    };
+
+    nodes = {
+      acme = {
+        nodes,
+        lib,
+        ...
+      }: {
+        imports = [
+          ../../../common/acme/server.nix
+          ../../../common/dns/client.nix
+        ];
+      };
+
+      name_server = import ./nodes/name_server.nix {inherit extraModules;};
+
+      mail1_server =
+        mkMailServer "mail1"
+        {
+          class = "individual";
+          name = "bob";
+          secret = "{PLAIN}bob-password";
+          email = ["bob@bob.com"];
+        };
+
+      mail2_server =
+        mkMailServer "mail2"
+        {
+          class = "individual";
+          name = "alice";
+          secret = "{PLAIN}alice-password";
+          email = ["alice@alice.com"];
+        };
+
+      bob = mkUser "bob" "mail1";
+      alice = mkUser "alice" "mail2";
+    };
+
+    testScript = {...}: let
+      checkEmailEmpty = pkgs.writeShellScript "assert-empty-emails" ''
+        set -xe
+
+        # fetchmail returns EXIT_CODE 1 when no new mail
+        fetchmail --verbose >&2 || [ "$?" -eq 1 ] || {
+          echo "Mail was not empty" >&2
+          exit 1
+        }
+      '';
+      checkEmailNotEmpty = pkgs.writeShellScript "assert-empty-emails" ''
+        set -xe
+
+        # fetchmail returns EXIT_CODE 1 when no new mail
+        fetchmail --verbose >&2 || [ "$?" -ne 1 ] || {
+          echo "No new mail" >&2
+          exit 1
+        }
+      '';
+      checkSpamEmailNotEmpty = pkgs.writeShellScript "assert-empty-emails" ''
+        set -xe
+
+        # fetchmail returns EXIT_CODE 1 when no new mail
+        fetchmail --folder JUNK --verbose >&2 || [ "$?" -ne 1 ] || {
+          echo "No new mail" >&2
+          exit 1
+        }
+      '';
+
+      acme = import ../../../common/acme {inherit pkgs;};
+    in
+      acme.prepare ["mail1_server" "mail2_server" "alice" "bob"]
+      # Python
+      ''
+        from time import sleep
+
+        mail1_server.wait_for_unit("stalwart-mail.service")
+        mail1_server.wait_for_open_port(993) # imap
+        mail1_server.wait_for_open_port(465) # smtp
+        mail2_server.wait_for_unit("stalwart-mail.service")
+        mail2_server.wait_for_open_port(993) # imap
+        mail2_server.wait_for_open_port(465) # smtp
+
+        alice.wait_for_unit("multi-user.target")
+        bob.wait_for_unit("multi-user.target")
+
+        name_server.wait_until_succeeds("stat /var/lib/acme/mta-sts.alice.com/cert.pem")
+        name_server.wait_until_succeeds("stat /var/lib/acme/mta-sts.bob.com/cert.pem")
+
+        with subtest("Both mailserver successfully started all services"):
+          import json
+          def all_services_running(host):
+            (status, output) = host.systemctl("list-units --state=failed --plain --no-pager --output=json")
+            host_failed = json.loads(output)
+            assert len(host_failed) == 0, f"Expected zero failing services, but found: {json.dumps(host_failed, indent=4)}"
+          all_services_running(mail1_server)
+          all_services_running(mail2_server)
+
+        with subtest("Both start without mail"):
+          alice.succeed("sudo -u alice ${checkEmailEmpty}")
+          bob.succeed("sudo -u bob ${checkEmailEmpty}")
+
+        with subtest("Alice can send an empty email to bob"):
+          alice.succeed("sudo -u alice ${pkgs.writeShellScript "alice-send" ''
+          set -xe
+
+          echo "" | msmtp --debug --account alice bob@bob.com >&2
+        ''}")
+
+          # Give `mail2_server` some time to send the email.
+          sleep(160)
+
+          bob.succeed("sudo -u bob ${checkSpamEmailNotEmpty}")
+
+        with subtest("Alice can send an non-empty email to bob"):
+          alice.succeed("sudo -u alice ${pkgs.writeShellScript "alice-send" ''
+          set -xe
+
+          cat << EOF | msmtp --debug --account alice bob@bob.com >&2
+          Subject: Hi bob, I'm Alice!
+
+          Good day, Bob!
+
+          This is an email.
+          It contains a subject and a body.
+          I also assert utf8 support by including my last name in this very message.
+
+          XOXO
+          Alice van DÃ¥ligen.
+
+          .
+          EOF
+        ''}")
+
+          # Give `mail2_server` some time to send the email.
+          sleep(120)
+
+          bob.succeed("sudo -u bob ${checkEmailNotEmpty}")
+
+        mail1_server.copy_from_vm("/var/lib/", "server1")
+        mail2_server.copy_from_vm("/var/lib/", "server2")
+        bob.copy_from_vm("/home/bob/mail", "bob")
+      '';
+  }
diff --git a/tests/by-name/em/email-http/nodes/mail_server.nix b/tests/by-name/em/email-http/nodes/mail_server.nix
new file mode 100644
index 0000000..e94c4e9
--- /dev/null
+++ b/tests/by-name/em/email-http/nodes/mail_server.nix
@@ -0,0 +1,57 @@
+{
+  extraModules,
+  pkgs,
+  vhackPackages,
+}: {
+  mkMailServer = serverName: principal: {
+    config,
+    lib,
+    nodes,
+    ...
+  }: {
+    imports =
+      extraModules
+      ++ [
+        ../../../../../modules
+        ../../../../common/acme/client.nix
+      ];
+
+    environment.systemPackages = [
+      pkgs.bind
+      pkgs.openssl
+    ];
+
+    networking.nameservers = lib.mkForce [
+      nodes.name_server.networking.primaryIPAddress
+      nodes.name_server.networking.primaryIPv6Address
+    ];
+
+    age.identityPaths = ["${../../../../common/email/hostKey}"];
+
+    vhack = {
+      stalwart-mail = {
+        enable = true;
+        fqdn = "${serverName}.server.com";
+        admin = "admin@${serverName}.server.com";
+        security = {
+          dkimKeys = let
+            loadKey = name: {
+              dkimPublicKey = builtins.readFile (../../../../common/email/dkim + "/${name}/public");
+              dkimPrivateKeyPath = ../../../../common/email/dkim + "/${name}/private.age";
+              keyAlgorithm = "ed25519-sha256";
+            };
+          in {
+            "mail.server.com" = loadKey "mail1.server.com";
+            "bob.com" = loadKey "bob.com";
+          };
+          verificationMode = "strict";
+        };
+        openFirewall = true;
+        principals =
+          if principal == null
+          then null
+          else [principal];
+      };
+    };
+  };
+}
diff --git a/tests/by-name/em/email-http/nodes/user.nix b/tests/by-name/em/email-http/nodes/user.nix
new file mode 100644
index 0000000..73b9ff7
--- /dev/null
+++ b/tests/by-name/em/email-http/nodes/user.nix
@@ -0,0 +1,26 @@
+{
+  pkgs,
+  vhackPackages,
+}: {
+  mkUser = user: serverName: {
+    nodes,
+    lib,
+    ...
+  }: {
+    imports = [
+      ../../../../common/acme/client.nix
+    ];
+
+    environment.systemPackages = [
+      pkgs.bind
+      pkgs.openssl
+    ];
+
+    networking.nameservers = lib.mkForce [
+      nodes.name_server.networking.primaryIPAddress
+      nodes.name_server.networking.primaryIPv6Address
+    ];
+
+    users.users."${user}" = {isNormalUser = true;};
+  };
+}
diff --git a/tests/by-name/em/email-http/test.nix b/tests/by-name/em/email-http/test.nix
new file mode 100644
index 0000000..82b4c45
--- /dev/null
+++ b/tests/by-name/em/email-http/test.nix
@@ -0,0 +1,100 @@
+{
+  nixos-lib,
+  pkgsUnstable,
+  nixpkgs-unstable,
+  vhackPackages,
+  pkgs,
+  extraModules,
+  nixLib,
+  ...
+}: let
+  mail_server = import ./nodes/mail_server.nix {inherit extraModules pkgs vhackPackages;};
+  inherit (mail_server) mkMailServer;
+  user = import ./nodes/user.nix {inherit pkgs vhackPackages;};
+  inherit (user) mkUser;
+in
+  nixos-lib.runTest {
+    hostPkgs = pkgs; # the Nixpkgs package set used outside the VMs
+
+    name = "email-http";
+
+    node = {
+      specialArgs = {inherit pkgsUnstable vhackPackages nixpkgs-unstable nixLib;};
+
+      # Use the nixpkgs as constructed by the `nixpkgs.*` options
+      pkgs = null;
+    };
+
+    nodes = {
+      acme = {
+        nodes,
+        lib,
+        ...
+      }: {
+        imports = [
+          ../../../common/acme/server.nix
+          ../../../common/dns/client.nix
+        ];
+      };
+
+      name_server = {nodes, ...}: {
+        imports =
+          extraModules
+          ++ [
+            ../../../common/acme/client.nix
+            ../../../common/dns/server.nix
+          ];
+
+        vhack.dns.zones = {
+          "mail.server.com" = {
+            SOA = {
+              nameServer = "ns";
+              adminEmail = "admin@server.com";
+              serial = 2025012301;
+            };
+            useOrigin = false;
+
+            A = [
+              nodes.mail_server.networking.primaryIPAddress
+            ];
+            AAAA = [
+              nodes.mail_server.networking.primaryIPv6Address
+            ];
+          };
+        };
+      };
+
+      mail_server = mkMailServer "mail" null;
+
+      bob = mkUser "bob" "mail";
+    };
+
+    # TODO(@bpeetz): This test should also test the http JMAP features of stalwart-mail. <2025-04-12>
+    testScript = _: let
+      acme = import ../../../common/acme {inherit pkgs;};
+    in
+      acme.prepare ["mail_server" "bob"]
+      # Python
+      ''
+        mail_server.wait_for_unit("stalwart-mail.service")
+        mail_server.wait_for_open_port(993) # imap
+        mail_server.wait_for_open_port(465) # smtp
+
+        bob.wait_for_unit("multi-user.target")
+
+        with subtest("The mailserver successfully started all services"):
+          import json
+          def all_services_running(host):
+            (status, output) = host.systemctl("list-units --state=failed --plain --no-pager --output=json")
+            host_failed = json.loads(output)
+            assert len(host_failed) == 0, f"Expected zero failing services, but found: {json.dumps(host_failed, indent=4)}"
+          all_services_running(mail_server)
+
+        with subtest("Bob can use the self-service interface"):
+          bob.succeed("${pkgs.writeShellScript "check-self-service" ''
+          curl mail.server.com --location --output /home/bob/output.html;
+        ''}")
+
+        bob.copy_from_vm("/home/bob", "")
+      '';
+  }
diff --git a/tests/by-name/em/email-ip/test.nix b/tests/by-name/em/email-ip/test.nix
new file mode 100644
index 0000000..dabc404
--- /dev/null
+++ b/tests/by-name/em/email-ip/test.nix
@@ -0,0 +1,174 @@
+{
+  nixos-lib,
+  pkgsUnstable,
+  nixpkgs-unstable,
+  vhackPackages,
+  pkgs,
+  extraModules,
+  nixLib,
+  ...
+}: let
+  domain = "mail.server.test";
+
+  scripts = {
+    checkEmailEmpty = pkgs.writeShellScript "assert-empty-emails" ''
+      set -xe
+
+      # fetchmail returns EXIT_CODE 1 when no new mail
+      fetchmail --nosslcertck --verbose >&2 || [ "$?" -eq 1 ] || {
+        echo "Expected exit code 1." >&2
+        exit 1
+      }
+    '';
+  };
+
+  mkUser = user: {nodes, ...}: let
+    domainIp = nodes.server.networking.primaryIPAddress;
+  in {
+    environment.systemPackages = with pkgs; [
+      fetchmail
+      msmtp
+      procmail
+    ];
+
+    users.users."${user}" = {isNormalUser = true;};
+
+    systemd.tmpfiles.rules = [
+      "d /home/${user}/mail         0700 ${user} users - -"
+      "L /home/${user}/.fetchmailrc -    -       -     - /etc/homeSetup/.fetchmailrc"
+      "L /home/${user}/.procmailrc  -    -       -     - /etc/homeSetup/.procmailrc"
+      "L /home/${user}/.msmtprc     -    -       -     - /etc/homeSetup/.msmtprc"
+    ];
+
+    environment.etc = {
+      "homeSetup/.fetchmailrc" = {
+        text = ''
+          poll "${domainIp}" protocol IMAP
+            username "${user}"
+            password "${user}-password"
+            ssl
+            mda procmail;
+        '';
+        mode = "0600";
+        inherit user;
+      };
+      "homeSetup/.procmailrc" = {
+        text = ''
+          DEFAULT=$HOME/mail
+        '';
+        mode = "0600";
+        inherit user;
+      };
+      "homeSetup/.msmtprc" = {
+        text = ''
+          account        ${user}
+          host           ${domainIp}
+          port           465
+          from           ${user}@${domain}
+          user           ${user}
+          password       ${user}-password
+          auth           on
+          tls            on
+          tls_starttls   off
+        '';
+        mode = "0600";
+        inherit user;
+      };
+    };
+  };
+in
+  nixos-lib.runTest {
+    hostPkgs = pkgs; # the Nixpkgs package set used outside the VMs
+
+    name = "email";
+
+    node = {
+      specialArgs = {inherit pkgsUnstable vhackPackages nixpkgs-unstable nixLib;};
+
+      # Use the nixpkgs as constructed by the `nixpkgs.*` options
+      pkgs = null;
+    };
+
+    nodes = {
+      server = {config, ...}: {
+        imports =
+          extraModules
+          ++ [
+            ../../../../modules
+          ];
+
+        vhack = {
+          nginx = {
+            enable = true;
+            selfsign = true;
+          };
+
+          stalwart-mail = {
+            enable = true;
+            fqdn = domain;
+            admin = "mailto:admin@${domain}";
+            security = null;
+            openFirewall = true;
+            principals = [
+              {
+                class = "individual";
+                name = "alice";
+                secret = "{PLAIN}alice-password";
+                email = ["alice@${domain}"];
+              }
+              {
+                class = "individual";
+                name = "bob";
+                secret = "{PLAIN}bob-password";
+                email = ["bob@${domain}"];
+              }
+            ];
+          };
+        };
+      };
+
+      alice = mkUser "alice";
+      bob = mkUser "bob";
+    };
+
+    testScript = {...}:
+    /*
+    python
+    */
+    ''
+      start_all()
+
+      server.wait_for_unit("stalwart-mail.service")
+      server.wait_for_open_port(993) # imap
+      server.wait_for_open_port(465) # smtp
+
+      with subtest("Both start without mail"):
+        alice.succeed("sudo -u alice ${scripts.checkEmailEmpty}")
+        bob.succeed("sudo -u bob ${scripts.checkEmailEmpty}")
+
+      with subtest("Alice can send an email to bob"):
+        alice.succeed("sudo -u alice ${pkgs.writeShellScript "alice-send" ''
+        set -xe
+
+        cat << EOF | msmtp --debug --account alice --tls-certcheck=off bob@${domain} >&2
+        Hi Bob!
+
+        This is an email.
+        It contains a subject and a body.
+
+        ALICE
+        EOF
+      ''}")
+        bob.succeed("sudo -u bob ${pkgs.writeShellScript "bob-receive" ''
+        set -xe
+
+        fetchmail --nosslcertck --verbose >&2 || {
+          echo New Mail did not arrive
+          exit 1
+        }
+      ''}")
+
+      server.copy_from_vm("/var/lib/", "server")
+      bob.copy_from_vm("/home/bob/mail", "bob")
+    '';
+  }
diff --git a/tests/by-name/ru/rust-motd/test.nix b/tests/by-name/ru/rust-motd/test.nix
new file mode 100644
index 0000000..fef1df8
--- /dev/null
+++ b/tests/by-name/ru/rust-motd/test.nix
@@ -0,0 +1,62 @@
+{
+  nixos-lib,
+  pkgsUnstable,
+  nixpkgs-unstable,
+  vhackPackages,
+  pkgs,
+  extraModules,
+  nixLib,
+  ...
+}:
+nixos-lib.runTest {
+  hostPkgs = pkgs;
+
+  name = "rust-motd";
+
+  node = {
+    specialArgs = {inherit pkgsUnstable extraModules vhackPackages nixpkgs-unstable nixLib;};
+
+    # Use the nixpkgs as constructed by the `nixpkgs.*` options
+    pkgs = null;
+  };
+
+  nodes = {
+    server = {config, ...}: {
+      imports =
+        extraModules
+        ++ [
+          ../../../../modules
+        ];
+
+      vhack = {
+        rust-motd.enable = true;
+      };
+    };
+  };
+
+  testScript = {nodes, ...}:
+  /*
+  python
+  */
+  ''
+    from time import sleep
+
+    start_all()
+
+    # Give the service time to run.
+    sleep(3)
+
+    with subtest("All services running"):
+      import json
+      def all_services_running(host):
+        (status, output) = host.systemctl("list-units --state=failed --plain --no-pager --output=json")
+        host_failed = json.loads(output)
+        assert len(host_failed) == 0, f"Expected zero failing services, but found: {json.dumps(host_failed, indent=4)}"
+      all_services_running(server)
+
+    with subtest("Motd generated"):
+      server.succeed("cat /var/lib/rust-motd/motd | tee /dev/stderr | grep --invert-match Error")
+
+    server.copy_from_vm("/var/lib/rust-motd/motd")
+  '';
+}
diff --git a/tests/by-name/sh/sharkey-cpu/test.nix b/tests/by-name/sh/sharkey-cpu/test.nix
new file mode 100644
index 0000000..438cfb3
--- /dev/null
+++ b/tests/by-name/sh/sharkey-cpu/test.nix
@@ -0,0 +1,81 @@
+{
+  nixos-lib,
+  pkgsUnstable,
+  nixpkgs-unstable,
+  vhackPackages,
+  pkgs,
+  extraModules,
+  nixLib,
+  ...
+}:
+nixos-lib.runTest {
+  hostPkgs = pkgs; # the Nixpkgs package set used outside the VMs
+
+  name = "sharkey-cpu";
+
+  node = {
+    specialArgs = {inherit pkgsUnstable extraModules vhackPackages nixpkgs-unstable nixLib;};
+
+    # Use the nixpkgs as constructed by the `nixpkgs.*` options
+    pkgs = null;
+  };
+
+  nodes = {
+    server = {config, ...}: {
+      imports =
+        extraModules
+        ++ [
+          ../../../../modules
+        ];
+
+      vhack = {
+        persist.enable = true;
+        nginx.enable = true;
+        sharkey = {
+          enable = true;
+          fqdn = "sharkey.server";
+        };
+      };
+      systemd.services = {
+        # Avoid an error from this service.
+        "acme-sharkey.server".serviceConfig.ExecStart = pkgs.lib.mkForce "${pkgs.lib.getExe' pkgs.coreutils "true"}";
+
+        # Test that sharkey's hardening still allows access to the CPUs.
+        sharkey.serviceConfig.ExecStart = let
+          nodejs = pkgs.lib.getExe pkgsUnstable.nodejs;
+          script = pkgs.writeTextFile {
+            name = "script.js";
+            text = ''
+              import * as os from 'node:os';
+
+              console.log(os.cpus()[0].model)
+              console.log(os.cpus().length)
+            '';
+          };
+        in
+          pkgs.lib.mkForce "${nodejs} ${script}";
+      };
+    };
+  };
+
+  testScript = {nodes, ...}:
+  /*
+  python
+  */
+  ''
+    from time import sleep
+
+    start_all()
+
+    # Give the service time to run.
+    sleep(3)
+
+    with subtest("All services running"):
+      import json
+      def all_services_running(host):
+        (status, output) = host.systemctl("list-units --state=failed --plain --no-pager --output=json")
+        host_failed = json.loads(output)
+        assert len(host_failed) == 0, f"Expected zero failing services, but found: {json.dumps(host_failed, indent=4)}"
+      all_services_running(server)
+  '';
+}
diff --git a/tests/by-name/sh/sharkey/test.nix b/tests/by-name/sh/sharkey/test.nix
new file mode 100644
index 0000000..0d79cd2
--- /dev/null
+++ b/tests/by-name/sh/sharkey/test.nix
@@ -0,0 +1,102 @@
+{
+  nixos-lib,
+  pkgsUnstable,
+  nixpkgs-unstable,
+  vhackPackages,
+  pkgs,
+  extraModules,
+  nixLib,
+  ...
+}:
+nixos-lib.runTest {
+  hostPkgs = pkgs; # the Nixpkgs package set used outside the VMs
+
+  name = "sharkey";
+
+  node = {
+    specialArgs = {inherit pkgsUnstable extraModules vhackPackages nixpkgs-unstable nixLib;};
+
+    # Use the nixpkgs as constructed by the `nixpkgs.*` options
+    pkgs = null;
+  };
+
+  nodes = {
+    acme = {...}: {
+      imports = [
+        ../../../common/acme/server.nix
+        ../../../common/dns/client.nix
+      ];
+    };
+    name_server = {nodes, ...}: {
+      imports =
+        extraModules
+        ++ [
+          ../../../common/acme/client.nix
+          ../../../common/dns/server.nix
+        ];
+
+      vhack.dns.zones = {
+        "sharkey.server" = {
+          SOA = {
+            nameServer = "ns";
+            adminEmail = "admin@server.com";
+            serial = 2025012301;
+          };
+          useOrigin = false;
+
+          A = [
+            nodes.server.networking.primaryIPAddress
+          ];
+          AAAA = [
+            nodes.server.networking.primaryIPv6Address
+          ];
+        };
+      };
+    };
+
+    server = {config, ...}: {
+      imports =
+        extraModules
+        ++ [
+          ../../../../modules
+          ../../../common/acme/client.nix
+          ../../../common/dns/client.nix
+        ];
+
+      vhack = {
+        persist.enable = true;
+        nginx.enable = true;
+        sharkey = {
+          enable = true;
+          fqdn = "sharkey.server";
+        };
+      };
+    };
+
+    client = {...}: {
+      imports = [
+        ../../../common/acme/client.nix
+        ../../../common/dns/client.nix
+      ];
+    };
+  };
+
+  testScript = {nodes, ...}: let
+    acme = import ../../../common/acme {inherit pkgs;};
+  in
+    acme.prepare ["server" "client"]
+    # Python
+    ''
+      server.wait_for_unit("sharkey.service")
+
+      with subtest("All services running"):
+        import json
+        def all_services_running(host):
+          (status, output) = host.systemctl("list-units --state=failed --plain --no-pager --output=json")
+          host_failed = json.loads(output)
+          assert len(host_failed) == 0, f"Expected zero failing services, but found: {json.dumps(host_failed, indent=4)}"
+        all_services_running(server)
+
+      client.wait_until_succeeds("curl --silent https://sharkey.server | grep 'Thank you for using Sharkey!'")
+    '';
+}
diff --git a/tests/by-name/ta/taskchampion-sync/test.nix b/tests/by-name/ta/taskchampion-sync/test.nix
new file mode 100644
index 0000000..cdbe062
--- /dev/null
+++ b/tests/by-name/ta/taskchampion-sync/test.nix
@@ -0,0 +1,151 @@
+{
+  nixos-lib,
+  pkgsUnstable,
+  nixpkgs-unstable,
+  vhackPackages,
+  pkgs,
+  extraModules,
+  nixLib,
+  ...
+}:
+nixos-lib.runTest {
+  hostPkgs = pkgs;
+  name = "taskchampion-sync";
+
+  node = {
+    specialArgs = {inherit pkgsUnstable vhackPackages nixpkgs-unstable nixLib;};
+
+    # Use the nixpkgs as constructed by the `nixpkgs.*` options
+    pkgs = null;
+  };
+
+  nodes = let
+    # The feature flag is only in version 3.2 and upwards. Stable is still on 3.1
+    taskwarriorPackage = pkgsUnstable.taskwarrior3.overrideAttrs (final: prev: {
+      cmakeFlags = (prev.cmakeFlags or []) ++ ["-DENABLE_TLS_NATIVE_ROOTS=true"];
+    });
+  in {
+    acme = {
+      imports = [
+        ../../../common/acme/server.nix
+        ../../../common/dns/client.nix
+      ];
+    };
+    name_server = {nodes, ...}: {
+      imports =
+        extraModules
+        ++ [
+          ../../../common/acme/client.nix
+          ../../../common/dns/server.nix
+        ];
+
+      vhack.dns.zones = {
+        "taskchampion.server" = {
+          SOA = {
+            nameServer = "ns";
+            adminEmail = "admin@server.com";
+            serial = 2025012301;
+          };
+          useOrigin = false;
+
+          A = [
+            nodes.server.networking.primaryIPAddress
+          ];
+          AAAA = [
+            nodes.server.networking.primaryIPv6Address
+          ];
+        };
+      };
+    };
+
+    server = {config, ...}: {
+      imports =
+        extraModules
+        ++ [
+          ../../../../modules
+          ../../../common/acme/client.nix
+          ../../../common/dns/client.nix
+        ];
+
+      vhack = {
+        persist.enable = true;
+        nginx.enable = true;
+        taskchampion-sync = {
+          enable = true;
+          fqdn = "taskchampion.server";
+        };
+      };
+    };
+
+    task_client1 = {config, ...}: {
+      imports = [
+        ../../../common/acme/client.nix
+        ../../../common/dns/client.nix
+      ];
+
+      environment.systemPackages = [
+        taskwarriorPackage
+      ];
+    };
+    task_client2 = {config, ...}: {
+      imports = [
+        ../../../common/acme/client.nix
+        ../../../common/dns/client.nix
+      ];
+
+      environment.systemPackages = [
+        taskwarriorPackage
+      ];
+    };
+  };
+
+  testScript = {nodes, ...}: let
+    # Generated with uuidgen
+    uuid = "bf01376e-04a4-435a-9263-608567531af3";
+    password = "nixos-test";
+
+    mkSyncConfig = path:
+      pkgs.writeShellScript "setup-config-file" ''
+        set -xe
+
+        mkdir --parents "$(dirname "${path}")"
+        echo 'sync.server.url=https://taskchampion.server' >> "${path}"
+        echo 'sync.server.client_id=${uuid}' >> "${path}"
+        echo 'sync.encryption_secret=${password}' >> "${path}"
+      '';
+
+    acme = import ../../../common/acme {inherit pkgs;};
+  in
+    acme.prepare ["server" "task_client1" "task_client2"]
+    # Python
+    ''
+      server.wait_for_unit("taskchampion-sync-server.service")
+      server.wait_for_open_port(443)
+
+      with subtest("Setup task syncing"):
+          for task in [task_client1, task_client2]:
+            # See man task-sync(5)
+            task.succeed("mkdir ~/.task")
+            task.succeed("${mkSyncConfig "$HOME/.taskrc"}")
+
+      with subtest("Can create tasks"):
+          task_client1.succeed("task add 'First task -- task_client1'")
+          task_client2.succeed("task add 'First task -- task_client2'")
+
+      # Wait for the server to acquire the acme certificate
+      task_client1.wait_until_succeeds("curl https://taskchampion.server")
+
+      with subtest("Can sync tasks"):
+          for task in [task_client1, task_client2]:
+            task.succeed("task sync")
+          task_client1.succeed("task sync")
+
+
+      with subtest("Have correct tasks"):
+          count1 = task_client1.succeed("task count")
+          count2 = task_client2.succeed("task count")
+
+          assert int(count1) == 2, f"We don't have exactly 2 tasks, but {count1}"
+          assert count1 == count2, f"The clients don't have the same amount of tasks, client1: {count1}, client2: {count2}"
+    '';
+}