diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 0526d9d361a3..89b6606a6942 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -1275,6 +1275,7 @@ in { vault-dev = handleTest ./vault-dev.nix {}; vault-postgresql = handleTest ./vault-postgresql.nix {}; vaultwarden = discoverTests (import ./vaultwarden.nix); + vdirsyncer = handleTest ./vdirsyncer.nix {}; vector = handleTest ./vector {}; velocity = runTest ./velocity.nix; vengi-tools = handleTest ./vengi-tools.nix {}; diff --git a/nixos/tests/vdirsyncer.nix b/nixos/tests/vdirsyncer.nix new file mode 100644 index 000000000000..bd7b8316eff3 --- /dev/null +++ b/nixos/tests/vdirsyncer.nix @@ -0,0 +1,303 @@ +import ./make-test-python.nix ( + { pkgs, lib, ... }: + + let + + radicale_calendars = { + type = "caldav"; + url = "http://localhost:5232/"; + # Radicale needs username/password. + username = "alice"; + password = "password"; + }; + + radicale_contacts = { + type = "carddav"; + url = "http://localhost:5232/"; + # Radicale needs username/password. + username = "alice"; + password = "password"; + }; + + xandikos_calendars = { + type = "caldav"; + url = "http://localhost:8080/user/calendars"; + # Xandikos warns + # > No current-user-principal returned, re-using URL http://localhost:8080/user/calendars/ + # but we do not need username/password. + }; + + xandikos_contacts = { + type = "carddav"; + url = "http://localhost:8080/user/contacts"; + }; + + local_calendars = { + type = "filesystem"; + path = "~/calendars"; + fileext = ".ics"; + }; + + local_contacts = { + type = "filesystem"; + path = "~/contacts"; + fileext = ".vcf"; + }; + + mkPairs = a: b: { + calendars = { + a = "${a}_calendars"; + b = "${b}_calendars"; + collections = [ + "from a" + "from b" + ]; + }; + contacts = { + a = "${a}_contacts"; + b = "${b}_contacts"; + collections = [ + "from a" + "from b" + ]; + }; + }; + + mkRadicaleProps = + tag: + pkgs.writeText "Radicale.props" ( + builtins.toJSON { + inherit tag; + } + ); + + writeLines = + name: eol: lines: + pkgs.writeText name (lib.concatMapStrings (l: "${l}${eol}") lines); + + prodid = "-//NixOS test//EN"; + dtstamp = "20231129T194743Z"; + + writeICS = + { + uid, + summary, + dtstart, + dtend, + }: + writeLines "${uid}.ics" "\r\n" [ + "BEGIN:VCALENDAR" + "VERSION:2.0" + "PRODID:${prodid}" + "BEGIN:VEVENT" + "UID:${uid}" + "SUMMARY:${summary}" + "DTSTART:${dtstart}" + "DTEND:${dtend}" + "DTSTAMP:${dtstamp}" + "END:VEVENT" + "END:VCALENDAR" + ]; + + foo_ics = writeICS { + uid = "foo"; + summary = "Epochalypse"; + dtstart = "19700101T000000Z"; + dtend = "20380119T031407Z"; + }; + + bar_ics = writeICS { + uid = "bar"; + summary = "One Billion Seconds"; + dtstart = "19700101T000000Z"; + dtend = "20010909T014640Z"; + }; + + writeVCF = + { + uid, + name, + displayName, + email, + }: + writeLines "${uid}.vcf" "\r\n" [ + # One of the tools enforces this order of fields. + "BEGIN:VCARD" + "VERSION:4.0" + "UID:${uid}" + "EMAIL;TYPE=INTERNET:${email}" + "FN:${displayName}" + "N:${name}" + "END:VCARD" + ]; + + foo_vcf = writeVCF { + uid = "foo"; + name = "Doe;John;;;"; + displayName = "John Doe"; + email = "john.doe@example.org"; + }; + + bar_vcf = writeVCF { + uid = "bar"; + name = "Doe;Jane;;;"; + displayName = "Jane Doe"; + email = "jane.doe@example.org"; + }; + + in + { + name = "vdirsyncer"; + + meta.maintainers = with lib.maintainers; [ schnusch ]; + + nodes = { + machine = { + services.radicale.enable = true; + services.xandikos = { + enable = true; + extraOptions = [ "--autocreate" ]; + }; + + services.vdirsyncer = { + enable = true; + jobs = { + + alice = { + user = "alice"; + group = "users"; + config = { + statusPath = "/home/alice/.vdirsyncer"; + storages = { + inherit + local_calendars + local_contacts + radicale_calendars + radicale_contacts + ; + }; + pairs = mkPairs "local" "radicale"; + }; + forceDiscover = true; + }; + + bob = { + user = "bob"; + group = "users"; + config = { + statusPath = "/home/bob/.vdirsyncer"; + storages = { + inherit + local_calendars + local_contacts + xandikos_calendars + xandikos_contacts + ; + }; + pairs = mkPairs "local" "xandikos"; + }; + forceDiscover = true; + }; + + remote = { + config = { + storages = { + inherit + radicale_calendars + radicale_contacts + xandikos_calendars + xandikos_contacts + ; + }; + pairs = mkPairs "radicale" "xandikos"; + }; + forceDiscover = true; + }; + + }; + }; + + # ProtectHome is the default, but we must access our storage + # in ~. + systemd.services = { + "vdirsyncer@alice".serviceConfig.ProtectHome = lib.mkForce false; + "vdirsyncer@bob".serviceConfig.ProtectHome = lib.mkForce false; + }; + + users.users = { + alice.isNormalUser = true; + bob.isNormalUser = true; + }; + }; + }; + + testScript = '' + def run_unit(name): + machine.systemctl(f"start {name}") + # The service is Type=oneshot without RemainAfterExit=yes. Once it + # is finished it is no longer active and wait_for_unit will fail. + # When that happens we check if it actually failed. + try: + machine.wait_for_unit(name) + except: + machine.fail(f"systemctl is-failed {name}") + + start_all() + + machine.wait_for_open_port(5232) + machine.wait_for_open_port(8080) + machine.wait_for_unit("multi-user.target") + + with subtest("alice -> radicale"): + # vdirsyncer cannot create create collections on Radicale, + # see https://vdirsyncer.pimutils.org/en/stable/tutorials/radicale.html + machine.succeed("runuser -u radicale -- install -Dm 644 ${mkRadicaleProps "VCALENDAR"} /var/lib/radicale/collections/collection-root/alice/foocal/.Radicale.props") + machine.succeed("runuser -u radicale -- install -Dm 644 ${mkRadicaleProps "VADDRESSBOOK"} /var/lib/radicale/collections/collection-root/alice/foocard/.Radicale.props") + + machine.succeed("runuser -u alice -- install -Dm 644 ${foo_ics} /home/alice/calendars/foocal/foo.ics") + machine.succeed("runuser -u alice -- install -Dm 644 ${foo_vcf} /home/alice/contacts/foocard/foo.vcf") + run_unit("vdirsyncer@alice.service") + + # test statusPath + machine.succeed("test -d /home/alice/.vdirsyncer") + machine.fail("test -e /var/lib/private/vdirsyncer/alice") + + with subtest("bob -> xandikos"): + # I suspect Radicale shares the namespace for calendars and + # contacts, but Xandikos separates them. We just use `barcal` and + # `barcard` with Xandikos as well to avoid conflicts. + machine.succeed("runuser -u bob -- install -Dm 644 ${bar_ics} /home/bob/calendars/barcal/bar.ics") + machine.succeed("runuser -u bob -- install -Dm 644 ${bar_vcf} /home/bob/contacts/barcard/bar.vcf") + run_unit("vdirsyncer@bob.service") + + # test statusPath + machine.succeed("test -d /home/bob/.vdirsyncer") + machine.fail("test -e /var/lib/private/vdirsyncer/bob") + + with subtest("radicale <-> xandikos"): + # vdirsyncer cannot create create collections on Radicale, + # see https://vdirsyncer.pimutils.org/en/stable/tutorials/radicale.html + machine.succeed("runuser -u radicale -- install -Dm 644 ${mkRadicaleProps "VCALENDAR"} /var/lib/radicale/collections/collection-root/alice/barcal/.Radicale.props") + machine.succeed("runuser -u radicale -- install -Dm 644 ${mkRadicaleProps "VADDRESSBOOK"} /var/lib/radicale/collections/collection-root/alice/barcard/.Radicale.props") + + run_unit("vdirsyncer@remote.service") + + # test statusPath + machine.succeed("test -d /var/lib/private/vdirsyncer/remote") + + with subtest("radicale -> alice"): + run_unit("vdirsyncer@alice.service") + + with subtest("xandikos -> bob"): + run_unit("vdirsyncer@bob.service") + + with subtest("compare synced files"): + # iCalendar files get reordered + machine.succeed("diff -u --strip-trailing-cr <(sort /home/alice/calendars/foocal/foo.ics) <(sort /home/bob/calendars/foocal/foo.ics) >&2") + machine.succeed("diff -u --strip-trailing-cr <(sort /home/bob/calendars/barcal/bar.ics) <(sort /home/alice/calendars/barcal/bar.ics) >&2") + + machine.succeed("diff -u --strip-trailing-cr /home/alice/contacts/foocard/foo.vcf /home/bob/contacts/foocard/foo.vcf >&2") + machine.succeed("diff -u --strip-trailing-cr /home/bob/contacts/barcard/bar.vcf /home/alice/contacts/barcard/bar.vcf >&2") + ''; + } +)