summaryrefslogtreecommitdiffstats
path: root/nixos
diff options
context:
space:
mode:
authorlassulus <github@lassul.us>2024-04-11 21:02:07 +0200
committerGitHub <noreply@github.com>2024-04-11 21:02:07 +0200
commit4cde9116a17eab9aa45e62a415114e268d6b5990 (patch)
treebf82d24e22650c68307dbdb9fee3a0cbe1746cef /nixos
parent578d870ed295804b8ae8589a675cb22278f5eb6d (diff)
parent0ba23300dec970d3226841a0002e1dd0a4320430 (diff)
Merge pull request #296641 from toastal/movim-service
nixos/movim: init, movim: additions + patches
Diffstat (limited to 'nixos')
-rw-r--r--nixos/modules/module-list.nix1
-rw-r--r--nixos/modules/services/web-apps/movim.nix711
-rw-r--r--nixos/tests/all-tests.nix1
-rw-r--r--nixos/tests/web-apps/movim/default.nix8
-rw-r--r--nixos/tests/web-apps/movim/standard.nix102
5 files changed, 823 insertions, 0 deletions
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index 32ce0f3ec58d..73ccf3136f17 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -1365,6 +1365,7 @@
./services/web-apps/miniflux.nix
./services/web-apps/monica.nix
./services/web-apps/moodle.nix
+ ./services/web-apps/movim.nix
./services/web-apps/netbox.nix
./services/web-apps/nextcloud.nix
./services/web-apps/nextcloud-notify_push.nix
diff --git a/nixos/modules/services/web-apps/movim.nix b/nixos/modules/services/web-apps/movim.nix
new file mode 100644
index 000000000000..bb88a185b461
--- /dev/null
+++ b/nixos/modules/services/web-apps/movim.nix
@@ -0,0 +1,711 @@
+{ config, lib, pkgs, ... }:
+
+let
+ inherit (lib)
+ filterAttrsRecursive
+ generators
+ literalExpression
+ mkDefault
+ mkIf
+ mkOption
+ mkEnableOption
+ mkPackageOption
+ mkMerge
+ pipe
+ types
+ ;
+
+ cfg = config.services.movim;
+
+ defaultPHPCfg = {
+ "output_buffering" = 0;
+ "error_reporting" = "E_ALL & ~E_DEPRECATED & ~E_STRICT";
+ "opcache.enable_cli" = 1;
+ "opcache.interned_strings_buffer" = 8;
+ "opcache.max_accelerated_files" = 6144;
+ "opcache.memory_consumption" = 128;
+ "opcache.revalidate_freq" = 2;
+ "opcache.fast_shutdown" = 1;
+ };
+
+ phpCfg = generators.toKeyValue
+ { mkKeyValue = generators.mkKeyValueDefault { } " = "; }
+ (defaultPHPCfg // cfg.phpCfg);
+
+ podConfigFlags =
+ let
+ bevalue = a: lib.escapeShellArg (generators.mkValueStringDefault { } a);
+ in
+ lib.concatStringsSep " "
+ (lib.attrsets.foldlAttrs
+ (acc: k: v: acc ++ lib.optional (v != null) "--${k}=${bevalue v}")
+ [ ]
+ cfg.podConfig);
+
+ package =
+ let
+ p = cfg.package.override
+ ({
+ inherit phpCfg;
+ withPgsql = cfg.database.type == "pgsql";
+ withMysql = cfg.database.type == "mysql";
+ inherit (cfg) minifyStaticFiles;
+ } // lib.optionalAttrs (lib.isAttrs cfg.minifyStaticFiles) (with cfg.minifyStaticFiles; {
+ esbuild = esbuild.package;
+ lightningcss = lightningcss.package;
+ scour = scour.package;
+ }));
+ in
+ p.overrideAttrs (finalAttrs: prevAttrs:
+ let
+ appDir = "$out/share/php/${finalAttrs.pname}";
+
+ stateDirectories = ''
+ # Symlinking in our state directories
+ rm -rf $out/.env $out/cache ${appDir}/public/cache
+ ln -s ${cfg.dataDir}/.env ${appDir}/.env
+ ln -s ${cfg.dataDir}/public/cache ${appDir}/public/cache
+ ln -s ${cfg.logDir} ${appDir}/log
+ ln -s ${cfg.runtimeDir}/cache ${appDir}/cache
+ '';
+
+ exposeComposer = ''
+ # Expose PHP Composer for scripts
+ mkdir -p $out/bin
+ echo "#!${lib.getExe pkgs.dash}" > $out/bin/movim-composer
+ echo "${finalAttrs.php.packages.composer}/bin/composer --working-dir="${appDir}" \"\$@\"" >> $out/bin/movim-composer
+ chmod +x $out/bin/movim-composer
+ '';
+
+ podConfigInputDisableReplace = lib.optionalString (podConfigFlags != "")
+ (lib.concatStringsSep "\n"
+ (lib.attrsets.foldlAttrs
+ (acc: k: v:
+ acc ++ lib.optional (v != null)
+ # Disable all Admin panel options that were set in the
+ # `cfg.podConfig` to prevent confusing situtions where the
+ # values are rewritten on server reboot
+ ''
+ substituteInPlace ${appDir}/app/widgets/AdminMain/adminmain.tpl \
+ --replace-warn 'name="${k}"' 'name="${k}" disabled'
+ '')
+ [ ]
+ cfg.podConfig));
+
+ precompressStaticFilesJobs =
+ let
+ inherit (cfg.precompressStaticFiles) brotli gzip;
+
+ findTextFileNames = lib.concatStringsSep " -o "
+ (builtins.map (n: ''-iname "*.${n}"'')
+ [ "css" "ini" "js" "json" "manifest" "mjs" "svg" "webmanifest" ]);
+ in
+ lib.concatStringsSep "\n" [
+ (lib.optionalString brotli.enable ''
+ echo -n "Precompressing static files with Brotli …"
+ find ${appDir}/public -type f ${findTextFileNames} \
+ | ${lib.getExe pkgs.parallel} ${lib.escapeShellArgs [
+ "--will-cite"
+ "-j $NIX_BUILD_CORES"
+ "${lib.getExe brotli.package} --keep --quality=${builtins.toString brotli.compressionLevel} --output={}.br {}"
+ ]}
+ echo " done."
+ '')
+ (lib.optionalString gzip.enable ''
+ echo -n "Precompressing static files with Gzip …"
+ find ${appDir}/public -type f ${findTextFileNames} \
+ | ${lib.getExe pkgs.parallel} ${lib.escapeShellArgs [
+ "--will-cite"
+ "-j $NIX_BUILD_CORES"
+ "${lib.getExe gzip.package} -c -${builtins.toString gzip.compressionLevel} {} > {}.gz"
+ ]}
+ echo " done."
+ '')
+ ];
+ in
+ {
+ postInstall = lib.concatStringsSep "\n\n" [
+ prevAttrs.postInstall
+ stateDirectories
+ exposeComposer
+ podConfigInputDisableReplace
+ precompressStaticFilesJobs
+ ];
+ });
+
+ configFile = pipe cfg.settings [
+ (filterAttrsRecursive (_: v: v != null))
+ (generators.toKeyValue { })
+ (pkgs.writeText "movim-env")
+ ];
+
+ pool = "movim";
+ fpm = config.services.phpfpm.pools.${pool};
+ phpExecutionUnit = "phpfpm-${pool}";
+
+ dbService = {
+ "postgresql" = "postgresql.service";
+ "mysql" = "mysql.service";
+ }.${cfg.database.type};
+in
+{
+ options.services = {
+ movim = {
+ enable = mkEnableOption "a Movim instance";
+ package = mkPackageOption pkgs "movim" { };
+ phpPackage = mkPackageOption pkgs "php" { };
+
+ phpCfg = mkOption {
+ type = with types; attrsOf (oneOf [ int str bool ]);
+ defaultText = literalExpression (generators.toPretty { } defaultPHPCfg);
+ default = { };
+ description = "Extra PHP INI options such as `memory_limit`, `max_execution_time`, etc.";
+ };
+
+ user = mkOption {
+ type = types.nonEmptyStr;
+ default = "movim";
+ description = "User running Movim service";
+ };
+
+ group = mkOption {
+ type = types.nonEmptyStr;
+ default = "movim";
+ description = "Group running Movim service";
+ };
+
+ dataDir = mkOption {
+ type = types.nonEmptyStr;
+ default = "/var/lib/movim";
+ description = "State directory of the `movim` user which holds the application’s state & data.";
+ };
+
+ logDir = mkOption {
+ type = types.nonEmptyStr;
+ default = "/var/log/movim";
+ description = "Log directory of the `movim` user which holds the application’s logs.";
+ };
+
+ runtimeDir = mkOption {
+ type = types.nonEmptyStr;
+ default = "/run/movim";
+ description = "Runtime directory of the `movim` user which holds the application’s caches & temporary files.";
+ };
+
+ domain = mkOption {
+ type = types.nonEmptyStr;
+ description = "Fully-qualified domain name (FQDN) for the Movim instance.";
+ };
+
+ port = mkOption {
+ type = types.port;
+ default = 8080;
+ description = "Movim daemon port.";
+ };
+
+ debug = mkOption {
+ type = types.bool;
+ default = false;
+ description = "Debugging logs.";
+ };
+
+ verbose = mkOption {
+ type = types.bool;
+ default = false;
+ description = "Verbose logs.";
+ };
+
+ minifyStaticFiles = mkOption {
+ type = with types; either bool (submodule {
+ options = {
+ script = mkOption {
+ type = types.submodule {
+ options = {
+ enable = mkEnableOption "Script minification";
+ package = mkPackageOption pkgs "esbuild" { };
+ target = mkOption {
+ type = with types; nullOr nonEmptyStr;
+ default = null;
+ };
+ };
+ };
+ };
+ style = mkOption {
+ type = types.submodule {
+ options = {
+ enable = mkEnableOption "Script minification";
+ package = mkPackageOption pkgs "lightningcss" { };
+ target = mkOption {
+ type = with types; nullOr nonEmptyStr;
+ default = null;
+ };
+ };
+ };
+ };
+ svg = mkOption {
+ type = types.submodule {
+ options = {
+ enable = mkEnableOption "SVG minification";
+ package = mkPackageOption pkgs "scour" { };
+ };
+ };
+ };
+ };
+ });
+ default = true;
+ description = "Do minification on public static files";
+ };
+
+ precompressStaticFiles = mkOption {
+ type = with types; submodule {
+ options = {
+ brotli = {
+ enable = mkEnableOption "Brotli precompression";
+ package = mkPackageOption pkgs "brotli" { };
+ compressionLevel = mkOption {
+ type = types.ints.between 0 11;
+ default = 11;
+ description = "Brotli compression level";
+ };
+ };
+ gzip = {
+ enable = mkEnableOption "Gzip precompression";
+ package = mkPackageOption pkgs "gzip" { };
+ compressionLevel = mkOption {
+ type = types.ints.between 1 9;
+ default = 9;
+ description = "Gzip compression level";
+ };
+ };
+ };
+ };
+ default = {
+ brotli.enable = true;
+ gzip.enable = false;
+ };
+ description = "Aggressively precompress static files";
+ };
+
+ podConfig = mkOption {
+ type = types.submodule {
+ options = {
+ info = mkOption {
+ type = with types; nullOr str;
+ default = null;
+ description = "Content of the info box on the login page";
+ };
+
+ description = mkOption {
+ type = with types; nullOr str;
+ default = null;
+ description = "General description of the instance";
+ };
+
+ timezone = mkOption {
+ type = with types; nullOr str;
+ default = null;
+ description = "The server timezone";
+ };
+
+ restrictsuggestions = mkOption {
+ type = with types; nullOr bool;
+ default = null;
+ description = "Only suggest chatrooms, Communities and other contents that are available on the user XMPP server and related services";
+ };
+
+ chatonly = mkOption {
+ type = with types; nullOr bool;
+ default = null;
+ description = "Disable all the social feature (Communities, Blog…) and keep only the chat ones";
+ };
+
+ disableregistration = mkOption {
+ type = with types; nullOr bool;
+ default = null;
+ description = "Remove the XMPP registration flow and buttons from the interface";
+ };
+
+ loglevel = mkOption {
+ type = with types; nullOr (ints.between 0 3);
+ default = null;
+ description = "The server loglevel";
+ };
+
+ locale = mkOption {
+ type = with types; nullOr str;
+ default = null;
+ description = "The server main locale";
+ };
+
+ xmppdomain = mkOption {
+ type = with types; nullOr str;
+ default = null;
+ description = "The default XMPP server domain";
+ };
+
+ xmppdescription = mkOption {
+ type = with types; nullOr str;
+ default = null;
+ description = "The default XMPP server description";
+ };
+
+ xmppwhitelist = mkOption {
+ type = with types; nullOr str;
+ default = null;
+ description = "The allowlisted XMPP servers";
+ };
+ };
+ };
+ default = { };
+ description = ''
+ Pod configuration (values from `php daemon.php config --help`).
+ Note that these values will now be disabled in the admin panel.
+ '';
+ };
+
+ settings = mkOption {
+ type = with types; attrsOf (nullOr (oneOf [ int str bool ]));
+ default = { };
+ description = ".env settings for Movim. Secrets should use `secretFile` option instead. `null`s will be culled.";
+ };
+
+ secretFile = mkOption {
+ type = with types; nullOr path;
+ default = null;
+ description = "The secret file to be sourced for the .env settings.";
+ };
+
+ database = {
+ type = mkOption {
+ type = types.enum [ "mysql" "postgresql" ];
+ example = "mysql";
+ default = "postgresql";
+ description = "Database engine to use.";
+ };
+
+ name = mkOption {
+ type = types.str;
+ default = "movim";
+ description = "Database name.";
+ };
+
+ user = mkOption {
+ type = types.str;
+ default = "movim";
+ description = "Database username.";
+ };
+
+ createLocally = mkOption {
+ type = types.bool;
+ default = true;
+ description = "local database using UNIX socket authentication";
+ };
+ };
+
+ nginx = mkOption {
+ type = with types; nullOr (submodule
+ (import ../web-servers/nginx/vhost-options.nix {
+ inherit config lib;
+ }));
+ default = null;
+ example = lib.literalExpression /* nginx */ ''
+ {
+ serverAliases = [
+ "pics.''${config.networking.domain}"
+ ];
+ enableACME = true;
+ forceHttps = true;
+ }
+ '';
+ description = ''
+ With this option, you can customize an nginx virtual host which already has sensible defaults for Movim.
+ Set to `{ }` if you do not need any customization to the virtual host.
+ If enabled, then by default, the {option}`serverName` is `''${domain}`,
+ If this is set to null (the default), no nginx virtualHost will be configured.
+ '';
+ };
+
+ poolConfig = mkOption {
+ type = with types; attrsOf (oneOf [ int str bool ]);
+ default = { };
+ description = "Options for Movim’s PHP-FPM pool.";
+ };
+ };
+ };
+
+ config = mkIf cfg.enable {
+ environment.systemPackages = [ cfg.package ];
+
+ users = {
+ users = {
+ movim = mkIf (cfg.user == "movim") {
+ isSystemUser = true;
+ group = cfg.group;
+ };
+ "${config.services.nginx.user}".extraGroups = [ cfg.group ];
+ };
+ groups = {
+ ${cfg.group} = { };
+ };
+ };
+
+ services = {
+ movim = {
+ settings = mkMerge [
+ {
+ DAEMON_URL = "//${cfg.domain}";
+ DAEMON_PORT = cfg.port;
+ DAEMON_INTERFACE = "127.0.0.1";
+ DAEMON_DEBUG = cfg.debug;
+ DAEMON_VERBOSE = cfg.verbose;
+ }
+ (mkIf cfg.database.createLocally {
+ DB_DRIVER = {
+ "postgresql" = "pgsql";
+ "mysql" = "mysql";
+ }.${cfg.database.type};
+ DB_HOST = "localhost";
+ DB_PORT = config.services.${cfg.database.type}.settings.port;
+ DB_DATABASE = cfg.database.name;
+ DB_USERNAME = cfg.database.user;
+ DB_PASSWORD = "";
+ })
+ ];
+
+ poolConfig = lib.mapAttrs' (n: v: lib.nameValuePair n (lib.mkDefault v)) {
+ "pm" = "dynamic";
+ "php_admin_value[error_log]" = "stderr";
+ "php_admin_flag[log_errors]" = true;
+ "catch_workers_output" = true;
+ "pm.max_children" = 32;
+ "pm.start_servers" = 2;
+ "pm.min_spare_servers" = 2;
+ "pm.max_spare_servers" = 8;
+ "pm.max_requests" = 500;
+ };
+ };
+
+ nginx = mkIf (cfg.nginx != null) {
+ enable = true;
+ recommendedOptimisation = true;
+ recommendedGzipSettings = true;
+ recommendedBrotliSettings = true;
+ recommendedProxySettings = true;
+ # TODO: recommended cache options already in Nginx⁇
+ appendHttpConfig = /* nginx */ ''
+ fastcgi_cache_path /tmp/nginx_cache levels=1:2 keys_zone=nginx_cache:100m inactive=60m;
+ fastcgi_cache_key "$scheme$request_method$host$request_uri";
+ '';
+ virtualHosts."${cfg.domain}" = mkMerge [
+ cfg.nginx
+ {
+ root = lib.mkForce "${package}/share/php/movim/public";
+ locations = {
+ "/favicon.ico" = {
+ priority = 100;
+ extraConfig = /* nginx */ ''
+ access_log off;
+ log_not_found off;
+ '';
+ };
+ "/robots.txt" = {
+ priority = 100;
+ extraConfig = /* nginx */ ''
+ access_log off;
+ log_not_found off;
+ '';
+ };
+ "~ /\\.(?!well-known).*" = {
+ priority = 210;
+ extraConfig = /* nginx */ ''
+ deny all;
+ '';
+ };
+ # Ask nginx to cache every URL starting with "/picture"
+ "/picture" = {
+ priority = 400;
+ tryFiles = "$uri $uri/ /index.php$is_args$args";
+ extraConfig = /* nginx */ ''
+ set $no_cache 0; # Enable cache only there
+ '';
+ };
+ "/" = {
+ priority = 490;
+ tryFiles = "$uri $uri/ /index.php$is_args$args";
+ extraConfig = /* nginx */ ''
+ # https://github.com/movim/movim/issues/314
+ add_header Content-Security-Policy "default-src 'self'; img-src 'self' aesgcm: https:; media-src 'self' aesgcm: https:; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline';";
+ set $no_cache 1;
+ '';
+ };
+ "~ \\.php$" = {
+ priority = 500;
+ tryFiles = "$uri =404";
+ extraConfig = /* nginx */ ''
+ include ${config.services.nginx.package}/conf/fastcgi.conf;
+ add_header X-Cache $upstream_cache_status;
+ fastcgi_ignore_headers "Cache-Control" "Expires" "Set-Cookie";
+ fastcgi_cache nginx_cache;
+ fastcgi_cache_valid any 7d;
+ fastcgi_cache_bypass $no_cache;
+ fastcgi_no_cache $no_cache;
+ fastcgi_split_path_info ^(.+\.php)(/.+)$;
+ fastcgi_index index.php;
+ fastcgi_pass unix:${fpm.socket};
+ '';
+ };
+ "/ws/" = {
+ priority = 900;
+ proxyPass = "http://${cfg.settings.DAEMON_INTERFACE}:${builtins.toString cfg.port}/";
+ proxyWebsockets = true;
+ recommendedProxySettings = true;
+ extraConfig = /* nginx */ ''
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_redirect off;
+ '';
+ };
+ };
+ extraConfig = /* ngnix */ ''
+ index index.php;
+ '';
+ }
+ ];
+ };
+
+ mysql = mkIf (cfg.database.createLocally && cfg.database.type == "mysql") {
+ enable = mkDefault true;
+ package = mkDefault pkgs.mariadb;
+ ensureDatabases = [ cfg.database.name ];
+ ensureUsers = [{
+ name = cfg.user;
+ ensureDBOwnership = true;
+ }];
+ };
+
+ postgresql = mkIf (cfg.database.createLocally && cfg.database.type == "postgresql") {
+ enable = mkDefault true;
+ ensureDatabases = [ cfg.database.name ];
+ ensureUsers = [{
+ name = cfg.user;
+ ensureDBOwnership = true;
+ }];
+ authentication = ''
+ host ${cfg.database.name} ${cfg.database.user} localhost trust
+ '';
+ };
+
+ phpfpm.pools.${pool} =
+ let
+ socketOwner =
+ if (cfg.nginx != null)
+ then config.services.nginx.user
+ else cfg.user;
+ in
+ {
+ phpPackage = package.php;
+ user = cfg.user;
+ group = cfg.group;
+
+ phpOptions = ''
+ error_log = 'stderr'
+ log_errors = on
+ '';
+
+ settings = {
+ "listen.owner" = socketOwner;
+ "listen.group" = cfg.group;
+ "listen.mode" = "0660";
+ "catch_workers_output" = true;
+ } // cfg.poolConfig;
+ };
+ };
+
+ systemd = {
+ services.movim-data-setup = {
+ description = "Movim setup: .env file, databases init, cache reload";
+ wantedBy = [ "multi-user.target" ];
+ requiredBy = [ "${phpExecutionUnit}.service" ];
+ before = [ "${phpExecutionUnit}.service" ];
+ after = lib.optional cfg.database.createLocally dbService;
+ requires = lib.optional cfg.database.createLocally dbService;
+
+ serviceConfig = {
+ Type = "oneshot";
+ User = cfg.user;
+ Group = cfg.group;
+ UMask = "077";
+ } // lib.optionalAttrs (cfg.secretFile != null) {
+ LoadCredential = "env-secrets:${cfg.secretFile}";
+ };
+
+ script = ''
+ # Env vars
+ rm -f ${cfg.dataDir}/.env
+ cp --no-preserve=all ${configFile} ${cfg.dataDir}/.env
+ echo -e '\n' >> ${cfg.dataDir}/.env
+ if [[ -f "$CREDENTIALS_DIRECTORY/env-secrets" ]]; then
+ cat "$CREDENTIALS_DIRECTORY/env-secrets" >> ${cfg.dataDir}/.env
+ echo -e '\n' >> ${cfg.dataDir}/.env
+ fi
+
+ # Caches, logs
+ mkdir -p ${cfg.dataDir}/public/cache ${cfg.logDir} ${cfg.runtimeDir}/cache
+ chmod -R ug+rw ${cfg.dataDir}/public/cache
+ chmod -R ug+rw ${cfg.logDir}
+ chmod -R ug+rwx ${cfg.runtimeDir}/cache
+
+ # Migrations
+ MOVIM_VERSION="${package.version}"
+ if [[ ! -f "${cfg.dataDir}/.migration-version" ]] || [[ "$MOVIM_VERSION" != "$(<${cfg.dataDir}/.migration-version)" ]]; then
+ ${package}/bin/movim-composer movim:migrate && echo $MOVIM_VERSION > ${cfg.dataDir}/.migration-version
+ fi
+ ''
+ + lib.optionalString (podConfigFlags != "") (
+ let
+ flags = lib.concatStringsSep " "
+ ([ "--no-interaction" ]
+ ++ lib.optional cfg.debug "-vvv"
+ ++ lib.optional (!cfg.debug && cfg.verbose) "-v");
+ in
+ ''
+ ${lib.getExe package} config ${podConfigFlags}
+ ''
+ );
+ };
+
+ services.movim = {
+ description = "Movim daemon";
+ wantedBy = [ "multi-user.target" ];
+ after = [ "movim-data-setup.service" ];
+ requires = [ "movim-data-setup.service" ]
+ ++ lib.optional cfg.database.createLocally dbService;
+ environment = {
+ PUBLIC_URL = "//${cfg.domain}";
+ WS_PORT = builtins.toString cfg.port;
+ };
+
+ serviceConfig = {
+ User = cfg.user;
+ Group = cfg.group;
+ WorkingDirectory = "${package}/share/php/movim";
+ ExecStart = "${lib.getExe package} start";
+ };
+ };
+
+ services.${phpExecutionUnit} = {
+ after = [ "movim-data-setup.service" ];
+ requires = [ "movim-data-setup.service" ]
+ ++ lib.optional cfg.database.createLocally dbService;
+ };
+
+ tmpfiles.settings."10-movim" = with cfg; {
+ "${dataDir}".d = { inherit user group; mode = "0710"; };
+ "${dataDir}/public".d = { inherit user group; mode = "0750"; };
+ "${dataDir}/public/cache".d = { inherit user group; mode = "0750"; };
+ "${runtimeDir}".d = { inherit user group; mode = "0700"; };
+ "${runtimeDir}/cache".d = { inherit user group; mode = "0700"; };
+ "${logDir}".d = { inherit user group; mode = "0700"; };
+ };
+ };
+ };
+}
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index 7d120d6bc09e..909eea38b35e 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -558,6 +558,7 @@ in {
morty = handleTest ./morty.nix {};
mosquitto = handleTest ./mosquitto.nix {};
moosefs = handleTest ./moosefs.nix {};
+ movim = discoverTests (import ./web-apps/movim { inherit handleTestOn; });
mpd = handleTest ./mpd.nix {};
mpv = handleTest ./mpv.nix {};
mtp = handleTest ./mtp.nix {};
diff --git a/nixos/tests/web-apps/movim/default.nix b/nixos/tests/web-apps/movim/default.nix
new file mode 100644
index 000000000000..5d6314e2b41b
--- /dev/null
+++ b/nixos/tests/web-apps/movim/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/nixos/tests/web-apps/movim/standard.nix b/nixos/tests/web-apps/movim/standard.nix
new file mode 100644
index 000000000000..470d81d8f722
--- /dev/null
+++ b/nixos/tests/web-apps/movim/standard.nix
@@ -0,0 +1,102 @@
+import ../../make-test-python.nix ({ lib, pkgs, ... }:
+
+let
+ movim = {
+ domain = "movim.local";
+ info = "No ToS in tests";
+ description = "NixOS testing server";
+ };
+ xmpp = {
+ domain = "xmpp.local";
+ admin = rec {
+ JID = "${username}@${xmpp.domain}";
+ username = "romeo";
+ password = "juliet";
+ };
+ };
+in
+{
+ name = "movim-standard";
+
+ meta = {
+ maintainers = with pkgs.lib.maintainers; [ toastal ];
+ };
+
+ nodes = {
+ server = { pkgs, ... }: {
+ services.movim = {
+ inherit (movim) domain;
+ enable = true;
+ verbose = true;
+ podConfig = {
+ inherit (movim) description info;
+ xmppdomain = xmpp.domain;
+ };
+ nginx = { };
+ };
+
+ services.prosody = {
+ enable = true;
+ xmppComplianceSuite = false;
+ disco_items = [
+ { url = "upload.${xmpp.domain}"; description = "File Uploads"; }
+ ];
+ virtualHosts."${xmpp.domain}" = {
+ inherit (xmpp) domain;
+ enabled = true;
+ extraConfig = ''
+ Component "pubsub.${xmpp.domain}" "pubsub"
+ pubsub_max_items = 10000
+ expose_publisher = true
+
+ Component "upload.${xmpp.domain}" "http_file_share"
+ http_external_url = "http://upload.${xmpp.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 ${xmpp.domain}
+ '';
+ };
+ };
+
+ testScript = /* python */ ''
+ server.wait_for_unit("phpfpm-movim.service")
+ server.wait_for_unit("nginx.service")
+ server.wait_for_open_port(80)
+
+ server.wait_for_unit("prosody.service")
+ server.succeed('prosodyctl status | grep "Prosody is running"')
+ server.succeed("prosodyctl register ${xmpp.admin.username} ${xmpp.domain} ${xmpp.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 \"\" | ${lib.getExe pkgs.websocat} 'ws://${movim.domain}/ws/?path=login&offset=0' --origin 'http://${movim.domain}'")
+
+ # 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=${xmpp.admin.JID}' --data-urlencode 'password=${xmpp.admin.password}' http://${movim.domain}/login")
+
+ server.succeed("curl -L --fail-with-body --max-redirs 1 -b /tmp/cookies http://${movim.domain}/chat")
+ '';
+})