push sheeet
Some checks failed
Periodic Merges (6h) / master → staging-nixos (push) Failing after 12m50s
Periodic Merges (6h) / master → staging-next (push) Failing after 12m54s
Periodic Merges (24h) / merge-base(master,staging) → haskell-updates (push) Failing after 11m54s
Periodic Merges (6h) / staging-next → staging (push) Failing after 12m13s
Periodic Merges (24h) / staging-next-25.05 → staging-25.05 (push) Failing after 13m24s
Periodic Merges (24h) / release-25.05 → staging-next-25.05 (push) Failing after 14m28s

This commit is contained in:
Dark Steveneq
2025-10-09 14:15:47 +02:00
commit 646b892680
49168 changed files with 5897842 additions and 0 deletions

View File

@@ -0,0 +1,135 @@
{ lib, pkgs, ... }:
let
genNodeId =
name:
pkgs.runCommand "syncthing-test-certs-${name}" { } ''
mkdir -p $out
${pkgs.syncthing}/bin/syncthing generate --home=$out
${pkgs.libxml2}/bin/xmllint --xpath 'string(configuration/device/@id)' $out/config.xml > $out/id
'';
idA = genNodeId "a";
idB = genNodeId "b";
idC = genNodeId "c";
testPassword = "it's a secret";
in
{
name = "syncthing";
meta.maintainers = with pkgs.lib.maintainers; [ zarelit ];
nodes = {
a =
{ config, ... }:
{
environment.etc.bar-encryption-password.text = testPassword;
services.syncthing = {
enable = true;
openDefaultPorts = true;
cert = "${idA}/cert.pem";
key = "${idA}/key.pem";
settings = {
devices.b.id = lib.fileContents "${idB}/id";
devices.c.id = lib.fileContents "${idC}/id";
folders.foo = {
path = "/var/lib/syncthing/foo";
devices = [ "b" ];
};
folders.bar = {
path = "/var/lib/syncthing/bar";
devices = [
{
name = "c";
encryptionPasswordFile = "/etc/${config.environment.etc.bar-encryption-password.target}";
}
];
};
};
};
};
b =
{ config, ... }:
{
environment.etc.bar-encryption-password.text = testPassword;
services.syncthing = {
enable = true;
openDefaultPorts = true;
cert = "${idB}/cert.pem";
key = "${idB}/key.pem";
settings = {
devices.a.id = lib.fileContents "${idA}/id";
devices.c.id = lib.fileContents "${idC}/id";
folders.foo = {
path = "/var/lib/syncthing/foo";
devices = [ "a" ];
};
folders.bar = {
path = "/var/lib/syncthing/bar";
devices = [
{
name = "c";
encryptionPasswordFile = "/etc/${config.environment.etc.bar-encryption-password.target}";
}
];
};
};
};
};
c = {
services.syncthing = {
enable = true;
openDefaultPorts = true;
cert = "${idC}/cert.pem";
key = "${idC}/key.pem";
settings = {
devices.a.id = lib.fileContents "${idA}/id";
devices.b.id = lib.fileContents "${idB}/id";
folders.bar = {
path = "/var/lib/syncthing/bar";
devices = [
"a"
"b"
];
type = "receiveencrypted";
};
};
};
};
};
testScript = ''
start_all()
a.wait_for_unit("syncthing.service")
b.wait_for_unit("syncthing.service")
c.wait_for_unit("syncthing.service")
a.wait_for_open_port(22000)
b.wait_for_open_port(22000)
c.wait_for_open_port(22000)
# Test foo
a.wait_for_file("/var/lib/syncthing/foo")
b.wait_for_file("/var/lib/syncthing/foo")
a.succeed("echo a2b > /var/lib/syncthing/foo/a2b")
b.succeed("echo b2a > /var/lib/syncthing/foo/b2a")
a.wait_for_file("/var/lib/syncthing/foo/b2a")
b.wait_for_file("/var/lib/syncthing/foo/a2b")
# Test bar
a.wait_for_file("/var/lib/syncthing/bar")
b.wait_for_file("/var/lib/syncthing/bar")
c.wait_for_file("/var/lib/syncthing/bar")
a.succeed("echo plaincontent > /var/lib/syncthing/bar/plainname")
# B should be able to decrypt, check that content of file matches
b.wait_for_file("/var/lib/syncthing/bar/plainname")
file_contents = b.succeed("cat /var/lib/syncthing/bar/plainname")
assert "plaincontent\n" == file_contents, f"Unexpected file contents: {file_contents=}"
# Bar on C is untrusted, check that content is not in cleartext
c.fail("grep -R plaincontent /var/lib/syncthing/bar")
'';
}

