Skip to content

Commit f4a4673

Browse files
committed
#54 ci: add api end2end tests
Signed-off-by: Patrizio Bekerle <[email protected]>
1 parent e38045f commit f4a4673

File tree

5 files changed

+348
-0
lines changed

5 files changed

+348
-0
lines changed

.github/workflows/vm-test.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: "🧪 Run VM test"
2+
3+
on:
4+
push:
5+
paths-ignore:
6+
- docker/**
7+
workflow_dispatch:
8+
pull_request:
9+
paths-ignore:
10+
- docker/**
11+
12+
jobs:
13+
check:
14+
name: "🧪 Run VM test"
15+
runs-on: ubuntu-latest
16+
permissions:
17+
contents: read
18+
steps:
19+
- name: "📥 Checkout repository"
20+
uses: actions/checkout@v5
21+
22+
- name: "📦 Install Nix"
23+
uses: cachix/install-nix-action@v31
24+
with:
25+
nix_path: nixpkgs=channel:nixos-unstable
26+
27+
- name: "🧪 Run Nextcloud App VM tests"
28+
run: nix build -L .#nixosTests.nextcloud-qownnotesapi

flake.lock

Lines changed: 61 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

flake.nix

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
description = "Nextcloud qownnotesapi app NixOS VM tests (Nextcloud 28-32)";
3+
4+
inputs = {
5+
# Add 24.11 channel for Nextcloud 29
6+
nixpkgs24_11.url = "github:NixOS/nixpkgs/nixos-24.11";
7+
# Add 25.05 channel for Nextcloud 30 & 31
8+
nixpkgs25_05.url = "github:NixOS/nixpkgs/nixos-25.05";
9+
# Add unstable channel for Nextcloud 32
10+
nixpkgs25_11.url = "github:NixOS/nixpkgs/daebeba791763abfe3cce5e0f16376ddf1b724d4";
11+
};
12+
13+
outputs =
14+
{
15+
nixpkgs24_11,
16+
nixpkgs25_05,
17+
nixpkgs25_11,
18+
...
19+
}:
20+
let
21+
system = "x86_64-linux";
22+
baseConfig = {
23+
# config.permittedInsecurePackages = [ "nextcloud-28.0.14" ];
24+
};
25+
pkgs24_11 = import nixpkgs24_11 ({ inherit system; } // baseConfig);
26+
pkgs25_05 = import nixpkgs25_05 ({ inherit system; } // baseConfig);
27+
pkgs25_11 = import nixpkgs25_11 ({ inherit system; } // baseConfig);
28+
combinedTest = import ./tests/vm/basic.nix {
29+
inherit pkgs24_11 pkgs25_05 pkgs25_11;
30+
};
31+
in
32+
{
33+
nixosTests = {
34+
nextcloud-qownnotesapi = combinedTest;
35+
};
36+
};
37+
}

justfile

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,27 @@ github-run-test:
4747
[group('dev')]
4848
open-browser:
4949
xdg-open http://localhost:8081
50+
51+
# Run tests for multiple versions of Nextcloud
52+
[group('test')]
53+
vm-test args='':
54+
nix build -L .#nixosTests.nextcloud-qownnotesapi {{ args }}
55+
56+
# Interactive NixOS test driver session for the combined Nextcloud versions.
57+
# Tries driverInteractive first (non-executing build), then falls back to driver (also non-executing) if needed.
58+
# Usage examples:
59+
# just vm-test-interactive
60+
# just vm-test-interactive args="--impure" # allow env vars: FORCE_REBUILD_NONCE=...
61+
62+
# Interactive NixOS test driver session for the combined Nextcloud versions test
63+
[group('test')]
64+
vm-test-interactive args='':
65+
if nix build -L .#nixosTests.nextcloud-qownnotesapi.driverInteractive {{ args }}; then \
66+
echo "Built driverInteractive attribute"; \
67+
elif nix build -L .#nixosTests.nextcloud-qownnotesapi.driver {{ args }}; then \
68+
echo "driverInteractive missing; using driver attribute"; \
69+
else \
70+
echo "Failed to build either driverInteractive or driver attribute"; \
71+
exit 1; \
72+
fi
73+
./result/bin/nixos-test-driver

tests/vm/basic.nix

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
# https://wiki.nixos.org/wiki/NixOS_VM_tests
2+
{
3+
pkgs24_11,
4+
pkgs25_05,
5+
pkgs25_11,
6+
...
7+
}:
8+
9+
let
10+
inherit (pkgs25_05) lib;
11+
tryAttr25 =
12+
name:
13+
let
14+
t = builtins.tryEval (builtins.getAttr name pkgs25_05);
15+
in
16+
if t.success then t.value else null;
17+
# Safe lookup on pkgs24_11 catching insecure-package eval errors
18+
tryAttr24 =
19+
name:
20+
if pkgs24_11 != null && builtins.hasAttr name pkgs24_11 then
21+
(
22+
let
23+
t = builtins.tryEval (builtins.getAttr name pkgs24_11);
24+
in
25+
if t.success then t.value else null
26+
)
27+
else
28+
null;
29+
# Safe lookup on pkgs25_11 catching eval errors
30+
tryAttr2511 =
31+
name:
32+
if pkgs25_11 != null && builtins.hasAttr name pkgs25_11 then
33+
(
34+
let
35+
t = builtins.tryEval (builtins.getAttr name pkgs25_11);
36+
in
37+
if t.success then t.value else null
38+
)
39+
else
40+
null;
41+
42+
# Wrapper to make legacy Nextcloud derivations ignore override args (e.g. caBundle introduced later)
43+
legacyCompat =
44+
pkg:
45+
pkg
46+
// {
47+
override = _args: legacyCompat pkg; # ignore args to avoid unexpected argument errors
48+
overrideDerivation = f: legacyCompat (pkg.overrideDerivation f);
49+
};
50+
51+
# Helper to fetch a legacy pkg (from 24_11 set) and wrap it if it exists
52+
legacyPkg =
53+
name:
54+
let
55+
p = tryAttr24 name;
56+
in
57+
if p != null then legacyCompat p else null;
58+
59+
# Flexible PHP package set selection for composer
60+
phpPkgSet =
61+
pkgs25_05.php84Packages
62+
or (pkgs25_05.php83Packages or (pkgs25_05.php82Packages or (pkgs25_05.php81Packages or null)));
63+
composerPkg =
64+
if phpPkgSet != null && phpPkgSet ? composer then phpPkgSet.composer else pkgs25_05.composer; # pkgs25_05.composer as last resort
65+
phpInterp = pkgs25_05.php or (if phpPkgSet != null && phpPkgSet ? php then phpPkgSet.php else null);
66+
67+
# Legacy (29) come from 24.11, 30/31 from 25.05, 32 from 25.11
68+
pkg29 = legacyPkg "nextcloud29";
69+
pkg30 = tryAttr25 "nextcloud30";
70+
pkg31 = tryAttr25 "nextcloud31";
71+
pkg32 = tryAttr2511 "nextcloud32";
72+
73+
has29 = pkg29 != null;
74+
has30 = pkg30 != null;
75+
has31 = pkg31 != null;
76+
has32 = pkg32 != null;
77+
78+
# Build the app once (using primary pkgs set)
79+
qownnotesapiApp =
80+
pkgs25_05.runCommand "qownnotesapi-app"
81+
{
82+
src = ../../.;
83+
buildInputs = lib.filter (x: x != null) [
84+
composerPkg
85+
phpInterp
86+
];
87+
preferLocalBuild = true;
88+
allowSubstitutes = false; # ensure we always build locally (still won't rebuild if output already exists)
89+
}
90+
''
91+
mkdir -p $out
92+
cp -r $src/* $out/
93+
chmod -R u+w $out
94+
if [ -n "$FORCE_REBUILD_NONCE" ]; then
95+
echo "$FORCE_REBUILD_NONCE" > $out/.force-rebuild-nonce
96+
echo "Force rebuild nonce embedded: $FORCE_REBUILD_NONCE"
97+
fi
98+
export COMPOSER_ALLOW_SUPERUSER=1
99+
export HOME=$TMPDIR
100+
if [ -f "$out/composer.json" ]; then
101+
if [ -d "$out/vendor" ]; then
102+
echo "Running composer install (offline, expects vendor already vendored)"
103+
(cd $out && composer install --no-dev --optimize-autoloader --no-interaction || composer dump-autoload --optimize || true)
104+
else
105+
echo "No vendor directory found; skipping composer install to avoid network (would fail)"
106+
fi
107+
fi
108+
'';
109+
110+
mkNode = pkg: name: {
111+
${name} = _: {
112+
services.nextcloud = {
113+
enable = true;
114+
package = pkg;
115+
hostName = "localhost";
116+
config = {
117+
adminuser = "admin";
118+
adminpassFile = "/etc/nextcloud-adminpass";
119+
dbtype = "sqlite";
120+
dbname = "nextcloud";
121+
};
122+
extraApps = {
123+
qownnotesapi = qownnotesapiApp;
124+
};
125+
extraAppsEnable = true;
126+
};
127+
networking.firewall.allowedTCPPorts = [
128+
80
129+
443
130+
];
131+
environment.etc."nextcloud-adminpass".text = "adminpass";
132+
};
133+
};
134+
135+
node29 = if has29 then mkNode pkg29 "nextcloud29" else { };
136+
node30 = if has30 then mkNode pkg30 "nextcloud30" else { };
137+
node31 = if has31 then mkNode pkg31 "nextcloud31" else { };
138+
node32 = if has32 then mkNode pkg32 "nextcloud32" else { };
139+
140+
in
141+
# Fail early if any required Nextcloud package is missing
142+
assert (lib.assertMsg has29 "Missing required package: nextcloud29 (expected in pkgs24_11)");
143+
assert (lib.assertMsg has30 "Missing required package: nextcloud30 (expected in pkgs25_05)");
144+
assert (lib.assertMsg has31 "Missing required package: nextcloud31 (expected in pkgs25_05)");
145+
assert (lib.assertMsg has32 "Missing required package: nextcloud32 (expected in pkgs25_11)");
146+
147+
pkgs25_05.nixosTest {
148+
name = "nextcloud_qownnotesapi";
149+
nodes = node29 // node30 // node31 // node32;
150+
interactive.sshBackdoor.enable = true; # provides ssh-config & vsock access (needs host vsock support)
151+
testScript = ''
152+
print("Has29=${toString has29} Has30=${toString has30} Has31=${toString has31} Has32=${toString has32}")
153+
start_all()
154+
155+
# Helper to test a Nextcloud node consistently
156+
def test_version(node, label, pkg_version):
157+
print(f"Testing Nextcloud {label} ({pkg_version})")
158+
node.wait_for_unit("phpfpm-nextcloud.service")
159+
node.wait_for_unit("nginx.service")
160+
node.succeed("curl -fsSL http://localhost/status.php | grep 'installed' | grep 'true'")
161+
node.succeed("sudo -u nextcloud nextcloud-occ app:list | grep -i qownnotesapi || (echo 'App missing ({label})'; sudo -u nextcloud nextcloud-occ app:list; exit 1)")
162+
assert "200" in node.succeed("curl -s -o /dev/null -w '%{http_code}' http://localhost/login"), "Login page needs to show up!"
163+
node.succeed("sudo -u nextcloud nextcloud-occ status | grep -i 'version:'")
164+
# Test qownnotesapi app endpoints
165+
assert "200" in node.succeed("curl -s -o /dev/null -w '%{http_code}' http://admin:adminpass@localhost/index.php/apps/qownnotesapi/api/v1/note/versions?format=json&file_name=/Notes/test.md"), "Version API request failed!"
166+
assert "200" in node.succeed("curl -s -o /dev/null -w '%{http_code}' http://admin:adminpass@localhost/index.php/apps/qownnotesapi/api/v1/note/trashed?format=json&dir=/Notes"), "Trash API request failed!"
167+
assert "200" in node.succeed("curl -s -o /dev/null -w '%{http_code}' http://admin:adminpass@localhost/index.php/apps/qownnotesapi/api/v1/note/app_info?notes_path=/Notes"), "App Info API request failed!"
168+
169+
${
170+
if has29 then
171+
''test_version(nextcloud29, "29", "${pkg29.version}")''
172+
else
173+
''print("Skipping Nextcloud 29: package not present")''
174+
}
175+
176+
${
177+
if has30 then
178+
''test_version(nextcloud30, "30", "${pkg30.version}")''
179+
else
180+
''print("Skipping Nextcloud 30: package not present")''
181+
}
182+
183+
${
184+
if has31 then
185+
''test_version(nextcloud31, "31", "${pkg31.version}")''
186+
else
187+
''print("Skipping Nextcloud 31: package not present")''
188+
}
189+
190+
${
191+
if has32 then
192+
''test_version(nextcloud32, "32", "${pkg32.version}")''
193+
else
194+
''print("Skipping Nextcloud 32: package not present")''
195+
}
196+
print("ALL_TESTS_DONE")
197+
'';
198+
}

0 commit comments

Comments
 (0)