summaryrefslogtreecommitdiffstats
path: root/nixos/tests/plausible.nix
blob: 5b0d58464ae545e92192bef7a7ccdb01f71da998 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
{ runTest }:

let
  secretKeybase = "nannannannannannannannannannannannannannannannannannannan_batman!";

  # A fixed bcrypt password hash is fine for a test; the plaintext is never
  # needed because the test only checks that an admin user exists (and thus the
  # "first launch" setup wizard is unreachable), not that login with the
  # password works.
  adminEmail = "admin@localhost";

  mkPlausibleTest =
    {
      seedAdmin ? false,
    }:
    runTest (
      { lib, pkgs, ... }:
      let
        adminPassword = "correct-horse-battery-staple";
        adminPasswordHashFile = pkgs.runCommand "plausible-admin-password-hash" { } ''
          ${lib.getExe pkgs.mkpasswd} -m bcrypt ${lib.escapeShellArg adminPassword} > "$out"
        '';
      in
      {
        name = "plausible" + lib.optionalString seedAdmin "-declarative-admin-user";
        meta = {
          maintainers = with lib.maintainers; [
            nh2
            stepbrobd
          ];
        };

        nodes.machine = {
          # On first boot, the ClickHouse migrations run by Plausible's
          # `migrate.sh` intermittently fail with `(Mint.TransportError) socket
          # closed`, which aborts startup before the web server opens its port.
          # The failure is transient and succeeds on a subsequent attempt, so
          # retry startup without a rate limit until the port opens. (Without
          # this, the test is flaky.)
          systemd.services.plausible.serviceConfig.Restart = lib.mkForce "always";
          systemd.services.plausible.serviceConfig.RestartSec = 1;
          systemd.services.plausible.unitConfig.StartLimitIntervalSec = 0;

          services.plausible = {
            enable = true;
            adminUser = lib.mkIf seedAdmin {
              email = adminEmail;
              name = "Test Admin";
              passwordHashFile = "${adminPasswordHashFile}";
            };
            server = {
              baseUrl = "http://localhost:8000";
              secretKeybaseFile = builtins.toFile "plausible-test-secret-keybase-file" secretKeybase;
            };
          };
        };

        testScript = ''
          machine.wait_for_unit("plausible.service")
          machine.wait_for_open_port(8000)

          # Ensure that the software does not make the machine
          # listen on any public interfaces by default.
          machine.fail("ss -tlpn 'src = 0.0.0.0 or src = [::]' | grep LISTEN")

          machine.succeed("curl -f localhost:8000 >&2")
          machine.succeed("curl -f localhost:8000/js/script.js >&2")

          def user_count():
            # Plausible's "first launch" state is defined as "no user exists"
            # (`Plausible.Release.should_be_first_launch?` is
            # `not Repo.exists?(Plausible.Auth.User)`), so we inspect the `users`
            # table directly. Local Postgres connections use `trust` auth in this
            # VM, so the `postgres` superuser can query without a password.
            return machine.succeed(
              "sudo -u postgres psql --dbname plausible --tuples-only --no-align "
              "--command 'SELECT count(*) FROM users'"
            ).strip()

          def login_redirect_url():
            return machine.succeed(
              "curl -s -o /dev/null -w '%{redirect_url}' localhost:8000/login"
            ).strip()
        ''
        + (
          if seedAdmin then
            ''
              with subtest("the admin user is seeded"):
                assert user_count() == "1", "expected exactly one seeded admin user"
                email = machine.succeed(
                  "sudo -u postgres psql --dbname plausible --tuples-only --no-align "
                  "--command 'SELECT email FROM users'"
                ).strip()
                assert email == "${adminEmail}", f"unexpected seeded admin email: {email!r}"

              with subtest("the setup wizard is NOT reachable"):
                # `/login` must render normally (HTTP 200) and must not redirect
                # to the first-launch `/register` wizard.
                status = machine.succeed(
                  "curl -s -o /dev/null -w '%{http_code}' localhost:8000/login"
                ).strip()
                assert status == "200", f"expected /login to render (200), got: {status!r}"
                location = login_redirect_url()
                assert "/register" not in location, (
                  f"/login unexpectedly redirected to the setup wizard: {location!r}"
                )

              with subtest("seeding is idempotent across restarts"):
                machine.succeed("systemctl restart plausible.service")
                machine.wait_for_open_port(8000)
                assert user_count() == "1", "expected still exactly one user after restart"
            ''
          else
            ''
              with subtest("without an admin user, the setup wizard is reachable"):
                assert user_count() == "0", "expected no users in first-launch state"
                # With no user seeded, `should_be_first_launch?` is true and the
                # browser pipeline's `FirstLaunchPlug` 302-redirects every page to
                # `/register`.
                location = login_redirect_url()
                assert "/register" in location, (
                  f"expected /login to redirect to the setup wizard /register, but redirect was: {location!r}"
                )
            ''
        );
      }
    );
in
{
  # Basic test: Plausible without a declaratively configured admin user is in
  # the "first launch" state, so the unauthenticated setup wizard is reachable.
  # This also asserts that the way the `declarative-admin-user` test detects
  # "wizard reachable" actually fires for this Plausible version, so a future
  # Plausible change that breaks the detection would make that test fail rather
  # than silently pass.
  basic = mkPlausibleTest { seedAdmin = false; };

  # Tests that a declaratively configured admin user is seeded before the web
  # server accepts connections, so the unauthenticated "first launch" setup
  # wizard is never reachable.
  declarative-admin-user = mkPlausibleTest { seedAdmin = true; };
}