Skip to content

Commit 0310687

Browse files
committed
test(exapp_integration): add Playwright file action menu regression for #848
Signed-off-by: Oleksander Piskun <oleksandr2088@icloud.com>
1 parent 912fcae commit 0310687

8 files changed

Lines changed: 364 additions & 24 deletions

File tree

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
# SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
2+
# SPDX-License-Identifier: MIT
3+
name: Tests - ExApp Integration
4+
5+
on:
6+
pull_request:
7+
branches: [main]
8+
push:
9+
branches: [main]
10+
workflow_dispatch:
11+
12+
permissions:
13+
contents: read
14+
15+
concurrency:
16+
group: tests-exapp-integration-${{ github.head_ref || github.run_id }}
17+
cancel-in-progress: true
18+
19+
env:
20+
NEXTCLOUD_URL: "http://127.0.0.1:8080"
21+
NEXTCLOUD_USER: "admin"
22+
NEXTCLOUD_PASS: "admin"
23+
APP_ID: "test_appapi"
24+
APP_VERSION: "1.0.0"
25+
APP_PORT: 9009
26+
APP_HOST: "127.0.0.1"
27+
APP_SECRET: "tC6vkwPhcppjMykD1r0n9NlI95uJMBYjs5blpIcA1PAdoPDmc5qoAjaBAkyocZ6E"
28+
29+
jobs:
30+
exapp-integration:
31+
runs-on: ubuntu-22.04
32+
name: ExApp integration (Playwright)
33+
34+
services:
35+
postgres:
36+
image: ghcr.io/nextcloud/continuous-integration-postgres-14:latest # zizmor: ignore[unpinned-images]
37+
ports:
38+
- 4444:5432/tcp
39+
env:
40+
POSTGRES_USER: root
41+
POSTGRES_PASSWORD: rootpassword
42+
POSTGRES_DB: nextcloud
43+
options: --health-cmd pg_isready --health-interval 5s --health-timeout 2s --health-retries 5
44+
45+
steps:
46+
- name: Set app env
47+
run: echo "APP_NAME=${GITHUB_REPOSITORY##*/}" >> $GITHUB_ENV
48+
49+
- name: Checkout server
50+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
51+
with:
52+
persist-credentials: false
53+
submodules: true
54+
repository: nextcloud/server
55+
ref: master
56+
57+
- name: Checkout AppAPI
58+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
59+
with:
60+
persist-credentials: false
61+
path: apps/${{ env.APP_NAME }}
62+
63+
- name: Set up php
64+
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2
65+
with:
66+
php-version: '8.3'
67+
extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, session, simplexml, xmlreader, xmlwriter, zip, zlib, pgsql, pdo_pgsql
68+
coverage: none
69+
ini-file: development
70+
env:
71+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
72+
73+
- name: Set up Python
74+
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
75+
with:
76+
python-version: '3.11'
77+
78+
- name: Read package.json node and npm engines version
79+
uses: skjnldsv/read-package-engines-version-actions@06d6baf7d8f41934ab630e97d9e6c0bc9c9ac5e4 # v3
80+
id: versions
81+
with:
82+
path: apps/${{ env.APP_NAME }}
83+
fallbackNode: '^24'
84+
fallbackNpm: '^11.3'
85+
86+
- name: Set up node ${{ steps.versions.outputs.nodeVersion }}
87+
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
88+
with:
89+
node-version: ${{ steps.versions.outputs.nodeVersion }}
90+
91+
- name: Set up npm ${{ steps.versions.outputs.npmVersion }}
92+
run: npm i -g 'npm@${{ steps.versions.outputs.npmVersion }}'
93+
94+
- name: Install AppAPI composer deps
95+
working-directory: apps/${{ env.APP_NAME }}
96+
run: composer i
97+
98+
- name: Build AppAPI frontend bundle
99+
working-directory: apps/${{ env.APP_NAME }}
100+
run: |
101+
npm ci
102+
npm run build
103+
104+
- name: Set up Nextcloud
105+
env:
106+
DB_PORT: 4444
107+
run: |
108+
mkdir data
109+
./occ maintenance:install --verbose --database=pgsql --database-name=nextcloud --database-host=127.0.0.1 \
110+
--database-port=$DB_PORT --database-user=root --database-pass=rootpassword \
111+
--admin-user "$NEXTCLOUD_USER" --admin-pass "$NEXTCLOUD_PASS"
112+
./occ config:system:set ratelimit.protection.enabled --value=false --type=boolean
113+
./occ app:enable --force ${{ env.APP_NAME }}
114+
115+
- name: Run Nextcloud
116+
run: PHP_CLI_SERVER_WORKERS=4 php -S 127.0.0.1:8080 &
117+
118+
- name: Install integration test Python deps
119+
run: python3 -m pip install -r apps/${{ env.APP_NAME }}/tests/exapp_integration/requirements.txt
120+
121+
- name: Install Playwright browser
122+
run: python3 -m playwright install --with-deps chromium
123+
124+
- name: Register manual_daemon
125+
run: php occ app_api:daemon:register manual_daemon "Manual Install" manual-install http "$APP_HOST" "$NEXTCLOUD_URL"
126+
127+
- name: Start test ExApp
128+
run: |
129+
cd apps/${{ env.APP_NAME }}/tests/exapp_integration
130+
nohup python3 _test_app.py > /tmp/test_appapi.log 2>&1 &
131+
echo $! > /tmp/test_appapi.pid
132+
for _ in $(seq 1 30); do
133+
curl -fs "http://$APP_HOST:$APP_PORT/heartbeat" >/dev/null && break
134+
sleep 1
135+
done
136+
curl -fs "http://$APP_HOST:$APP_PORT/heartbeat"
137+
138+
- name: Register test ExApp
139+
run: |
140+
php occ app_api:app:register "$APP_ID" manual_daemon --json-info \
141+
"{\"id\":\"$APP_ID\",\"name\":\"AppAPI Integration Test ExApp\",\"version\":\"$APP_VERSION\",\"secret\":\"$APP_SECRET\",\"port\":$APP_PORT,\"host\":\"$APP_HOST\",\"protocol\":\"http\",\"system_app\":0}" \
142+
--wait-finish
143+
php occ app_api:app:list | grep -q "^$APP_ID .* \[enabled\]$"
144+
145+
- name: Run pytest
146+
working-directory: apps/${{ env.APP_NAME }}/tests/exapp_integration
147+
env:
148+
OCC_CMD: "php ${{ github.workspace }}/occ"
149+
run: python3 -m pytest -v
150+
151+
- name: Stop test ExApp
152+
if: always()
153+
run: |
154+
[ -f /tmp/test_appapi.pid ] && kill -15 "$(cat /tmp/test_appapi.pid)" || true
155+
156+
- name: Upload NC logs
157+
if: always()
158+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
159+
with:
160+
name: exapp-integration-nextcloud.log
161+
path: data/nextcloud.log
162+
if-no-files-found: warn
163+
164+
- name: Upload test ExApp log
165+
if: always()
166+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
167+
with:
168+
name: exapp-integration-test_appapi.log
169+
path: /tmp/test_appapi.log
170+
if-no-files-found: warn
171+
172+
tests-exapp-integration-success:
173+
permissions:
174+
contents: none
175+
runs-on: ubuntu-22.04
176+
needs: [exapp-integration]
177+
name: TestsExAppIntegration-OK
178+
steps:
179+
- run: echo "ExApp integration tests passed"

