diff options
Diffstat (limited to 'tests/by-name')
-rw-r--r-- | tests/by-name/at/atuin-sync/test.nix | 191 | ||||
-rw-r--r-- | tests/by-name/ba/back/test.nix | 147 | ||||
-rw-r--r-- | tests/by-name/dn/dns/test.nix | 129 | ||||
-rw-r--r-- | tests/by-name/em/email-dns/nodes/mail_server.nix | 52 | ||||
-rw-r--r-- | tests/by-name/em/email-dns/nodes/name_server.nix | 292 | ||||
-rw-r--r-- | tests/by-name/em/email-dns/nodes/user.nix | 70 | ||||
-rw-r--r-- | tests/by-name/em/email-dns/test.nix | 167 | ||||
-rw-r--r-- | tests/by-name/em/email-http/nodes/mail_server.nix | 57 | ||||
-rw-r--r-- | tests/by-name/em/email-http/nodes/user.nix | 26 | ||||
-rw-r--r-- | tests/by-name/em/email-http/test.nix | 100 | ||||
-rw-r--r-- | tests/by-name/em/email-ip/test.nix | 174 | ||||
-rw-r--r-- | tests/by-name/ru/rust-motd/test.nix | 62 | ||||
-rw-r--r-- | tests/by-name/sh/sharkey-cpu/test.nix | 81 | ||||
-rw-r--r-- | tests/by-name/sh/sharkey/test.nix | 102 | ||||
-rw-r--r-- | tests/by-name/ta/taskchampion-sync/test.nix | 151 |
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 -- "<No description>" /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}" + ''; +} |