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,43 @@
{ pkgs, ... }:
{
name = "agorakit";
meta.maintainers = with pkgs.lib.maintainers; [ julienmalka ];
nodes = {
agorakit =
{ ... }:
{
services.agorakit = {
enable = true;
appKeyFile = toString (
pkgs.writeText "agorakit-app-key" "uTqGUN5GUmUrh/zSAYmhyzRk62pnpXICyXv9eeITI8k="
);
hostName = "localhost";
database.createLocally = true;
mail = {
driver = "smtp";
encryption = "tls";
host = "localhost";
port = 1025;
fromName = "Agorakit";
from = "agorakit@localhost";
user = "agorakit@localhost";
passwordFile = toString (pkgs.writeText "agorakit-mail-pass" "a-secure-mail-password");
};
};
};
};
testScript = ''
start_all()
agorakit.wait_for_unit("nginx.service")
agorakit.wait_for_unit("agorakit-setup.service")
# Login page should now contain the configured site name
agorakit.succeed("curl http://localhost/login | grep Agorakit")
'';
}

View File

@@ -0,0 +1,75 @@
{ pkgs, lib, ... }:
let
customSettings = {
pageInfo = {
title = "My Custom Dashy Title";
};
sections = [
{
name = "My Section";
items = [
{
name = "NixOS";
url = "https://nixos.org";
}
];
}
];
};
customSettingsYaml = (pkgs.formats.yaml_1_1 { }).generate "custom-conf.yaml" customSettings;
in
{
name = "dashy";
meta.maintainers = [ lib.maintainers.therealgramdalf ];
defaults =
{ config, ... }:
{
services.dashy = {
enable = true;
virtualHost = {
enableNginx = true;
domain = "dashy.local";
};
};
networking.extraHosts = "127.0.0.1 dashy.local";
services.nginx.virtualHosts."${config.services.dashy.virtualHost.domain}".listen = [
{
addr = "127.0.0.1";
port = 80;
}
];
};
nodes = {
machine = { };
machine-custom = {
services.dashy.settings = customSettings;
};
};
testScript = ''
start_all()
machine.wait_for_unit("nginx.service")
machine.wait_for_open_port(80)
actual = machine.succeed("curl -v --stderr - http://dashy.local/", timeout=10)
expected = "<title>Dashy</title>"
assert expected in actual, \
f"unexpected reply from Dashy, expected: '{expected}' got: '{actual}'"
machine_custom.wait_for_unit("nginx.service")
machine_custom.wait_for_open_port(80)
actual_custom = machine_custom.succeed("curl -s --stderr - http://dashy.local/conf.yml", timeout=10).strip()
expected_custom = machine_custom.succeed("cat ${customSettingsYaml}").strip()
assert expected_custom == actual_custom, \
f"unexpected reply from Dashy, expected: '{expected_custom}' got: '{actual_custom}'"
'';
}

View File

@@ -0,0 +1,44 @@
{
lib,
pkgs,
...
}:
{
name = "froide-govplan";
meta.maintainers = with lib.maintainers; [ onny ];
nodes = {
machine = {
virtualisation.memorySize = 2048;
services.froide-govplan.enable = true;
};
};
testScript =
let
changePassword = pkgs.writeText "change-password.py" ''
from django.contrib.auth.models import User
u = User.objects.get(username='govplan')
u.set_password('govplan')
u.save()
'';
in
''
start_all()
machine.wait_for_unit("froide-govplan.service")
machine.wait_for_file("/run/froide-govplan/froide-govplan.socket")
with subtest("Home screen loads"):
machine.succeed(
"curl -sSfL http://[::1]:80 | grep '<title>django CMS</title>'"
)
with subtest("Superuser can be created"):
machine.succeed(
"froide-govplan createsuperuser --noinput --username govplan --email govplan@example.com"
)
# Django doesn't have a "clean" way of inputting the password from the command line
machine.succeed("cat '${changePassword}' | froide-govplan shell")
'';
}

View File

@@ -0,0 +1,32 @@
{ lib, ... }:
{
name = "gotosocial";
meta.maintainers = with lib.maintainers; [ blakesmith ];
nodes.machine =
{ pkgs, ... }:
{
environment.systemPackages = [ pkgs.jq ];
services.gotosocial = {
enable = true;
setupPostgresqlDB = true;
settings = {
host = "localhost:8081";
port = 8081;
instance-stats-mode = "serve";
};
};
};
testScript = ''
machine.wait_for_unit("gotosocial.service")
machine.wait_for_unit("postgresql.target")
machine.wait_for_open_port(8081)
# Database migrations are running, wait until gotosocial no longer serves 503
machine.wait_until_succeeds("curl -sS -f http://localhost:8081/readyz", timeout=300)
# check user registration via cli
machine.succeed("gotosocial-admin account create --username nickname --email email@example.com --password kurtz575VPeBgjVm")
machine.wait_until_succeeds("curl -sS -f http://localhost:8081/nodeinfo/2.0 | jq '.usage.users.total' | grep -q '^1$'")
'';
}

View File

@@ -0,0 +1,25 @@
{ pkgs, ... }:
{
name = "grav";
nodes = {
machine =
{ pkgs, ... }:
{
services.grav.enable = true;
};
};
testScript = ''
start_all()
machine.wait_for_unit("phpfpm-grav.service")
machine.wait_for_open_port(80)
# The first request to a fresh install should result in a redirect to the
# admin page, where the user is expected to set up an admin user.
actual = machine.succeed("curl -v --stderr - http://localhost/", timeout=10).splitlines()
expected = "< Location: /admin"
assert expected in actual, \
f"unexpected reply from Grav: '{actual}'"
'';
}

View File

@@ -0,0 +1,49 @@
{ lib, pkgs, ... }:
{
name = "healthchecks";
meta = with lib.maintainers; {
maintainers = [ phaer ];
};
nodes.machine =
{ ... }:
{
services.healthchecks = {
enable = true;
settings = {
SITE_NAME = "MyUniqueInstance";
COMPRESS_ENABLED = "True";
SECRET_KEY_FILE = pkgs.writeText "secret" "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
};
};
};
testScript = ''
machine.start()
machine.wait_for_unit("healthchecks.target")
machine.wait_until_succeeds("journalctl --since -1m --unit healthchecks --grep Listening")
with subtest("Home screen loads"):
machine.succeed(
"curl -sSfL http://localhost:8000 | grep '<title>Log In'"
)
with subtest("Setting SITE_NAME via freeform option works"):
machine.succeed(
"curl -sSfL http://localhost:8000 | grep 'MyUniqueInstance</title>'"
)
with subtest("Manage script works"):
# "shell" sucommand should succeed, needs python in PATH.
t.assertIn(
"\nfoo\n",
machine.succeed("echo 'print(\"foo\")' | sudo -u healthchecks healthchecks-manage shell")
)
# Shouldn't fail if not called by healthchecks user
t.assertIn(
"\nfoo\n",
machine.succeed("echo 'print(\"foo\")' | healthchecks-manage shell")
)
'';
}

View File

@@ -0,0 +1,106 @@
{ pkgs, lib, ... }:
{
name = "immich-public-proxy";
nodes.machine =
{ pkgs, ... }@args:
{
environment.systemPackages = [
pkgs.imagemagick
pkgs.immich-cli
];
services.immich = {
enable = true;
port = 2283;
# disable a lot of features that aren't needed for this test
machine-learning.enable = false;
settings = {
backup.database.enabled = false;
machineLearning.enabled = false;
map.enabled = false;
reverseGeocoding.enabled = false;
metadata.faces.import = false;
newVersionCheck.enabled = false;
notifications.smtp.enabled = false;
};
};
services.immich-public-proxy = {
enable = true;
immichUrl = "http://localhost:2283";
port = 8002;
settings.ipp.responseHeaders."X-NixOS" = "Rules";
};
# TODO: Remove when PostgreSQL 17 is supported.
services.postgresql.package = pkgs.postgresql_16;
};
testScript = ''
import json
machine.wait_for_unit("immich-server.service")
machine.wait_for_unit("immich-public-proxy.service")
machine.wait_for_open_port(2283)
machine.wait_for_open_port(8002)
# The proxy should be up
machine.succeed("curl -sf http://localhost:8002")
# Verify the static assets are served
machine.succeed("curl -sf http://localhost:8002/robots.txt")
machine.succeed("curl -sf http://localhost:8002/share/static/style.css")
# Check that the response header in the settings is sent
res = machine.succeed("""
curl -sD - http://localhost:8002 -o /dev/null
""")
assert "x-nixos: rules" in res.lower(), res
# Log in to Immich and create an access key
machine.succeed("""
curl -sf --json '{ "email": "test@example.com", "name": "Admin", "password": "admin" }' http://localhost:2283/api/auth/admin-sign-up
""")
res = machine.succeed("""
curl -sf --json '{ "email": "test@example.com", "password": "admin" }' http://localhost:2283/api/auth/login
""")
token = json.loads(res)['accessToken']
res = machine.succeed("""
curl -sf -H 'Cookie: immich_access_token=%s' --json '{ "name": "API Key", "permissions": ["all"] }' http://localhost:2283/api/api-keys
""" % token)
key = json.loads(res)['secret']
machine.succeed(f"immich login http://localhost:2283/api {key}")
res = machine.succeed("immich server-info")
print(res)
# Upload some blank images to a new album
# If there's only one image, the proxy serves the image directly
machine.succeed("magick -size 800x600 canvas:white /tmp/white.png")
machine.succeed("immich upload -A ' Reproducible Moments ' /tmp/white.png")
machine.succeed("magick -size 800x600 canvas:black /tmp/black.png")
machine.succeed("immich upload -A ' Reproducible Moments ' /tmp/black.png")
res = machine.succeed("immich server-info")
print(res)
# Get the new album id
res = machine.succeed("""
curl -sf -H 'Cookie: immich_access_token=%s' http://localhost:2283/api/albums
""" % token)
album_id = json.loads(res)[0]['id']
# Create a shared link
res = machine.succeed("""
curl -sf -H 'Cookie: immich_access_token=%s' --json '{ "albumId": "%s", "type": "ALBUM" }' http://localhost:2283/api/shared-links
""" % (token, album_id))
share_key = json.loads(res)['key']
# Access the share
machine.succeed("""
curl -sf http://localhost:2283/share/%s
""" % share_key)
# Access the share through the proxy
machine.succeed("""
curl -sf http://localhost:8002/share/%s
""" % share_key)
'';
}

View File

@@ -0,0 +1,71 @@
{ ... }:
{
name = "immich-vectorchord-migration";
nodes.machine =
{ lib, pkgs, ... }:
{
# These tests need a little more juice
virtualisation = {
cores = 2;
memorySize = 2048;
diskSize = 4096;
};
environment.systemPackages = with pkgs; [ immich-cli ];
services.immich = {
enable = true;
environment.IMMICH_LOG_LEVEL = "verbose";
# Simulate an existing setup
database.enableVectorChord = lib.mkDefault false;
database.enableVectors = lib.mkDefault true;
};
# TODO: Remove when PostgreSQL 17 is supported.
services.postgresql.package = pkgs.postgresql_16;
specialisation."immich-vectorchord-enabled".configuration = {
services.immich.database.enableVectorChord = true;
};
specialisation."immich-vectorchord-only".configuration = {
services.immich.database = {
enableVectorChord = true;
enableVectors = false;
};
};
};
testScript =
{ nodes, ... }:
let
specBase = "${nodes.machine.system.build.toplevel}/specialisation";
vectorchordEnabled = "${specBase}/immich-vectorchord-enabled";
vectorchordOnly = "${specBase}/immich-vectorchord-only";
in
''
def psql(command: str):
machine.succeed(f"sudo -u postgres psql -d ${nodes.machine.services.immich.database.name} -c '{command}'")
def immich_works():
machine.wait_for_unit("immich-server.service")
machine.wait_for_open_port(2283) # Server
machine.wait_for_open_port(3003) # Machine learning
machine.succeed("curl --fail http://localhost:2283/")
immich_works()
machine.succeed("${vectorchordEnabled}/bin/switch-to-configuration test")
immich_works()
psql("DROP EXTENSION vectors;")
psql("DROP SCHEMA vectors;")
machine.succeed("${vectorchordOnly}/bin/switch-to-configuration test")
immich_works()
'';
}

View File