tests/exapp_integration/_test_app.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,14 @@
33
"""Minimal test ExApp for AppAPI integration tests.
44
55
Replaces the nc_py_api-based `tests/_install.py`. Implements only the
6-
callbacks AppAPI invokes (`/init`, `/enabled`, `/heartbeat`) plus a few
7-
introspection endpoints used by the integration tests.
6+
callbacks AppAPI invokes (`/enabled`, `/heartbeat`) plus a few introspection
7+
endpoints used by the integration tests.
8+
9+
We deliberately do not register a `/init` route: AppAPI's
10+
`dispatchExAppInitInternal` treats a 404/501 from the ExApp as "no init step
11+
needed" and marks init progress = 100. This lets `app_api:app:register
12+
--wait-finish` complete promptly without the test app having to call back to
13+
`/ex-app/status` itself.
814
"""
915

1016
import os
@@ -40,12 +46,6 @@ def verify_auth(request: FastAPIRequest) -> str:
4046
return username
4147

4248

43-
@APP.post("/init")
44-
async def init_callback(request: FastAPIRequest):
45-
verify_auth(request)
46-
return JSONResponse(content={}, status_code=200)
47-
48-
4949
@APP.put("/enabled")
5050
async def enabled_callback(enabled: bool, request: FastAPIRequest):
5151
verify_auth(request)

