at master 8.4 kB view raw
1{ pkgs, lib, ... }: 2 3let 4 5 radicale_calendars = { 6 type = "caldav"; 7 url = "http://localhost:5232/"; 8 # Radicale needs username/password. 9 username = "alice"; 10 password = "password"; 11 }; 12 13 radicale_contacts = { 14 type = "carddav"; 15 url = "http://localhost:5232/"; 16 # Radicale needs username/password. 17 username = "alice"; 18 password = "password"; 19 }; 20 21 xandikos_calendars = { 22 type = "caldav"; 23 url = "http://localhost:8080/user/calendars"; 24 # Xandikos warns 25 # > No current-user-principal returned, re-using URL http://localhost:8080/user/calendars/ 26 # but we do not need username/password. 27 }; 28 29 xandikos_contacts = { 30 type = "carddav"; 31 url = "http://localhost:8080/user/contacts"; 32 }; 33 34 local_calendars = { 35 type = "filesystem"; 36 path = "~/calendars"; 37 fileext = ".ics"; 38 }; 39 40 local_contacts = { 41 type = "filesystem"; 42 path = "~/contacts"; 43 fileext = ".vcf"; 44 }; 45 46 mkPairs = a: b: { 47 calendars = { 48 a = "${a}_calendars"; 49 b = "${b}_calendars"; 50 collections = [ 51 "from a" 52 "from b" 53 ]; 54 }; 55 contacts = { 56 a = "${a}_contacts"; 57 b = "${b}_contacts"; 58 collections = [ 59 "from a" 60 "from b" 61 ]; 62 }; 63 }; 64 65 mkRadicaleProps = 66 tag: 67 pkgs.writeText "Radicale.props" ( 68 builtins.toJSON { 69 inherit tag; 70 } 71 ); 72 73 writeLines = 74 name: eol: lines: 75 pkgs.writeText name (lib.concatMapStrings (l: "${l}${eol}") lines); 76 77 prodid = "-//NixOS test//EN"; 78 dtstamp = "20231129T194743Z"; 79 80 writeICS = 81 { 82 uid, 83 summary, 84 dtstart, 85 dtend, 86 }: 87 writeLines "${uid}.ics" "\r\n" [ 88 "BEGIN:VCALENDAR" 89 "VERSION:2.0" 90 "PRODID:${prodid}" 91 "BEGIN:VEVENT" 92 "UID:${uid}" 93 "SUMMARY:${summary}" 94 "DTSTART:${dtstart}" 95 "DTEND:${dtend}" 96 "DTSTAMP:${dtstamp}" 97 "END:VEVENT" 98 "END:VCALENDAR" 99 ]; 100 101 foo_ics = writeICS { 102 uid = "foo"; 103 summary = "Epochalypse"; 104 dtstart = "19700101T000000Z"; 105 dtend = "20380119T031407Z"; 106 }; 107 108 bar_ics = writeICS { 109 uid = "bar"; 110 summary = "One Billion Seconds"; 111 dtstart = "19700101T000000Z"; 112 dtend = "20010909T014640Z"; 113 }; 114 115 writeVCF = 116 { 117 uid, 118 name, 119 displayName, 120 email, 121 }: 122 writeLines "${uid}.vcf" "\r\n" [ 123 # One of the tools enforces this order of fields. 124 "BEGIN:VCARD" 125 "VERSION:4.0" 126 "UID:${uid}" 127 "EMAIL;TYPE=INTERNET:${email}" 128 "FN:${displayName}" 129 "N:${name}" 130 "END:VCARD" 131 ]; 132 133 foo_vcf = writeVCF { 134 uid = "foo"; 135 name = "Doe;John;;;"; 136 displayName = "John Doe"; 137 email = "john.doe@example.org"; 138 }; 139 140 bar_vcf = writeVCF { 141 uid = "bar"; 142 name = "Doe;Jane;;;"; 143 displayName = "Jane Doe"; 144 email = "jane.doe@example.org"; 145 }; 146 147in 148{ 149 name = "vdirsyncer"; 150 151 meta.maintainers = with lib.maintainers; [ schnusch ]; 152 153 nodes = { 154 machine = { 155 services.radicale = { 156 enable = true; 157 settings.auth.type = "none"; 158 }; 159 160 services.xandikos = { 161 enable = true; 162 extraOptions = [ "--autocreate" ]; 163 }; 164 165 services.vdirsyncer = { 166 enable = true; 167 jobs = { 168 169 alice = { 170 user = "alice"; 171 group = "users"; 172 config = { 173 statusPath = "/home/alice/.vdirsyncer"; 174 storages = { 175 inherit 176 local_calendars 177 local_contacts 178 radicale_calendars 179 radicale_contacts 180 ; 181 }; 182 pairs = mkPairs "local" "radicale"; 183 }; 184 forceDiscover = true; 185 }; 186 187 bob = { 188 user = "bob"; 189 group = "users"; 190 config = { 191 statusPath = "/home/bob/.vdirsyncer"; 192 storages = { 193 inherit 194 local_calendars 195 local_contacts 196 xandikos_calendars 197 xandikos_contacts 198 ; 199 }; 200 pairs = mkPairs "local" "xandikos"; 201 }; 202 forceDiscover = true; 203 }; 204 205 remote = { 206 config = { 207 storages = { 208 inherit 209 radicale_calendars 210 radicale_contacts 211 xandikos_calendars 212 xandikos_contacts 213 ; 214 }; 215 pairs = mkPairs "radicale" "xandikos"; 216 }; 217 forceDiscover = true; 218 }; 219 220 }; 221 }; 222 223 users.users = { 224 alice.isNormalUser = true; 225 bob.isNormalUser = true; 226 }; 227 }; 228 }; 229 230 testScript = '' 231 def run_unit(name): 232 machine.systemctl(f"start {name}") 233 # The service is Type=oneshot without RemainAfterExit=yes. Once it 234 # is finished it is no longer active and wait_for_unit will fail. 235 # When that happens we check if it actually failed. 236 try: 237 machine.wait_for_unit(name) 238 except: 239 machine.fail(f"systemctl is-failed {name}") 240 241 start_all() 242 243 machine.wait_for_open_port(5232) 244 machine.wait_for_open_port(8080) 245 machine.wait_for_unit("multi-user.target") 246 247 with subtest("alice -> radicale"): 248 # vdirsyncer cannot create create collections on Radicale, 249 # see https://vdirsyncer.pimutils.org/en/stable/tutorials/radicale.html 250 machine.succeed("runuser -u radicale -- install -Dm 644 ${mkRadicaleProps "VCALENDAR"} /var/lib/radicale/collections/collection-root/alice/foocal/.Radicale.props") 251 machine.succeed("runuser -u radicale -- install -Dm 644 ${mkRadicaleProps "VADDRESSBOOK"} /var/lib/radicale/collections/collection-root/alice/foocard/.Radicale.props") 252 253 machine.succeed("runuser -u alice -- install -Dm 644 ${foo_ics} /home/alice/calendars/foocal/foo.ics") 254 machine.succeed("runuser -u alice -- install -Dm 644 ${foo_vcf} /home/alice/contacts/foocard/foo.vcf") 255 run_unit("vdirsyncer@alice.service") 256 257 # test statusPath 258 machine.succeed("test -d /home/alice/.vdirsyncer") 259 machine.fail("test -e /var/lib/private/vdirsyncer/alice") 260 261 with subtest("bob -> xandikos"): 262 # I suspect Radicale shares the namespace for calendars and 263 # contacts, but Xandikos separates them. We just use `barcal` and 264 # `barcard` with Xandikos as well to avoid conflicts. 265 machine.succeed("runuser -u bob -- install -Dm 644 ${bar_ics} /home/bob/calendars/barcal/bar.ics") 266 machine.succeed("runuser -u bob -- install -Dm 644 ${bar_vcf} /home/bob/contacts/barcard/bar.vcf") 267 run_unit("vdirsyncer@bob.service") 268 269 # test statusPath 270 machine.succeed("test -d /home/bob/.vdirsyncer") 271 machine.fail("test -e /var/lib/private/vdirsyncer/bob") 272 273 with subtest("radicale <-> xandikos"): 274 # vdirsyncer cannot create create collections on Radicale, 275 # see https://vdirsyncer.pimutils.org/en/stable/tutorials/radicale.html 276 machine.succeed("runuser -u radicale -- install -Dm 644 ${mkRadicaleProps "VCALENDAR"} /var/lib/radicale/collections/collection-root/alice/barcal/.Radicale.props") 277 machine.succeed("runuser -u radicale -- install -Dm 644 ${mkRadicaleProps "VADDRESSBOOK"} /var/lib/radicale/collections/collection-root/alice/barcard/.Radicale.props") 278 279 run_unit("vdirsyncer@remote.service") 280 281 # test statusPath 282 machine.succeed("test -d /var/lib/private/vdirsyncer/remote") 283 284 with subtest("radicale -> alice"): 285 run_unit("vdirsyncer@alice.service") 286 287 with subtest("xandikos -> bob"): 288 run_unit("vdirsyncer@bob.service") 289 290 with subtest("compare synced files"): 291 # iCalendar files get reordered 292 machine.succeed("diff -u --strip-trailing-cr <(sort /home/alice/calendars/foocal/foo.ics) <(sort /home/bob/calendars/foocal/foo.ics) >&2") 293 machine.succeed("diff -u --strip-trailing-cr <(sort /home/bob/calendars/barcal/bar.ics) <(sort /home/alice/calendars/barcal/bar.ics) >&2") 294 295 machine.succeed("diff -u --strip-trailing-cr /home/alice/contacts/foocard/foo.vcf /home/bob/contacts/foocard/foo.vcf >&2") 296 machine.succeed("diff -u --strip-trailing-cr /home/bob/contacts/barcard/bar.vcf /home/alice/contacts/barcard/bar.vcf >&2") 297 ''; 298}