@@ -0,0 +1,69 @@
{ ... }:
{
name = "immich-nixos";
nodes.machine =
{ pkgs, ... }:
{
# These tests need a little more juice
virtualisation = {
cores = 2;
memorySize = 2048;
diskSize = 4096;
};
environment.systemPackages = with pkgs; [ immich-cli ];
services.immich = {
enable = true;
environment.IMMICH_LOG_LEVEL = "verbose";
settings.backup.database = {
enabled = true;
cronExpression = "invalid";
};
secretSettings = {
backup.database.cronExpression = "${pkgs.writeText "cron" "0 02 * * *"}";
# thanks to LoadCredential files only readable by root should work
notifications.smtp.transport.password = "/etc/shadow";
};
};
};
testScript = ''
import json
machine.wait_for_unit("immich-server.service")
machine.succeed("stat -L -c '%a %U %G' /run/immich/config.json | grep '600 immich immich'")
machine.wait_for_open_port(2283) # Server
machine.wait_for_open_port(3003) # Machine learning
machine.succeed("curl --fail http://localhost:2283/")
machine.succeed("""
curl -f --json '{ "email": "test@example.com", "name": "Admin", "password": "admin" }' http://localhost:2283/api/auth/admin-sign-up
""")
res = machine.succeed("""
curl -f --json '{ "email": "test@example.com", "password": "admin" }' http://localhost:2283/api/auth/login
""")
token = json.loads(res)['accessToken']
res = machine.succeed("""
curl -f -H 'Cookie: immich_access_token=%s' --json '{ "name": "API Key", "permissions": ["all"] }' http://localhost:2283/api/api-keys
""" % token)
key = json.loads(res)['secret']
machine.succeed(f"immich login http://localhost:2283/api {key}")
res = machine.succeed("immich server-info")
print(res)
machine.succeed("""
curl -f -X PUT -H 'Cookie: immich_access_token=%s' --json '{ "command": "start" }' http://localhost:2283/api/jobs/backupDatabase
""" % token)
res = machine.succeed("""
curl -f -H 'Cookie: immich_access_token=%s' http://localhost:2283/api/jobs
""" % token)
assert sum(json.loads(res)["backupDatabase"]["jobCounts"].values()) >= 1
machine.wait_until_succeeds("ls /var/lib/immich/backups/*.sql.gz")
'';
}

View File

@@ -0,0 +1,23 @@
{ lib, ... }:
{
name = "kanboard";
meta.maintainers = with lib.maintainers; [ yzx9 ];
nodes = {
machine = {
services.kanboard = {
enable = true;
};
};
};
testScript = ''
start_all()
machine.wait_for_unit("nginx.service")
machine.wait_for_unit("phpfpm-kanboard.service")
machine.wait_for_open_port(80)
machine.succeed("curl -k --fail http://localhost", timeout=10)
'';
}

View File

@@ -0,0 +1,168 @@
{ lib, ... }:
let
domain = "docs.local";
oidcDomain = "127.0.0.1:8080";
s3Domain = "127.0.0.1:9000";
minioAccessKey = "a8dff633d164068418a5";
minioSecretKey = "d546ea5f9c9bfdcf83755a7c09f2f7fb";
in
{
name = "lasuite-docs";
meta.maintainers = with lib.maintainers; [
soyouzpanda
];
nodes.machine =
{ pkgs, ... }:
{
virtualisation.diskSize = 4 * 1024;
virtualisation.memorySize = 4 * 1024;
networking.hosts."127.0.0.1" = [ domain ];
environment.systemPackages = with pkgs; [
jq
minio-client
];
services.lasuite-docs = {
enable = true;
enableNginx = true;
redis.createLocally = true;
postgresql.createLocally = true;
inherit domain;
s3Url = "http://${s3Domain}/lasuite-docs";
settings = {
DJANGO_SECRET_KEY_FILE = pkgs.writeText "django-secret-file" ''
8540db59c03943d48c3ed1a0f96ce3b560e0f45274f120f7ee4dace3cc366a6b
'';
OIDC_OP_JWKS_ENDPOINT = "http://${oidcDomain}/dex/keys";
OIDC_OP_AUTHORIZATION_ENDPOINT = "http://${oidcDomain}/dex/auth/mock";
OIDC_OP_TOKEN_ENDPOINT = "http://${oidcDomain}/dex/token";
OIDC_OP_USER_ENDPOINT = "http://${oidcDomain}/dex/userinfo";
OIDC_RP_CLIENT_ID = "lasuite-docs";
OIDC_RP_SIGN_ALGO = "RS256";
OIDC_RP_SCOPES = "openid email";
OIDC_RP_CLIENT_SECRET = "lasuitedocsclientsecret";
LOGIN_REDIRECT_URL = "http://${domain}";
LOGIN_REDIRECT_URL_FAILURE = "http://${domain}";
LOGOUT_REDIRECT_URL = "http://${domain}";
AWS_S3_ENDPOINT_URL = "http://${s3Domain}";
AWS_S3_ACCESS_KEY_ID = minioAccessKey;
AWS_S3_SECRET_ACCESS_KEY = minioSecretKey;
AWS_STORAGE_BUCKET_NAME = "lasuite-docs";
MEDIA_BASE_URL = "http://${domain}";
# Disable HTTPS feature in tests because we're running on a HTTP connection
DJANGO_SECURE_PROXY_SSL_HEADER = "";
DJANGO_SECURE_SSL_REDIRECT = false;
DJANGO_CSRF_COOKIE_SECURE = false;
DJANGO_SESSION_COOKIE_SECURE = false;
DJANGO_CSRF_TRUSTED_ORIGINS = "http://*";
};
};
services.dex = {
enable = true;
settings = {
issuer = "http://${oidcDomain}/dex";
storage = {
type = "postgres";
config.host = "/var/run/postgresql";
};
web.http = "127.0.0.1:8080";
oauth2.skipApprovalScreen = true;
staticClients = [
{
id = "lasuite-docs";
name = "Docs";
redirectURIs = [ "http://${domain}/api/v1.0/callback/" ];
secretFile = "/etc/dex/lasuite-docs";
}
];
connectors = [
{
type = "mockPassword";
id = "mock";
name = "Example";
config = {
username = "admin";
password = "password";
};
}
];
};
};
services.minio = {
enable = true;
rootCredentialsFile = "/etc/minio/minio-root-credentials";
};
environment.etc."dex/lasuite-docs" = {
mode = "0400";
user = "dex";
text = "lasuitedocsclientsecret";
};
environment.etc."minio/minio-root-credentials" = {
mode = "0400";
text = ''
MINIO_ROOT_USER=${minioAccessKey}
MINIO_ROOT_PASSWORD=${minioSecretKey}
'';
};
services.postgresql = {
enable = true;
ensureDatabases = [ "dex" ];
ensureUsers = [
{
name = "dex";
ensureDBOwnership = true;
}
];
};
};
testScript = ''
with subtest("Wait for units to start"):
machine.wait_for_unit("dex.service")
machine.wait_for_unit("minio.service")
machine.wait_for_unit("lasuite-docs.service")
machine.wait_for_unit("lasuite-docs-celery.service")
machine.wait_for_unit("lasuite-docs-collaboration-server.service")
with subtest("Create S3 bucket"):
machine.succeed("mc alias set minio http://${s3Domain} ${minioAccessKey} ${minioSecretKey} --api s3v4")
machine.succeed("mc mb lasuite-docs")
with subtest("Wait for web servers to start"):
machine.wait_until_succeeds("curl -fs 'http://${domain}/api/v1.0/authenticate/'", timeout=120)
machine.wait_until_succeeds("curl -fs '${oidcDomain}/dex/auth/mock?client_id=lasuite-docs&response_type=code&redirect_uri=http://${domain}/api/v1.0/callback/&scope=openid'", timeout=120)
with subtest("Login"):
state, nonce = machine.succeed("curl -fs -c cjar 'http://${domain}/api/v1.0/authenticate/' -w '%{redirect_url}' | sed -n 's/.*state=\\(.*\\)&nonce=\\(.*\\)/\\1 \\2/p'").strip().split(' ')
oidc_state = machine.succeed(f"curl -fs '${oidcDomain}/dex/auth/mock?client_id=lasuite-docs&response_type=code&redirect_uri=http://${domain}/api/v1.0/callback/&scope=openid+email&state={state}&nonce={nonce}' | sed -n 's/.*state=\\(.*\\)\">.*/\\1/p'").strip()
code = machine.succeed(f"curl -fs '${oidcDomain}/dex/auth/mock/login?back=&state={oidc_state}' -d 'login=admin&password=password' -w '%{{redirect_url}}' | sed -n 's/.*code=\\(.*\\)&.*/\\1/p'").strip()
print(f"Got approval code {code}")
machine.succeed(f"curl -fs -c cjar -b cjar 'http://${domain}/api/v1.0/callback/?code={code}&state={state}'")
with subtest("Create a document"):
csrf_token = machine.succeed("grep csrftoken cjar | cut -f 7 | tr -d '\n'")
document_id = machine.succeed(f"curl -fs -c cjar -b cjar 'http://${domain}/api/v1.0/documents/' -X POST -H 'X-CSRFToken: {csrf_token}' -H 'Referer: http://${domain}' | jq .id -r").strip()
print(f"Created document with id {document_id}")
'';
}

View File

@@ -0,0 +1,141 @@
{ lib, ... }:
let
domain = "meet.local";
oidcDomain = "127.0.0.1:8080";
in
{
name = "lasuite-meet";
meta.maintainers = with lib.maintainers; [ soyouzpanda ];
nodes.machine =
{ pkgs, ... }:
{
virtualisation.memorySize = 4 * 1024;
networking.hosts."127.0.0.1" = [ domain ];
environment.systemPackages = with pkgs; [ jq ];
services.lasuite-meet = {
enable = true;
enableNginx = true;
livekit = {
enable = true;
keyFile = pkgs.writeText "lasuite-meet-livekit-keys" ''
lasuite-meet: ca50qKzxEXVIu61wHshAyJNzlWw8vlIwUuzxQbUK1rG
'';
};
redis.createLocally = true;
postgresql.createLocally = true;
inherit domain;
settings = {
DJANGO_SECRET_KEY_FILE = pkgs.writeText "django-secret-file" ''
8540db59c03943d48c3ed1a0f96ce3b560e0f45274f120f7ee4dace3cc366a6b
'';
OIDC_OP_JWKS_ENDPOINT = "http://${oidcDomain}/dex/keys";
OIDC_OP_AUTHORIZATION_ENDPOINT = "http://${oidcDomain}/dex/auth/mock";
OIDC_OP_TOKEN_ENDPOINT = "http://${oidcDomain}/dex/token";
OIDC_OP_USER_ENDPOINT = "http://${oidcDomain}/dex/userinfo";
OIDC_RP_CLIENT_ID = "lasuite-meet";
OIDC_RP_SIGN_ALGO = "RS256";
OIDC_RP_SCOPES = "openid email";
OIDC_RP_CLIENT_SECRET = "lasuitemeetclientsecret";
LOGIN_REDIRECT_URL = "http://${domain}";
LOGIN_REDIRECT_URL_FAILURE = "http://${domain}";
LOGOUT_REDIRECT_URL = "http://${domain}";
LIVEKIT_API_KEY = "lasuite-meet";
LIVEKIT_API_SECRET = "ca50qKzxEXVIu61wHshAyJNzlWw8vlIwUuzxQbUK1rG";
# Disable HTTPS feature in tests because we're running on a HTTP connection
DJANGO_SECURE_PROXY_SSL_HEADER = "";
DJANGO_SECURE_SSL_REDIRECT = false;
DJANGO_CSRF_COOKIE_SECURE = false;
DJANGO_SESSION_COOKIE_SECURE = false;
DJANGO_CSRF_TRUSTED_ORIGINS = "http://*";
};
};
services.dex = {
enable = true;
settings = {
issuer = "http://${oidcDomain}/dex";
storage = {
type = "postgres";
config.host = "/var/run/postgresql";
};
web.http = "127.0.0.1:8080";
oauth2.skipApprovalScreen = true;
staticClients = [
{
id = "lasuite-meet";
name = "Meet";
redirectURIs = [ "http://${domain}/api/v1.0/callback/" ];
secretFile = "/etc/dex/lasuite-meet";
}
];
connectors = [
{
type = "mockPassword";
id = "mock";
name = "Example";
config = {
username = "admin";
password = "password";
};
}
];
};
};
environment.etc."dex/lasuite-meet" = {
mode = "0400";
user = "dex";
text = "lasuitemeetclientsecret";
};
services.postgresql = {
enable = true;
ensureDatabases = [ "dex" ];
ensureUsers = [
{
name = "dex";
ensureDBOwnership = true;
}
];
};
};
testScript = ''
with subtest("Wait for units to start"):
machine.wait_for_unit("dex.service")
machine.wait_for_unit("lasuite-meet.service")
machine.wait_for_unit("lasuite-meet-celery.service")
with subtest("Wait for web servers to start"):
machine.wait_until_succeeds("curl -fs 'http://${domain}/api/v1.0/authenticate/'", timeout=120)
machine.wait_until_succeeds("curl -fs '${oidcDomain}/dex/auth/mock?client_id=lasuite-meet&response_type=code&redirect_uri=http://${domain}/api/v1.0/callback/&scope=openid'", timeout=120)
with subtest("Login"):
state, nonce = machine.succeed("curl -fs -c cjar 'http://${domain}/api/v1.0/authenticate/' -w '%{redirect_url}' | sed -n 's/.*state=\\(.*\\)&nonce=\\(.*\\)/\\1 \\2/p'").strip().split(' ')
oidc_state = machine.succeed(f"curl -fs '${oidcDomain}/dex/auth/mock?client_id=lasuite-meet&response_type=code&redirect_uri=http://${domain}/api/v1.0/callback/&scope=openid+email&state={state}&nonce={nonce}' | sed -n 's/.*state=\\(.*\\)\">.*/\\1/p'").strip()
code = machine.succeed(f"curl -fs '${oidcDomain}/dex/auth/mock/login?back=&state={oidc_state}' -d 'login=admin&password=password' -w '%{{redirect_url}}' | sed -n 's/.*code=\\(.*\\)&.*/\\1/p'").strip()
print(f"Got approval code {code}")
machine.succeed(f"curl -fs -c cjar -b cjar 'http://${domain}/api/v1.0/callback/?code={code}&state={state}'")
with subtest("Create a room"):
csrf_token = machine.succeed("grep csrftoken cjar | cut -f 7 | tr -d '\n'")
room_id = machine.succeed(f"curl -fs -c cjar -b cjar 'http://${domain}/api/v1.0/rooms/' -X POST -H 'Content-Type: application/json' -H 'X-CSRFToken: {csrf_token}' -H 'Referer: http://${domain}' -d '{{\"name\": \"aaa-bbbb-ccc\"}}' | jq .id -r").strip()
print(f"Created room with id {room_id}")
'';
}

