1import ./make-test-python.nix (
2 { lib, pkgs, ... }:
3
4 # This nixosTest is supposed to check the following:
5 #
6 # - Whether syncthing's API handles multiple requests for many devices, see
7 # https://github.com/NixOS/nixpkgs/issues/260262
8 #
9 # - Whether syncthing-init.service generated bash script removes devices and
10 # folders that are not present in the user's configuration, which is partly
11 # injected into the script. See also:
12 # https://github.com/NixOS/nixpkgs/issues/259256
13 #
14
15 let
16 # Just a long path not to copy paste
17 configPath = "/var/lib/syncthing/.config/syncthing/config.xml";
18
19 # We will iterate this and more attribute sets defined here, later in the
20 # testScript. Start with this, and distinguish these settings from other
21 # settings, as we check these differently with xmllint, due to the ID.
22 settingsWithId = {
23 devices = {
24 # All of the device IDs used here were generated by the following command:
25 #
26 # (${pkgs.syncthing}/bin/syncthing generate --home /tmp/foo\
27 # | grep ID: | sed 's/.*ID: *//') && rm -rf /tmp/foo
28 #
29 # See also discussion at:
30 # https://forum.syncthing.net/t/how-to-generate-dummy-device-ids/20927/8
31 test_device1.id = "IVTZ5XF-EF3GKFT-GS4AZLG-IT6H2ZP-6WK75SF-AFXQXJJ-BNRZ4N6-XPDKVAU";
32 test_device2.id = "5C35H56-Z2GFF4F-F3IVD4B-GJYVWIE-SMDBJZN-GI66KWP-52JIQGN-4AVLYAM";
33 test_device3.id = "XKLSKHE-BZOHV7B-WQZACEF-GTH36NP-6JSBB6L-RXS3M7C-EEVWO2L-C5B4OAJ";
34 test_device4.id = "APN5Q7J-35GZETO-5KCLF35-ZA7KBWK-HGWPBNG-FERF24R-UTLGMEX-4VJ6PQX";
35 test_device5.id = "D4YXQEE-5MK6LIK-BRU5QWM-ZRXJCK2-N3RQBJE-23JKTQQ-LYGDPHF-RFPZIQX";
36 test_device6.id = "TKMCH64-T44VSLI-6FN2YLF-URBZOBR-ATO4DYX-GEDRIII-CSMRQAI-UAQMDQG";
37 test_device7.id = "472EEBG-Q4PZCD4-4CX6PGF-XS3FSQ2-UFXBZVB-PGNXWLX-7FKBLER-NJ3EMAR";
38 test_device8.id = "HW6KUMK-WTBG24L-2HZQXLO-TGJSG2M-2JG3FHX-5OGYRUJ-T6L5NN7-L364QAZ";
39 test_device9.id = "YAE24AP-7LSVY4T-J74ZSEM-A2IK6RB-FGA35TP-AG4CSLU-ED4UYYY-2J2TDQU";
40 test_device10.id = "277XFSB-OFMQOBI-3XGNGUE-Y7FWRV3-QQDADIY-QIIPQ26-EOGTYKW-JP2EXAI";
41 test_device11.id = "2WWXVTN-Q3QWAAY-XFORMRM-2FDI5XZ-OGN33BD-XOLL42R-DHLT2ML-QYXDQAU";
42 };
43 # Generates a few folders with IDs and paths as written...
44 folders = lib.pipe 6 [
45 (builtins.genList (x: {
46 name = "/var/lib/syncthing/test_folder${builtins.toString x}";
47 value = {
48 id = "DontDeleteMe${builtins.toString x}";
49 };
50 }))
51 builtins.listToAttrs
52 ];
53 };
54 # Non default options that we check later if were applied
55 settingsWithoutId = {
56 options = {
57 autoUpgradeIntervalH = 0;
58 urAccepted = -1;
59 };
60 gui = {
61 theme = "dark";
62 };
63 };
64 # Used later when checking whether settings were set in config.xml:
65 checkSettingWithId =
66 {
67 t, # t for type
68 id,
69 not ? false,
70 }:
71 ''
72 print("Searching for a ${t} with id ${id}")
73 configVal_${t} = machine.succeed(
74 "${pkgs.libxml2}/bin/xmllint "
75 "--xpath 'string(//${t}[@id=\"${id}\"]/@id)' ${configPath}"
76 )
77 print("${t}.id = {}".format(configVal_${t}))
78 assert "${id}" ${if not then "not" else ""} in configVal_${t}
79 '';
80 # Same as checkSettingWithId, but for 'options' and 'gui'
81 checkSettingWithoutId =
82 {
83 t, # t for type
84 n, # n for name
85 v, # v for value
86 not ? false,
87 }:
88 ''
89 print("checking whether setting ${t}.${n} is set to ${v}")
90 configVal_${t}_${n} = machine.succeed(
91 "${pkgs.libxml2}/bin/xmllint "
92 "--xpath 'string(/configuration/${t}/${n})' ${configPath}"
93 )
94 print("${t}.${n} = {}".format(configVal_${t}_${n}))
95 assert "${v}" ${if not then "not" else ""} in configVal_${t}_${n}
96 '';
97 # Removes duplication a bit to define this function for the IDs to delete -
98 # we check whether they were added after our script ran, and before the
99 # systemd unit's bash script ran, and afterwards - whether the systemd unit
100 # worked.
101 checkSettingsToDelete =
102 {
103 not,
104 }:
105 lib.pipe IDsToDelete [
106 (lib.mapAttrsToList (
107 t: id:
108 checkSettingWithId {
109 inherit t id;
110 inherit not;
111 }
112 ))
113 lib.concatStrings
114 ];
115 # These IDs are added to syncthing using the API, similarly to how the
116 # generated systemd unit's bash script does it. Only we add it and expect the
117 # systemd unit bash script to remove them when executed.
118 IDsToDelete = {
119 # Also created using the syncthing generate command above
120 device = "LZ2CTHT-3W2M7BC-CMKDFZL-DLUQJFS-WJR73PA-NZGODWG-DZBHCHI-OXTQXAK";
121 # Intentionally this is a substring of the IDs of the 'test_folder's, as
122 # explained in: https://github.com/NixOS/nixpkgs/issues/259256
123 folder = "DeleteMe";
124 };
125 addDeviceToDeleteScript = pkgs.writers.writeBash "syncthing-add-device-to-delete.sh" ''
126 set -euo pipefail
127
128 export RUNTIME_DIRECTORY=/tmp
129
130 curl() {
131 # get the api key by parsing the config.xml
132 while
133 ! ${pkgs.libxml2}/bin/xmllint \
134 --xpath 'string(configuration/gui/apikey)' \
135 ${configPath} \
136 >"$RUNTIME_DIRECTORY/api_key"
137 do sleep 1; done
138
139 (printf "X-API-Key: "; cat "$RUNTIME_DIRECTORY/api_key") >"$RUNTIME_DIRECTORY/headers"
140
141 ${pkgs.curl}/bin/curl -sSLk -H "@$RUNTIME_DIRECTORY/headers" \
142 --retry 1000 --retry-delay 1 --retry-all-errors \
143 "$@"
144 }
145 curl -d ${lib.escapeShellArg (builtins.toJSON { deviceID = IDsToDelete.device; })} \
146 -X POST 127.0.0.1:8384/rest/config/devices
147 curl -d ${lib.escapeShellArg (builtins.toJSON { id = IDsToDelete.folder; })} \
148 -X POST 127.0.0.1:8384/rest/config/folders
149 '';
150 in
151 {
152 name = "syncthing-many-devices";
153 meta.maintainers = with lib.maintainers; [ doronbehar ];
154
155 nodes.machine = {
156 services.syncthing = {
157 enable = true;
158 overrideDevices = true;
159 overrideFolders = true;
160 settings = settingsWithoutId // settingsWithId;
161 };
162 };
163 testScript =
164 ''
165 machine.wait_for_unit("syncthing-init.service")
166 ''
167 + (lib.pipe settingsWithId [
168 # Check that folders and devices were added properly and that all IDs exist
169 (lib.mapAttrsRecursive (
170 path: id:
171 checkSettingWithId {
172 # plural -> solitary
173 t = (lib.removeSuffix "s" (builtins.elemAt path 0));
174 inherit id;
175 }
176 ))
177 # Get all the values we applied the above function upon
178 (lib.collect builtins.isString)
179 lib.concatStrings
180 ])
181 + (lib.pipe settingsWithoutId [
182 # Check that all other syncthing.settings were added properly with correct
183 # values
184 (lib.mapAttrsRecursive (
185 path: value:
186 checkSettingWithoutId {
187 t = (builtins.elemAt path 0);
188 n = (builtins.elemAt path 1);
189 v = (builtins.toString value);
190 }
191 ))
192 # Get all the values we applied the above function upon
193 (lib.collect builtins.isString)
194 lib.concatStrings
195 ])
196 + ''
197 # Run the script on the machine
198 machine.succeed("${addDeviceToDeleteScript}")
199 ''
200 + (checkSettingsToDelete {
201 not = false;
202 })
203 + ''
204 # Useful for debugging later
205 machine.copy_from_vm("${configPath}", "before")
206
207 machine.systemctl("restart syncthing-init.service")
208 machine.wait_for_unit("syncthing-init.service")
209 ''
210 + (checkSettingsToDelete {
211 not = true;
212 })
213 + ''
214 # Useful for debugging later
215 machine.copy_from_vm("${configPath}", "after")
216
217 # Copy the systemd unit's bash script, to inspect it for debugging.
218 mergeScript = machine.succeed(
219 "systemctl cat syncthing-init.service | "
220 "${pkgs.initool}/bin/initool g - Service ExecStart --value-only"
221 ).strip() # strip from new lines
222 machine.copy_from_vm(mergeScript, "")
223 '';
224 }
225)