diff options
Diffstat (limited to '')
58 files changed, 1876 insertions, 388 deletions
diff --git a/tests/README.md b/tests/README.md index 7811f32..aaa76b4 100644 --- a/tests/README.md +++ b/tests/README.md @@ -1,6 +1,6 @@ # Tests This directory tree mirrors the modules defined in the -[modules](%60../modules/%60) directory. Each module should have at least +[modules](%60../modules/%60) directory. Each module should have at least one test in the mirrored directory, effectively replacing the module's `module.nix` file. 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..3b8f1de --- /dev/null +++ b/tests/by-name/at/atuin-sync/test.nix @@ -0,0 +1,154 @@ +{ + pkgs, + turtle, + vhack, + ... +}: +vhack.runTest { + name = "atuin-sync"; + serverDomains = [ + { + server = "atuin-sync.server"; + } + ]; + + nodes = let + atuinSession = "01969ec6b8d07e30a9d2df0911fbfe2a"; + atuin = turtle.packages."${pkgs.stdenv.hostPlatform.system}".default; + in { + server = {config, ...}: { + vhack = { + persist.enable = true; + nginx.enable = true; + atuin-sync = { + enable = true; + fqdn = "atuin-sync.server"; + }; + }; + }; + + client1 = {config, ...}: { + environment.sessionVariables.ATUIN_SESSION = atuinSession; + + environment.systemPackages = [ + atuin + pkgs.sqlite-interactive + ]; + }; + client2 = {config, ...}: { + environment.sessionVariables.ATUIN_SESSION = atuinSession; + + environment.systemPackages = [ + atuin + pkgs.sqlite-interactive + ]; + }; + }; + + services = [ + {server = "turtle.service";} + ]; + + testScript = {nodes, ...}: let + mkSyncConfig = pkgs.writeShellScript "register-atuin-sync-account" '' + mkdir --parents ~/.config/turtle/ + + cat << EOF > ~/.config/turtle/config.toml + + [sync] + address = "https://atuin-sync.server" + user_id_path = "${pkgs.writeText "user-id" "019eb88a-6b51-7e52-b12c-7d30bd8e5928"}" + encryption_key_path = "${pkgs.writeText "encryption-key" "3AAgbWsDzL7M00/Mq0LMjsyOCy3MnsypBsyQzKbMywNGzNnMrUBozIINAxdbIiDMhQ=="}" + EOF + ''; + + runCommandAndRecordInTurtle = 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="$(turtle history start -- "$escaped_command")" + "$@" + status=$? + turtle history end --exit $status "$id" + return $status + } + # SPDX-SnippetEnd + + run_and_record_in_atuin "$@" + ''; + in + # Python + '' + 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}") + + with subtest("Start turtle daemon"): + for client in [client1, client2]: + client.succeed("systemd-run turtle-daemon start") + + for client in [client1, client2]: + client.wait_until_succeeds("turtle daemon status") + + with subtest("Can generate shell history"): + client1.succeed("${runCommandAndRecordInTurtle} echo hi - client 1") + client2.succeed("${runCommandAndRecordInTurtle} echo hi - client 2") + + with subtest("Can sync"): + for client in [client1, client2]: + client.succeed("turtle sync perform") + client1.succeed("turtle sync perform") + + with subtest("Have correct tasks"): + hist1 = client1.succeed("turtle history list --cmd-only").strip().split('\n') + hist2 = client2.succeed("turtle history list --cmd-only").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}'" + + with subtest("Can sync again"): + client1.succeed("${runCommandAndRecordInTurtle} echo second try - client 1") + client2.succeed("${runCommandAndRecordInTurtle} echo second try - client 2") + + for client in [client1, client2]: + client.succeed("turtle sync perform") + client1.succeed("turtle sync perform") + + hist1 = client1.succeed("turtle history list --cmd-only").strip().split('\n') + hist2 = client2.succeed("turtle history list --cmd-only").strip().split('\n') + + hist1.sort() + hist2.sort() + + canonicalHistory = [ + # The old history + "echo hi - client 1", + "echo hi - client 2", + + # Our newly added history entries + "echo second try - client 1", + "echo second try - 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, got: '{hist1}', expected: '{canonicalHistory}'" + ''; +} diff --git a/tests/by-name/ba/back/test.nix b/tests/by-name/ba/back/test.nix index 85cb611..758980e 100644 --- a/tests/by-name/ba/back/test.nix +++ b/tests/by-name/ba/back/test.nix @@ -1,11 +1,6 @@ { - nixos-lib, - pkgsUnstable, - nixpkgs-unstable, - vhackPackages, pkgs, - extraModules, - nixLib, + vhack, ... }: let domain = "server"; @@ -22,48 +17,34 @@ 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 - + vhack.runTest { name = "back"; - node = { - specialArgs = {inherit pkgsUnstable vhackPackages nixpkgs-unstable nixLib;}; + ignore = "We are not using back currently."; - # Use the nixpkgs as constructed by the `nixpkgs.*` options - pkgs = null; - }; + serverDomains = [ + {server = "git.${domain}";} + {server = "issues.${domain}";} + ]; nodes = { server = {config, ...}: { environment.systemPackages = [pkgs.git]; - imports = - extraModules - ++ [ - ../../../../modules - ]; - 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; domain = "issues.${domain}"; - - settings = { - scan_path = "${config.services.gitolite.dataDir}/repositories"; - project_list = "${config.services.gitolite.dataDir}/projects.list"; - }; }; }; }; @@ -78,23 +59,17 @@ in PreferredAuthentications publickey ''; users.users.alice = {isNormalUser = true;}; - networking.hosts = { - "${nodes.server.networking.primaryIPAddress}" = [ - "git.${domain}" - "issues.${domain}" - "${domain}" - ]; - }; }; }; + services = [ + {server = "gitolite-init.service";} + {server = "sshd.service";} + ]; + testScript = {nodes, ...}: - /* - python - */ + # Python '' - start_all() - with subtest("can setup ssh keys on client"): client.succeed( "mkdir -p ~root/.ssh", @@ -108,12 +83,15 @@ in ) 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"): + server.succeed("sudo -u git ${pkgs.writeShellScript "delete_main_branch_on_server" '' + set -xe + + cd ~git/repositories/gitolite-admin.git + git branch --move --force main master + ''}") client.succeed("${pkgs.writeShellScript "setup-gitolite-admin.git" '' set -xe @@ -123,12 +101,9 @@ in 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) ''}") with subtest("alice can create a repo"): @@ -152,35 +127,35 @@ in cd alice/repo1 - git bug user create --avatar "" --email "alice@server.org" --name "alice" --non-interactive + git bug user new --avatar "" --email "alice@server.org" --name "alice" --non-interactive - git bug add \ + 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 comment add --message "Some comment message" --non-interactive - git bug comment add --message "Second comment message" --non-interactive + git bug bug comment new --message "Some comment message" --non-interactive + git bug bug comment new --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/* - ssh git@${domain} -- config alice/repo1 --add cgit.owner Alice - ssh git@${domain} -- perms alice/repo1 + READERS @all + 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"): @@ -190,12 +165,12 @@ in client.succeed("${pkgs.writeShellScript "curl-back" '' set -xe - curl --insecure --fail --show-error "https://issues.${domain}/alice/repo1.git/issues/open" --output /root/issues.html + curl --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 + curl --fail --show-error "https://issues.${domain}/" --output /root/repos.html grep -- 'repo' /root/repos.html - grep -- "<No description>" /root/repos.html + grep -- "<No description>" /root/repos.html grep -- '<span class="user-name">Alice</span>' /root/repos.html ''} >&2") diff --git a/tests/by-name/dn/dns/test.nix b/tests/by-name/dn/dns/test.nix index 07a6e8c..01d8833 100644 --- a/tests/by-name/dn/dns/test.nix +++ b/tests/by-name/dn/dns/test.nix @@ -55,7 +55,7 @@ in vhack = { dns = { enable = true; - interfaces = lib.debug.traceValSeqN 2 [ + interfaces = [ (lib.head config.networking.interfaces.eth1.ipv4.addresses).address (lib.head config.networking.interfaces.eth1.ipv6.addresses).address ]; diff --git a/tests/by-name/em/email-dns/secrets/dkim/alice.com/private.age b/tests/by-name/em/email-dns/secrets/dkim/alice.com/private.age deleted file mode 100644 index 5415fdc..0000000 --- a/tests/by-name/em/email-dns/secrets/dkim/alice.com/private.age +++ /dev/null @@ -1,15 +0,0 @@ ------BEGIN AGE ENCRYPTED FILE----- -YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB3dkJqTFNjOUphUWpBTmUv -MldLUUVtUWJWY2p4NGNDVlluWUdJNUVGTDBnCkRMZ3VsVGkvUUJsbUgrUHA5d1pY -d3lvQkdsSnRxYWxSbGlkQUlnMEFiWncKLT4gWDI1NTE5IGljWDF2a3JqZHBzZC9J -ZjlxSVdKQ0ErRGc1VVRTNE91RC8rR2ZhMlN5d28KYVNyMWZWSnZFNS9zWUdLS01T -UkJLaGxqWmwySzdxRk5GVUZ4NFR1aXcxcwotPiBzc2gtZWQyNTUxOSBSc2dXcFEg -alJlckZsT21YUUQrS05Xb25TeFJXUmFFTFl1U3NYOXZoWEZwa0Qrd2dnWQoxZVla -TFV3VURsaUxZdUZVRkZhdXcyV3AzMTJCc1hXUDJlK2FPc0pIU2hvCi0+IFN6Nlc4 -R0dqLWdyZWFzZSBfIQpUdERFc2xwZGkwR2p3QW0vNlIyR0pYTkkyeU02ckw2RnEv -bHE5TFQ4TlBaL3lNd1ZFbDRPZXo1RmpSeStWcjljCmFpV28KLS0tIEE2SURxTHdl -RWt5NXZRT3hFUHpjSm1BdTlwZGM2NmQvNm5Pb0s4dW5pc2sKTKoHvkHEkuPkMTDw -Un+0mSGY82ZxfpNuYH6YEjUYsXwAw4y0HFvsObc0XuYzCmQrBX8vLos0Sg5kzboT -EU81RdjI1AQPwhd+jBIDNFpUzYZ/IaQsz7Yp3G1WDfkeYPk10IuzOJoeRjLC9HPa -wfYCKZDhegV6n//kAxBGM0Q3MzyxO3eYJ+k3TaWnUPLnw+nFMAANgkYqfg== ------END AGE ENCRYPTED FILE----- diff --git a/tests/by-name/em/email-dns/secrets/dkim/alice.com/public b/tests/by-name/em/email-dns/secrets/dkim/alice.com/public deleted file mode 100644 index 0f3c3b2..0000000 --- a/tests/by-name/em/email-dns/secrets/dkim/alice.com/public +++ /dev/null @@ -1 +0,0 @@ -cLWzd3zg51ITME1Fnu16/h07lXIUxfhdLivktUMoVQs=
\ No newline at end of file diff --git a/tests/by-name/em/email-dns/secrets/dkim/bob.com/private.age b/tests/by-name/em/email-dns/secrets/dkim/bob.com/private.age deleted file mode 100644 index c07c997..0000000 --- a/tests/by-name/em/email-dns/secrets/dkim/bob.com/private.age +++ /dev/null @@ -1,14 +0,0 @@ ------BEGIN AGE ENCRYPTED FILE----- -YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBmN0FHQ2ZhVEVpNnB1S2Zu -T3JBS0xLUklKNUxKeENYeis3MUZHdUF1STFFCmdkTFQvcG9qWk55QjlEUDN2NGFa -Ump1TW1xRUZ3K251bFJsaEMrTXd3VzgKLT4gWDI1NTE5IFF6ZGNJbWkrc1Z5M3Rk -blpNUGpuNU0rZi9Ed2dicnM4WXhrbjdQVDFzeGsKd2V6clA0UjZWYmJxZytaa3B3 -T2drNkhiQVBKU2tOL2gxeGtjQnAra2NxawotPiBzc2gtZWQyNTUxOSBSc2dXcFEg -Y2xrRDduWXo1Qk81VFMxMHZ0T3FhaXZXUm1VMDNhVGZscEJ6Q2owWXFCMApsMTQ4 -K2hGNm4ybGNkSTFOeHdwZnI5YjRZQm1uVC9xUEtRSjBxV0JKdERRCi0+IFlzLWdy -ZWFzZSBbZztrCk1sWFhaZXNZOUhMbAotLS0gbkNRK2hkZ0dEZEkxeFVJKysyUUY3 -UWVBRjZoVXJCWk5YTEh4ZktpM1IwNArrxTAoCB+Ubb79nB53ZnbakCaDfrfLx7Oi -Khq4CZh64r2LaQWFP0m5dhHWDF/8Fg+E42zbXN1KQwggz2h59EqI2ouOXmjiZeHN -O50mfruF9xybdAFVIThEsjRTRQDPfO5qq6PI+g+w4s+S8kl6yp61t6z3y+zI3GGw -RxCsY2uEn5naMuBMqkYL5dhA/G5deNMpUQ8rcRZw ------END AGE ENCRYPTED FILE----- diff --git a/tests/by-name/em/email-dns/secrets/dkim/bob.com/public b/tests/by-name/em/email-dns/secrets/dkim/bob.com/public deleted file mode 100644 index ddea670..0000000 --- a/tests/by-name/em/email-dns/secrets/dkim/bob.com/public +++ /dev/null @@ -1 +0,0 @@ -3yrKD52yd5hBA6ue5uQVl7FXGK8UOlUE9Y+yCdBRfVQ=
\ No newline at end of file diff --git a/tests/by-name/em/email-dns/secrets/dkim/mail1.server.com/private.age b/tests/by-name/em/email-dns/secrets/dkim/mail1.server.com/private.age deleted file mode 100644 index 8c5d3c3..0000000 --- a/tests/by-name/em/email-dns/secrets/dkim/mail1.server.com/private.age +++ /dev/null @@ -1,15 +0,0 @@ ------BEGIN AGE ENCRYPTED FILE----- -YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBOYXRiK29XallqZG1vc1Bl -TlpFbk9WZmZIK1FrUHRIbmx6bEI5UkhJZUZ3Ck8zcHVGWHJjdG5wZnh0SDljc2F5 -WU1DMUNxdFdVQmRUMFNpa0VCSUkxZmsKLT4gWDI1NTE5IFhXREJyTVNueGp6YVg1 -b3FUemlodUMraWRMVnEyQTZqRlpKanhxdDBSMlkKYVNxcXFockpleGJBaU1iUUQ4 -bml2azlPSE9Jc0N0dDU2d1hYOXFpcnV2ZwotPiBzc2gtZWQyNTUxOSBSc2dXcFEg -ZUVuOHZyM3pRcTZHKzFsV2VheDkrdml4NUVIWldQY1BqcGNpTG5lU2VrVQoydnVZ -UHdpa1FmT3hNbHA1dll1dVFxS0VFanV3N3Y5Z1E5ejBiMkhRSHhRCi0+ICVuPzx1 -Ky1ncmVhc2UgYXY5PE43Om4KcWplaklGSXlMSk0yNktld21ucUdVS1dFVmt4NFhi -VG5WYUNMUGdxZ0dta1F6RVpRRTdoek1WT0hwaC9CVEcyMworUQotLS0genJEN2Jq -Y1JVbzc3NVFoSXAxSUxEQUZzZkRCcXNmSXBwZDB0eDMybE5NMApv3ghQF9tC00yI -v7Sa9ZKrA8HDb/wpUn0X+D+ShLC95rW8+oPonN6gt+z+PoVUFvXwKsP/1I+D6z+B -PRMurlvpspkIGlgi5S5H6brj9UJ7Pt61+Ld2/gaLevzCPy1QCmlAlqRKvZuZMUUR -hWbmEXi8j56ClcxpVe62p+4GCI41T2cjAogi2C33dtviIcGedq9byD5tbVt4 ------END AGE ENCRYPTED FILE----- diff --git a/tests/by-name/em/email-dns/secrets/dkim/mail1.server.com/public b/tests/by-name/em/email-dns/secrets/dkim/mail1.server.com/public deleted file mode 100644 index 4941b85..0000000 --- a/tests/by-name/em/email-dns/secrets/dkim/mail1.server.com/public +++ /dev/null @@ -1 +0,0 @@ -quDd9+ogqiIUWybfegosFFkG7jAsblij2VrkuUXEzzY=
\ No newline at end of file diff --git a/tests/by-name/em/email-dns/secrets/dkim/mail2.server.com/private.age b/tests/by-name/em/email-dns/secrets/dkim/mail2.server.com/private.age deleted file mode 100644 index d39631a..0000000 --- a/tests/by-name/em/email-dns/secrets/dkim/mail2.server.com/private.age +++ /dev/null @@ -1,15 +0,0 @@ ------BEGIN AGE ENCRYPTED FILE----- -YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBCRkl1SThtVnN3bUg4cmJL -aXBBUU4yRE1DOGdYclg1UXVhS1Y0UTJPVVIwCjlSOXZJdVB0R0VxbGdUWHd5U2RO -RGpFdUw4c3EyUFZNZUx4Mm5yeGNIeVUKLT4gWDI1NTE5IHc5K05EeE1xNnBHd1dS -WDJwSTdzLys3MnRncmN0WWZjTWkvM0tqeWRRU1EKek13RGRsNUhadncxQ3V6ZFcv -Ni9aMTFtRllFK0lvd2tidnRwUjE0V0d2OAotPiBzc2gtZWQyNTUxOSBSc2dXcFEg -Mml5VEt5b3R3d09Gc1hzR2pRZzJFY3VHT0JwUnBaTTNIWG5QaGdsWkh4bwp1Nzdx -dWxpbjdNRGZ6aThxM0d6a2MraklkSW03ek15YXRkSXVyaS96QlFjCi0+IDAkRDEp -bC4tZ3JlYXNlCi9lUnduUFJreTdpMEU5ejZjS1JyWmJzRkZNRVdLbnRRZC81aldN -c2FqTTgrNnFCSEYxQUhSQWxyMW9icCtQYkgKTUEKLS0tIDNmQkwwbjJiSldraDVw -ZUs3SHNpU0pGWXRLSVBtZ0Y1QW5uT0N5bnpjek0KiujqGmiYB3hCso3u4uCtZuO3 -EmPPJkbHKPNyUQy1V/Bv+sjgPDQi/0y7UR759G91iNIYQB0fWg7njWsl8GNdJwXI -YO+2utDKkrm0DowWNKpHX1KE1g93e0H5ZoptygLFtx8TWDP2i8R7JJqJ9QevSz6r -Nwjj+d3HGTxA7WvAZnNdvyfHbRRdvW+6q0uIjS5zHXAs8YmepA== ------END AGE ENCRYPTED FILE----- diff --git a/tests/by-name/em/email-dns/secrets/dkim/mail2.server.com/public b/tests/by-name/em/email-dns/secrets/dkim/mail2.server.com/public deleted file mode 100644 index 5c4406d..0000000 --- a/tests/by-name/em/email-dns/secrets/dkim/mail2.server.com/public +++ /dev/null @@ -1 +0,0 @@ -th9exwaYvoAjxW1tAj3k/VNLl5jKzSC/dxKrxM2mTZE=
\ No newline at end of file diff --git a/tests/by-name/gi/git-server/test.nix b/tests/by-name/gi/git-server/test.nix index 5cd8c33..09e559c 100644 --- a/tests/by-name/gi/git-server/test.nix +++ b/tests/by-name/gi/git-server/test.nix @@ -1,10 +1,6 @@ { - nixos-lib, - pkgsUnstable, - nixpkgs-unstable, pkgs, - extraModules, - nixLib, + vhack, ... }: let sshKeys = @@ -35,39 +31,24 @@ option user-configs = cgit\.owner cgit\.desc cgit\.section cgit\.homepage ''; - expectedHtmlReadme = pkgs.writeText "expectedHtmlReadme" '' - <h1>Alice's Repo</h1> - ''; expectedMdReadme = pkgs.writeText "expectedMdReadme" '' # Alice's Repo ''; in - nixos-lib.runTest { - hostPkgs = pkgs; # the Nixpkgs package set used outside the VMs - + vhack.runTest { name = "git-server"; - node = { - specialArgs = {inherit pkgsUnstable nixpkgs-unstable nixLib;}; - - # Use the nixpkgs as constructed by the `nixpkgs.*` options - pkgs = null; - }; + serverDomains = [ + {server = gitServerDomain;} + ]; nodes = { server = {config, ...}: { - imports = - extraModules - ++ [ - ../../../../modules - ]; - vhack = { persist.enable = true; openssh.enable = true; nginx = { enable = true; - selfsign = true; }; git-server = { enable = true; @@ -77,7 +58,7 @@ in }; }; - client = {...}: { + client = { environment.systemPackages = [pkgs.git]; programs.ssh.extraConfig = '' Host * @@ -91,13 +72,14 @@ in }; }; - testScript = {nodes, ...}: - /* - python - */ - '' - start_all() + services = [ + {server = "gitolite-init.service";} + {server = "sshd.service";} + ]; + testScript = {...}: + # Python + '' with subtest("can setup ssh keys on client"): client.succeed( "mkdir -p ~root/.ssh", @@ -116,12 +98,16 @@ in ) with subtest("gitolite server starts"): - server.wait_for_unit("gitolite-init.service") - server.wait_for_unit("sshd.service") client.succeed("ssh -n git@server info") with subtest("admin can clone and configure gitolite-admin.git"): + server.succeed("sudo -u git ${pkgs.writeShellScript "delete_main_branch_on_server" '' + set -xe + + cd ~git/repositories/gitolite-admin.git + git branch --move --force main master + ''}") client.succeed("${pkgs.writeShellScript "setup-gitolite-admin.git" '' set -xe @@ -132,12 +118,9 @@ in cp ${sshKeys.alice.pub} gitolite-admin/keydir/alice.pub cp ${sshKeys.bob.pub} gitolite-admin/keydir/bob.pub - (cd gitolite-admin && git switch -c master && git branch -D main) - (cd gitolite-admin && git add . && git commit -m 'Add keys for alice, bob' && 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) ''}") server.succeed("${pkgs.writeShellScript "verify gitolite-admin.conf" '' @@ -202,7 +185,7 @@ in cd ~bob # Disable ssl verification, as the certs are self-signed - git -c http.sslVerify=false clone https://server/alice/alice-project.git + git -c http.sslVerify=false clone https://server/alice/alice-project ''}") with subtest("Alice can change settings in her repo"): @@ -221,15 +204,13 @@ in } ''}") + with subtest("Bob can see alice's README"): + client.succeed("sudo -u bob ${pkgs.writeShellScript "bob-alice-readme" '' + set -xe - # He can't see the readme (FIXME: find out why this does not work. <2024-08-13> ) - # with subtest("Bob can see alice's README"): - # client.succeed("sudo -u bob ${pkgs.writeShellScript "bob-alice-readme" '' - # set -xe - # - # curl --insecure --silent --fail --show-error 'https://server/alice/alice-project/about' > readme.html - # cat readme.html - # diff --side-by-side ${expectedHtmlReadme} readme.html - # ''}") + curl --fail --show-error 'https://server/alice/alice-project/about/' > readme.html + grep 'alice-project - My nice project.' ./readme.html + grep 'My nice project.' ./readme.html + ''}") ''; } diff --git a/tests/by-name/mo/monitoring-basic/test.nix b/tests/by-name/mo/monitoring-basic/test.nix new file mode 100644 index 0000000..2983085 --- /dev/null +++ b/tests/by-name/mo/monitoring-basic/test.nix @@ -0,0 +1,57 @@ +{vhack, ...}: let + grafanaDomain = "grafana.server.org"; +in + vhack.runTest { + name = "monitoring"; + + serverDomains = [ + {server = grafanaDomain;} + ]; + + nodes = { + client = {}; + + server = {config, ...}: { + # there are no SMART available disk in the VM, so this service always fails. + systemd.services."smartd".enable = false; + + age.identityPaths = ["${../../../common/email/hostKey}"]; + + vhack = { + monitoring = { + grafana = { + enable = true; + contactPoints = ["me@example.com"]; + fqdn = grafanaDomain; + + adminPassword = ../../../common/email/dkim/alice.com/private.age; + secretKey = ../../../common/email/dkim/bob.com/private.age; + + # TODO: Add a test for that <2026-07-19> + smtp = null; + }; + + loki = { + enable = true; + }; + + prometheus = { + enable = true; + }; + }; + }; + }; + }; + + services = [ + {server = "grafana.service";} + {server = "fluent-bit.service";} + {server = "loki.service";} + ]; + + testScript = {...}: + # Python + '' + # TODO: We should probably query some of the prometheus metrics here? <2026-07-19> + ''; + } diff --git a/tests/by-name/mo/monitoring-federation/test.nix b/tests/by-name/mo/monitoring-federation/test.nix new file mode 100644 index 0000000..278626f --- /dev/null +++ b/tests/by-name/mo/monitoring-federation/test.nix @@ -0,0 +1,90 @@ +{ + pkgs, + vhack, + ... +}: let + prometheusMain = "prometheus.server2.server.org"; + remoteWriteTo = { + url = "https://${prometheusMain}"; + }; + promPort = 3001; +in + vhack.runTest { + name = "monitoring"; + + serverDomains = [ + {server2 = prometheusMain;} + ]; + + nodes = { + server2 = {config, ...}: { + environment.systemPackages = [ + pkgs.curl + ]; + + vhack = { + monitoring = { + prometheus = { + enable = true; + port = promPort; + remoteWriteReceiver = prometheusMain; + }; + }; + }; + }; + server3 = {config, ...}: { + vhack = { + monitoring = { + prometheus = { + enable = true; + port = promPort; + inherit remoteWriteTo; + }; + }; + }; + }; + server4 = {config, ...}: { + vhack = { + monitoring = { + prometheus = { + enable = true; + port = promPort; + inherit remoteWriteTo; + }; + }; + }; + }; + }; + + services = [ + {server2 = "prometheus.service";} + {server3 = "prometheus.service";} + {server4 = "prometheus.service";} + ]; + + testScript = {...}: + # Python + '' + import json, time + + # Give the Prometheus servers some time to generate metrics and sync with the main + # one. + time.sleep(15) + + metrics = server2.succeed("${pkgs.writeShellScript "query-metrics" '' + curl --silent \ + http://127.0.0.1:${toString promPort}/api/v1/query?query="go_gc_cleanups_queued_cleanups_total" + ''}") + + metrics = json.loads(metrics) + + hosts = [] + for result in metrics["data"]["result"]: + server2.log(json.dumps(result,indent=4)) + hosts.append(result["metric"]["hostname"]) + + hosts.sort() + + assert hosts == ["server2", "server3", "server4"], f"Not all hosts used the remote write (got {hosts})" + ''; + } diff --git a/tests/by-name/ro/rocie/secrets/login.age b/tests/by-name/ro/rocie/secrets/login.age new file mode 100644 index 0000000..33d63be --- /dev/null +++ b/tests/by-name/ro/rocie/secrets/login.age @@ -0,0 +1,16 @@ +-----BEGIN AGE ENCRYPTED FILE----- +YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAzMWE5dTBiU0hDUC9jUi93 +Y1phYllHRk9YSHBzUGQ2YmF5ZC9ydXNGV0JrClRpTjZZUHZ5MEFFa0VrYVVhTkE2 +eCtSaEU1YVlhNjFNYlRRYzNCdjhYRWMKLT4gWDI1NTE5IDUrZWdDUmpQcFBOcE0x +UE5QRDR5NXVXUHdQOVk3UGV1S3lCc0pUQmZIZ00KUldVSVF3TzB0cHFVaDZuNlZR +b2FoT0lVSTZydHFTNHhnQ3U0NGdSR1k1MAotPiBzc2gtZWQyNTUxOSBSc2dXcFEg +dldGZU15UXgrRTMxRkp2MEVKUllWQ3VFdnJDMnM4OS8wc202WW9lNW5BYwpPWjV0 +cmNuaDlPZndtUVVScm5TaGlvVUhHa0JiN1MvbDhCTTUxYzNhM3RRCi0+IDxXeEhv +cC1ncmVhc2UKM3N5OHRLNTJEY1NIeGlWYm9yR096Y1NpSlVOM1lYQk9jOHkxU3N2 +K2c3QitDYnR6QTJOOWczV0xBa2dEUE1PTQpYU2Z1elZwRzU0Tm1RVDE2VWVqekUw +bFROLzU0c2NNTXYwL2N5QkxTaGtXUWxKVVF6SE0KLS0tIGlYMHIvUkJpZUR0SHo4 +cldLSTdnbU90SGJTcGZGaHkyOTZON0hka3BLdlEKeP4nHmKWvJfqgEXuiLBMzldi +n1qIsnlF3IU1EA0abJg/RK1BFwWlx4wBlLmViw6UTL+VEw8lv23PuZl2t7UtXVzQ +smXDapW8nInNmTaElBPdwJ072/dD0Ly+KF95Qr0FDDv+jlKG/D/Mw+xD4jvuJHSo +2HQnPF6MLTjCxpyPPggleWgKrBQggHBjm/pHtOKmPC5qfp+LAjmQoJXny/0X6cA= +-----END AGE ENCRYPTED FILE----- diff --git a/tests/by-name/ro/rocie/test.nix b/tests/by-name/ro/rocie/test.nix new file mode 100644 index 0000000..5882cd4 --- /dev/null +++ b/tests/by-name/ro/rocie/test.nix @@ -0,0 +1,42 @@ +{vhack, ...}: +vhack.runTest { + name = "rocie"; + + serverDomains = [ + {server = "rocie.server";} + ]; + + nodes = { + server = {config, ...}: { + age.identityPaths = ["${../../../common/email/hostKey}"]; + + vhack = { + persist.enable = true; + nginx.enable = true; + rocie = { + enable = true; + domain = "rocie.server"; + loginSecret = ./secrets/login.age; + }; + }; + }; + + client = {_, ...}: {}; + }; + + services = [ + {server = "rocie.service";} + ]; + + ports = [ + {server = 80;} + {server = 443;} + ]; + + testScript = {...}: + # Python + '' + client.wait_until_succeeds("curl --verbose https://rocie.server/api/can-be-provisioned > out.file") + client.copy_from_machine("out.file") + ''; +} 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..b50abf5 --- /dev/null +++ b/tests/by-name/ru/rust-motd/test.nix @@ -0,0 +1,30 @@ +{vhack, ...}: +vhack.runTest { + name = "rust-motd"; + + nodes = { + server = {config, ...}: { + vhack = { + rust-motd.enable = true; + }; + }; + }; + + services = [ + { + server = { + name = "rust-motd.service"; + should_be_started = true; + }; + } + ]; + + testScript = {nodes, ...}: + # Python + '' + with subtest("Motd generated"): + server.succeed("cat /var/lib/rust-motd/motd | tee /dev/stderr | grep --invert-match --ignore-case Error") + + server.copy_from_machine("/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..f38fd46 --- /dev/null +++ b/tests/by-name/sh/sharkey-cpu/test.nix @@ -0,0 +1,64 @@ +{ + pkgs, + vhack, + ... +}: +vhack.runTest { + name = "sharkey-cpu"; + ignore = true; + + nodes = { + server = {config, ...}: { + vhack = { + persist.enable = true; + nginx.enable = true; + sharkey = { + enable = true; + fqdn = "sharkey.server"; + }; + }; + systemd.services = { + # Avoid an error from this service. + "acme-sharkey.server".enable = false; + + # Test that sharkey's hardening still allows access to the CPUs. + sharkey.serviceConfig.ExecStart = let + nodejs = pkgs.lib.getExe pkgs.nodejs; + script = pkgs.writeTextFile { + name = "script.js"; + text = + # js + '' + import * as os from 'node:os'; + + var cpus = os.cpus() + + if (cpus.length != 0) { + console.log(cpus[0].model) + } else { + // Fail? + } + + while (true) { + } + ''; + }; + in + pkgs.lib.mkForce "${nodejs} ${script}"; + }; + }; + }; + + services = [ + {server = "sharkey.service";} + ]; + + testScript = {...}: + # Python + '' + from time import sleep + + # Give the service time to run. + sleep(3) + ''; +} diff --git a/tests/by-name/sh/sharkey/test.nix b/tests/by-name/sh/sharkey/test.nix new file mode 100644 index 0000000..03a8281 --- /dev/null +++ b/tests/by-name/sh/sharkey/test.nix @@ -0,0 +1,34 @@ +{vhack, ...}: +vhack.runTest { + name = "sharkey"; + ignore = true; + + serverDomains = [ + {server = "sharkey.server";} + ]; + + nodes = { + server = {config, ...}: { + vhack = { + persist.enable = true; + nginx.enable = true; + sharkey = { + enable = true; + fqdn = "sharkey.server"; + }; + }; + }; + + client = {...}: {}; + }; + + services = [ + {server = "sharkey.service";} + ]; + + testScript = {...}: + # Python + '' + client.wait_until_succeeds("curl --verbose https://sharkey.server | grep 'Thank you for using Sharkey!'") + ''; +} diff --git a/tests/by-name/sn/snm-email-dual-server/test.nix b/tests/by-name/sn/snm-email-dual-server/test.nix new file mode 100644 index 0000000..6f7bebb --- /dev/null +++ b/tests/by-name/sn/snm-email-dual-server/test.nix @@ -0,0 +1,134 @@ +{ + lib, + vhack, + pkgs, + vhackPackages, + ... +}: let + server1Domain = "mail.server1"; + server2Domain = "mail.server2"; + + common = import ../../../common/email/functions.nix {inherit pkgs vhackPackages;}; + + # That's a horrible hack. We should instead wait, until the delivery of the email is + # confirmed. + sleepTime = "40"; +in + vhack.runTest { + name = "email"; + + serverDomains = [ + {server1 = server1Domain;} + {server2 = server2Domain;} + ]; + + nodes = { + server1 = common.mkMailServer server1Domain "alice"; + server2 = common.mkMailServer server2Domain "bob"; + + alice = common.mkUser "alice" server1Domain; + bob = common.mkUser "bob" server2Domain; + }; + + dns = { + server1 = common.mkDnsZone server1Domain; + server2 = common.mkDnsZone server2Domain; + }; + + services = [ + {server1 = "postfix.service";} + {server1 = "dovecot.service";} + {server2 = "postfix.service";} + {server2 = "dovecot.service";} + + {alice = "multi-user.target";} + {bob = "multi-user.target";} + ]; + + ports = [ + {server1 = 993;} # imap + {server1 = 465;} # smtp + + {server2 = 993;} # imap + {server2 = 465;} # smtp + ]; + + testScript = {...}: + # Python + '' + from time import sleep + + with subtest("Dkim keys work"): + server1.succeed("sudo -u rspamd ${pkgs.writeShellScript "dkim-keys-check" '' + set -xe + + stat /run/agenix/mailDkim + + ${lib.getExe pkgs.openssl} pkey -text -noout -in /run/agenix/mailDkim | grep Private-Key + ${lib.getExe pkgs.openssl} pkey -text -noout -in /run/agenix/mailDkim + ''}") + + with subtest("Both start without mail"): + alice.succeed("sudo -u alice ${common.scripts.expectNoNewMail}") + bob.succeed("sudo -u bob ${common.scripts.expectNoNewMail}") + + + 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@${server2Domain} >&2 + ''}") + + # Give the mail server some time to send the email. + sleep(${sleepTime}) + + bob.succeed("sudo -u bob ${common.scripts.expectNewSpamMail}") + + 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@${server2Domain} >&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 the mail server some time to send the email. + sleep(${sleepTime}) + + bob.succeed("sudo -u bob ${common.scripts.expectNewSpamMail}") + + with subtest("Bob can send an email to alice"): + bob.succeed("sudo -u bob ${pkgs.writeShellScript "bob-send" '' + set -xe + + cat << EOF | msmtp --debug --account bob alice@${server1Domain} >&2 + Hi Alice! + + Thank you for your emails! (Although they seemed kinda weird tbh :?) + + XXX + Bob Älšter + + . + EOF + ''}") + + # Give the mail server some time to send the email. + sleep(${sleepTime}) + + alice.succeed("sudo -u alice ${common.scripts.expectNewSpamMail}") + ''; + } diff --git a/tests/by-name/sn/snm-email-single-server/test.nix b/tests/by-name/sn/snm-email-single-server/test.nix new file mode 100644 index 0000000..7b5ec03 --- /dev/null +++ b/tests/by-name/sn/snm-email-single-server/test.nix @@ -0,0 +1,86 @@ +{ + vhack, + pkgs, + vhackPackages, + ... +}: let + domain = "mail.server.test"; + + common = import ../../../common/email/functions.nix {inherit pkgs vhackPackages;}; +in + vhack.runTest { + name = "email"; + + serverDomains = [ + {server = domain;} + ]; + + nodes = { + server = {config, ...}: { + systemd.tmpfiles.rules = [ + #- path mode user group - content + "f /passwords/alice 777 root root - alice-password" + "f /passwords/bob 777 root root - bob-password" + ]; + + # This DNS resolver is only there for rspamd (and apparently not even needed). But it + # also removes our own name server changes, so the test node could no longer find + # e.g. `acme.test` and so on. + # See: https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/work_items/100 + mailserver.localDnsResolver = false; + + vhack = { + mail = { + enable = true; + fqdn = domain; + domains = [ + domain + ]; + accounts = { + "alice@${domain}" = { + passwordFile = "/passwords/alice"; + }; + "bob@${domain}" = { + passwordFile = "/passwords/bob"; + }; + }; + }; + }; + }; + + alice = common.mkUser "alice" domain; + bob = common.mkUser "bob" domain; + }; + + services = [ + {server = "postfix.service";} + ]; + + ports = [ + {server = 993;} # imap + {server = 465;} # smtp + ]; + + testScript = {...}: + # Python + '' + with subtest("Both start without mail"): + alice.succeed("sudo -u alice ${common.scripts.expectNoNewMail}") + bob.succeed("sudo -u bob ${common.scripts.expectNoNewMail}") + + 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 bob@${domain} >&2 + Hi Bob! + + This is an email. + It contains a subject and a body. + + ALICE + EOF + ''}") + bob.succeed("sudo -u bob ${common.scripts.expectNewMail}") + ''; + } diff --git a/tests/by-name/em/email-dns/nodes/mail_server.nix b/tests/by-name/st/stalwart-email-dns/nodes/mail_server.nix index a8c528a..279d289 100644 --- a/tests/by-name/em/email-dns/nodes/mail_server.nix +++ b/tests/by-name/st/stalwart-email-dns/nodes/mail_server.nix @@ -13,7 +13,8 @@ extraModules ++ [ ../../../../../modules - ./acme/client.nix + ../../../../common/acme/client.nix + ../../../../common/dns/client.nix ]; environment.systemPackages = [ @@ -21,12 +22,7 @@ pkgs.openssl ]; - networking.nameservers = lib.mkForce [ - nodes.name_server.networking.primaryIPAddress - nodes.name_server.networking.primaryIPv6Address - ]; - - age.identityPaths = ["${../secrets/hostKey}"]; + age.identityPaths = ["${../../../../common/email/hostKey}"]; vhack = { stalwart-mail = { @@ -36,8 +32,8 @@ security = { dkimKeys = let loadKey = name: { - dkimPublicKey = builtins.readFile (../secrets/dkim + "/${name}/public"); - dkimPrivateKeyPath = ../secrets/dkim + "/${name}/private.age"; + dkimPublicKey = builtins.readFile (../../../../common/email/dkim + "/${name}/public"); + dkimPrivateKeyPath = ../../../../common/email/dkim + "/${name}/private.age"; keyAlgorithm = "ed25519-sha256"; }; in { diff --git a/tests/by-name/em/email-dns/nodes/name_server.nix b/tests/by-name/st/stalwart-email-dns/nodes/name_server.nix index ef657f4..bde1a16 100644 --- a/tests/by-name/em/email-dns/nodes/name_server.nix +++ b/tests/by-name/st/stalwart-email-dns/nodes/name_server.nix @@ -63,7 +63,7 @@ adkim = "strict"; aspf = "strict"; fo = ["0" "1" "d" "s"]; - p = "quarantine"; + p = "reject"; rua = cfg.admin; ruf = [cfg.admin]; } @@ -139,14 +139,10 @@ in { extraModules ++ [ ../../../../../modules - ./acme/client.nix + ../../../../common/acme/client.nix + ../../../../common/dns/server.nix ]; - networking.nameservers = lib.mkForce [ - nodes.name_server.networking.primaryIPAddress - nodes.name_server.networking.primaryIPv6Address - ]; - services.nginx = { logError = "stderr debug"; virtualHosts = let @@ -175,145 +171,121 @@ in { nginx = { enable = true; }; - dns = { - enable = true; - openFirewall = true; - interfaces = [ - nodes.name_server.networking.primaryIPAddress - nodes.name_server.networking.primaryIPv6Address - ]; - - zones = let - stsZone = { - SOA = { - nameServer = "ns"; - adminEmail = "admin@server.com"; - serial = 2025012301; - }; + dns.zones = let + stsZone = { + SOA = { + nameServer = "ns"; + adminEmail = "admin@server.com"; + serial = 2025012301; + }; - useOrigin = false; + useOrigin = false; - A = [ - nodes.name_server.networking.primaryIPAddress - ]; - AAAA = [ - nodes.name_server.networking.primaryIPv6Address - ]; + A = [ + nodes.name_server.networking.primaryIPAddress + ]; + AAAA = [ + nodes.name_server.networking.primaryIPv6Address + ]; + }; + in { + "arpa" = { + SOA = { + nameServer = "ns"; + adminEmail = "admin@server.com"; + serial = 2025012301; }; - 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; - } + useOrigin = false; - { - name = "alice.com"; - ip.v4 = nodes.alice.networking.primaryIPAddress; - } - { - name = "alice.com"; - ip.v6 = nodes.alice.networking.primaryIPv6Address; - } + PTR = [ + { + name = "acme.test"; + ip.v4 = nodes.acme.networking.primaryIPAddress; + } + { + name = "acme.test"; + ip.v6 = nodes.acme.networking.primaryIPv6Address; + } - { - name = "bob"; - ip.v4 = nodes.bob.networking.primaryIPAddress; - } - { - name = "bob"; - ip.v6 = nodes.bob.networking.primaryIPv6Address; - } + { + name = "alice.com"; + ip.v4 = nodes.alice.networking.primaryIPAddress; + } + { + name = "alice.com"; + ip.v6 = nodes.alice.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 = "bob"; + ip.v4 = nodes.bob.networking.primaryIPAddress; + } + { + name = "bob"; + ip.v6 = nodes.bob.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 = "mail1.server.com"; + ip.v4 = nodes.mail1_server.networking.primaryIPAddress; + } + { + name = "mail1.server.com"; + ip.v6 = nodes.mail1_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; - } - ]; - }; + { + name = "mail2.server.com"; + ip.v4 = nodes.mail2_server.networking.primaryIPAddress; + } + { + name = "mail2.server.com"; + ip.v6 = nodes.mail2_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; + { + name = "ns.server.com"; + ip.v4 = nodes.name_server.networking.primaryIPAddress; + } + { + name = "ns.server.com"; + ip.v6 = nodes.name_server.networking.primaryIPv6Address; + } + ]; + }; - A = [ - nodes.name_server.networking.primaryIPAddress - ]; - AAAA = [ - 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; }; - "acme.test" = { - SOA = { - nameServer = "ns"; - adminEmail = "admin@server.com"; - serial = 2025012301; - }; - useOrigin = false; + useOrigin = false; - A = [ - nodes.acme.networking.primaryIPAddress - ]; - AAAA = [ - nodes.acme.networking.primaryIPv6Address - ]; + A = [ + nodes.name_server.networking.primaryIPAddress + ]; + AAAA = [ + nodes.name_server.networking.primaryIPv6Address + ]; + }; + "server.com" = { + SOA = { + nameServer = "ns"; + adminEmail = "admin@server.com"; + serial = 2025012301; }; - "server.com" = { - SOA = { - nameServer = "ns"; - adminEmail = "admin@server.com"; - serial = 2025012301; - }; - useOrigin = false; - NS = [ - "ns.server.com." - ]; - }; + useOrigin = false; + NS = [ + "ns.server.com." + ]; }; }; }; diff --git a/tests/by-name/em/email-dns/nodes/user.nix b/tests/by-name/st/stalwart-email-dns/nodes/user.nix index e4db347..fba02ce 100644 --- a/tests/by-name/em/email-dns/nodes/user.nix +++ b/tests/by-name/st/stalwart-email-dns/nodes/user.nix @@ -8,7 +8,8 @@ ... }: { imports = [ - ./acme/client.nix + ../../../../common/acme/client.nix + ../../../../common/dns/client.nix ]; environment.systemPackages = [ @@ -20,11 +21,6 @@ pkgs.openssl ]; - networking.nameservers = lib.mkForce [ - nodes.name_server.networking.primaryIPAddress - nodes.name_server.networking.primaryIPv6Address - ]; - users.users."${user}" = {isNormalUser = true;}; systemd.tmpfiles.rules = [ diff --git a/tests/by-name/em/email-dns/test.nix b/tests/by-name/st/stalwart-email-dns/test.nix index 32447ae..33955d8 100644 --- a/tests/by-name/em/email-dns/test.nix +++ b/tests/by-name/st/stalwart-email-dns/test.nix @@ -14,6 +14,9 @@ inherit (user) mkUser; in nixos-lib.runTest { + # Wait until we actually use stalwart again + meta.broken = true; + hostPkgs = pkgs; # the Nixpkgs package set used outside the VMs name = "email-dns"; @@ -31,9 +34,9 @@ in lib, ... }: { - imports = [./nodes/acme]; - networking.nameservers = lib.mkForce [ - nodes.name_server.networking.primaryIPAddress + imports = [ + ../../../common/acme/server.nix + ../../../common/dns/client.nix ]; }; @@ -44,7 +47,7 @@ in { class = "individual"; name = "bob"; - secret = "bob-password"; + secret = "{PLAIN}bob-password"; email = ["bob@bob.com"]; }; @@ -53,7 +56,7 @@ in { class = "individual"; name = "alice"; - secret = "alice-password"; + secret = "{PLAIN}alice-password"; email = ["alice@alice.com"]; }; @@ -89,23 +92,14 @@ in exit 1 } ''; - inherit (pkgs) lib; + + acme = import ../../../common/acme {inherit pkgs;}; in - /* - python - */ + acme.prepare ["mail1_server" "mail2_server" "alice" "bob"] + # Python '' from time import sleep - # Start dependencies for the other services - acme.start() - acme.wait_for_unit("pebble.service") - name_server.start() - name_server.wait_for_unit("nsd.service") - - # Start the actual testing machines - start_all() - mail1_server.wait_for_unit("stalwart-mail.service") mail1_server.wait_for_open_port(993) # imap mail1_server.wait_for_open_port(465) # smtp @@ -119,33 +113,6 @@ in 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("Add pebble ca key to all services"): - for node in [name_server, mail1_server, mail2_server, alice, bob]: - node.succeed("${pkgs.writeShellScript "fetch-and-set-ca" '' - set -xe - - # Fetch the randomly generated ca certificate - curl https://acme.test:15000/roots/0 > /tmp/ca.crt - curl https://acme.test:15000/intermediates/0 >> /tmp/ca.crt - - # Append it to the various system stores - # The file paths are from <nixpgks>/modules/security/ca.nix - for cert_path in "ssl/certs/ca-certificates.crt" "ssl/certs/ca-bundle.crt" "pki/tls/certs/ca-bundle.crt"; do - cert_path="/etc/$cert_path" - - mv "$cert_path" "$cert_path.old" - cat "$cert_path.old" > "$cert_path" - cat /tmp/ca.crt >> "$cert_path" - done - - export NIX_SSL_CERT_FILE=/tmp/ca.crt - export SSL_CERT_FILE=/tmp/ca.crt - - # TODO - # # P11-Kit trust source. - # environment.etc."ssl/trust-source".source = "$${cacertPackage.p11kit}/etc/ssl/trust-source"; - ''}") - with subtest("Both mailserver successfully started all services"): import json def all_services_running(host): diff --git a/tests/by-name/st/stalwart-email-http/nodes/mail_server.nix b/tests/by-name/st/stalwart-email-http/nodes/mail_server.nix new file mode 100644 index 0000000..e94c4e9 --- /dev/null +++ b/tests/by-name/st/stalwart-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/st/stalwart-email-http/nodes/user.nix b/tests/by-name/st/stalwart-email-http/nodes/user.nix new file mode 100644 index 0000000..73b9ff7 --- /dev/null +++ b/tests/by-name/st/stalwart-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/st/stalwart-email-http/test.nix b/tests/by-name/st/stalwart-email-http/test.nix new file mode 100644 index 0000000..42fd22b --- /dev/null +++ b/tests/by-name/st/stalwart-email-http/test.nix @@ -0,0 +1,103 @@ +{ + 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 { + # Wait until we actually use stalwart again + meta.broken = true; + + 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/st/stalwart-email-ip/test.nix index 688cd8f..c0b2d7e 100644 --- a/tests/by-name/em/email-ip/test.nix +++ b/tests/by-name/st/stalwart-email-ip/test.nix @@ -78,6 +78,9 @@ }; in nixos-lib.runTest { + # Wait until we actually use stalwart again + meta.broken = true; + hostPkgs = pkgs; # the Nixpkgs package set used outside the VMs name = "email"; @@ -113,13 +116,13 @@ in { class = "individual"; name = "alice"; - secret = "alice-password"; + secret = "{PLAIN}alice-password"; email = ["alice@${domain}"]; } { class = "individual"; name = "bob"; - secret = "bob-password"; + secret = "{PLAIN}bob-password"; email = ["bob@${domain}"]; } ]; 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..54eef45 --- /dev/null +++ b/tests/by-name/ta/taskchampion-sync/test.nix @@ -0,0 +1,92 @@ +{ + pkgs, + vhack, + ... +}: +vhack.runTest { + name = "taskchampion-sync"; + + serverDomains = [ + {server = "taskchampion.server";} + ]; + + nodes = let + taskwarriorPackage = pkgs.taskwarrior3.overrideAttrs (final: prev: { + cmakeFlags = (prev.cmakeFlags or []) ++ ["-DENABLE_TLS_NATIVE_ROOTS=true"]; + }); + in { + server = {config, ...}: { + vhack = { + persist.enable = true; + nginx.enable = true; + taskchampion-sync = { + enable = true; + fqdn = "taskchampion.server"; + }; + }; + }; + + task_client1 = {config, ...}: { + environment.systemPackages = [ + taskwarriorPackage + ]; + }; + task_client2 = {config, ...}: { + environment.systemPackages = [ + taskwarriorPackage + ]; + }; + }; + + services = [ + {server = "taskchampion-sync-server.service";} + ]; + + ports = [ + {server = 443;} + ]; + + 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}" + ''; + in + # Python + '' + 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}" + ''; +} diff --git a/tests/by-name/em/email-dns/nodes/acme/certs/generate b/tests/common/acme/certs/generate index 0d6258e..0d6258e 100755 --- a/tests/by-name/em/email-dns/nodes/acme/certs/generate +++ b/tests/common/acme/certs/generate diff --git a/tests/by-name/em/email-dns/nodes/acme/certs/generate.ca b/tests/common/acme/certs/generate.ca index 92832c5..92832c5 100755 --- a/tests/by-name/em/email-dns/nodes/acme/certs/generate.ca +++ b/tests/common/acme/certs/generate.ca diff --git a/tests/by-name/em/email-dns/nodes/acme/certs/generate.client b/tests/common/acme/certs/generate.client index 5930298..5930298 100755 --- a/tests/by-name/em/email-dns/nodes/acme/certs/generate.client +++ b/tests/common/acme/certs/generate.client diff --git a/tests/by-name/em/email-dns/nodes/acme/certs/output/acme.test.cert.pem b/tests/common/acme/certs/output/acme.test.cert.pem index 687101d..687101d 100644 --- a/tests/by-name/em/email-dns/nodes/acme/certs/output/acme.test.cert.pem +++ b/tests/common/acme/certs/output/acme.test.cert.pem diff --git a/tests/by-name/em/email-dns/nodes/acme/certs/output/acme.test.key.pem b/tests/common/acme/certs/output/acme.test.key.pem index 06195b8..06195b8 100644 --- a/tests/by-name/em/email-dns/nodes/acme/certs/output/acme.test.key.pem +++ b/tests/common/acme/certs/output/acme.test.key.pem diff --git a/tests/by-name/em/email-dns/nodes/acme/certs/output/acme.test.template b/tests/common/acme/certs/output/acme.test.template index 320a170..320a170 100644 --- a/tests/by-name/em/email-dns/nodes/acme/certs/output/acme.test.template +++ b/tests/common/acme/certs/output/acme.test.template diff --git a/tests/by-name/em/email-dns/nodes/acme/certs/output/ca.cert.pem b/tests/common/acme/certs/output/ca.cert.pem index 0fa9d14..0fa9d14 100644 --- a/tests/by-name/em/email-dns/nodes/acme/certs/output/ca.cert.pem +++ b/tests/common/acme/certs/output/ca.cert.pem diff --git a/tests/by-name/em/email-dns/nodes/acme/certs/output/ca.key.pem b/tests/common/acme/certs/output/ca.key.pem index 64263bc..64263bc 100644 --- a/tests/by-name/em/email-dns/nodes/acme/certs/output/ca.key.pem +++ b/tests/common/acme/certs/output/ca.key.pem diff --git a/tests/by-name/em/email-dns/nodes/acme/certs/output/ca.template b/tests/common/acme/certs/output/ca.template index a2295d8..a2295d8 100644 --- a/tests/by-name/em/email-dns/nodes/acme/certs/output/ca.template +++ b/tests/common/acme/certs/output/ca.template diff --git a/tests/by-name/em/email-dns/nodes/acme/certs/snakeoil-certs.nix b/tests/common/acme/certs/snakeoil-certs.nix index aeb6dfc..aeb6dfc 100644 --- a/tests/by-name/em/email-dns/nodes/acme/certs/snakeoil-certs.nix +++ b/tests/common/acme/certs/snakeoil-certs.nix diff --git a/tests/by-name/em/email-dns/nodes/acme/client.nix b/tests/common/acme/client.nix index 2b870e8..2b870e8 100644 --- a/tests/by-name/em/email-dns/nodes/acme/client.nix +++ b/tests/common/acme/client.nix diff --git a/tests/common/acme/default.nix b/tests/common/acme/default.nix new file mode 100644 index 0000000..e7869c2 --- /dev/null +++ b/tests/common/acme/default.nix @@ -0,0 +1,44 @@ +{pkgs}: let + add_pebble_ca_certs = pkgs.writeShellScript "fetch-and-set-ca" '' + set -xe + + # Fetch the randomly generated ca certificate + curl https://acme.test:15000/roots/0 > /tmp/ca.crt + curl https://acme.test:15000/intermediates/0 >> /tmp/ca.crt + + # Append it to the various system stores + # The file paths are from <nixpgks>/modules/security/ca.nix + for cert_path in "ssl/certs/ca-certificates.crt" "ssl/certs/ca-bundle.crt" "pki/tls/certs/ca-bundle.crt"; do + cert_path="/etc/$cert_path" + + mv "$cert_path" "$cert_path.old" + cat "$cert_path.old" > "$cert_path" + cat /tmp/ca.crt >> "$cert_path" + done + + export NIX_SSL_CERT_FILE=/tmp/ca.crt + export SSL_CERT_FILE=/tmp/ca.crt + + # TODO + # # P11-Kit trust source. + # environment.etc."ssl/trust-source".source = "$${cacertPackage.p11kit}/etc/ssl/trust-source"; + ''; +in { + prepare = clients: extra: + # The parens are needed for the syntax highlighting to work. + ( # python + '' + # Start dependencies for the other services + start_all() + + acme.wait_for_unit("pebble.service") + name_server.wait_for_unit("nsd.service") + + with subtest("Add pebble ca key to all services"): + for node in [name_server, ${builtins.concatStringsSep "," clients}]: + node.wait_until_succeeds("curl https://acme.test:15000/roots/0") + node.succeed("${add_pebble_ca_certs}") + '' + ) + + extra; +} diff --git a/tests/by-name/em/email-dns/nodes/acme/default.nix b/tests/common/acme/server.nix index 236ba6a..997c944 100644 --- a/tests/by-name/em/email-dns/nodes/acme/default.nix +++ b/tests/common/acme/server.nix @@ -1,28 +1,5 @@ -# The certificate for the ACME service is exported as: -# -# config.test-support.acme.caCert -# -# This value can be used inside the configuration of other test nodes to inject -# the test certificate into security.pki.certificateFiles or into package -# overlays. -# -# { -# acme = { nodes, lib, ... }: { -# imports = [ ./common/acme/server ]; -# networking.nameservers = lib.mkForce [ -# nodes.mydnsresolver.networking.primaryIPAddress -# ]; -# }; -# -# dnsmyresolver = ...; -# } -# -# Keep in mind, that currently only _one_ resolver is supported, if you have -# more than one resolver in networking.nameservers only the first one will be -# used. -# -# Also make sure that whenever you use a resolver from a different test node -# that it has to be started _before_ the ACME service. +# Add this node as acme server. +# This also needs a DNS server. { config, pkgs, diff --git a/tests/common/dns/client.nix b/tests/common/dns/client.nix new file mode 100644 index 0000000..52f3267 --- /dev/null +++ b/tests/common/dns/client.nix @@ -0,0 +1,10 @@ +{ + lib, + nodes, + ... +}: { + networking.nameservers = lib.mkForce [ + nodes.name_server.networking.primaryIPAddress + nodes.name_server.networking.primaryIPv6Address + ]; +} diff --git a/tests/common/dns/server.nix b/tests/common/dns/server.nix new file mode 100644 index 0000000..0c8d72c --- /dev/null +++ b/tests/common/dns/server.nix @@ -0,0 +1,43 @@ +{ + lib, + nodes, + ... +}: { + imports = [ + ../../../modules + ]; + + networking.nameservers = lib.mkForce [ + nodes.name_server.networking.primaryIPAddress + nodes.name_server.networking.primaryIPv6Address + ]; + + vhack = { + dns = { + enable = true; + openFirewall = true; + interfaces = [ + nodes.name_server.networking.primaryIPAddress + nodes.name_server.networking.primaryIPv6Address + ]; + + zones = { + "acme.test" = { + SOA = { + nameServer = "ns"; + adminEmail = "admin@server.com"; + serial = 2025012301; + }; + useOrigin = false; + + A = [ + nodes.acme.networking.primaryIPAddress + ]; + AAAA = [ + nodes.acme.networking.primaryIPv6Address + ]; + }; + }; + }; + }; +} diff --git a/tests/common/email/dkim/alice.com/private.age b/tests/common/email/dkim/alice.com/private.age new file mode 100644 index 0000000..cb590c6 --- /dev/null +++ b/tests/common/email/dkim/alice.com/private.age @@ -0,0 +1,11 @@ +-----BEGIN AGE ENCRYPTED FILE----- +YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IFJzZ1dwUSA3bVJS +NjBTaitjUEhxMVZmejI1L3Fmd0xGK3ZFVlM2eVBaellDYWs3ZW5nClZOckc5cFFZ +dVlXYmxWZXpQU3lPTnlwTC9KUGNZak9VVS9aNzVEaFkrUG8KLT4gUy1ncmVhc2UK +Y21CMkZlQVdUUHVIclRpMk5yNmRTb3lwUVQzTWM2a0YwSjhUQU1kbTRqK1lrVUtk +ZkVVCi0tLSByRDVCeTJIK2JQWittWjlUWklSOWJaenluL1FLdXN2ZEM4bVlnU2U1 +RFk0CiAxOLq5ePeM8MemSES9Q86mBix3hooazHZZ7xCIQKnLvZmiHWCKUYw83PWl +QpmWI7S3wgdguOncKirQccJbNtVLYb/noYEBA1y7lOzO1FELPcFgevtB3kt5EAOA +6OaPsFKSsz71t++K011dgFUcnYmOtrqiQeKKEkZ0gey4+jNwf4buDygdiPOL/fQS +uJiIUwsLPqZI/FQ= +-----END AGE ENCRYPTED FILE----- diff --git a/tests/common/email/dkim/alice.com/public b/tests/common/email/dkim/alice.com/public new file mode 100644 index 0000000..be30723 --- /dev/null +++ b/tests/common/email/dkim/alice.com/public @@ -0,0 +1 @@ +LviKY6SBD2mtLJlUTon9AkWPYzB2D+Ivk0XnJOalAZM=
\ No newline at end of file diff --git a/tests/common/email/dkim/bob.com/private.age b/tests/common/email/dkim/bob.com/private.age new file mode 100644 index 0000000..70eddca --- /dev/null +++ b/tests/common/email/dkim/bob.com/private.age @@ -0,0 +1,12 @@ +-----BEGIN AGE ENCRYPTED FILE----- +YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IFJzZ1dwUSAvSmk4 +YlQ3SElud2c3TWgxaUZCaC9mYytlbmNVdlVRcS9VTjlneFBBQ2hrCnE3b1dNdytR +Z0l5dVlMYUhVT0ZIMkFBYkNRKzZ6U1pBYStJTUJsNUEvN28KLT4gcX5kMWtnXi1n +cmVhc2UgczdieGAKNjNwRmVMYmN2cE5LaGFEOS9QZHFtaVhtZXI4dTFpazZhdUN4 +bUZ0cjQ4Y09nOVRjM2IzTGgweS9NbHkxMXlMSwpjdWhQekk0a0VnMTNFTXdjTmdl +bHluNDZWT0R5L25zbXlTL3l3NnRuTWhtNWs0dVByMWQ2TlEKLS0tIEZMK2dqRktZ +Y0hlN2l0ZVB4c3Exd3NsanRuTitrVklaaS9ZTU5ZWVF0cGsKuTNM9zBVFmOHkEqx +jMZaJQd/XqA4WzOi/iFi4WmDUDb8XcVsS0s/37fyZfeMW3YjoCzDb/treKMXyTDC +8l5N/xMG20UGC/Z16W24c/MXMFzpDLuFrjHiUtpQvBzwi6JUDMlKC8t0YWNcqQRr +2vu7qij0XgasxrGDg86hoc72YmYPmpHnE86O1il0PWy9sDRlkn6Ah/DUNg== +-----END AGE ENCRYPTED FILE----- diff --git a/tests/common/email/dkim/bob.com/public b/tests/common/email/dkim/bob.com/public new file mode 100644 index 0000000..915188c --- /dev/null +++ b/tests/common/email/dkim/bob.com/public @@ -0,0 +1 @@ +6USjzSE5zuUNrMdgsjAHFnq0BRKmyZh2IoXFHBCv3To=
\ No newline at end of file diff --git a/tests/by-name/em/email-dns/secrets/dkim/gen_key.sh b/tests/common/email/dkim/gen_key.sh index 1e090f4..48b4434 100755 --- a/tests/by-name/em/email-dns/secrets/dkim/gen_key.sh +++ b/tests/common/email/dkim/gen_key.sh @@ -1,6 +1,8 @@ #! /usr/bin/env nix-shell #! nix-shell -p rage -p openssl -p dash -i dash --impure +# shellcheck shell=dash + cd "$(dirname "$0")" || { echo "No basedir?!" exit 1 diff --git a/tests/common/email/dkim/mail.server1/private.age b/tests/common/email/dkim/mail.server1/private.age new file mode 100644 index 0000000..a97c70b --- /dev/null +++ b/tests/common/email/dkim/mail.server1/private.age @@ -0,0 +1,13 @@ +-----BEGIN AGE ENCRYPTED FILE----- +YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IFJzZ1dwUSBndElV +WFFTcEZpTGwrTUg5WHk1MElLSU5ZZWp1NU5ocit4VGtnM09KZFNBCjNvaWVPR3Zt +RG14N2E3b0RwK21IUzBZZmg1UzFwakMwTVVOdDgrTTd4U1UKLT4geD9OYm0tZ3Jl +YXNlIDwgcjkKQUJtaDlVbzk3Slk3S1ozNS9STGwrdzhvWTlodWgxNVd1WVRadTV5 +VmFVUmRmSTV3ckIzYmpQYUFsMUxkdGNzUgo1UXozYzBCK3UxU1BPSnlvVVVxQjI2 +QVdWY0tBbUg1R3p5NmNaZ2RiVGNlL3pWN3FHZU5QOEN2VVdraDBzM1R2CgotLS0g +TElGejJDYUk4WlhxajdXekI4S0tUMVlrNmdxelpjUnBUa1VCU2krSFloawpUfMj8 +iqVP45yV8fON6fDUur6q/UYqTaaHhCW07LTsBZQpReG8LA+PiOhHw1TmPSJlc/JH +yEkwLachwvLGCGEO2yoFUFhYwoNhl3vrBsuDLXpqfNuoPrg3wcFXBdrg9zBjZ5Pc +C/rdzpysjW/52gKRCdGgFj2WyTW2wO2yVmmUk1RWhMabmdd+XLkKhynaMtWsBF0j +NQ3F +-----END AGE ENCRYPTED FILE----- diff --git a/tests/common/email/dkim/mail.server1/public b/tests/common/email/dkim/mail.server1/public new file mode 100644 index 0000000..6c9e653 --- /dev/null +++ b/tests/common/email/dkim/mail.server1/public @@ -0,0 +1 @@ +3/bsVrnWag3eMCuj78VpsHDQSONi97fIzY+WPKvAYWM=
\ No newline at end of file diff --git a/tests/common/email/dkim/mail.server2/private.age b/tests/common/email/dkim/mail.server2/private.age new file mode 100644 index 0000000..fbd88fc --- /dev/null +++ b/tests/common/email/dkim/mail.server2/private.age @@ -0,0 +1,11 @@ +-----BEGIN AGE ENCRYPTED FILE----- +YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IFJzZ1dwUSBTcktS +L0ovODhxd2tyWFdINWRtbXNNYksyNzl6RUxRcTU2T251eVNhQlg4CjVzTVpnc0FC +eHBNaUtEQnJVRXVpTjg2c3gxcC90VXJ3cUJVaHZJY1pnb00KLT4gKDEkKS1ncmVh +c2UgYnYqQSBAVSU+CnVRbkRhdUhRS0Z6NGIybll1WVdmSmxzS0lxYlAwSXp0TnA4 +anRDSmQKLS0tIGxxSXRaM0lKeXRoeHFxWW9qN3Y1OE55b1FBVGJTZzB2RmFmZVk5 +eW94aW8KleHhm6fKalehWR0IfHYiC+0bsm0SeYjnJeA60hePdwikmr0QQv5D2xCX +Nf2QRuai/BpsiJpnvKUiP1uwkm5x6a2Zgwa7t8BwJ7SHqD4oJ+VgCsWMffLs55uC +A1pKLRmAI26cNt7oibVxNSCCuxZRWeCn+xAsQ393jVCUuDW2FkgyDsKFYHvKRpnC +BNfwDWWHJ+pifPPrqg== +-----END AGE ENCRYPTED FILE----- diff --git a/tests/common/email/dkim/mail.server2/public b/tests/common/email/dkim/mail.server2/public new file mode 100644 index 0000000..40b6ff7 --- /dev/null +++ b/tests/common/email/dkim/mail.server2/public @@ -0,0 +1 @@ +QmbJbp/b671OCOQa0Qx1XGl7c1jzFuASuzBr1f5ATpE=
\ No newline at end of file diff --git a/tests/common/email/functions.nix b/tests/common/email/functions.nix new file mode 100644 index 0000000..7be1bfa --- /dev/null +++ b/tests/common/email/functions.nix @@ -0,0 +1,224 @@ +{ + pkgs, + vhackPackages, +}: { + scripts = { + expectNoNewMail = 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 + } + ''; + expectNewMail = 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 + } + ''; + expectNewSpamMail = 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 spam mail" >&2 + exit 1 + } + ''; + }; + + mkMailServer = serverName: userName: { + config, + lib, + nodes, + ... + }: { + age.identityPaths = ["${../../common/email/hostKey}"]; + + systemd.tmpfiles.rules = [ + #- path mode user group - content + "f /passwords/${userName} 777 root root - ${userName}-password" + ]; + + age.secrets = { + "mailDkim" = { + file = ../../common/email/dkim + "/${serverName}/private.age"; + mode = "600"; + owner = "rspamd"; + group = "rspamd"; + }; + }; + + # Disable rspamd. rspamd can't do the dns lookups in the sandbox for + # determining if something is spam or not. Therefore it just rejects every mail with + # 'Try again later.'. + # services.rspamd.enable = lib.mkForce false; + # users.users.rspamd.group = "rspamd"; + # users.groups.rspamd = {}; + # + # services.postfix.settings.main = { + # smtpd_milters = lib.mkForce []; + # non_smtpd_milters = lib.mkForce []; + # }; + # mailserver.dkim.enable = false; + services = { + rspamd = { + locals = { + "asn.conf".text = lib.mkForce '' + enabled = false; + ''; + "rbl.conf".text = lib.mkForce '' + enabled = false; + ''; + "greylist.conf".text = lib.mkForce '' + enabled = false; + ''; + }; + }; + }; + + # This DNS resolver is only there for rspamd (and apparently not even needed). But it + # also removes our own name server changes, so the test node could no longer find + # e.g. `acme.test` and so on. + # See: https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/work_items/100 + mailserver.localDnsResolver = false; + + vhack = { + mail = { + enable = true; + fqdn = serverName; + domains = [ + serverName + ]; + + security.dkimKeys."${serverName}" = { + dkimPrivateKeyPath = config.age.secrets."mailDkim".path; + dkimPublicKey = + builtins.readFile (../../common/email/dkim + "/${serverName}/public"); + keyAlgorithm = "rsa-sha-256"; + }; + + accounts = { + "${userName}@${serverName}" = { + passwordFile = "/passwords/${userName}"; + }; + }; + }; + }; + }; + + mkDnsZone = serverName: {config, ...}: let + cfg = config.vhack.mail; + + keyAlgoToKeyType = keyAlgo: + if keyAlgo == "ed25519-sha256" + then "ed25519" + else if keyAlgo == "rsa-sha-256" || keyAlgo == "rsa-sha-1" + then "rsa" + else builtins.throw "Impossible"; + in { + "${serverName}" = { + MX = [ + { + preference = 10; + exchange = "${serverName}."; + } + ]; + + # 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}".keyAlgorithm; + p = cfg.security.dkimKeys."${serverName}".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 = "admin@vhack.eu"; + ruf = ["admin@vhack.eu"]; + } + ]; + + # 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. + ]) + ]; + }; + }; + + mkUser = user: mailServer: {nodes, ...}: { + environment.systemPackages = [ + vhackPackages.fetchmail-common-name + pkgs.msmtp + pkgs.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 "${mailServer}" protocol IMAP + username "${user}@${mailServer}" + 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 ${mailServer} + domain ${mailServer} + port 465 + from ${user}@${mailServer} + user ${user}@${mailServer} + password ${user}-password + auth on + tls on + tls_starttls off + ''; + mode = "0600"; + inherit user; + }; + }; + }; +} diff --git a/tests/by-name/em/email-dns/secrets/hostKey b/tests/common/email/hostKey index 79c9d6c..79c9d6c 100644 --- a/tests/by-name/em/email-dns/secrets/hostKey +++ b/tests/common/email/hostKey diff --git a/tests/default.nix b/tests/default.nix index d9b354a..88ea7d9 100644 --- a/tests/default.nix +++ b/tests/default.nix @@ -3,11 +3,336 @@ nixLib, pkgs, }: let + splitAttr = attr: let + pairs = builtins.attrNames attr; + firstName = builtins.elemAt pairs 0; + name = + if (builtins.length pairs) == 1 + then firstName + else builtins.throw "The name value pair (`${firstName}` and some more) has more than one name value pair."; + in { + inherit name; + value = attr.${name}; + }; + + vhack.runTest = { + name, + # Node definition, like in a normal nixos vm test. + nodes ? {}, + # Extra DNS configuration with the node's config as input. + # Example: + # ``` + # server1 = {config, ...}: { + # "domain.example.org" = { + # TXT = [ "test" ]; + # }; + # }; + # ``` + # That will add the TXT record to the `domain.example.org` domain, the `config` will + # be from the `server1` node. + dns ? {}, + # Server domains that all clients and the server can resolve via dns. + # The format follows the common one, e.g.: + # `{nodeName = "domain.example.com";}` + # means, that `domain.example.com` resolves to the node with name `nodeName`. + serverDomains ? [], + # Services to wait for. + # The format follows the common one, e.g.: + # `{nodeName = "example.service";}` + # means, that the test will not start, until `example.service` on the node with name + # `nodeName` has started. + services ? [], + # Open ports to wait for. + # The format follows the common one, e.g.: + # `{nodeName = 993;}` + # means, that the test will not start, until port `993` on the node with name + # `nodeName` is opened. + ports ? [], + # If `true`, will ignore this test. + ignore ? null, + testScript ? {_, ...}: "", + }: let + # Apparently the default's (e.g. `serivces ? []`), are not applied for the `@args` + # syntax. + args = {inherit dns ignore name nodes ports serverDomains services testScript;}; + in + specialArgs.nixos-lib.runTest { + hostPkgs = pkgs; + inherit (args) name; + + meta.broken = pkgs.lib.mkIf (args.ignore != null) true; + + node = { + specialArgs = { + inherit + (specialArgs) + pkgsUnstable + vhackPackages + nixpkgs-unstable + ; + inherit nixLib; + }; + + # Use the nixpkgs as constructed by the `nixpkgs.*` options + pkgs = null; + }; + + nodes = pkgs.lib.mkMerge [ + { + acme = { + imports = [ + ./common/acme/server.nix + ./common/dns/client.nix + ]; + }; + name_server = {nodes, ...}: { + imports = + specialArgs.extraModules + ++ [ + ./common/acme/client.nix + ./common/dns/server.nix + ]; + + vhack.dns.zones = let + mkDomain = attr: let + split = splitAttr attr; + in { + name = "${split.value}"; + value = { + SOA = { + nameServer = "ns"; + adminEmail = "admin@server.com"; + serial = 2025012301; + }; + useOrigin = false; + + A = [ + nodes.${split.name}.networking.primaryIPAddress + ]; + AAAA = [ + nodes.${split.name}.networking.primaryIPv6Address + ]; + }; + }; + in + builtins.listToAttrs (builtins.map mkDomain args.serverDomains); + }; + } + + (pkgs.lib.mapAttrs' (nodeName: dnsOverride: { + name = "name_server"; + value = {nodes, ...}: { + vhack.dns.zones = dnsOverride {config = nodes."${nodeName}";}; + }; + }) + args.dns) + + args.nodes + (builtins.mapAttrs (_: _: { + imports = + specialArgs.extraModules + ++ [ + ./common/acme/client.nix + ./common/dns/client.nix + + ../modules + ]; + }) + args.nodes) + ]; + + testScript = {nodes, ...} @ testScriptInput: let + acme = import ./common/acme {inherit pkgs;}; + + waitForServices = builtins.concatStringsSep "\n" ( + builtins.map (attr: let + split = splitAttr attr; + name = + if builtins.isAttrs split.value + then split.value.name + else split.value; + should_be_started = + if builtins.isAttrs split.value + then + if split.value.should_be_started + then "True" + else "False" + else "False"; + in " maybe_wait(${split.name},\"${name}\", ${should_be_started})") + args.services + ); + waitForPorts = builtins.concatStringsSep "\n" ( + builtins.map ( + attr: let + split = splitAttr attr; + in " ${split.name}.wait_for_open_port(${builtins.toString split.value})" + ) + args.ports + ); + + acmeOnline = builtins.concatStringsSep "\n" (builtins.map (attr: let + split = splitAttr attr; + in " status.append(acme_online(${split.name}, \"${split.value}\", renewRan.get(\"acme-order-renew-${split.value}.service\", False)))") + args.serverDomains); + + allRunning = + builtins.concatStringsSep "\n" (builtins.map (name: " all_services_running(${name})") + (builtins.attrNames args.nodes)); + + optional = condition: value: + if condition + then value + else ""; + in + acme.prepare (builtins.attrNames args.nodes) + # The acme code runs `start_all` before it hands execution back to us. + # So we don't need to run it. + ( + optional (args.services != [] || args.serverDomains != []) + # Python + '' + def unit_exists(host, unitName: str) -> bool: + (status, _) = host.execute(f"test -f '/etc/systemd/system/{unitName}'") + return status == 0 + + wasAlive: bool + deadCounter: int + def run_and_wait(host, unit: str): + global wasAlive + global deadCounter + + wasAlive = False + deadCounter = 0 + + def wait_state(_last_try: bool) -> bool: + global wasAlive + global deadCounter + + state = host.get_unit_property(unit, "SubState") + host.log(f"(waiting) {unit} -> {state}") + + match state: + case "failed": + assert False, f'Unit "{unit}" reached state "{state}"' + case "start" | "active" | "running": + wasAlive = True + case "dead": + if wasAlive: + return True + else: + if deadCounter >= 5: + return True + + deadCounter = deadCounter + 1 + return False + + return False + + with host.nested(f"Waiting for unit {unit}"): + retry(wait_state) + '' + + optional (args.serverDomains != []) + # Python + '' + def acme_online(host, domain, renewRan): + from typing import assert_never + + unitMain = f"acme-{domain}.service" + unitRenew = f"acme-order-renew-{domain}.service" + + infoMain = host.get_unit_property(unitMain, "SubState") + infoRenew = host.get_unit_property(unitRenew, "SubState") + + match (infoMain, infoRenew): + case ("exited", "dead") | ("dead", "dead") if not renewRan: + # The unit might not exist, because we use the DNS values for + # generating this code (there is no needed mapping between DNS -> acme + # host). So we check, if it is an actual unit. + if unit_exists(host, unitMain): + host.start_job(f"acme-{domain}") + return ("Wait", unitRenew, host) + else: + return ("Done", None, None) + + case ("exited", "dead") | ("exited", "dead") if renewRan: + # The unit is done, so it should be good? + host.require_unit_state(unitMain, "active") + host.succeed(f"${pkgs.lib.getExe pkgs.openssl} s_client -connect {domain}:443 -verify_return_error </dev/null") + return ("Done", None, None) + + case (_, "running") | (_, "start"): + # It's currently running, let's wait + return ("Wait", unitRenew, host) + + case other: + assert False, f"Wrong unit sub-states for ({unitMain},{unitRenew}): {other}" + assert_never() # i.e. unreachable + + with subtest("Ensure, that all acme certificates have been fetched"): + renewRan = {} + while True: + status = [] + ${acmeOnline} + done = True + for (state, unit, host) in status: + if state == "Wait": + run_and_wait(host, unit) + renewRan[unit] = True + if state != "Done": + done = False + if done: + break + '' + + optional (args.services != []) + # Python + '' + def maybe_wait(host, unit: str, should_start: bool): + if unit_exists(host, unit): + if should_start: + host.start_job(unit.removesuffix(".service")) + run_and_wait(host, unit) + else: + host.wait_for_unit(unit) + else: + assert False, f"Unit '{unit}' does not exist, test misconfigured" + + with subtest("Waiting for units"): + ${waitForServices} + '' + + optional (args.ports != []) + # Python + '' + with subtest("Waiting for ports"): + ${waitForPorts} + '' + + + # Python + '' + def all_services_running(host): + import json + (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)}" + + with subtest("All services running"): + ${allRunning} + + ${(args.testScript testScriptInput)} + + with subtest("All services running"): + ${allRunning} + '' + ); + }; + tests = nixLib.mkByName { baseDirectory = ./by-name; fileName = "test.nix"; finalizeFunction = name: value: - import value (nixLib.warnMerge specialArgs {inherit pkgs;} "the test args set"); + import value (nixLib.warnMerge specialArgs { + inherit pkgs vhack; + inherit (pkgs) lib; + } "the test args set"); }; in tests |