View File

@@ -0,0 +1,56 @@
{ lib, pkgs, ... }:
{
name = "syncthing-guiPassword";
meta.maintainers = with lib.maintainers; [ nullcube ];
enableOCR = true;
nodes.machine = {
imports = [ ../common/x11.nix ];
environment.systemPackages = with pkgs; [
syncthing
xdotool
];
programs.firefox = {
enable = true;
preferences = {
# Prevent firefox from asking to save the password
"signon.rememberSignons" = false;
};
};
services.syncthing = {
enable = true;
settings.options.urAccepted = -1;
settings.gui = {
insecureAdminAccess = false;
user = "alice";
password = "alice_password";
};
};
};
testScript = ''
machine.wait_for_unit("syncthing.service")
machine.wait_for_x()
machine.execute("xterm -e 'firefox 127.0.0.1:8384' >&2 &")
machine.wait_for_window("Syncthing")
machine.screenshot("pre-login")
with subtest("Syncthing requests authentication"):
machine.wait_for_text("Authentication Required", 10)
with subtest("Syncthing password is valid"):
machine.execute("xdotool type \"alice\"")
machine.execute("xdotool key Tab")
machine.execute("xdotool type \"alice_password\"")
machine.execute("xdotool key Enter")
machine.sleep(2)
machine.wait_for_text("This Device", 10)
machine.screenshot("post-login")
with subtest("Plaintext Syncthing password is not in final config"):
config = machine.succeed("cat /var/lib/syncthing/.config/syncthing/config.xml")
assert "alice_password" not in config
'';
}

View File

@@ -0,0 +1,56 @@
{ lib, pkgs, ... }:
{
name = "syncthing-guiPasswordFile";
meta.maintainers = with lib.maintainers; [ nullcube ];
enableOCR = true;
nodes.machine = {
imports = [ ../common/x11.nix ];
environment.systemPackages = with pkgs; [
syncthing
xdotool
];
programs.firefox = {
enable = true;
preferences = {
# Prevent firefox from asking to save the password
"signon.rememberSignons" = false;
};
};
services.syncthing = {
enable = true;
settings.options.urAccepted = -1;
settings.gui = {
insecureAdminAccess = false;
user = "alice";
};
guiPasswordFile = (pkgs.writeText "syncthing-password-file" ''alice_password'').outPath;
};
};
testScript = ''
machine.wait_for_unit("syncthing.service")
machine.wait_for_x()
machine.execute("xterm -e 'firefox 127.0.0.1:8384' >&2 &")
machine.wait_for_window("Syncthing")
machine.screenshot("pre-login")
with subtest("Syncthing requests authentication"):
machine.wait_for_text("Authentication Required", 10)
with subtest("Syncthing password is valid"):
machine.execute("xdotool type \"alice\"")
machine.execute("xdotool key Tab")
machine.execute("xdotool type \"alice_password\"")
machine.execute("xdotool key Enter")
machine.sleep(2)
machine.wait_for_text("This Device", 10)
machine.screenshot("post-login")
with subtest("Plaintext Syncthing password is not in final config"):
config = machine.succeed("cat /var/lib/syncthing/.config/syncthing/config.xml")
assert "alice_password" not in config
'';
}

View File

@@ -0,0 +1,33 @@
{ lib, pkgs, ... }:
let
testId = "7CFNTQM-IMTJBHJ-3UWRDIU-ZGQJFR6-VCXZ3NB-XUH3KZO-N52ITXR-LAIYUAU";
in
{
name = "syncthing-init";
meta.maintainers = with pkgs.lib.maintainers; [ lassulus ];
nodes.machine = {
services.syncthing = {
enable = true;
settings.devices.testDevice = {
id = testId;
};
settings.folders.testFolder = {
path = "/tmp/test";
devices = [ "testDevice" ];
};
settings.gui.user = "guiUser";
};
};
testScript = ''
machine.wait_for_unit("syncthing-init.service")
config = machine.succeed("cat /var/lib/syncthing/.config/syncthing/config.xml")
assert "testFolder" in config
assert "${testId}" in config
assert "guiUser" in config
'';
}