View File

@@ -0,0 +1,60 @@
{ ... }:
{
name = "linkwarden-nixos";
nodes.machine =
{ pkgs, ... }:
let
secretsFile = pkgs.writeText "linkwarden-secret-env" ''
VERY_SENSITIVE_SECRET
'';
webroot = pkgs.runCommand "webroot" { } ''
mkdir $out
cd $out
echo '<!DOCTYPE html><html><body><h1>HELLO LINKWARDEN</h1></body></html>' > index.html
'';
in
{
services.linkwarden = {
enable = true;
enableRegistration = true;
secretFiles = {
NEXTAUTH_SECRET = toString secretsFile;
};
environment = {
NEXTAUTH_URL = "http://localhost:3000/api/v1/auth";
};
};
services.nginx = {
enable = true;
virtualHosts.localhost.root = webroot;
};
};
testScript = ''
import json
machine.wait_for_unit("linkwarden.service")
machine.wait_for_unit("linkwarden-worker.service")
machine.wait_for_open_port(3000)
machine.succeed("curl --fail -s http://localhost:3000/")
machine.succeed("curl -L --fail -s --data '{\"name\":\"Admin\",\"username\":\"admin\",\"password\":\"adminadmin\"}' -H 'Content-Type: application/json' -X POST http://localhost:3000/api/v1/users")
response = machine.succeed("curl -L --fail -s -c next_cookies.txt -H 'Content-Type: application/json' -X GET http://localhost:3000/api/v1/auth/csrf")
csrfToken = json.loads(response)['csrfToken']
machine.succeed("curl -L --fail -s -b next_cookies.txt -c next_cookies.txt -H 'Content-Type: application/x-www-form-urlencoded' --data-urlencode 'username=admin' --data-urlencode 'password=adminadmin' --data-urlencode 'csrfToken=%s' http://localhost:3000/api/v1/auth/callback/credentials" % csrfToken)
curlCmd = "curl -L --fail -s -b next_cookies.txt -H 'Content-Type: application/json' "
machine.succeed(curlCmd + "--data '{\"url\":\"http://localhost/\"}' -X POST http://localhost:3000/api/v1/links")
machine.succeed(curlCmd + "-X GET http://localhost:3000/api/v1/links")
machine.wait_for_file("/var/lib/linkwarden/archives/1/1.html")
machine.succeed("grep -q '<h1>HELLO LINKWARDEN</h1>' </var/lib/linkwarden/archives/1/1.html")
'';
}

View File

@@ -0,0 +1,18 @@
{
system ? builtins.currentSystem,
pkgs,
handleTestOn,
...
}:
let
supportedSystems = [
"x86_64-linux"
"i686-linux"
"aarch64-linux"
];
in
{
standard = handleTestOn supportedSystems ./standard.nix { inherit system pkgs; };
remote-databases = handleTestOn supportedSystems ./remote-databases.nix { inherit system pkgs; };
}

View File

@@ -0,0 +1,220 @@
import ../../make-test-python.nix (
{ pkgs, ... }:
let
cert =
pkgs:
pkgs.runCommand "selfSignedCerts" { buildInputs = [ pkgs.openssl ]; } ''
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -nodes -subj '/CN=mastodon.local' -days 36500
mkdir -p $out
cp key.pem cert.pem $out
'';
hosts = ''
192.168.2.103 mastodon.local
'';
postgresqlPassword = "thisisnotasecret";
redisPassword = "thisisnotasecrettoo";
in
{
name = "mastodon-remote-postgresql";
meta.maintainers = with pkgs.lib.maintainers; [
erictapen
izorkin
];
nodes = {
databases =
{ config, ... }:
{
environment = {
etc = {
"redis/password-redis-db".text = redisPassword;
};
};
networking = {
interfaces.eth1 = {
ipv4.addresses = [
{
address = "192.168.2.102";
prefixLength = 24;
}
];
};
extraHosts = hosts;
firewall.allowedTCPPorts = [
config.services.redis.servers.mastodon.port
config.services.postgresql.settings.port
];
};
services.redis.servers.mastodon = {
enable = true;
bind = "0.0.0.0";
port = 31637;
requirePassFile = "/etc/redis/password-redis-db";
};
services.postgresql = {
enable = true;
enableTCPIP = true;
authentication = ''
hostnossl mastodon mastodon 192.168.2.201/32 md5
'';
ensureDatabases = [ "mastodon" ];
ensureUsers = [
{
name = "mastodon";
ensureDBOwnership = true;
}
];
initialScript = pkgs.writeText "postgresql_init.sql" ''
CREATE ROLE mastodon LOGIN PASSWORD '${postgresqlPassword}';
'';
};
};
nginx =
{ nodes, ... }:
{
networking = {
interfaces.eth1 = {
ipv4.addresses = [
{
address = "192.168.2.103";
prefixLength = 24;
}
];
};
extraHosts = hosts;
firewall.allowedTCPPorts = [
80
443
];
};
security = {
pki.certificateFiles = [ "${cert pkgs}/cert.pem" ];
};
services.nginx = {
enable = true;
recommendedProxySettings = true;
virtualHosts."mastodon.local" = {
root = "/var/empty";
forceSSL = true;
enableACME = pkgs.lib.mkForce false;
sslCertificate = "${cert pkgs}/cert.pem";
sslCertificateKey = "${cert pkgs}/key.pem";
locations."/" = {
tryFiles = "$uri @proxy";
};
locations."@proxy" = {
proxyPass = "http://192.168.2.201:${toString nodes.server.services.mastodon.webPort}";
proxyWebsockets = true;
};
};
};
};
server =
{ config, pkgs, ... }:
{
virtualisation.memorySize = 2048;
environment = {
etc = {
"mastodon/password-redis-db".text = redisPassword;
"mastodon/password-posgressql-db".text = postgresqlPassword;
};
};
networking = {
interfaces.eth1 = {
ipv4.addresses = [
{
address = "192.168.2.201";
prefixLength = 24;
}
];
};
extraHosts = hosts;
firewall.allowedTCPPorts = [
config.services.mastodon.webPort
config.services.mastodon.sidekiqPort
];
};
services.mastodon = {
enable = true;
configureNginx = false;
localDomain = "mastodon.local";
enableUnixSocket = false;
streamingProcesses = 2;
redis = {
createLocally = false;
host = "192.168.2.102";
port = 31637;
passwordFile = "/etc/mastodon/password-redis-db";
};
database = {
createLocally = false;
host = "192.168.2.102";
port = 5432;
name = "mastodon";
user = "mastodon";
passwordFile = "/etc/mastodon/password-posgressql-db";
};
smtp = {
createLocally = false;
fromAddress = "mastodon@mastodon.local";
};
extraConfig = {
BIND = "0.0.0.0";
EMAIL_DOMAIN_ALLOWLIST = "example.com";
RAILS_SERVE_STATIC_FILES = "true";
TRUSTED_PROXY_IP = "192.168.2.103";
};
};
};
client =
{ pkgs, ... }:
{
environment.systemPackages = [ pkgs.jq ];
networking = {
interfaces.eth1 = {
ipv4.addresses = [
{
address = "192.168.2.202";
prefixLength = 24;
}
];
};
extraHosts = hosts;
};
security = {
pki.certificateFiles = [ "${cert pkgs}/cert.pem" ];
};
};
};
testScript = import ./script.nix {
inherit pkgs;
extraInit = ''
nginx.wait_for_unit("nginx.service")
nginx.wait_for_open_port(443)
databases.wait_for_unit("redis-mastodon.service")
databases.wait_for_unit("postgresql.target")
databases.wait_for_open_port(31637)
databases.wait_for_open_port(5432)
'';
extraShutdown = ''
nginx.shutdown()
databases.shutdown()
'';
};
}
)

View File

@@ -0,0 +1,53 @@
{
pkgs,
extraInit ? "",
extraShutdown ? "",
}:
''
start_all()
${extraInit}
server.wait_for_unit("mastodon-sidekiq-all.service")
server.wait_for_unit("mastodon-streaming.target")
server.wait_for_unit("mastodon-web.service")
server.wait_for_open_port(55001)
# Check that mastodon-media-auto-remove is scheduled
server.succeed("systemctl status mastodon-media-auto-remove.timer")
# Check Mastodon version from remote client
client.succeed("curl --fail https://mastodon.local/api/v1/instance | jq -r '.version' | grep '${pkgs.mastodon.version}'")
# Check access from remote client
client.succeed("curl --fail https://mastodon.local/about | grep 'Mastodon hosted on mastodon.local'")
client.succeed("curl --fail $(curl https://mastodon.local/api/v1/instance 2> /dev/null | jq -r .thumbnail) --output /dev/null")
# Simple check tootctl commands
# Check Mastodon version
server.succeed("mastodon-tootctl version | grep '${pkgs.mastodon.version}'")
# Manage accounts
server.succeed("mastodon-tootctl email_domain_blocks add example.com")
server.succeed("mastodon-tootctl email_domain_blocks list | grep example.com")
server.fail("mastodon-tootctl email_domain_blocks list | grep mastodon.local")
server.fail("mastodon-tootctl accounts create alice --email=alice@example.com")
server.succeed("mastodon-tootctl email_domain_blocks remove example.com")
server.succeed("mastodon-tootctl accounts create bob --email=bob@example.com")
server.succeed("mastodon-tootctl accounts approve bob")
server.succeed("mastodon-tootctl accounts delete bob")
# Manage IP access
server.succeed("mastodon-tootctl ip_blocks add 192.168.0.0/16 --severity=no_access")
server.succeed("mastodon-tootctl ip_blocks export | grep 192.168.0.0/16")
server.fail("mastodon-tootctl ip_blocks export | grep 172.16.0.0/16")
client.fail("curl --fail https://mastodon.local/about")
server.succeed("mastodon-tootctl ip_blocks remove 192.168.0.0/16")
client.succeed("curl --fail https://mastodon.local/about")
server.shutdown()
client.shutdown()
${extraShutdown}
''

View File

