about summary refs log tree commit diff
path: root/nixpkgs/nixos/tests/web-apps
diff options
context:
space:
mode:
Diffstat (limited to 'nixpkgs/nixos/tests/web-apps')
-rw-r--r--nixpkgs/nixos/tests/web-apps/gotosocial.nix28
-rw-r--r--nixpkgs/nixos/tests/web-apps/healthchecks.nix42
-rw-r--r--nixpkgs/nixos/tests/web-apps/mastodon/default.nix9
-rw-r--r--nixpkgs/nixos/tests/web-apps/mastodon/remote-databases.nix190
-rw-r--r--nixpkgs/nixos/tests/web-apps/mastodon/script.nix52
-rw-r--r--nixpkgs/nixos/tests/web-apps/mastodon/standard.nix91
-rw-r--r--nixpkgs/nixos/tests/web-apps/monica.nix33
-rw-r--r--nixpkgs/nixos/tests/web-apps/netbox-upgrade.nix87
-rw-r--r--nixpkgs/nixos/tests/web-apps/netbox.nix318
-rw-r--r--nixpkgs/nixos/tests/web-apps/nifi.nix30
-rw-r--r--nixpkgs/nixos/tests/web-apps/peering-manager.nix40
-rw-r--r--nixpkgs/nixos/tests/web-apps/peertube.nix139
-rw-r--r--nixpkgs/nixos/tests/web-apps/phylactery.nix20
-rw-r--r--nixpkgs/nixos/tests/web-apps/pixelfed/default.nix8
-rw-r--r--nixpkgs/nixos/tests/web-apps/pixelfed/standard.nix38
-rw-r--r--nixpkgs/nixos/tests/web-apps/pretalx.nix31
-rw-r--r--nixpkgs/nixos/tests/web-apps/snipe-it.nix101
-rw-r--r--nixpkgs/nixos/tests/web-apps/writefreely.nix44
18 files changed, 1301 insertions, 0 deletions
diff --git a/nixpkgs/nixos/tests/web-apps/gotosocial.nix b/nixpkgs/nixos/tests/web-apps/gotosocial.nix
new file mode 100644
index 000000000000..6d279ab63a79
--- /dev/null
+++ b/nixpkgs/nixos/tests/web-apps/gotosocial.nix
@@ -0,0 +1,28 @@
+{ lib, ... }:
+{
+  name = "gotosocial";
+  meta.maintainers = with lib.maintainers; [ misuzu ];
+
+  nodes.machine = { pkgs, ... }: {
+    environment.systemPackages = [ pkgs.jq ];
+    services.gotosocial = {
+      enable = true;
+      setupPostgresqlDB = true;
+      settings = {
+        host = "localhost:8081";
+        port = 8081;
+      };
+    };
+  };
+
+  testScript = ''
+    machine.wait_for_unit("gotosocial.service")
+    machine.wait_for_unit("postgresql.service")
+    machine.wait_for_open_port(8081)
+
+    # check user registration via cli
+    machine.succeed("curl -sS -f http://localhost:8081/nodeinfo/2.0 | jq '.usage.users.total' | grep -q '^0$'")
+    machine.succeed("gotosocial-admin account create --username nickname --email email@example.com --password kurtz575VPeBgjVm")
+    machine.succeed("curl -sS -f http://localhost:8081/nodeinfo/2.0 | jq '.usage.users.total' | grep -q '^1$'")
+  '';
+}
diff --git a/nixpkgs/nixos/tests/web-apps/healthchecks.nix b/nixpkgs/nixos/tests/web-apps/healthchecks.nix
new file mode 100644
index 000000000000..41c40cd5dd8d
--- /dev/null
+++ b/nixpkgs/nixos/tests/web-apps/healthchecks.nix
@@ -0,0 +1,42 @@
+import ../make-test-python.nix ({ 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>Sign 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.
+        assert "foo\n" == machine.succeed("echo 'print(\"foo\")' | sudo -u healthchecks healthchecks-manage shell")
+
+        # Shouldn't fail if not called by healthchecks user
+        assert "foo\n" == machine.succeed("echo 'print(\"foo\")' | healthchecks-manage shell")
+  '';
+})
diff --git a/nixpkgs/nixos/tests/web-apps/mastodon/default.nix b/nixpkgs/nixos/tests/web-apps/mastodon/default.nix
new file mode 100644
index 000000000000..178590d13b63
--- /dev/null
+++ b/nixpkgs/nixos/tests/web-apps/mastodon/default.nix
@@ -0,0 +1,9 @@
+{ system ? builtins.currentSystem, handleTestOn }:
+let
+  supportedSystems = [ "x86_64-linux" "i686-linux" "aarch64-linux" ];
+
+in
+{
+  standard = handleTestOn supportedSystems ./standard.nix { inherit system; };
+  remote-databases = handleTestOn supportedSystems ./remote-databases.nix { inherit system; };
+}
diff --git a/nixpkgs/nixos/tests/web-apps/mastodon/remote-databases.nix b/nixpkgs/nixos/tests/web-apps/mastodon/remote-databases.nix
new file mode 100644
index 000000000000..fa6430a99353
--- /dev/null
+++ b/nixpkgs/nixos/tests/web-apps/mastodon/remote-databases.nix
@@ -0,0 +1,190 @@
+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
+  '';
+
+in
+{
+  name = "mastodon-remote-postgresql";
+  meta.maintainers = with pkgs.lib.maintainers; [ erictapen izorkin ];
+
+  nodes = {
+    databases = { config, ... }: {
+      environment = {
+        etc = {
+          "redis/password-redis-db".text = ''
+            ogjhJL8ynrP7MazjYOF6
+          '';
+        };
+      };
+      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.port
+        ];
+      };
+
+      services.redis.servers.mastodon = {
+        enable = true;
+        bind = "0.0.0.0";
+        port = 31637;
+        requirePassFile = "/etc/redis/password-redis-db";
+      };
+
+      services.postgresql = {
+        enable = true;
+        # TODO remove once https://github.com/NixOS/nixpkgs/pull/266270 is resolved.
+        package = pkgs.postgresql_14;
+        enableTCPIP = true;
+        authentication = ''
+          hostnossl mastodon_local mastodon_test 192.168.2.201/32 md5
+        '';
+        initialScript = pkgs.writeText "postgresql_init.sql" ''
+          CREATE ROLE mastodon_test LOGIN PASSWORD 'SoDTZcISc3f1M1LJsRLT';
+          CREATE DATABASE mastodon_local TEMPLATE template0 ENCODING UTF8;
+          GRANT ALL PRIVILEGES ON DATABASE mastodon_local TO mastodon_test;
+        '';
+      };
+    };
+
+    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 = ''
+            ogjhJL8ynrP7MazjYOF6
+          '';
+          "mastodon/password-posgressql-db".text = ''
+            SoDTZcISc3f1M1LJsRLT
+          '';
+        };
+      };
+
+      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_local";
+          user = "mastodon_test";
+          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.service")
+      databases.wait_for_open_port(31637)
+      databases.wait_for_open_port(5432)
+    '';
+    extraShutdown = ''
+      nginx.shutdown()
+      databases.shutdown()
+    '';
+  };
+})
diff --git a/nixpkgs/nixos/tests/web-apps/mastodon/script.nix b/nixpkgs/nixos/tests/web-apps/mastodon/script.nix
new file mode 100644
index 000000000000..9184c63c8941
--- /dev/null
+++ b/nixpkgs/nixos/tests/web-apps/mastodon/script.nix
@@ -0,0 +1,52 @@
+{ 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}
+''
diff --git a/nixpkgs/nixos/tests/web-apps/mastodon/standard.nix b/nixpkgs/nixos/tests/web-apps/mastodon/standard.nix
new file mode 100644
index 000000000000..ddc764e2168c
--- /dev/null
+++ b/nixpkgs/nixos/tests/web-apps/mastodon/standard.nix
@@ -0,0 +1,91 @@
+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" ];
+      };
+
+      # TODO remove once https://github.com/NixOS/nixpkgs/pull/266270 is resolved.
+      services.postgresql.package = pkgs.postgresql_14;
+
+      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.service")
+      server.wait_for_open_port(5432)
+    '';
+  };
+})
diff --git a/nixpkgs/nixos/tests/web-apps/monica.nix b/nixpkgs/nixos/tests/web-apps/monica.nix
new file mode 100644
index 000000000000..29f5cb85bb13
--- /dev/null
+++ b/nixpkgs/nixos/tests/web-apps/monica.nix
@@ -0,0 +1,33 @@
+import ../make-test-python.nix ({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)
+  '';
+})
diff --git a/nixpkgs/nixos/tests/web-apps/netbox-upgrade.nix b/nixpkgs/nixos/tests/web-apps/netbox-upgrade.nix
new file mode 100644
index 000000000000..4c554e7ae613
--- /dev/null
+++ b/nixpkgs/nixos/tests/web-apps/netbox-upgrade.nix
@@ -0,0 +1,87 @@
+import ../make-test-python.nix ({ lib, pkgs, ... }: let
+  oldNetbox = pkgs.netbox_3_6;
+  newNetbox = pkgs.netbox_3_7;
+in {
+  name = "netbox-upgrade";
+
+  meta = with lib.maintainers; {
+    maintainers = [ minijackson raitobezarius ];
+  };
+
+  nodes.machine = { config, ... }: {
+    virtualisation.memorySize = 2048;
+    services.netbox = {
+      enable = true;
+      package = 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 ];
+
+    specialisation.upgrade.configuration.services.netbox.package = lib.mkForce newNetbox;
+  };
+
+  testScript = { nodes, ... }:
+    let
+      apiVersion = version: lib.pipe version [
+        (lib.splitString ".")
+        (lib.take 2)
+        (lib.concatStringsSep ".")
+      ];
+      oldApiVersion = apiVersion oldNetbox.version;
+      newApiVersion = apiVersion newNetbox.version;
+    in
+    ''
+      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):
+          headers = machine.succeed(
+            "curl -sSfL 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}")
+    '';
+})
diff --git a/nixpkgs/nixos/tests/web-apps/netbox.nix b/nixpkgs/nixos/tests/web-apps/netbox.nix
new file mode 100644
index 000000000000..233f16a8fe0d
--- /dev/null
+++ b/nixpkgs/nixos/tests/web-apps/netbox.nix
@@ -0,0 +1,318 @@
+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 n0emis ];
+  };
+
+  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 django.contrib.auth.models import User
+      u = User.objects.get(username='netbox')
+      u.set_password('netbox')
+      u.save()
+    '';
+  in ''
+    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")
+
+    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/")
+
+    with subtest("Can interact with API"):
+        json.loads(
+            machine.succeed("curl -sSfL -H 'Accept: application/json' 'http://localhost/api/'")
+        )
+
+    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)
+
+    with subtest("Can create objects"):
+        result = post("/dcim/sites/", {"name": "Test site", "slug": "test-site"})
+        site_id = result["id"]
+
+        # Example from:
+        # http://netbox.extra.cea.fr/static/docs/integrations/rest-api/#creating-a-new-object
+        post("/ipam/prefixes/", {"prefix": "192.0.2.0/24", "site": 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
+
+    with subtest("Can use the GraphQL API"):
+        encoded_data = json.dumps({
+            "query": "query { prefix_list { prefix, site { id, description } } }",
+        })
+        result = json.loads(
+            machine.succeed(
+                "curl -sSfL "
+                "-H 'Accept: application/json' "
+                "-H 'Content-Type: application/json' "
+                f"-H 'Authorization: Token {auth_token}' "
+                "'http://localhost/graphql/' "
+                f"--data '{encoded_data}'"
+            )
+        )
+
+        assert len(result["data"]["prefix_list"]) == 1
+        assert result["data"]["prefix_list"][0]["prefix"] == "192.0.2.0/24"
+        assert result["data"]["prefix_list"][0]["site"]["id"] == str(site_id)
+        assert result["data"]["prefix_list"][0]["site"]["description"] == new_description
+
+    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"])
+  '';
+})
diff --git a/nixpkgs/nixos/tests/web-apps/nifi.nix b/nixpkgs/nixos/tests/web-apps/nifi.nix
new file mode 100644
index 000000000000..92f7fa231df3
--- /dev/null
+++ b/nixpkgs/nixos/tests/web-apps/nifi.nix
@@ -0,0 +1,30 @@
+import ../make-test-python.nix ({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()
+  '';
+})
diff --git a/nixpkgs/nixos/tests/web-apps/peering-manager.nix b/nixpkgs/nixos/tests/web-apps/peering-manager.nix
new file mode 100644
index 000000000000..3f0acd560d13
--- /dev/null
+++ b/nixpkgs/nixos/tests/web-apps/peering-manager.nix
@@ -0,0 +1,40 @@
+import ../make-test-python.nix ({ 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"
+        )
+  '';
+})
diff --git a/nixpkgs/nixos/tests/web-apps/peertube.nix b/nixpkgs/nixos/tests/web-apps/peertube.nix
new file mode 100644
index 000000000000..0e5f39c08a02
--- /dev/null
+++ b/nixpkgs/nixos/tests/web-apps/peertube.nix
@@ -0,0 +1,139 @@
+import ../make-test-python.nix ({pkgs, ...}:
+{
+  name = "peertube";
+  meta.maintainers = with pkgs.lib.maintainers; [ izorkin ];
+
+  nodes = {
+    database = {
+      networking = {
+       interfaces.eth1 = {
+          ipv4.addresses = [
+            { address = "192.168.2.10"; prefixLength = 24; }
+          ];
+        };
+        firewall.allowedTCPPorts = [ 5432 31638 ];
+      };
+
+      services.postgresql = {
+        enable = true;
+        enableTCPIP = true;
+        authentication = ''
+          hostnossl peertube_local peertube_test 192.168.2.11/32 md5
+        '';
+        initialScript = pkgs.writeText "postgresql_init.sql" ''
+          CREATE ROLE peertube_test LOGIN PASSWORD '0gUN0C1mgST6czvjZ8T9';
+          CREATE DATABASE peertube_local TEMPLATE template0 ENCODING UTF8;
+          GRANT ALL PRIVILEGES ON DATABASE peertube_local TO peertube_test;
+          \connect peertube_local
+          CREATE EXTENSION IF NOT EXISTS pg_trgm;
+          CREATE EXTENSION IF NOT EXISTS unaccent;
+        '';
+      };
+
+      services.redis.servers.peertube = {
+        enable = true;
+        bind = "0.0.0.0";
+        requirePass = "turrQfaQwnanGbcsdhxy";
+        port = 31638;
+      };
+    };
+
+    server = { pkgs, ... }: {
+      environment = {
+        etc = {
+          "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 peertube.local
+        '';
+        firewall.allowedTCPPorts = [ 9000 ];
+      };
+
+      services.peertube = {
+        enable = true;
+        localDomain = "peertube.local";
+        enableWebHttps = false;
+
+        secrets = {
+          secretsFile = "/etc/peertube/secrets-peertube";
+        };
+
+        database = {
+          host = "192.168.2.10";
+          name = "peertube_local";
+          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 ];
+      networking = {
+       interfaces.eth1 = {
+          ipv4.addresses = [
+            { address = "192.168.2.12"; prefixLength = 24; }
+          ];
+        };
+        extraHosts = ''
+          192.168.2.11 peertube.local
+        '';
+      };
+    };
+
+  };
+
+  testScript = ''
+    start_all()
+
+    database.wait_for_unit("postgresql.service")
+    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(9000)
+
+    # Check if PeerTube is running
+    client.succeed("curl --fail http://peertube.local:9000/api/v1/config/about | jq -r '.instance.name' | grep 'PeerTube\ Test\ Server'")
+
+    # Check PeerTube CLI version
+    assert "${pkgs.peertube.version}" in server.succeed('su - peertube -s /bin/sh -c "peertube --version"')
+
+    client.shutdown()
+    server.shutdown()
+    database.shutdown()
+  '';
+})
diff --git a/nixpkgs/nixos/tests/web-apps/phylactery.nix b/nixpkgs/nixos/tests/web-apps/phylactery.nix
new file mode 100644
index 000000000000..cf2689d2300d
--- /dev/null
+++ b/nixpkgs/nixos/tests/web-apps/phylactery.nix
@@ -0,0 +1,20 @@
+import ../make-test-python.nix ({ pkgs, lib, ... }: {
+  name = "phylactery";
+
+  nodes.machine = { ... }: {
+    services.phylactery = rec {
+      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 ];
+})
diff --git a/nixpkgs/nixos/tests/web-apps/pixelfed/default.nix b/nixpkgs/nixos/tests/web-apps/pixelfed/default.nix
new file mode 100644
index 000000000000..4464ebe43486
--- /dev/null
+++ b/nixpkgs/nixos/tests/web-apps/pixelfed/default.nix
@@ -0,0 +1,8 @@
+{ system ? builtins.currentSystem, handleTestOn }:
+let
+  supportedSystems = [ "x86_64-linux" "i686-linux" ];
+
+in
+{
+  standard = handleTestOn supportedSystems ./standard.nix { inherit system; };
+}
diff --git a/nixpkgs/nixos/tests/web-apps/pixelfed/standard.nix b/nixpkgs/nixos/tests/web-apps/pixelfed/standard.nix
new file mode 100644
index 000000000000..9260e27af960
--- /dev/null
+++ b/nixpkgs/nixos/tests/web-apps/pixelfed/standard.nix
@@ -0,0 +1,38 @@
+import ../../make-test-python.nix ({pkgs, ...}:
+{
+  name = "pixelfed-standard";
+  meta.maintainers = with pkgs.lib.maintainers; [ raitobezarius ];
+
+  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")
+  '';
+})
diff --git a/nixpkgs/nixos/tests/web-apps/pretalx.nix b/nixpkgs/nixos/tests/web-apps/pretalx.nix
new file mode 100644
index 000000000000..a226639b076b
--- /dev/null
+++ b/nixpkgs/nixos/tests/web-apps/pretalx.nix
@@ -0,0 +1,31 @@
+{ lib, ... }:
+
+{
+  name = "pretalx";
+  meta.maintainers = lib.teams.c3d2.members;
+
+  nodes = {
+    pretalx = {
+      networking.extraHosts = ''
+        127.0.0.1 talks.local
+      '';
+
+      services.pretalx = {
+        enable = true;
+        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/")
+  '';
+}
diff --git a/nixpkgs/nixos/tests/web-apps/snipe-it.nix b/nixpkgs/nixos/tests/web-apps/snipe-it.nix
new file mode 100644
index 000000000000..123d7742056b
--- /dev/null
+++ b/nixpkgs/nixos/tests/web-apps/snipe-it.nix
@@ -0,0 +1,101 @@
+/*
+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, user_id, site_name) VALUES ('1', '1', '${siteName}');"
+            """
+        )
+
+        # 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 {}}
+  '';
+}
diff --git a/nixpkgs/nixos/tests/web-apps/writefreely.nix b/nixpkgs/nixos/tests/web-apps/writefreely.nix
new file mode 100644
index 000000000000..ce614909706b
--- /dev/null
+++ b/nixpkgs/nixos/tests/web-apps/writefreely.nix
@@ -0,0 +1,44 @@
+{ system ? builtins.currentSystem, config ? { }
+, pkgs ? import ../../.. { inherit system config; } }:
+
+with import ../../lib/testing-python.nix { inherit system pkgs; };
+with pkgs.lib;
+
+let
+  writefreelyTest = { name, type }:
+    makeTest {
+      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";
+  };
+}