add playwright test for forgejo

This commit is contained in:
ibizaman 2025-01-26 01:20:47 +01:00 committed by Pierre Penninckx
parent cf3442fb13
commit 2c713ae9ad
7 changed files with 120 additions and 34 deletions

View file

@ -105,6 +105,10 @@ Self Host Blocks takes care of common self-hosting needs:
- Automatic reverse proxy and certificate management for HTTPS. - Automatic reverse proxy and certificate management for HTTPS.
- VPN and proxy tunneling services. - VPN and proxy tunneling services.
Great care is taken to make the proposed stack robust.
This translates into a test suite comprised of automated NixOS VM tests
including playwright tests.
### Services ### Services
[Provided services](https://shb.skarabox.com/services.html) are: [Provided services](https://shb.skarabox.com/services.html) are:
@ -305,7 +309,8 @@ the services on your own server.
## Community ## Community
All issues and PRs are welcome. For PRs, if they are substantial changes, please open an issue to All issues and PRs are welcome. For PRs, if they are substantial changes, please open an issue to
discuss the details first. More details in [here](https://shb.skarabox.com/contributing.html). discuss the details first. More details in [the contributing section](https://shb.skarabox.com/contributing.html)
of the manual.
Come hang out in the [Matrix channel](https://matrix.to/#/%23selfhostblocks%3Amatrix.org). :) Come hang out in the [Matrix channel](https://matrix.to/#/%23selfhostblocks%3Amatrix.org). :)

View file

@ -43,6 +43,12 @@ When you get to the shell, run either `start_all()` or `test_script()`. The form
the VMs and service, then you can introspect. The latter also starts the VMs if they are not yet and the VMs and service, then you can introspect. The latter also starts the VMs if they are not yet and
then will run the test script. then will run the test script.
If the test includes playwright tests, you can see the playwright trace with:
```bash
$ nix run .#playwright -- show-trace path/to/trace.zip
```
## Upload test results to CI {#contributing-upload} ## Upload test results to CI {#contributing-upload}
Github actions do now have hardware acceleration, so running them there is not slow anymore. If Github actions do now have hardware acceleration, so running them there is not slow anymore. If

View file

@ -968,6 +968,9 @@
"services-forgejo-options": [ "services-forgejo-options": [
"services-forgejo.html#services-forgejo-options" "services-forgejo.html#services-forgejo-options"
], ],
"services-forgejo-options-shb.forgejo.adminUsername": [
"services-forgejo.html#services-forgejo-options-shb.forgejo.adminUsername"
],
"services-forgejo-options-shb.forgejo.adminPassword": [ "services-forgejo-options-shb.forgejo.adminPassword": [
"services-forgejo.html#services-forgejo-options-shb.forgejo.adminPassword" "services-forgejo.html#services-forgejo-options-shb.forgejo.adminPassword"
], ],

View file

@ -155,6 +155,25 @@
// (vm_test "contracts-databasebackup" ./test/contracts/databasebackup.nix) // (vm_test "contracts-databasebackup" ./test/contracts/databasebackup.nix)
// (vm_test "contracts-secret" ./test/contracts/secret.nix) // (vm_test "contracts-secret" ./test/contracts/secret.nix)
)); ));
# Run nix .#playwright -- show-trace path/to/trace.zip
packages.playwright =
pkgs.callPackage ({ stdenvNoCC, makeWrapper, playwright }: stdenvNoCC.mkDerivation {
name = "playwright";
src = playwright;
nativeBuildInputs = [
makeWrapper
];
# No quotes around the value for LLDAP_PASSWORD because we want the value to not be enclosed in quotes.
installPhase = ''
makeWrapper ${pkgs.python3Packages.playwright}/bin/playwright $out/bin/playwright \
--set PLAYWRIGHT_BROWSERS_PATH ${pkgs.playwright-driver.browsers} \
--set PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS true
'';
}) {};
} }
) // { ) // {
herculesCI.ciSystems = [ "x86_64-linux" ]; herculesCI.ciSystems = [ "x86_64-linux" ];

View file