tests/exapp_integration/conftest.py

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
3. The ExApp has been registered & enabled through OCC against the
99
manual_daemon (see register_test_exapp.sh).
1010
11-
In the dev VM all three are arranged before running pytest. See README.md.
11+
All three are arranged before running pytest (locally via
12+
register_test_exapp.sh, in CI via tests-exapp-integration.yml).
1213
"""
1314

1415
from __future__ import annotations
@@ -21,13 +22,20 @@
2122
from ._client import AppAPIClient
2223

2324
NEXTCLOUD_URL = os.environ.get("NEXTCLOUD_URL", "http://nextcloud.appapi")
25+
NEXTCLOUD_USER = os.environ.get("NEXTCLOUD_USER", "admin")
26+
NEXTCLOUD_PASS = os.environ.get("NEXTCLOUD_PASS", "admin")
2427
APP_ID = os.environ.get("APP_ID", "test_appapi")
2528
APP_VERSION = os.environ.get("APP_VERSION", "1.0.0")
2629
APP_SECRET = os.environ["APP_SECRET"]
2730
TEST_APP_URL = os.environ.get("TEST_APP_URL", "http://127.0.0.1:9009")
2831

29-
OCC = ["docker", "exec", "appapi-nextcloud-1",
30-
"sudo", "-u", "www-data", "php", "occ"]
32+
# OCC invocation. Override with `OCC_CMD` (space-separated) when Nextcloud
33+
# isn't running inside the appapi-nextcloud-1 docker container (e.g. in CI,
34+
# where Nextcloud runs on the host via `php -S` and `php occ` is enough).
35+
OCC = os.environ.get(
36+
"OCC_CMD",
37+
"docker exec appapi-nextcloud-1 sudo -u www-data php occ",
38+
).split()
3139

3240

3341
@pytest.fixture(scope="session")
@@ -58,6 +66,16 @@ def test_app_url() -> str:
5866
return TEST_APP_URL
5967

6068

69+
@pytest.fixture(scope="session")
70+
def nextcloud_url() -> str:
71+
return NEXTCLOUD_URL
72+
73+
74+
@pytest.fixture(scope="session")
75+
def admin_credentials() -> tuple[str, str]:
76+
return NEXTCLOUD_USER, NEXTCLOUD_PASS
77+
78+
6179
@pytest.fixture(scope="session", autouse=True)
6280
def _ensure_test_app_registered() -> None:
6381
r = subprocess.run(OCC + ["app_api:app:list"], capture_output=True, text=True, check=True)
@@ -67,3 +85,19 @@ def _ensure_test_app_registered() -> None:
6785
)
6886
if "[enabled]" not in next((line for line in r.stdout.splitlines() if APP_ID in line), ""):
6987
raise RuntimeError(f"ExApp '{APP_ID}' is registered but not enabled.")
88+
89+
90+
@pytest.fixture
91+
def logged_in_page(page, nextcloud_url, admin_credentials):
92+
"""Yield a Playwright Page authenticated as admin.
93+
94+
Uses the standard NC login form (no app password / no token plumbing) so
95+
the test exercises the same client-side bootstrap path real users hit.
96+
"""
97+
user, password = admin_credentials
98+
page.goto(f"{nextcloud_url}/index.php/login")
99+
page.get_by_label("Account name or email").fill(user)
100+
page.get_by_label("Password", exact=True).fill(password)
101+
page.get_by_role("button", name="Log in", exact=True).click()
102+
page.wait_for_url(lambda url: "/login" not in url, timeout=15_000)
103+
return page

tests/exapp_integration/register_test_exapp.sh

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
#
1616
# The ExApp itself (uvicorn _test_app:APP) must be running and reachable from
1717
# the Nextcloud container at $APP_HOST:$APP_PORT BEFORE this script runs,
18-
# because `app_api:app:enable` calls /init synchronously.
18+
# because `app_api:app:register --wait-finish` calls /heartbeat and /init
19+
# synchronously and would fail/hang otherwise.
1920
set -euo pipefail
2021

2122
NEXTCLOUD_CONTAINER=${NEXTCLOUD_CONTAINER:-appapi-nextcloud-1}
@@ -49,7 +50,7 @@ JSON=$(cat <<EOF
4950
{"id":"$APP_ID","name":"AppAPI Integration Test ExApp","version":"$APP_VERSION","secret":"$APP_SECRET","port":$APP_PORT,"host":"$APP_HOST","protocol":"http","system_app":0}
5051
EOF
5152
)
52-
"${OCC[@]}" app_api:app:register "$APP_ID" "$DAEMON_NAME" --json-info="$JSON" --silent
53+
"${OCC[@]}" app_api:app:register "$APP_ID" "$DAEMON_NAME" --json-info="$JSON" --silent --wait-finish
5354
"${OCC[@]}" app_api:app:enable "$APP_ID" || true
5455

5556
# Confirm
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
2+
# SPDX-License-Identifier: AGPL-3.0-or-later
3+
fastapi
4+
pytest
5+
pytest-playwright
6+
requests
7+
uvicorn

0 commit comments

Comments
 (0)