@@ -0,0 +1,109 @@
import ../../make-test-python.nix (
{ pkgs, ... }:
let
cert =
pkgs:
pkgs.runCommand "selfSignedCerts" { buildInputs = [ pkgs.openssl ]; } ''
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -nodes -subj '/CN=mastodon.local' -days 36500
mkdir -p $out
cp key.pem cert.pem $out
'';
hosts = ''
192.168.2.101 mastodon.local
'';
in
{
name = "mastodon-standard";
meta.maintainers = with pkgs.lib.maintainers; [
erictapen
izorkin
turion
];
nodes = {
server =
{ pkgs, ... }:
{
virtualisation.memorySize = 2048;
networking = {
interfaces.eth1 = {
ipv4.addresses = [
{
address = "192.168.2.101";
prefixLength = 24;
}
];
};
extraHosts = hosts;
firewall.allowedTCPPorts = [
80
443
];
};
security = {
pki.certificateFiles = [ "${cert pkgs}/cert.pem" ];
};
services.mastodon = {
enable = true;
configureNginx = true;
localDomain = "mastodon.local";
enableUnixSocket = false;
streamingProcesses = 2;
smtp = {
createLocally = false;
fromAddress = "mastodon@mastodon.local";
};
extraConfig = {
EMAIL_DOMAIN_ALLOWLIST = "example.com";
};
};
services.nginx = {
virtualHosts."mastodon.local" = {
enableACME = pkgs.lib.mkForce false;
sslCertificate = "${cert pkgs}/cert.pem";
sslCertificateKey = "${cert pkgs}/key.pem";
};
};
};
client =
{ pkgs, ... }:
{
environment.systemPackages = [ pkgs.jq ];
networking = {
interfaces.eth1 = {
ipv4.addresses = [
{
address = "192.168.2.102";
prefixLength = 24;
}
];
};
extraHosts = hosts;
};
security = {
pki.certificateFiles = [ "${cert pkgs}/cert.pem" ];
};
};
};
testScript = import ./script.nix {
inherit pkgs;
extraInit = ''
server.wait_for_unit("nginx.service")
server.wait_for_open_port(443)
server.wait_for_unit("redis-mastodon.service")
server.wait_for_unit("postgresql.target")
server.wait_for_open_port(5432)
'';
};
}
)

View File

@@ -0,0 +1,35 @@
{ pkgs, ... }:
let
cert = pkgs.runCommand "selfSignedCerts" { nativeBuildInputs = [ pkgs.openssl ]; } ''
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -nodes -subj '/CN=localhost' -days 36500
mkdir -p $out
cp key.pem cert.pem $out
'';
in
{
name = "monica";
nodes = {
machine =
{ pkgs, ... }:
{
services.monica = {
enable = true;
hostname = "localhost";
appKeyFile = "${pkgs.writeText "keyfile" "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}";
nginx = {
forceSSL = true;
sslCertificate = "${cert}/cert.pem";
sslCertificateKey = "${cert}/key.pem";
};
};
};
};
testScript = ''
start_all()
machine.wait_for_unit("monica-setup.service")
machine.wait_for_open_port(443)
machine.succeed("curl -k --fail https://localhost", timeout=10)
'';
}

View File

@@ -0,0 +1,6 @@
{ recurseIntoAttrs, runTest }:
recurseIntoAttrs {
ejabberd-h2o = runTest ./ejabberd-h2o.nix;
prosody-nginx = runTest ./prosody-nginx.nix;
}

View File

@@ -0,0 +1,274 @@
{ hostPkgs, lib, ... }:
let
movim = {
domain = "movim.local";
port = 8080;
info = "No ToS in tests";
description = "NixOS testing server";
};
ejabberd = {
domain = "ejabberd.local";
ports = {
c2s = 5222;
s2s = 5269;
http = 5280;
};
spoolDir = "/var/lib/ejabberd";
admin = rec {
JID = "${username}@${ejabberd.domain}";
username = "romeo";
password = "juliet";
};
};
# START OF EJABBERD CONFIG ##################################################
#
# Ejabberd has sparse defaults as it is a generic XMPP server. As such this
# config might be longer than expected for a test.
#
# Movim suggests: https://github.com/movim/movim/wiki/Configure ejabberd
#
# In the future this may be the default setup
# See: https://github.com/NixOS/nixpkgs/pull/312316
ejabberd_config_file =
let
settingsFormat = hostPkgs.formats.yaml { };
in
settingsFormat.generate "ejabberd.yml" {
loglevel = "info";
hide_sensitive_log_data = false;
hosts = [ ejabberd.domain ];
default_db = "mnesia";
acme.auto = false;
s2s_access = "s2s";
s2s_use_starttls = false;
new_sql_schema = true;
acl = {
admin = [
{ user = ejabberd.admin.JID; }
];
local.user_regexp = "";
loopback.ip = [
"127.0.0.1/8"
"::1/128"
];
};
access_rules = {
c2s = {
deny = "blocked";
allow = "all";
};
s2s = {
allow = "all";
};
local.allow = "local";
announce.allow = "admin";
configure.allow = "admin";
pubsub_createnode.allow = "local";
trusted_network.allow = "loopback";
};
api_permissions = {
"console commands" = {
from = [ "ejabberd_ctl" ];
who = "all";
what = "*";
};
};
shaper = {
normal = {
rate = 3000;
burst_size = 20000;
};
fast = 100000;
};
modules = {
mod_caps = { };
mod_disco = { };
mod_mam = { };
mod_http_upload = {
docroot = "${ejabberd.spoolDir}/uploads";
dir_mode = "0755";
file_mode = "0644";
get_url = "http://@HOST@/upload";
put_url = "http://@HOST@/upload";
max_size = 65536;
custom_headers = {
Access-Control-Allow-Origin = "http://@HOST@,http://${movim.domain}";
Access-Control-Allow-Methods = "GET,HEAD,PUT,OPTIONS";
Access-Control-Allow-Headers = "Content-Type";
};
};
# This PubSub block is required for Movim to work.
#
# See: https://github.com/movim/movim/wiki/Configure ejabberd#pubsub
mod_pubsub = {
hosts = [ "pubsub.@HOST@" ];
access_createnode = "pubsub_createnode";
ignore_pep_from_offline = false;
last_item_cache = false;
max_items_node = 2048;
default_node_config = {
max_items = 2048;
};
plugins = [
"flat"
"pep"
];
force_node_config = {
"storage:bookmarks".access_model = "whitelist";
"eu.siacs.conversations.axolotl.*".access_model = "open";
"urn:xmpp:bookmarks:0" = {
access_model = "whitelist";
send_last_published_item = "never";
max_items = "infinity";
persist_items = true;
};
"urn:xmpp:bookmarks:1" = {
access_model = "whitelist";
send_last_published_item = "never";
max_items = "infinity";
persist_items = true;
};
"urn:xmpp:pubsub:movim-public-subscription" = {
access_model = "whitelist";
max_items = "infinity";
persist_items = true;
};
"urn:xmpp:microblog:0" = {
notify_retract = true;
max_items = "infinity";
persist_items = true;
};
"urn:xmpp:microblog:0:comments*" = {
access_model = "open";
notify_retract = true;
max_items = "infinity";
persist_items = true;
};
};
};
mod_stream_mgmt = { };
};
listen = [
{
module = "ejabberd_c2s";
port = ejabberd.ports.c2s;
max_stanza_size = 262144;
access = "c2s";
starttls_required = false;
}
{
module = "ejabberd_s2s_in";
port = ejabberd.ports.s2s;
max_stanza_size = 524288;
shaper = "fast";
}
{
module = "ejabberd_http";
port = ejabberd.ports.http;
request_handlers = {
"/upload" = "mod_http_upload";
};
}
];
};
# END OF EJABBERD CONFIG ##################################################
in
{
name = "movim-ejabberd-h2o";
meta = {
maintainers = with lib.maintainers; [ toastal ];
};
nodes = {
server =
{ pkgs, ... }:
{
environment.systemPackages = [
# For testing
pkgs.websocat
];
services.movim = {
inherit (movim) domain port;
enable = true;
verbose = true;
podConfig = {
inherit (movim) description info;
xmppdomain = ejabberd.domain;
};
database = {
type = "postgresql";
createLocally = true;
};
h2o = { };
};
services.ejabberd = {
inherit (ejabberd) spoolDir;
enable = true;
configFile = ejabberd_config_file;
imagemagick = false;
};
services.h2o.settings = {
compress = "ON";
};
systemd.services.ejabberd = {
serviceConfig = {
# Certain misconfigurations can cause RAM usage to swell before
# crashing; fail sooner with more-than-liberal memory limits
StartupMemoryMax = "1G";
MemoryMax = "512M";
};
};
networking = {
firewall.allowedTCPPorts = with ejabberd.ports; [
c2s
s2s
];
extraHosts = ''
127.0.0.1 ${movim.domain}
127.0.0.1 ${ejabberd.domain}
'';
};
};
};
testScript = # python
''
ejabberdctl = "su ejabberd -s $(which ejabberdctl) "
server.wait_for_unit("phpfpm-movim.service")
server.wait_for_unit("h2o.service")
server.wait_for_open_port(${builtins.toString movim.port})
server.wait_for_open_port(80)
server.wait_for_unit("ejabberd.service")
ejabberd_status = server.succeed(ejabberdctl + "status")
assert "status: started" in ejabberd_status
server.succeed(ejabberdctl + "register ${ejabberd.admin.username} ${ejabberd.domain} ${ejabberd.admin.password}")
server.wait_for_unit("movim.service")
# Test unauthenticated
server.fail("curl -L --fail-with-body --max-redirs 0 http://${movim.domain}/chat")
# Test basic Websocket
server.succeed("echo | websocat --origin 'http://${movim.domain}' 'ws://${movim.domain}/ws/?path=login&offset=0'")
# Test login + create cookiejar
login_html = server.succeed("curl --fail-with-body -c /tmp/cookies http://${movim.domain}/login")
assert "${movim.description}" in login_html
assert "${movim.info}" in login_html
# Test authentication POST
server.succeed("curl --fail-with-body -b /tmp/cookies -X POST --data-urlencode 'username=${ejabberd.admin.JID}' --data-urlencode 'password=${ejabberd.admin.password}' http://${movim.domain}/login")
server.succeed("curl -L --fail-with-body --max-redirs 1 -b /tmp/cookies http://${movim.domain}/chat")
'';
}

View File

@@ -0,0 +1,115 @@
{ lib, ... }:
let
movim = {
domain = "movim.local";
port = 8080;
info = "No ToS in tests";
description = "NixOS testing server";
};
prosody = {
domain = "prosody.local";
admin = rec {
JID = "${username}@${prosody.domain}";
username = "romeo";
password = "juliet";
};
};
in
{
name = "movim-prosody-nginx";
meta = {
maintainers = with lib.maintainers; [ toastal ];
};
nodes = {
server =
{ pkgs, ... }:
{
environment.systemPackages = [
# For testing
pkgs.websocat
];
services.movim = {
inherit (movim) domain port;
enable = true;
verbose = true;
podConfig = {
inherit (movim) description info;
xmppdomain = prosody.domain;
};
nginx = { };
};
services.prosody = {
enable = true;
xmppComplianceSuite = false;
disco_items = [
{
url = "upload.${prosody.domain}";
description = "File Uploads";
}
];
virtualHosts."${prosody.domain}" = {
inherit (prosody) domain;
enabled = true;
extraConfig = ''
Component "pubsub.${prosody.domain}" "pubsub"
pubsub_max_items = 10000
expose_publisher = true
Component "upload.${prosody.domain}" "http_file_share"
http_external_url = "http://upload.${prosody.domain}"
http_file_share_expires_after = 300 * 24 * 60 * 60
http_file_share_size_limit = 1024 * 1024 * 1024
http_file_share_daily_quota = 4 * 1024 * 1024 * 1024
'';
};
extraConfig = ''
pep_max_items = 10000
http_paths = {
file_share = "/";
}
'';
};
networking.extraHosts = ''
127.0.0.1 ${movim.domain}
127.0.0.1 ${prosody.domain}
'';
};
};
testScript = # python
''
server.wait_for_unit("phpfpm-movim.service")
server.wait_for_unit("nginx.service")
server.wait_for_open_port(${builtins.toString movim.port})
server.wait_for_open_port(80)
server.wait_for_unit("prosody.service")
server.succeed('prosodyctl status | grep "Prosody is running"')
server.succeed("prosodyctl register ${prosody.admin.username} ${prosody.domain} ${prosody.admin.password}")
server.wait_for_unit("movim.service")
# Test unauthenticated
server.fail("curl -L --fail-with-body --max-redirs 0 http://${movim.domain}/chat")
# Test basic Websocket
server.succeed("echo | websocat --origin 'http://${movim.domain}' 'ws://${movim.domain}/ws/?path=login&offset=0'")
# Test login + create cookiejar
login_html = server.succeed("curl --fail-with-body -c /tmp/cookies http://${movim.domain}/login")
assert "${movim.description}" in login_html
assert "${movim.info}" in login_html
# Test authentication POST
server.succeed("curl --fail-with-body -b /tmp/cookies -X POST --data-urlencode 'username=${prosody.admin.JID}' --data-urlencode 'password=${prosody.admin.password}' http://${movim.domain}/login")
server.succeed("curl -L --fail-with-body --max-redirs 1 -b /tmp/cookies http://${movim.domain}/chat")
'';
}