View File

@@ -0,0 +1,70 @@
{ lib, pkgs, ... }:
{
name = "syncthing";
meta.maintainers = with pkgs.lib.maintainers; [ chkno ];
nodes = rec {
a = {
environment.systemPackages = with pkgs; [
curl
libxml2
syncthing
];
services.syncthing = {
enable = true;
openDefaultPorts = true;
};
};
b = a;
};
testScript = ''
import json
import shlex
confdir = "/var/lib/syncthing/.config/syncthing"
def addPeer(host, name, deviceID):
APIKey = host.succeed(
"xmllint --xpath 'string(configuration/gui/apikey)' %s/config.xml" % confdir
).strip()
oldConf = host.succeed(
"curl -Ssf -H 'X-API-Key: %s' 127.0.0.1:8384/rest/config" % APIKey
)
conf = json.loads(oldConf)
conf["devices"].append({"deviceID": deviceID, "id": name})
conf["folders"].append(
{
"devices": [{"deviceID": deviceID}],
"id": "foo",
"path": "/var/lib/syncthing/foo",
"rescanIntervalS": 1,
}
)
newConf = json.dumps(conf)
host.succeed(
"curl -Ssf -H 'X-API-Key: %s' 127.0.0.1:8384/rest/config -X PUT -d %s"
% (APIKey, shlex.quote(newConf))
)
start_all()
a.wait_for_unit("syncthing.service")
b.wait_for_unit("syncthing.service")
a.wait_for_open_port(22000)
b.wait_for_open_port(22000)
aDeviceID = a.succeed("syncthing --home=%s device-id" % confdir).strip()
bDeviceID = b.succeed("syncthing --home=%s device-id" % confdir).strip()
addPeer(a, "b", bDeviceID)
addPeer(b, "a", aDeviceID)
a.wait_for_file("/var/lib/syncthing/foo")
b.wait_for_file("/var/lib/syncthing/foo")
a.succeed("echo a2b > /var/lib/syncthing/foo/a2b")
b.succeed("echo b2a > /var/lib/syncthing/foo/b2a")
a.wait_for_file("/var/lib/syncthing/foo/b2a")
b.wait_for_file("/var/lib/syncthing/foo/a2b")
'';
}

View File

