From 7203310af608723711c2ce60a086dc2eb26f33ce Mon Sep 17 00:00:00 2001 From: phanirithvij Date: Tue, 17 Feb 2026 17:08:34 +0530 Subject: [PATCH] nixos/goupile: add basic playwright smoke test Signed-off-by: phanirithvij Co-authored-by: Sizhe Zhao --- .../goupile/basic_interaction_test.py | 178 ++++++++++++++++++ nixos/tests/web-apps/goupile/default.nix | 103 +++++++++- 2 files changed, 272 insertions(+), 9 deletions(-) create mode 100644 nixos/tests/web-apps/goupile/basic_interaction_test.py diff --git a/nixos/tests/web-apps/goupile/basic_interaction_test.py b/nixos/tests/web-apps/goupile/basic_interaction_test.py new file mode 100644 index 000000000000..46b0f78903fc --- /dev/null +++ b/nixos/tests/web-apps/goupile/basic_interaction_test.py @@ -0,0 +1,178 @@ +import os +import openpyxl +import tempfile +from playwright.sync_api import sync_playwright + +BASE_URL = "http://localhost:8889" + +# NOTE: these are the passwords +ADMIN_PASSWD = "car-shop-in-the-mall" +ALICE_PASSWD = "user-goes-to-the-car-shop" + + +def run_test(): + is_headful = os.getenv("HEADFUL") == "1" + + with sync_playwright() as p: + browser = p.chromium.launch(headless=not is_headful) + context = browser.new_context( + accept_downloads=True, record_video_dir="/tmp/videos/" + ) + # more default timeout for slow nixos test vms + context.set_default_timeout(90 * 1000) + page = context.new_page() + + page.goto(f"{BASE_URL}/admin") + + # admin and doman setup + page.get_by_role("textbox", name="Domain name *").fill("domain") + page.get_by_role("textbox", name="Domain title *").fill("domain") + page.get_by_role("textbox", name="Password *").fill(ADMIN_PASSWD) + page.get_by_role("textbox", name="Confirmation").fill(ADMIN_PASSWD) + page.get_by_role("textbox", name="Decryption key *").click() + page.get_by_role("button", name="Installer").click() + + # login to admin dashboard as admin + page.get_by_role("textbox", name="Username *").fill("admin") + page.get_by_role("textbox", name="Password *").fill(ADMIN_PASSWD) + page.get_by_role("button", name="Login").click() + + # create a sample project, it will switch the view to project's configure page + page.get_by_text("Create new project").click() + page.get_by_role("textbox", name="Name *").fill("proj1") + page.get_by_role("button", name="Create").click() + + # create a test non-root user, alice + page.get_by_text("Create new user").click() + page.get_by_role("textbox", name="Username *").fill("alice") + page.get_by_role("button", name="No", exact=True).click() + page.get_by_role("textbox", name="Password *").fill(ALICE_PASSWD) + page.get_by_role("textbox", name="Confirmation").fill(ALICE_PASSWD) + page.get_by_role("button", name="Create").click() + + # give alice, permissions to access the project + page.get_by_role("button", name="Assign").nth(1).click() + page.get_by_text("Read", exact=True).click() + page.get_by_text("Save", exact=True).click() + page.get_by_text("Export", exact=True).click() + page.get_by_text("Download", exact=True).click() + + # Open the project in new page + page.locator("form").get_by_role("button", name="Edit").click() + with page.expect_popup() as page1_info: + page.get_by_role("link", name="access").click() + page1 = page1_info.value + page1.set_default_timeout(120 * 1000) + + # fill entries as admin (enter 1 for everything) + page1.get_by_role("button", name="Create new record").click() + + page1.locator("#ins_tiles").get_by_text("Introduction").click() + page1.get_by_role("textbox", name="Inclusion date *").fill("2000-01-01") + page1.get_by_role("spinbutton", name="Age *").click() + page1.get_by_role("spinbutton", name="Age *").fill("1") + page1.get_by_role("button", name="Save").click() + page1.wait_for_timeout(1000) + + page1.get_by_role("button", name="Advanced").click() + page1.get_by_role("spinbutton", name="Age *").click() + page1.get_by_role("spinbutton", name="Age *").fill("1") + page1.get_by_role("button", name="Save").click() + page1.wait_for_timeout(1000) + + page1.get_by_role("button", name="Page layout").click() + page1.get_by_role("spinbutton", name="Variable A1").fill("1") + page1.get_by_role("button", name="Save").click() + page1.wait_for_timeout(1000) + + # create export #1 + page1.get_by_role("button", name="Data").click() + page1.wait_for_timeout(1000) + + page1.get_by_role("button", name="Data exports").click() + with page1.expect_download() as download_info: + page1.get_by_role("button", name="Create export").click() + + # logout as admin + page.get_by_role("button", name="admin", exact=True).click() + with page.expect_popup() as page2_info: + page.get_by_role("link", name="access").click() + page2 = page2_info.value + page2.set_default_timeout(120 * 1000) + + page2.get_by_role("button", name="admin").click() + page2.get_by_role("button", name="Logout").click() + + # login as alice + page2.get_by_role("textbox", name="Username *").fill("alice") + page2.get_by_role("textbox", name="Password *").fill(ALICE_PASSWD) + page2.get_by_role("button", name="Login").click() + + # create entry as alice (fill `2` for everything) + page2.get_by_role("button", name="Create new record").click() + + page2.get_by_text("1 Introduction").click() + page2.get_by_role("textbox", name="Inclusion date *").fill("2000-01-01") + page2.get_by_role("spinbutton", name="Age *").click() + page2.get_by_role("spinbutton", name="Age *").fill("2") + page2.get_by_role("button", name="Save").click() + page2.wait_for_timeout(1000) + + page2.get_by_role("button", name="Advanced").click() + page2.get_by_role("spinbutton", name="Age *").click() + page2.get_by_role("spinbutton", name="Age *").fill("2") + page2.get_by_role("button", name="Save").click() + page2.wait_for_timeout(1000) + + page2.get_by_role("button", name="Page layout").click() + page2.get_by_role("spinbutton", name="Variable A1").click() + page2.get_by_role("spinbutton", name="Variable A1").fill("2") + page2.get_by_role("button", name="Save").click() + page2.wait_for_timeout(1000) + + # create export #2 + page2.get_by_role("button", name="Data").click() + page2.wait_for_timeout(1000) + + page2.get_by_role("button", name="Data exports").click() + page2.get_by_role("button", name="Previous exports").click() + + with page2.expect_download() as download1_info: + page2.locator("a").filter(has_text="Download").click() + + download1 = download1_info.value + save_path1 = os.path.join(tempfile.gettempdir(), download1.suggested_filename) + download1.save_as(save_path1) + + print(f"exported all records to {save_path1}") + + page2.get_by_role("button", name="Data exports").click() + with page2.expect_download() as download2_info: + page2.get_by_role("button", name="Create export").click() + + download2 = download2_info.value + save_path2 = os.path.join(tempfile.gettempdir(), download2.suggested_filename) + download2.save_as(save_path2) + + print(f"exported all records to {save_path2}") + + context.close() + browser.close() + + # check that exported files have correct entries + + wb1 = openpyxl.load_workbook(save_path1) + for sheet, cell in zip(["intro", "advanced", "layout"], ["D2", "D2", "C2"]): + val = wb1[sheet][cell].value + assert val == 1, f"Sheet {sheet}, Cell {cell}: Expected 1 (admin), got {val}" + + wb2 = openpyxl.load_workbook(save_path2) + for sheet, cell in zip(["intro", "advanced", "layout"], ["D3", "D3", "C3"]): + val = wb2[sheet][cell].value + assert val == 2, f"Sheet {sheet}, Cell {cell}: Expected 2 (alice), got {val}" + + print("Test passed successfully!") + + +if __name__ == "__main__": + run_test() diff --git a/nixos/tests/web-apps/goupile/default.nix b/nixos/tests/web-apps/goupile/default.nix index f0d89e8c0777..48215af9fa45 100644 --- a/nixos/tests/web-apps/goupile/default.nix +++ b/nixos/tests/web-apps/goupile/default.nix @@ -1,7 +1,62 @@ -{ lib, pkgs, ... }: +{ + lib, + pkgs, + ... +}: +let + python = pkgs.python3.withPackages ( + ps: with ps; [ + requests + playwright + openpyxl + ] + ); + + runScript = "${lib.getExe python} ${./basic_interaction_test.py}"; + + run-goupile-test = pkgs.writeShellScriptBin "run-goupile-test" '' + set -euo pipefail + + export PLAYWRIGHT_BROWSERS_PATH=${pkgs.playwright-driver.browsers} + export PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 + + # check if attached to a terminal + if [ -t 1 ]; then + # interactive testing + export HEADFUL=''${HEADFUL:-1} + export PWDEBUG=''${PWDEBUG:-0} + export DISPLAY=''${DISPLAY:-:0} + if [ "$(id -u)" = "0" ] && [ -d "/home/alice" ]; then + runuser -u alice \ + -w DISPLAY,HEADFUL,PWDEBUG,PLAYWRIGHT_BROWSERS_PATH,PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD \ + -- ${runScript} + else + ${runScript} + fi + else + # non-interactive nixos test + + # Print instructions to the nix logs + cat <<'EOF' | tee >(systemd-cat -t goupile-e2e) + ================================================================================ + NOTE: The goupile e2e test can be run interactively either inside the vm or on the host + - First, run `nix-build -A nixosTests.goupile.driverInteractive` and `./result/bin/nixos-test-driver` + - Run `start_all()` inside the repl + - Then `$(nix-build -A nixosTests.goupile.interactive-script)/bin/run-goupile-test` to run the full test interactively + - Or `env PWDEBUG=1 $(nix-build -A nixosTests.goupile.interactive-script)/bin/run-goupile-test` to show the playwright inspector to debug + ================================================================================ + EOF + + echo "Starting smoke test..." | systemd-cat -t goupile-e2e + ${runScript} 2>&1 | tee >(systemd-cat -t goupile-e2e) + fi + ''; +in { name = "goupile"; + passthru.interactive-script = run-goupile-test; + nodes.machine = { lib, @@ -21,6 +76,22 @@ hostName = "goupile"; domain = "local"; }; + + # goupile tries to resolve it at runtime, resolve it instead of patching it out + # as the dns resolution step serves a purpose, to force glibc to load NSS libraries + # see server/goupile.cc and search for getaddrinfo or www.example.com + networking.extraHosts = '' + 127.0.0.1 www.example.com + ''; + + environment.systemPackages = [ + python + run-goupile-test + ]; + + # more cores and memory to improve chromium performance + virtualisation.memorySize = lib.mkForce 8192; + virtualisation.cores = 4; }; testScript = @@ -30,6 +101,7 @@ in # py '' + import os start_all() machine.wait_for_unit("goupile.service") @@ -38,10 +110,15 @@ machine.succeed("curl -q http://localhost:${port}") machine.succeed("curl -q http://goupile.local") machine.succeed("curl -q http://localhost") + + machine.succeed("run-goupile-test") + out_dir = os.environ.get("out", os.getcwd()) + machine.copy_from_vm("/tmp/videos", out_dir) ''; # Debug interactively with: - # - nix run .#nixosTests.goupile.driverInteractive -L + # - nix-build -A nixosTests.goupile.driverInteractive + # - ./result/bin/nixos-test-driver # - run_tests() # ssh -o User=root vsock%3 (can also do vsock/3, but % works with scp etc.) interactive.sshBackdoor.enable = true; @@ -52,16 +129,24 @@ port = config.services.goupile.settings.HTTP.Port; in { - virtualisation.forwardPorts = map (port: { - from = "host"; - host.port = port; - guest.port = port; - }) [ port ]; + imports = [ + # enable graphical session + users (alice, bob) + ../../common/x11.nix + ../../common/user-account.nix + ]; + services.xserver.enable = true; + test-support.displayManager.auto.user = "alice"; + + virtualisation.forwardPorts = [ + { + from = "host"; + host.port = port; + guest.port = port; + } + ]; # forwarded ports need to be accessible networking.firewall.allowedTCPPorts = [ port ]; - - virtualisation.graphics = false; }; meta.maintainers = lib.teams.ngi.members;