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