@@ -0,0 +1,222 @@
{ lib, pkgs, ... }:
# This nixosTest is supposed to check the following:
#
# - Whether syncthing's API handles multiple requests for many devices, see
# https://github.com/NixOS/nixpkgs/issues/260262
#
# - Whether syncthing-init.service generated bash script removes devices and
# folders that are not present in the user's configuration, which is partly
# injected into the script. See also:
# https://github.com/NixOS/nixpkgs/issues/259256
#
let
# Just a long path not to copy paste
configPath = "/var/lib/syncthing/.config/syncthing/config.xml";
# We will iterate this and more attribute sets defined here, later in the
# testScript. Start with this, and distinguish these settings from other
# settings, as we check these differently with xmllint, due to the ID.
settingsWithId = {
devices = {
# All of the device IDs used here were generated by the following command:
#
# (${pkgs.syncthing}/bin/syncthing generate --home /tmp/foo\
# | grep ID: | sed 's/.*ID: *//') && rm -rf /tmp/foo
#
# See also discussion at:
# https://forum.syncthing.net/t/how-to-generate-dummy-device-ids/20927/8
test_device1.id = "IVTZ5XF-EF3GKFT-GS4AZLG-IT6H2ZP-6WK75SF-AFXQXJJ-BNRZ4N6-XPDKVAU";
test_device2.id = "5C35H56-Z2GFF4F-F3IVD4B-GJYVWIE-SMDBJZN-GI66KWP-52JIQGN-4AVLYAM";
test_device3.id = "XKLSKHE-BZOHV7B-WQZACEF-GTH36NP-6JSBB6L-RXS3M7C-EEVWO2L-C5B4OAJ";
test_device4.id = "APN5Q7J-35GZETO-5KCLF35-ZA7KBWK-HGWPBNG-FERF24R-UTLGMEX-4VJ6PQX";
test_device5.id = "D4YXQEE-5MK6LIK-BRU5QWM-ZRXJCK2-N3RQBJE-23JKTQQ-LYGDPHF-RFPZIQX";
test_device6.id = "TKMCH64-T44VSLI-6FN2YLF-URBZOBR-ATO4DYX-GEDRIII-CSMRQAI-UAQMDQG";
test_device7.id = "472EEBG-Q4PZCD4-4CX6PGF-XS3FSQ2-UFXBZVB-PGNXWLX-7FKBLER-NJ3EMAR";
test_device8.id = "HW6KUMK-WTBG24L-2HZQXLO-TGJSG2M-2JG3FHX-5OGYRUJ-T6L5NN7-L364QAZ";
test_device9.id = "YAE24AP-7LSVY4T-J74ZSEM-A2IK6RB-FGA35TP-AG4CSLU-ED4UYYY-2J2TDQU";
test_device10.id = "277XFSB-OFMQOBI-3XGNGUE-Y7FWRV3-QQDADIY-QIIPQ26-EOGTYKW-JP2EXAI";
test_device11.id = "2WWXVTN-Q3QWAAY-XFORMRM-2FDI5XZ-OGN33BD-XOLL42R-DHLT2ML-QYXDQAU";
};
# Generates a few folders with IDs and paths as written...
folders = lib.pipe 6 [
(builtins.genList (x: {
name = "/var/lib/syncthing/test_folder${builtins.toString x}";
value = {
id = "DontDeleteMe${builtins.toString x}";
};
}))
builtins.listToAttrs
];
};
# Non default options that we check later if were applied
settingsWithoutId = {
options = {
autoUpgradeIntervalH = 0;
urAccepted = -1;
};
gui = {
theme = "dark";
};
};
# Used later when checking whether settings were set in config.xml:
checkSettingWithId =
{
t, # t for type
id,
not ? false,
}:
''
print("Searching for a ${t} with id ${id}")
configVal_${t} = machine.succeed(
"${pkgs.libxml2}/bin/xmllint "
"--xpath 'string(//${t}[@id=\"${id}\"]/@id)' ${configPath}"
)
print("${t}.id = {}".format(configVal_${t}))
assert "${id}" ${if not then "not" else ""} in configVal_${t}
'';
# Same as checkSettingWithId, but for 'options' and 'gui'
checkSettingWithoutId =
{
t, # t for type
n, # n for name
v, # v for value
not ? false,
}:
''
print("checking whether setting ${t}.${n} is set to ${v}")
configVal_${t}_${n} = machine.succeed(
"${pkgs.libxml2}/bin/xmllint "
"--xpath 'string(/configuration/${t}/${n})' ${configPath}"
)
print("${t}.${n} = {}".format(configVal_${t}_${n}))
assert "${v}" ${if not then "not" else ""} in configVal_${t}_${n}
'';
# Removes duplication a bit to define this function for the IDs to delete -
# we check whether they were added after our script ran, and before the
# systemd unit's bash script ran, and afterwards - whether the systemd unit
# worked.
checkSettingsToDelete =
{
not,
}:
lib.pipe IDsToDelete [
(lib.mapAttrsToList (
t: id:
checkSettingWithId {
inherit t id;
inherit not;
}
))
lib.concatStrings
];
# These IDs are added to syncthing using the API, similarly to how the
# generated systemd unit's bash script does it. Only we add it and expect the
# systemd unit bash script to remove them when executed.
IDsToDelete = {
# Also created using the syncthing generate command above
device = "LZ2CTHT-3W2M7BC-CMKDFZL-DLUQJFS-WJR73PA-NZGODWG-DZBHCHI-OXTQXAK";
# Intentionally this is a substring of the IDs of the 'test_folder's, as
# explained in: https://github.com/NixOS/nixpkgs/issues/259256
folder = "DeleteMe";
};
addDeviceToDeleteScript = pkgs.writers.writeBash "syncthing-add-device-to-delete.sh" ''
set -euo pipefail
export RUNTIME_DIRECTORY=/tmp
curl() {
# get the api key by parsing the config.xml
while
! ${pkgs.libxml2}/bin/xmllint \
--xpath 'string(configuration/gui/apikey)' \
${configPath} \
>"$RUNTIME_DIRECTORY/api_key"
do sleep 1; done
(printf "X-API-Key: "; cat "$RUNTIME_DIRECTORY/api_key") >"$RUNTIME_DIRECTORY/headers"
${pkgs.curl}/bin/curl -sSLk -H "@$RUNTIME_DIRECTORY/headers" \
--retry 1000 --retry-delay 1 --retry-all-errors \
"$@"
}
curl -d ${lib.escapeShellArg (builtins.toJSON { deviceID = IDsToDelete.device; })} \
-X POST 127.0.0.1:8384/rest/config/devices
curl -d ${lib.escapeShellArg (builtins.toJSON { id = IDsToDelete.folder; })} \
-X POST 127.0.0.1:8384/rest/config/folders
'';
in
{
name = "syncthing-many-devices";
meta.maintainers = with lib.maintainers; [ doronbehar ];
nodes.machine = {
services.syncthing = {
enable = true;
overrideDevices = true;
overrideFolders = true;
settings = settingsWithoutId // settingsWithId;
};
};
testScript = ''
machine.wait_for_unit("syncthing-init.service")
''
+ (lib.pipe settingsWithId [
# Check that folders and devices were added properly and that all IDs exist
(lib.mapAttrsRecursive (
path: id:
checkSettingWithId {
# plural -> solitary
t = (lib.removeSuffix "s" (builtins.elemAt path 0));
inherit id;
}
))
# Get all the values we applied the above function upon
(lib.collect builtins.isString)
lib.concatStrings
])
+ (lib.pipe settingsWithoutId [
# Check that all other syncthing.settings were added properly with correct
# values
(lib.mapAttrsRecursive (
path: value:
checkSettingWithoutId {
t = (builtins.elemAt path 0);
n = (builtins.elemAt path 1);
v = (builtins.toString value);
}
))
# Get all the values we applied the above function upon
(lib.collect builtins.isString)
lib.concatStrings
])
+ ''
# Run the script on the machine
machine.succeed("${addDeviceToDeleteScript}")
''
+ (checkSettingsToDelete {
not = false;
})
+ ''
# Useful for debugging later
machine.copy_from_vm("${configPath}", "before")
machine.systemctl("restart syncthing-init.service")
machine.wait_for_unit("syncthing-init.service")
''
+ (checkSettingsToDelete {
not = true;
})
+ ''
# Useful for debugging later
machine.copy_from_vm("${configPath}", "after")
# Copy the systemd unit's bash script, to inspect it for debugging.
mergeScript = machine.succeed(
"systemctl cat syncthing-init.service | "
"${pkgs.initool}/bin/initool g - Service ExecStart --value-only"
).strip() # strip from new lines
machine.copy_from_vm(mergeScript, "")
'';
}

