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; settings.auth.type = "none"; }; 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; }; }; }; 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") ''; } )