View File

@@ -0,0 +1,105 @@
{ lib, pkgs, ... }:
let
oldNetbox = "netbox_4_2";
newNetbox = "netbox_4_3";
apiVersion =
version:
lib.pipe version [
(lib.splitString ".")
(lib.take 2)
(lib.concatStringsSep ".")
];
oldApiVersion = apiVersion pkgs.${oldNetbox}.version;
newApiVersion = apiVersion pkgs.${newNetbox}.version;
in
{
name = "netbox-upgrade";
meta.maintainers = with lib.maintainers; [
minijackson
raitobezarius
];
node.pkgsReadOnly = false;
nodes.machine =
let
pkgs' = pkgs;
in
{ config, pkgs, ... }:
{
virtualisation.memorySize = 2048;
services.netbox = {
enable = true;
# Pick the NetBox package from this config's "pkgs" argument,
# so that `nixpkgs.config.permittedInsecurePackages` works
package = pkgs.${oldNetbox};
secretKeyFile = pkgs.writeText "secret" ''
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789
'';
};
services.nginx = {
enable = true;
recommendedProxySettings = true;
virtualHosts.netbox = {
default = true;
locations."/".proxyPass = "http://localhost:${toString config.services.netbox.port}";
locations."/static/".alias = "/var/lib/netbox/static/";
};
};
users.users.nginx.extraGroups = [ "netbox" ];
networking.firewall.allowedTCPPorts = [ 80 ];
nixpkgs.config.permittedInsecurePackages = [ pkgs'.${oldNetbox}.name ];
specialisation.upgrade.configuration.services.netbox.package = lib.mkForce pkgs.${newNetbox};
};
testScript =
{ nodes, ... }:
''
start_all()
machine.wait_for_unit("netbox.target")
machine.wait_for_unit("nginx.service")
machine.wait_until_succeeds("journalctl --since -1m --unit netbox --grep Listening")
def api_version(headers):
header = [header for header in headers.splitlines() if header.startswith("API-Version:")][0]
return header.split()[1]
def check_api_version(version):
# Returns 403 with NetBox >= 4.0,
# but we still get the API version in the headers
headers = machine.succeed(
"curl -sSL http://localhost/api/ --head -H 'Content-Type: application/json'"
)
assert api_version(headers) == version
with subtest("NetBox version is the old one"):
check_api_version("${oldApiVersion}")
# Somehow, even though netbox-housekeeping.service has After=netbox.service,
# netbox-housekeeping.service and netbox.service still get started at the
# same time, making netbox-housekeeping fail (can't really do some house
# keeping job if the database is not correctly formed).
#
# So we don't check that the upgrade went well, we just check that
# netbox.service is active, and that netbox-housekeeping can be run
# successfully afterwards.
#
# This is not good UX, but the system should be working nonetheless.
machine.execute("${nodes.machine.system.build.toplevel}/specialisation/upgrade/bin/switch-to-configuration test >&2")
machine.wait_for_unit("netbox.service")
machine.succeed("systemctl start netbox-housekeeping.service")
with subtest("NetBox version is the new one"):
check_api_version("${newApiVersion}")
'';
}

View File

@@ -0,0 +1,164 @@
let
ldapDomain = "example.org";
ldapSuffix = "dc=example,dc=org";
ldapRootUser = "admin";
ldapRootPassword = "foobar";
testUser = "alice";
testPassword = "verySecure";
testGroup = "netbox-users";
in
import ../../make-test-python.nix (
{
lib,
pkgs,
netbox,
...
}:
{
name = "netbox";
meta = with lib.maintainers; {
maintainers = [
minijackson
];
};
skipTypeCheck = true;
nodes.machine =
{ config, ... }:
{
virtualisation.memorySize = 2048;
services.netbox = {
enable = true;
package = netbox;
secretKeyFile = pkgs.writeText "secret" ''
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789
'';
enableLdap = true;
ldapConfigPath = pkgs.writeText "ldap_config.py" ''
import ldap
from django_auth_ldap.config import LDAPSearch, PosixGroupType
AUTH_LDAP_SERVER_URI = "ldap://localhost/"
AUTH_LDAP_USER_SEARCH = LDAPSearch(
"ou=accounts,ou=posix,${ldapSuffix}",
ldap.SCOPE_SUBTREE,
"(uid=%(user)s)",
)
AUTH_LDAP_GROUP_SEARCH = LDAPSearch(
"ou=groups,ou=posix,${ldapSuffix}",
ldap.SCOPE_SUBTREE,
"(objectClass=posixGroup)",
)
AUTH_LDAP_GROUP_TYPE = PosixGroupType()
# Mirror LDAP group assignments.
AUTH_LDAP_MIRROR_GROUPS = True
# For more granular permissions, we can map LDAP groups to Django groups.
AUTH_LDAP_FIND_GROUP_PERMS = True
'';
};
services.nginx = {
enable = true;
recommendedProxySettings = true;
virtualHosts.netbox = {
default = true;
locations."/".proxyPass = "http://localhost:${toString config.services.netbox.port}";
locations."/static/".alias = "/var/lib/netbox/static/";
};
};
# Adapted from the sssd-ldap NixOS test
services.openldap = {
enable = true;
settings = {
children = {
"cn=schema".includes = [
"${pkgs.openldap}/etc/schema/core.ldif"
"${pkgs.openldap}/etc/schema/cosine.ldif"
"${pkgs.openldap}/etc/schema/inetorgperson.ldif"
"${pkgs.openldap}/etc/schema/nis.ldif"
];
"olcDatabase={1}mdb" = {
attrs = {
objectClass = [
"olcDatabaseConfig"
"olcMdbConfig"
];
olcDatabase = "{1}mdb";
olcDbDirectory = "/var/lib/openldap/db";
olcSuffix = ldapSuffix;
olcRootDN = "cn=${ldapRootUser},${ldapSuffix}";
olcRootPW = ldapRootPassword;
};
};
};
};
declarativeContents = {
${ldapSuffix} = ''
dn: ${ldapSuffix}
objectClass: top
objectClass: dcObject
objectClass: organization
o: ${ldapDomain}
dn: ou=posix,${ldapSuffix}
objectClass: top
objectClass: organizationalUnit
dn: ou=accounts,ou=posix,${ldapSuffix}
objectClass: top
objectClass: organizationalUnit
dn: uid=${testUser},ou=accounts,ou=posix,${ldapSuffix}
objectClass: person
objectClass: posixAccount
userPassword: ${testPassword}
homeDirectory: /home/${testUser}
uidNumber: 1234
gidNumber: 1234
cn: ""
sn: ""
dn: ou=groups,ou=posix,${ldapSuffix}
objectClass: top
objectClass: organizationalUnit
dn: cn=${testGroup},ou=groups,ou=posix,${ldapSuffix}
objectClass: posixGroup
gidNumber: 2345
memberUid: ${testUser}
'';
};
};
users.users.nginx.extraGroups = [ "netbox" ];
networking.firewall.allowedTCPPorts = [ 80 ];
};
testScript =
let
changePassword = pkgs.writeText "change-password.py" ''
from users.models import User
u = User.objects.get(username='netbox')
u.set_password('netbox')
u.save()
'';
in
builtins.replaceStrings
[ "$\{changePassword}" "$\{testUser}" "$\{testPassword}" "$\{testGroup}" ]
[ "${changePassword}" "${testUser}" "${testPassword}" "${testGroup}" ]
(lib.readFile "${./testScript.py}");
}
)

View File

@@ -0,0 +1,274 @@
from typing import Any, Dict
import json
start_all()
machine.wait_for_unit("netbox.target")
machine.wait_until_succeeds("journalctl --since -1m --unit netbox --grep Listening")
test_objects = {
"sites": {
"test-site": {
"name": "Test site",
"slug": "test-site"
},
"test-site-two": {
"name": "Test site 2",
"slug": "test-site-second-edition"
}
},
"prefixes": {
"v4-with-updated-desc": {
"prefix": "192.0.2.0/24",
"class_type": "Prefix",
"family": { "label": "IPv4" },
"scope": {
"__typename": "SiteType",
"id": "1",
"description": "Test site description"
}
},
"v6-cidr-32": {
"prefix": "2001:db8::/32",
"class_type": "Prefix",
"family": { "label": "IPv6" },
"scope": {
"__typename": "SiteType",
"id": "1",
"description": "Test site description"
}
},
"v6-cidr-48": {
"prefix": "2001:db8:c0fe::/48",
"class_type": "Prefix",
"family": { "label": "IPv6" },
"scope": {
"__typename": "SiteType",
"id": "1",
"description": "Test site description"
}
}
}
}
def compare(a: str, b: str):
differences = [(x - y) for (x,y) in list(zip(
list(map(int, a.split('.'))),
list(map(int, b.split('.')))
))]
for d in differences:
if d != 0:
return d
return 0
with subtest("Home screen loads"):
machine.succeed(
"curl -sSfL http://[::1]:8001 | grep '<title>Home | NetBox</title>'"
)
with subtest("Staticfiles are generated"):
machine.succeed("test -e /var/lib/netbox/static/netbox.js")
with subtest("Superuser can be created"):
machine.succeed(
"netbox-manage createsuperuser --noinput --username netbox --email netbox@example.com"
)
# Django doesn't have a "clean" way of inputting the password from the command line
machine.succeed("cat '${changePassword}' | netbox-manage shell")
machine.wait_for_unit("network.target")
with subtest("Home screen loads from nginx"):
machine.succeed(
"curl -sSfL http://localhost | grep '<title>Home | NetBox</title>'"
)
with subtest("Staticfiles can be fetched"):
machine.succeed("curl -sSfL http://localhost/static/netbox.js")
machine.succeed("curl -sSfL http://localhost/static/docs/")
def login(username: str, password: str):
encoded_data = json.dumps({"username": username, "password": password})
uri = "/users/tokens/provision/"
result = json.loads(
machine.succeed(
"curl -sSfL "
"-X POST "
"-H 'Accept: application/json' "
"-H 'Content-Type: application/json' "
f"'http://localhost/api{uri}' "
f"--data '{encoded_data}'"
)
)
return result["key"]
with subtest("Can login"):
auth_token = login("netbox", "netbox")
def get(uri: str):
return json.loads(
machine.succeed(
"curl -sSfL "
"-H 'Accept: application/json' "
f"-H 'Authorization: Token {auth_token}' "
f"'http://localhost/api{uri}'"
)
)
def delete(uri: str):
return machine.succeed(
"curl -sSfL "
f"-X DELETE "
"-H 'Accept: application/json' "
f"-H 'Authorization: Token {auth_token}' "
f"'http://localhost/api{uri}'"
)
def data_request(uri: str, method: str, data: Dict[str, Any]):
encoded_data = json.dumps(data)
return json.loads(
machine.succeed(
"curl -sSfL "
f"-X {method} "
"-H 'Accept: application/json' "
"-H 'Content-Type: application/json' "
f"-H 'Authorization: Token {auth_token}' "
f"'http://localhost/api{uri}' "
f"--data '{encoded_data}'"
)
)
def post(uri: str, data: Dict[str, Any]):
return data_request(uri, "POST", data)
def patch(uri: str, data: Dict[str, Any]):
return data_request(uri, "PATCH", data)
# Retrieve netbox version
netbox_version = get("/status/")["netbox-version"]
with subtest("Can create objects"):
result = post("/dcim/sites/", {"name": "Test site", "slug": "test-site"})
site_id = result["id"]
for prefix in test_objects["prefixes"].values():
if compare(netbox_version, '4.2.0') >= 0:
post("/ipam/prefixes/", {
"prefix": prefix["prefix"],
"scope_id": site_id,
"scope_type": "dcim." + prefix["scope"]["__typename"].replace("Type", "").lower()
})
prefix["scope"]["id"] = str(site_id)
else:
post("/ipam/prefixes/", {
"prefix": prefix["prefix"],
"site": str(site_id),
})
result = post(
"/dcim/manufacturers/",
{"name": "Test manufacturer", "slug": "test-manufacturer"}
)
manufacturer_id = result["id"]
# Had an issue with device-types before NetBox 3.4.0
result = post(
"/dcim/device-types/",
{
"model": "Test device type",
"manufacturer": manufacturer_id,
"slug": "test-device-type",
},
)
device_type_id = result["id"]
with subtest("Can list objects"):
result = get("/dcim/sites/")
assert result["count"] == 1
assert result["results"][0]["id"] == site_id
assert result["results"][0]["name"] == "Test site"
assert result["results"][0]["description"] == ""
result = get("/dcim/device-types/")
assert result["count"] == 1
assert result["results"][0]["id"] == device_type_id
assert result["results"][0]["model"] == "Test device type"
with subtest("Can update objects"):
new_description = "Test site description"
patch(f"/dcim/sites/{site_id}/", {"description": new_description})
result = get(f"/dcim/sites/{site_id}/")
assert result["description"] == new_description
with subtest("Can delete objects"):
# Delete a device-type since no object depends on it
delete(f"/dcim/device-types/{device_type_id}/")
result = get("/dcim/device-types/")
assert result["count"] == 0
def request_graphql(query: str):
return machine.succeed(
"curl -sSfL "
"-H 'Accept: application/json' "
"-H 'Content-Type: application/json' "
f"-H 'Authorization: Token {auth_token}' "
"'http://localhost/graphql/' "
f"--data '{json.dumps({"query": query})}'"
)
if compare(netbox_version, '4.2.0') >= 0:
with subtest("Can use the GraphQL API (NetBox 4.2.0+)"):
graphql_query = '''query {
prefix_list {
prefix
class_type
family {
label
}
scope {
__typename
... on SiteType {
id
description
}
}
}
}
'''
answer = request_graphql(graphql_query)
result = json.loads(answer)
assert len(result["data"]["prefix_list"]) == 3
assert test_objects["prefixes"]["v4-with-updated-desc"] in result["data"]["prefix_list"]
assert test_objects["prefixes"]["v6-cidr-32"] in result["data"]["prefix_list"]
assert test_objects["prefixes"]["v6-cidr-48"] in result["data"]["prefix_list"]
if compare(netbox_version, '4.2.0') < 0:
with subtest("Can use the GraphQL API (Netbox <= 4.2.0)"):
answer = request_graphql('''query {
prefix_list {
prefix
site {
id
}
}
}
''')
result = json.loads(answer)
print(result["data"]["prefix_list"][0])
assert result["data"]["prefix_list"][0]["prefix"] == test_objects["prefixes"]["v4-with-updated-desc"]["prefix"]
assert int(result["data"]["prefix_list"][0]["site"]["id"]) == int(test_objects["prefixes"]["v4-with-updated-desc"]["scope"]["id"])
with subtest("Can login with LDAP"):
machine.wait_for_unit("openldap.service")
login("alice", "${testPassword}")
with subtest("Can associate LDAP groups"):
result = get("/users/users/?username=${testUser}")
assert result["count"] == 1
assert any(group["name"] == "${testGroup}" for group in result["results"][0]["groups"])

View File

@@ -0,0 +1,22 @@
{ lib, ... }:
{
name = "nextjs-ollama-llm-ui";
meta.maintainers = with lib.maintainers; [ malteneuss ];
nodes.machine =
{ pkgs, ... }:
{
services.nextjs-ollama-llm-ui = {
enable = true;
port = 8080;
};
};
testScript = ''
# Ensure the service is started and reachable
machine.wait_for_unit("nextjs-ollama-llm-ui.service")
machine.wait_for_open_port(8080)
machine.succeed("curl --fail http://127.0.0.1:8080")
'';
}

View File

@@ -0,0 +1,32 @@
{ pkgs, ... }:
{
name = "nifi";
meta.maintainers = with pkgs.lib.maintainers; [ izorkin ];
nodes = {
nifi =
{ pkgs, ... }:
{
virtualisation = {
memorySize = 2048;
diskSize = 4096;
};
services.nifi = {
enable = true;
enableHTTPS = false;
};
};
};
testScript = ''
nifi.start()
nifi.wait_for_unit("nifi.service")
nifi.wait_for_open_port(8080)
# Check if NiFi is running
nifi.succeed("curl --fail http://127.0.0.1:8080/nifi/login 2> /dev/null | grep 'NiFi Login'")
nifi.shutdown()
'';
}

View File

@@ -0,0 +1,69 @@
{ pkgs, lib, ... }:
let
nipapRc = pkgs.writeText "nipaprc" ''
[global]
hostname = [::1]
port = 1337
username = nixostest
password = nIx0st3st
default_vrf_rt = -
default_list_vrf_rt = all
'';
in
{
name = "lukegb";
meta.maintainers = [ lib.maintainers.lukegb ];
nodes.main =
{ ... }:
{
services.nipap = {
enable = true;
};
environment.systemPackages = [
pkgs.nipap-cli
];
};
testScript = ''
main.wait_for_unit("nipapd.service")
main.wait_for_unit("nipap-www.service")
# Make sure the web UI is up.
main.wait_for_open_port(21337)
main.succeed("curl -fvvv -Ls http://localhost:21337/ | grep 'NIPAP'")
# Check that none of the files we created in /var/lib/nipap are readable.
out = main.succeed("ls -l /var/lib/nipap")
bad_perms = False
for ln in out.split("\n"):
ln = ln.strip()
if not ln or ln.startswith('total '):
continue
if not ln.startswith('-rw------- '):
print(f"Bad file permissions: {ln}")
bad_perms = True
if bad_perms:
t.fail("One or more files were overly permissive.")
# Check we created a web-frontend user.
main.succeed("nipap-passwd list | grep nipap-www")
# Create a test user
main.succeed("nipap-passwd add -u nixostest -p nIx0st3st -n 'NixOS Test User'")
# Try to log in with it on the web frontend
main.succeed("curl -fvvv -Ls -b \"\" -d username=nixostest -d password=nIx0st3st http://localhost:21337/auth/login | grep 'PrefixListController'")
# Try to log in with it using the CLI
main.copy_from_host("${nipapRc}", "/root/.nipaprc")
main.succeed("chmod u=rw,go= /root/.nipaprc")
main.succeed("nipap address add prefix 192.0.2.0/24 type assignment description RFC1166")
main.succeed("nipap address add prefix 192.0.2.1/32 type host description 'test host'")
main.succeed("nipap address add prefix 2001:db8::/32 type reservation description RFC3849")
main.succeed("nipap address add prefix 2001:db8:f00f::/48 type assignment description 'eye pee vee six'")
main.succeed("nipap address add prefix 2001:db8:f00f:face:dead:beef:cafe:feed/128 type host description 'test host 2'")
'';
}

View File

@@ -0,0 +1,156 @@
{
lib,
pkgs,
config,
...
}:
let
ldapDomain = "example.org";
ldapSuffix = "dc=example,dc=org";
ldapRootUser = "root";
ldapRootPassword = "foobar23";
testUser = "myuser";
testPassword = "foobar23";
teamName = "myteam";
in
{
name = "oncall";
meta.maintainers = with lib.maintainers; [ onny ];
nodes = {
machine = {
virtualisation.memorySize = 2048;
environment.etc."oncall-secrets.yml".text = ''
auth:
ldap_bind_password: "${ldapRootPassword}"
'';
environment.systemPackages = [ pkgs.jq ];
services.oncall = {
enable = true;
settings = {
auth = {
module = "oncall.auth.modules.ldap_import";
ldap_url = "ldap://localhost";
ldap_user_suffix = "";
ldap_bind_user = "cn=${ldapRootUser},${ldapSuffix}";
ldap_base_dn = "ou=accounts,${ldapSuffix}";
ldap_search_filter = "(uid=%s)";
import_user = true;
attrs = {
username = "uid";
full_name = "cn";
email = "mail";
call = "telephoneNumber";
sms = "mobile";
};
};
};
secretFile = "/etc/oncall-secrets.yml";
};
services.openldap = {
enable = true;
settings = {
children = {
"cn=schema".includes = [
"${pkgs.openldap}/etc/schema/core.ldif"
"${pkgs.openldap}/etc/schema/cosine.ldif"
"${pkgs.openldap}/etc/schema/inetorgperson.ldif"
"${pkgs.openldap}/etc/schema/nis.ldif"
];
"olcDatabase={1}mdb" = {
attrs = {
objectClass = [
"olcDatabaseConfig"
"olcMdbConfig"
];
olcDatabase = "{1}mdb";
olcDbDirectory = "/var/lib/openldap/db";
olcSuffix = ldapSuffix;
olcRootDN = "cn=${ldapRootUser},${ldapSuffix}";
olcRootPW = ldapRootPassword;
};
};
};
};
declarativeContents = {
${ldapSuffix} = ''
dn: ${ldapSuffix}
objectClass: top
objectClass: dcObject
objectClass: organization
o: ${ldapDomain}
dn: ou=accounts,${ldapSuffix}
objectClass: top
objectClass: organizationalUnit
dn: uid=${testUser},ou=accounts,${ldapSuffix}
objectClass: top
objectClass: inetOrgPerson
uid: ${testUser}
userPassword: ${testPassword}
cn: Test User
sn: User
mail: test@example.org
telephoneNumber: 012345678910
mobile: 012345678910
'';
};
};
};
};
testScript = ''
start_all()
machine.wait_for_unit("uwsgi.service")
machine.wait_for_unit("nginx.service")
machine.wait_for_file("/run/uwsgi/oncall.sock")
machine.wait_for_unit("oncall-setup-database.service")
with subtest("Home screen loads"):
machine.succeed(
"curl -sSfL http://[::1]:80 | grep '<title>Oncall</title>'"
)
with subtest("Staticfiles can be fetched"):
machine.wait_until_succeeds(
"curl -sSfL http://[::1]:80/static/bundles/libs.js"
)
with subtest("Staticfiles are generated"):
machine.succeed(
"test -e /var/lib/oncall/static/bundles/libs.js"
)
with subtest("Create and verify team via REST API"):
import json
# Log in and store the session cookie
login_response = machine.succeed("""
curl -sSfL -c cookies -X POST \
--data-raw 'username=${testUser}&password=${testPassword}' \
http://[::1]:80/login
""")
# Parse csrf token
login_response_data = json.loads(login_response)
csrf_token = login_response_data["csrf_token"]
# Create the team
machine.succeed(
f"""curl -sSfL -b cookies -X POST -H 'Content-Type: application/json' -H 'X-CSRF-Token: {csrf_token}' -d '{{"name": "${teamName}", "email": "test@example.com", "scheduling_timezone": "Europe/Berlin", "iris_enabled": false}}' http://[::1]:80/api/v0/teams/"""
)
# Query the created team
machine.succeed("""
curl -sSfL -b cookies http://[::1]:80/api/v0/teams/${teamName} | jq -e '.name == "${teamName}"'
""")
'';
}

View File

@@ -0,0 +1,49 @@
{ pkgs, ... }:
let
certs = import ../common/acme/server/snakeoil-certs.nix;
serverDomain = certs.domain;
in
{
name = "open-web-calendar";
meta.maintainers = with pkgs.lib.maintainers; [ erictapen ];
nodes.server =
{ pkgs, lib, ... }:
{
services.open-web-calendar = {
enable = true;
domain = serverDomain;
calendarSettings.title = "My custom title";
};
services.nginx.virtualHosts."${serverDomain}" = {
enableACME = lib.mkForce false;
sslCertificate = certs."${serverDomain}".cert;
sslCertificateKey = certs."${serverDomain}".key;
};
security.pki.certificateFiles = [ certs.ca.cert ];
networking.hosts."::1" = [ "${serverDomain}" ];
networking.firewall.allowedTCPPorts = [
80
443
];
};
nodes.client =
{ pkgs, nodes, ... }:
{
networking.hosts."${nodes.server.networking.primaryIPAddress}" = [ "${serverDomain}" ];
security.pki.certificateFiles = [ certs.ca.cert ];
};
testScript = ''
start_all()
server.wait_for_unit("open-web-calendar.socket")
server.wait_until_succeeds("curl -f https://${serverDomain}/ | grep 'My custom title'")
'';
}

View File

@@ -0,0 +1,45 @@
{ lib, pkgs, ... }:
{
name = "peering-manager";
meta = with lib.maintainers; {
maintainers = [ yuka ];
};
nodes.machine =
{ ... }:
{
services.peering-manager = {
enable = true;
secretKeyFile = pkgs.writeText "secret" ''
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789
'';
};
};
testScript =
{ nodes }:
''
machine.start()
machine.wait_for_unit("peering-manager.target")
machine.wait_until_succeeds("journalctl --since -1m --unit peering-manager --grep Listening")
print(machine.succeed(
"curl -sSfL http://[::1]:8001"
))
with subtest("Home screen loads"):
machine.succeed(
"curl -sSfL http://[::1]:8001 | grep '<title>Home - Peering Manager</title>'"
)
with subtest("checks succeed"):
machine.succeed(
"systemctl stop peering-manager peering-manager-rq"
)
machine.succeed(
"sudo -u postgres psql -c 'ALTER USER \"peering-manager\" WITH SUPERUSER;'"
)
machine.succeed(
"cd ${nodes.machine.system.build.peeringManagerPkg}/opt/peering-manager ; peering-manager-manage test --no-input"
)
'';
}

View File

@@ -0,0 +1,227 @@
import ../make-test-python.nix (
{ lib, pkgs, ... }:
let
domain = "peertube.local";
port = 9000;
url = "http://${domain}:${toString port}";
password = "zw4SqYVdcsXUfRX8aaFX";
registrationTokenFile = "/etc/peertube-runner-registration-token";
in
{
name = "peertube";
meta.maintainers = with lib.maintainers; [ izorkin ] ++ lib.teams.ngi.members;
nodes = {
database = {
networking = {
interfaces.eth1 = {
ipv4.addresses = [
{
address = "192.168.2.10";
prefixLength = 24;
}
];
};
firewall.allowedTCPPorts = [
5432
31638
];
};
services.postgresql = {
enable = true;
enableTCPIP = true;
ensureDatabases = [ "peertube_test" ];
ensureUsers = [
{
name = "peertube_test";
ensureDBOwnership = true;
}
];
authentication = ''
hostnossl peertube_test peertube_test 192.168.2.11/32 md5
'';
initialScript = pkgs.writeText "postgresql_init.sql" ''
CREATE ROLE peertube_test LOGIN PASSWORD '0gUN0C1mgST6czvjZ8T9';
'';
};
services.redis.servers.peertube = {
enable = true;
bind = "0.0.0.0";
requirePass = "turrQfaQwnanGbcsdhxy";
port = 31638;
};
};
server =
{ pkgs, ... }:
{
environment = {
etc = {
"peertube/password-init-root".text = ''
PT_INITIAL_ROOT_PASSWORD=${password}
'';
"peertube/secrets-peertube".text = ''
063d9c60d519597acef26003d5ecc32729083965d09181ef3949200cbe5f09ee
'';
"peertube/password-posgressql-db".text = ''
0gUN0C1mgST6czvjZ8T9
'';
"peertube/password-redis-db".text = ''
turrQfaQwnanGbcsdhxy
'';
};
};
networking = {
interfaces.eth1 = {
ipv4.addresses = [
{
address = "192.168.2.11";
prefixLength = 24;
}
];
};
extraHosts = ''
192.168.2.11 ${domain}
'';
firewall.allowedTCPPorts = [ port ];
};
services.peertube = {
enable = true;
localDomain = domain;
enableWebHttps = false;
serviceEnvironmentFile = "/etc/peertube/password-init-root";
secrets = {
secretsFile = "/etc/peertube/secrets-peertube";
};
database = {
host = "192.168.2.10";
name = "peertube_test";
user = "peertube_test";
passwordFile = "/etc/peertube/password-posgressql-db";
};
redis = {
host = "192.168.2.10";
port = 31638;
passwordFile = "/etc/peertube/password-redis-db";
};
settings = {
listen = {
hostname = "0.0.0.0";
};
instance = {
name = "PeerTube Test Server";
};
};
};
};
client = {
environment.systemPackages = [
pkgs.jq
pkgs.peertube.cli
];
networking = {
interfaces.eth1 = {
ipv4.addresses = [
{
address = "192.168.2.12";
prefixLength = 24;
}
];
};
extraHosts = ''
192.168.2.11 ${domain}
'';
};
services.peertube-runner = {
enable = true;
# Don't pull in unneeded dependencies.
enabledJobTypes = [ "video-studio-transcoding" ];
instancesToRegister = {
testServer1 = {
inherit url registrationTokenFile;
runnerName = "I'm a test!!!";
};
testServer2 = {
inherit url registrationTokenFile;
runnerName = "I'm also a test...";
runnerDescription = "Even more testing?!?!";
};
};
};
# Will be manually started in test script.
systemd.services.peertube-runner.wantedBy = lib.mkForce [ ];
};
};
testScript = ''
start_all()
database.wait_for_unit("postgresql.target")
database.wait_for_unit("redis-peertube.service")
database.wait_for_open_port(5432)
database.wait_for_open_port(31638)
server.wait_for_unit("peertube.service")
server.wait_for_open_port(${toString port})
# Check if PeerTube is running
client.succeed("curl --fail ${url}/api/v1/config/about | jq -r '.instance.name' | grep 'PeerTube Test Server'")
# PeerTube CLI
client.succeed('peertube-cli auth add -u "${url}" -U "root" --password "${password}"')
client.succeed('peertube-cli auth list | grep "${url}"')
client.succeed('peertube-cli auth del "${url}"')
client.fail('peertube-cli auth list | grep "${url}"')
# peertube-runner
access_token = client.succeed(
'peertube-cli get-access-token --url "${url}" --username "root" --password "${password}"'
).strip()
# Generate registration token.
client.succeed(f"curl --fail -X POST -H 'Authorization: Bearer {access_token}' ${url}/api/v1/runners/registration-tokens/generate")
# Get registration token, and put it where `registrationTokenFile` from the
# peertube-runner module points to.
client.succeed(
f"curl --fail -H 'Authorization: Bearer {access_token}' ${url}/api/v1/runners/registration-tokens" \
" | jq --raw-output '.data[0].registrationToken'" \
" > ${registrationTokenFile}"
)
client.systemctl("start peertube-runner.service")
client.wait_for_unit("peertube-runner.service")
runner_command = "sudo -u prunner peertube-runner"
client.succeed(f'{runner_command} list-registered | grep "I\'m a test!!!"')
client.succeed(f'{runner_command} list-registered | grep "I\'m also a test..."')
client.succeed(f'{runner_command} list-registered | grep "Even more testing?!?!"')
# Service should still work once instances are already registered.
client.systemctl("restart peertube-runner.service")
client.wait_for_unit("peertube-runner.service")
# Cleanup
client.shutdown()
server.shutdown()
database.shutdown()
'';
}
)

View File

@@ -0,0 +1,23 @@
{ pkgs, lib, ... }:
{
name = "phylactery";
nodes.machine =
{ ... }:
{
services.phylactery = {
enable = true;
port = 8080;
library = "/tmp";
};
};
testScript = ''
start_all()
machine.wait_for_unit('phylactery')
machine.wait_for_open_port(8080)
machine.wait_until_succeeds('curl localhost:8080')
'';
meta.maintainers = with lib.maintainers; [ McSinyx ];
}

View File

@@ -0,0 +1,12 @@
{
runTestOn,
}:
let
supportedSystems = [
"x86_64-linux"
"i686-linux"
];
in
{
standard = runTestOn supportedSystems ./standard.nix;
}

View File

@@ -0,0 +1,41 @@
{
name = "pixelfed-standard";
meta.maintainers = [ ];
nodes = {
server =
{ pkgs, ... }:
{
services.pixelfed = {
enable = true;
domain = "pixelfed.local";
# Configure NGINX.
nginx = { };
secretFile = (
pkgs.writeText "secrets.env" ''
# Snakeoil secret, can be any random 32-chars secret via CSPRNG.
APP_KEY=adKK9EcY8Hcj3PLU7rzG9rJ6KKTOtYfA
''
);
settings."FORCE_HTTPS_URLS" = false;
};
};
};
testScript = ''
# Wait for Pixelfed PHP pool
server.wait_for_unit("phpfpm-pixelfed.service")
# Wait for NGINX
server.wait_for_unit("nginx.service")
# Wait for HTTP port
server.wait_for_open_port(80)
# Access the homepage.
server.succeed("curl -H 'Host: pixelfed.local' http://localhost")
# Create an account
server.succeed("pixelfed-manage user:create --name=test --username=test --email=test@test.com --password=test")
# Create a OAuth token.
# TODO: figure out how to use it to send a image/toot
# server.succeed("pixelfed-manage passport:client --personal")
# server.succeed("curl -H 'Host: pixefed.local' -H 'Accept: application/json' -H 'Authorization: Bearer secret' -F'status'='test' http://localhost/api/v1/statuses")
'';
}

View File

@@ -0,0 +1,40 @@
{ lib, ... }:
{
name = "pretalx";
meta.maintainers = lib.teams.c3d2.members;
nodes = {
pretalx =
{ config, ... }:
{
networking.extraHosts = ''
127.0.0.1 talks.local
'';
services.pretalx = {
enable = true;
plugins = with config.services.pretalx.package.plugins; [
pages
];
nginx.domain = "talks.local";
settings = {
site.url = "http://talks.local";
};
};
};
};
testScript = ''
start_all()
pretalx.wait_for_unit("pretalx-web.service")
pretalx.wait_for_unit("pretalx-worker.service")
pretalx.wait_until_succeeds("curl -q --fail http://talks.local/orga/")
pretalx.succeed("pretalx-manage --help")
pretalx.log(pretalx.succeed("systemd-analyze security pretalx-web.service"))
'';
}

View File

@@ -0,0 +1,50 @@
{
lib,
pkgs,
...
}:
{
name = "pretix";
meta.maintainers = with lib.maintainers; [ hexa ];
nodes = {
pretix = {
virtualisation.memorySize = 2048;
networking.extraHosts = ''
127.0.0.1 tickets.local
'';
services.pretix = {
enable = true;
nginx.domain = "tickets.local";
plugins = with pkgs.pretix.plugins; [
passbook
pages
zugferd
];
settings = {
pretix = {
instance_name = "NixOS Test";
url = "http://tickets.local";
};
mail.from = "hello@tickets.local";
};
};
};
};
testScript = ''
start_all()
pretix.wait_for_unit("pretix-web.service")
pretix.wait_for_unit("pretix-worker.service")
pretix.wait_until_succeeds("curl -q --fail http://tickets.local")
pretix.succeed("pretix-manage --help")
pretix.log(pretix.succeed("systemd-analyze security pretix-web.service"))
'';
}

View File

@@ -0,0 +1,28 @@
import ../../make-test-python.nix (
{ lib, pkgs, ... }:
{
name = "rss-bridge-caddy";
meta.maintainers = with lib.maintainers; [ mynacol ];
nodes.machine =
{ ... }:
{
services.rss-bridge = {
enable = true;
webserver = "caddy";
virtualHost = "localhost:80";
config.system.enabled_bridges = [ "DemoBridge" ];
};
};
testScript = ''
machine.wait_for_unit("caddy.service")
machine.wait_for_unit("phpfpm-rss-bridge.service")
machine.wait_for_open_port(80)
# check for successful feed download
response = machine.succeed("curl -f 'http://localhost:80/?action=display&bridge=DemoBridge&context=testCheckbox&format=Atom'")
assert '<title type="html">Test</title>' in response, "Feed didn't load successfully"
'';
}
)

View File

@@ -0,0 +1,5 @@
{ system, pkgs, ... }:
{
nginx = import ./nginx.nix { inherit system pkgs; };
caddy = import ./caddy.nix { inherit system pkgs; };
}

View File

@@ -0,0 +1,27 @@
import ../../make-test-python.nix (
{ lib, pkgs, ... }:
{
name = "rss-bridge-nginx";
meta.maintainers = with lib.maintainers; [ mynacol ];
nodes.machine =
{ ... }:
{
services.rss-bridge = {
enable = true;
webserver = "nginx";
config.system.enabled_bridges = [ "DemoBridge" ];
};
};
testScript = ''
machine.wait_for_unit("nginx.service")
machine.wait_for_unit("phpfpm-rss-bridge.service")
machine.wait_for_open_port(80)
# check for successful feed download
response = machine.succeed("curl -f 'http://localhost:80/?action=display&bridge=DemoBridge&context=testCheckbox&format=Atom'")
assert '<title type="html">Test</title>' in response, "Feed didn't load successfully"
'';
}
)

View File

@@ -0,0 +1,55 @@
{ lib, ... }:
let
meilisearchKey = "TESTKEY-naXRkVX7nhvLaGOmGGuicDKxZAj0khEaoOZPeEZafv8w9j8V6aKb0NVdXRChL5kR";
in
{
name = "sharkey";
nodes.machine =
{ pkgs, ... }:
{
services.sharkey = {
enable = true;
setupMeilisearch = true;
environmentFiles = [ "/run/secrets/sharkey-env" ];
settings = {
url = "http://exampleurl.invalid";
meilisearch.index = "exampleurl_invalid";
};
};
services.meilisearch.masterKeyFile = pkgs.writeText "meilisearch-key" meilisearchKey;
};
testScript =
let
createIndexPayload = builtins.toJSON {
description = "Sharkey API key";
actions = [ "*" ];
indexes = [ "exampleurl_invalid---notes" ];
expiresAt = null;
};
in
''
import json
with subtest("Setting up Meilisearch API key and index"):
machine.wait_for_unit("meilisearch.service")
machine.wait_for_open_port(7700)
json_body = '${createIndexPayload}'
create_index_result = json.loads(machine.succeed(f"curl -s -X POST 'http://localhost:7700/keys' -H 'Content-Type: application/json' -H 'Authorization: Bearer ${meilisearchKey}' --data-binary '{json_body}'"))
machine.succeed(f"mkdir /run/secrets; echo 'MK_CONFIG_MEILISEARCH_APIKEY={create_index_result["key"]}' > /run/secrets/sharkey-env")
with subtest("Testing Sharkey is running and listening to HTTP requests"):
machine.systemctl("restart sharkey")
machine.wait_for_open_port(3000)
machine.succeed("curl --fail http://localhost:3000")
'';
meta.maintainers = with lib.maintainers; [
tmarkus
];
}

View File

@@ -0,0 +1,118 @@
/*
Snipe-IT NixOS test
It covers the following scenario:
- Installation
- Backup and restore
Scenarios NOT covered by this test (but perhaps in the future):
- Sending and receiving emails
*/
{ pkgs, ... }:
let
siteName = "NixOS Snipe-IT Test Instance";
in
{
name = "snipe-it";
meta.maintainers = with pkgs.lib.maintainers; [ yayayayaka ];
nodes = {
snipeit =
{ ... }:
{
services.snipe-it = {
enable = true;
appKeyFile = toString (
pkgs.writeText "snipe-it-app-key" "uTqGUN5GUmUrh/zSAYmhyzRk62pnpXICyXv9eeITI8k="
);
hostName = "localhost";
database.createLocally = true;
mail = {
driver = "smtp";
encryption = "tls";
host = "localhost";
port = 1025;
from.name = "Snipe-IT NixOS test";
from.address = "snipe-it@localhost";
replyTo.address = "snipe-it@localhost";
user = "snipe-it@localhost";
passwordFile = toString (pkgs.writeText "snipe-it-mail-pass" "a-secure-mail-password");
};
};
};
};
testScript =
{ nodes }:
let
backupPath = "${nodes.snipeit.services.snipe-it.dataDir}/storage/app/backups";
# Snipe-IT has been installed successfully if the site name shows up on the login page
checkLoginPage =
{
shouldSucceed ? true,
}:
''
snipeit.${
if shouldSucceed then "succeed" else "fail"
}("""curl http://localhost/login | grep '${siteName}'""")
'';
in
''
start_all()
snipeit.wait_for_unit("nginx.service")
snipeit.wait_for_unit("snipe-it-setup.service")
# Create an admin user
snipeit.succeed(
"""
snipe-it snipeit:create-admin \
--username="admin" \
--email="janedoe@localhost" \
--password="extremesecurepassword" \
--first_name="Jane" \
--last_name="Doe"
"""
)
with subtest("Circumvent the pre-flight setup by just writing some settings into the database ourself"):
snipeit.succeed(
"""
mysql -D ${nodes.snipeit.services.snipe-it.database.name} -e "
INSERT INTO settings (id, site_name, login_remote_user_custom_logout_url, login_remote_user_header_name)
VALUES ('1', '${siteName}', 'https://whatever.invalid', 'whatever');"
"""
)
# Usually these are generated during the pre-flight setup
snipeit.succeed("snipe-it passport:keys")
# Login page should now contain the configured site name
${checkLoginPage { }}
with subtest("Test Backup and restore"):
snipeit.succeed("snipe-it snipeit:backup")
# One zip file should have been created
snipeit.succeed("""[ "$(ls -1 "${backupPath}" | wc -l)" -eq 1 ]""")
# Purge the state
snipeit.succeed("snipe-it migrate:fresh --force")
# Login page should disappear
${checkLoginPage { shouldSucceed = false; }}
# Restore the state
snipeit.succeed(
"""
snipe-it snipeit:restore --force $(find "${backupPath}/" -type f -name "*.zip")
"""
)
# Login page should be back again
${checkLoginPage { }}
'';
}

View File

@@ -0,0 +1,32 @@
{ lib, pkgs, ... }:
{
name = "sshwifty";
nodes.machine =
{ ... }:
{
services.sshwifty = {
enable = true;
sharedKeyFile = pkgs.writeText "sharedkey" "rpz2E4QI6uPMLr";
settings = {
HostName = "localhost";
Servers = [
{
ListenInterface = "::1";
ListenPort = 80;
ServerMessage = "NixOS test";
}
];
};
};
};
testScript = ''
machine.wait_for_unit("sshwifty.service")
machine.wait_for_open_port(80)
machine.wait_until_succeeds("curl --fail -6 http://localhost/", timeout=60)
machine.wait_until_succeeds("${lib.getExe pkgs.nodejs} ${./sshwifty-test.js}", timeout=60)
'';
meta.maintainers = [ lib.maintainers.ungeskriptet ];
}

View File

@@ -0,0 +1,83 @@
#!/usr/bin/env node
/* Based on ui/app.js from Sshwifty. */
const { subtle } = require('node:crypto')
const sshwiftyURL = 'http://localhost/sshwifty/socket/verify'
const sharedKey = 'rpz2E4QI6uPMLr'
const serverMessage = 'NixOS test'
async function hmac512(secret, data) {
const key = await subtle.importKey(
'raw',
secret,
{ name: 'HMAC', hash: { name: 'SHA-512' } },
false,
['sign', 'verify'],
)
return subtle.sign(key.algorithm, key, data)
}
async function getSocketAuthKey(privateKey) {
const enc = new TextEncoder(),
rTime = Number(Math.trunc(Date.now() / 100000))
return new Uint8Array(
await hmac512(enc.encode(privateKey), enc.encode(rTime)),
).slice(0, 32)
}
async function requestAuth(privateKey) {
const authKey = await getSocketAuthKey(privateKey)
const h = await fetch(sshwiftyURL, {
headers: { 'X-Key': btoa(String.fromCharCode.apply(null, authKey)) },
})
const serverDate = h.headers.get('Date')
return {
result: h.status,
date: serverDate ? new Date(serverDate) : null,
text: await h.text(),
}
}
async function tryInitialAuth() {
try {
const result = await requestAuth(sharedKey)
if (result.date) {
const serverRespondTime = result.date,
serverRespondTimestamp = serverRespondTime.getTime(),
clientCurrent = new Date(),
clientTimestamp = clientCurrent.getTime(),
timeDiff = Math.abs(serverRespondTimestamp - clientTimestamp)
if (timeDiff > 30000) {
console.log('Time difference between client and server too big.')
process.exit(1)
}
}
switch (result.result) {
case 200:
if (result.text.includes(serverMessage)) {
console.log('All good.')
process.exit()
} else {
console.log('Server message not found')
process.exit(1)
}
break
case 403:
console.log('We need auth.')
process.exit(1)
break
case 0:
console.log('Timeout?')
process.exit(1)
break
default:
console.log('wghat')
process.exit(1)
}
} catch {
console.log('Something went horribly wrong, ouch.')
process.exit(1)
}
}
console.log('Testing Sshwifty')
tryInitialAuth()

View File

@@ -0,0 +1,45 @@
{ lib, ... }:
{
name = "umami-nixos";
meta.maintainers = with lib.maintainers; [ diogotcorreia ];
nodes.machine =
{ pkgs, ... }:
{
services.umami = {
enable = true;
settings = {
APP_SECRET = "very_secret";
};
};
};
testScript = ''
import json
machine.wait_for_unit("umami.service")
machine.wait_for_open_port(3000)
machine.succeed("curl --fail http://localhost:3000/")
machine.succeed("curl --fail http://localhost:3000/script.js")
res = machine.succeed("""
curl -f --json '{ "username": "admin", "password": "umami" }' http://localhost:3000/api/auth/login
""")
token = json.loads(res)['token']
res = machine.succeed("""
curl -f -H 'Authorization: Bearer %s' --json '{ "domain": "localhost", "name": "Test" }' http://localhost:3000/api/websites
""" % token)
print(res)
websiteId = json.loads(res)['id']
res = machine.succeed("""
curl -f -H 'Authorization: Bearer %s' http://localhost:3000/api/websites/%s
""" % (token, websiteId))
website = json.loads(res)
assert website["name"] == "Test"
assert website["domain"] == "localhost"
'';
}

View File

@@ -0,0 +1,103 @@
{ pkgs, ... }:
let
certs = import ../common/acme/server/snakeoil-certs.nix;
serverDomain = certs.domain;
admin = {
username = "admin";
password = "snakeoilpass";
};
# An API token that we manually insert into the db as a valid one.
apiToken = "OVJh65sXaAfQMZ4NTcIGbFZIyBZbEZqWTi7azdDf";
in
{
name = "weblate";
meta.maintainers = with pkgs.lib.maintainers; [ erictapen ];
nodes.server =
{ pkgs, lib, ... }:
{
virtualisation.memorySize = 2048;
services.weblate = {
enable = true;
localDomain = "${serverDomain}";
djangoSecretKeyFile = pkgs.writeText "weblate-django-secret" "thisissnakeoilsecretwithmorethan50characterscorrecthorsebatterystaple";
extraConfig = ''
# Weblate tries to fetch Avatars from the network
ENABLE_AVATARS = False
'';
};
services.nginx.virtualHosts."${serverDomain}" = {
enableACME = lib.mkForce false;
sslCertificate = certs."${serverDomain}".cert;
sslCertificateKey = certs."${serverDomain}".key;
};
security.pki.certificateFiles = [ certs.ca.cert ];
networking.hosts."::1" = [ "${serverDomain}" ];
networking.firewall.allowedTCPPorts = [
80
443
];
users.users.weblate.shell = pkgs.bashInteractive;
};
nodes.client =
{ pkgs, nodes, ... }:
{
environment.systemPackages = [ pkgs.wlc ];
environment.etc."xdg/weblate".text = ''
[weblate]
url = https://${serverDomain}/api/
key = ${apiToken}
'';
networking.hosts."${nodes.server.networking.primaryIPAddress}" = [ "${serverDomain}" ];
security.pki.certificateFiles = [ certs.ca.cert ];
};
testScript = ''
import json
start_all()
server.wait_for_unit("weblate.socket")
server.wait_until_succeeds("curl -f https://${serverDomain}/")
server.succeed("sudo -iu weblate -- weblate createadmin --username ${admin.username} --password ${admin.password} --email weblate@example.org")
# It's easier to replace the generated API token with a predefined one than
# to extract it at runtime.
server.succeed("sudo -iu weblate -- psql -d weblate -c \"UPDATE authtoken_token SET key = '${apiToken}' WHERE user_id = (SELECT id FROM weblate_auth_user WHERE username = 'admin');\"")
client.wait_for_unit("multi-user.target")
# Test the official Weblate client wlc.
client.wait_until_succeeds("REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt wlc --debug list-projects")
def call_wl_api(arg):
(rv, result) = client.execute("curl -H \"Content-Type: application/json\" -H \"Authorization: Token ${apiToken}\" https://${serverDomain}/api/{}".format(arg))
assert rv == 0
print(result)
call_wl_api("users/ --data '{}'".format(
json.dumps(
{"username": "test1",
"full_name": "test1",
"email": "test1@example.org"
})))
# TODO: Check sending and receiving email.
# server.wait_for_unit("postfix.service")
server.succeed("sudo -iu weblate -- weblate check")
# TODO: The goal is for this to succeed, but there are still some checks failing.
# server.succeed("sudo -iu weblate -- weblate check --deploy")
'';
}

View File

@@ -0,0 +1,47 @@
{
runTest,
...
}:
let
writefreelyTest =
{ name, type }:
runTest {
name = "writefreely-${name}";
nodes.machine =
{ config, pkgs, ... }:
{
services.writefreely = {
enable = true;
host = "localhost:3000";
admin.name = "nixos";
database = {
inherit type;
createLocally = type == "mysql";
passwordFile = pkgs.writeText "db-pass" "pass";
};
settings.server.port = 3000;
};
};
testScript = ''
start_all()
machine.wait_for_unit("writefreely.service")
machine.wait_for_open_port(3000)
machine.succeed("curl --fail http://localhost:3000")
'';
};
in
{
sqlite = writefreelyTest {
name = "sqlite";
type = "sqlite3";
};
mysql = writefreelyTest {
name = "mysql";
type = "mysql";
};
}