View File

@@ -0,0 +1,24 @@
{ lib, pkgs, ... }:
{
name = "syncthing";
meta.maintainers = with pkgs.lib.maintainers; [ chkno ];
nodes = {
a = {
environment.systemPackages = with pkgs; [
curl
libxml2
syncthing
];
services.syncthing = {
enable = true;
};
};
};
# Test that indeed a syncthing-init.service systemd service is not created.
#
testScript = # python
''
a.succeed("systemctl list-unit-files | awk '$1 == \"syncthing-init.service\" {exit 1;}'")
'';
}

View File

@@ -0,0 +1,27 @@
{ lib, pkgs, ... }:
{
name = "syncthing-relay";
meta.maintainers = [ ];
nodes.machine = {
environment.systemPackages = [ pkgs.jq ];
services.syncthing.relay = {
enable = true;
providedBy = "nixos-test";
pools = [ ]; # Don't connect to any pool while testing.
port = 12345;
statusPort = 12346;
};
};
testScript = ''
machine.wait_for_unit("syncthing-relay.service")
machine.wait_for_open_port(12345)
machine.wait_for_open_port(12346)
out = machine.succeed(
"curl -sSf http://localhost:12346/status | jq -r '.options.\"provided-by\"'"
)
assert "nixos-test" in out
'';
}