@ -81,7 +81,7 @@ in
adminName = mkOption { adminName = mkOption {
type = str; type = str;
description = "Admin user of the LDAP server."; description = "Admin user of the LDAP server. Cannot be reserved word 'admin'.";
default = "admin"; default = "admin";
}; };
@ -170,8 +170,14 @@ in
}; };
}; };
adminUsername = mkOption {
type = str;
description = "Forgejo admin user name.";
default = "admin";
};
adminPassword = mkOption { adminPassword = mkOption {
description = "File containing the Forgejo admin user password."; description = "Forgejo admin user password.";
type = submodule { type = submodule {
options = contracts.secret.mkRequester { options = contracts.secret.mkRequester {
mode = "0440"; mode = "0440";
@ -495,10 +501,17 @@ in
}) })
(mkIf cfg.enable { (mkIf cfg.enable {
assertions = [
{
assertion = cfg.adminUsername != "admin";
message = "Admin username cannot be 'admin'.";
}
];
systemd.services.forgejo.preStart = '' systemd.services.forgejo.preStart = ''
admin="${getExe config.services.forgejo.package} admin user" admin="${getExe config.services.forgejo.package} admin user"
$admin create --admin --email "root@localhost" --username meadmin --password "$(tr -d '\n' < ${cfg.adminPassword.result.path})" || true $admin create --admin --must-change-password=false --email "root@localhost" --username "${cfg.adminUsername}" --password "$(tr -d '\n' < ${cfg.adminPassword.result.path})" || true
$admin change-password --username meadmin --password "$(tr -d '\n' < ${cfg.adminPassword.result.path})" || true $admin change-password --must-change-password=false --username "${cfg.adminUsername}" --password "$(tr -d '\n' < ${cfg.adminPassword.result.path})" || true
''; '';
}) })

View file

@ -1,7 +1,7 @@
{ pkgs, lib }: { pkgs, lib }:
let let
inherit (lib) hasAttr mkOption optionalString; inherit (lib) hasAttr mkOption optionalString;
inherit (lib.types) bool listOf nullOr submodule str; inherit (lib.types) listOf nullOr submodule str;
baseImports = { baseImports = {
imports = [ imports = [
@ -93,7 +93,11 @@ let
'') '')
+ (optionalString (hasAttr "test" nodes.client && hasAttr "login" nodes.client.test) '' + (optionalString (hasAttr "test" nodes.client && hasAttr "login" nodes.client.test) ''
with subtest("Login"): with subtest("Login"):
print(client.succeed("login_playwright firefox")) code, logs = client.execute("login_playwright firefox")
client.copy_from_vm("trace")
print(logs)
if code != 0:
raise Exception("login_playwright did not succeed")
'') '')
+ (let + (let
script = extraScript args; script = extraScript args;
@ -201,8 +205,8 @@ in
(let (let
testCfg = pkgs.writeText "users.json" (builtins.toJSON cfg); testCfg = pkgs.writeText "users.json" (builtins.toJSON cfg);
in '' in ''
# import re
import json import json
import re
import sys import sys
from playwright.sync_api import expect from playwright.sync_api import expect
from playwright.sync_api import sync_playwright from playwright.sync_api import sync_playwright
@ -226,24 +230,33 @@ in
with sync_playwright() as p: with sync_playwright() as p:
browser = getattr(p, browser_name).launch(args=browser_args) browser = getattr(p, browser_name).launch(args=browser_args)
for u in testCfg["testLoginWith"]: for i, u in enumerate(testCfg["testLoginWith"]):
print(f"Testing for user {u['username']} and password {u['password']}") print(f"Testing for user {u['username']} and password {u['password']}")
context = browser.new_context(ignore_https_errors=True) context = browser.new_context(ignore_https_errors=True)
context.tracing.start(screenshots=True, snapshots=True, sources=True) context.tracing.start(screenshots=True, snapshots=True, sources=True)
page = context.new_page() try:
page.goto(testCfg['startUrl']) page = context.new_page()
print(f"Going to {testCfg['startUrl']}")
page.goto(testCfg['startUrl'])
page.screenshot(path="example.png") if u['username'] is not None:
if u['username'] is not None: print(f"Filling field {testCfg['usernameFieldLabel']} with {u['username']}")
page.get_by_label(testCfg['usernameFieldLabel']).fill(u['username']) page.get_by_label(testCfg['usernameFieldLabel']).fill(u['username'])
if u['password'] is not None: if u['password'] is not None:
page.get_by_label(testCfg['passwordFieldLabel']).fill(u['password']) print(f"Filling field {testCfg['passwordFieldLabel']} with {u['password']}")
page.get_by_role("button", name=testCfg['loginButtonName']).click() page.get_by_label(testCfg['passwordFieldLabel']).fill(u['password'])
for line in u['nextPageExpect']:
eval(line)
context.tracing.stop(path="trace.zip") print(f"Clicking button {testCfg['loginButtonName']}")
page.get_by_role("button", name=testCfg['loginButtonName']).click()
for line in u['nextPageExpect']:
print(f"Running: {line}")
print(f"Page has title: {page.title()}")
eval(line)
finally:
page.screenshot(path=f"trace/{i}/final.png")
context.tracing.stop(path=f"trace/{i}.zip")
browser.close() browser.close()
'') '')

View file

@ -20,6 +20,12 @@ let
}; };
basic = { config, ... }: { basic = { config, ... }: {
imports = [
testLib.baseModule
../../modules/blocks/hardcodedsecret.nix
../../modules/services/forgejo.nix
];
test = { test = {
subdomain = "f"; subdomain = "f";
}; };
@ -28,6 +34,7 @@ let
enable = true; enable = true;
inherit (config.test) subdomain domain; inherit (config.test) subdomain domain;
adminUsername = "theadmin";
adminPassword.result = config.shb.hardcodedsecret.forgejoAdminPassword.result; adminPassword.result = config.shb.hardcodedsecret.forgejoAdminPassword.result;
databasePassword.result = config.shb.hardcodedsecret.forgejoDatabasePassword.result; databasePassword.result = config.shb.hardcodedsecret.forgejoDatabasePassword.result;
}; };
@ -48,6 +55,33 @@ let
}; };
}; };
clientLogin = { config, ... }: {
imports = [
testLib.baseModule
testLib.clientLoginModule
];
test = {
subdomain = "f";
};
test.login = {
startUrl = "http://${config.test.fqdn}/user/login";
usernameFieldLabel = "Username or email address";
passwordFieldLabel = "Password";
loginButtonName = "Sign In";
testLoginWith = [
{ username = "theadmin"; password = adminPassword + "oops"; nextPageExpect = [
"expect(page.get_by_text('Username or password is incorrect.')).to_be_visible()"
]; }
{ username = "theadmin"; password = adminPassword; nextPageExpect = [
"expect(page.get_by_text('Username or password is incorrect.')).not_to_be_visible()"
"expect(page.get_by_role('button', name=re.compile('Sign In'))).not_to_be_visible()"
"expect(page).to_have_title(re.compile('Dashboard'))"
]; }
];
};
};
https = { config, ... }: { https = { config, ... }: {
shb.forgejo = { shb.forgejo = {
ssl = config.shb.certs.certs.selfsigned.n; ssl = config.shb.certs.certs.selfsigned.n;
@ -96,16 +130,17 @@ in
basic = pkgs.testers.runNixOSTest { basic = pkgs.testers.runNixOSTest {
name = "forgejo_basic"; name = "forgejo_basic";
nodes.client = {
imports = [
clientLogin
];
};
nodes.server = { nodes.server = {
imports = [ imports = [
testLib.baseModule
../../modules/services/forgejo.nix
basic basic
]; ];
}; };
nodes.client = {};
testScript = commonTestScript.access; testScript = commonTestScript.access;
}; };
@ -114,8 +149,6 @@ in
nodes.server = { config, ... }: { nodes.server = { config, ... }: {
imports = [ imports = [
testLib.baseModule
../../modules/services/forgejo.nix
basic basic
(testLib.backup config.shb.forgejo.backup) (testLib.backup config.shb.forgejo.backup)
]; ];
@ -131,10 +164,8 @@ in
nodes.server = { nodes.server = {
imports = [ imports = [
testLib.baseModule
../../modules/services/forgejo.nix
testLib.certs
basic basic
testLib.certs
https https
]; ];
}; };
@ -149,8 +180,6 @@ in
nodes.server = { nodes.server = {
imports = [ imports = [
testLib.baseModule
../../modules/services/forgejo.nix
basic basic
testLib.ldap testLib.ldap
ldap ldap
@ -167,10 +196,8 @@ in
nodes.server = { config, pkgs, ... }: { nodes.server = { config, pkgs, ... }: {
imports = [ imports = [
testLib.baseModule
../../modules/services/forgejo.nix
testLib.certs
basic basic
testLib.certs
https https
testLib.ldap testLib.ldap
(testLib.sso config.shb.certs.certs.selfsigned.n) (testLib.sso config.shb.certs.certs.selfsigned.n)