Skip to content

Commit 8daee73

Browse files
authored
playwright (#6025)
1 parent 688c3e1 commit 8daee73

63 files changed

Lines changed: 5037 additions & 673 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/config/.files.yaml

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,23 @@ licenses-frontend: &licenses-frontend
6363

6464
licenses-backend: &licenses-backend
6565
- ".github/workflows/frontend-backend-licenses-update.yml"
66-
- *build
66+
- *build
67+
68+
# Files that can affect premium / enterprise behaviour. Gate the enterprise
69+
# Playwright job on changes to any of these on PRs.
70+
proprietary: &proprietary
71+
- app/proprietary/**
72+
- frontend/src/proprietary/**
73+
- frontend/src/core/tests/enterprise/**
74+
- testing/compose/docker-compose-keycloak-oauth.yml
75+
- testing/compose/docker-compose-keycloak-saml.yml
76+
- testing/compose/keycloak-realm-oauth.json
77+
- testing/compose/keycloak-realm-saml.json
78+
- testing/compose/start-oauth-test.sh
79+
- testing/compose/start-saml-test.sh
80+
- testing/compose/validate-oauth-test.sh
81+
- testing/compose/validate-saml-test.sh
82+
- configs/settings.yml.template
83+
- build.gradle
84+
- app/proprietary/build.gradle
85+
- .github/workflows/build-enterprise.yml
Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
name: Enterprise E2E (Playwright)
2+
3+
# Enterprise Playwright suite — exercises premium-key gated features (audit,
4+
# teams, analytics) plus full OAuth + SAML logins via the Keycloak compose
5+
# stacks under testing/compose. Slow and secret-gated, so it runs in three
6+
# situations:
7+
#
8+
# - PRs that touch proprietary / premium / SSO compose / enterprise tests
9+
# (path-filtered against .github/config/.files.yaml `proprietary`),
10+
# - every push to main (post-merge safety net),
11+
# - on a nightly cron schedule (catches Keycloak image drift, license
12+
# expiry, upstream proprietary changes),
13+
# - manual workflow_dispatch.
14+
#
15+
# Auto-skipped when secrets.PREMIUM_KEY_ENTERPRISE is missing (forks, dependabot).
16+
17+
on:
18+
push:
19+
branches: ["main"]
20+
pull_request:
21+
branches: ["main"]
22+
schedule:
23+
- cron: "0 4 * * *"
24+
workflow_dispatch:
25+
26+
concurrency:
27+
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref_name || github.ref }}
28+
cancel-in-progress: true
29+
30+
permissions:
31+
contents: read
32+
33+
jobs:
34+
files-changed:
35+
name: detect what files changed
36+
runs-on: ubuntu-latest
37+
timeout-minutes: 3
38+
outputs:
39+
proprietary: ${{ steps.changes.outputs.proprietary }}
40+
steps:
41+
- name: Harden Runner
42+
uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1
43+
with:
44+
egress-policy: audit
45+
- name: Checkout repository
46+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
47+
- name: Check for file changes
48+
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
49+
id: changes
50+
with:
51+
filters: .github/config/.files.yaml
52+
53+
playwright-e2e-enterprise:
54+
# Run on PRs only if relevant files changed; always run on push-to-main,
55+
# cron, and manual dispatch. Fork PRs without the secret will fail at
56+
# the compose step (PREMIUM_KEY empty) — that's intentional, not silent.
57+
if: github.event_name != 'pull_request' || needs.files-changed.outputs.proprietary == 'true'
58+
needs: files-changed
59+
runs-on: ubuntu-latest
60+
timeout-minutes: 45
61+
env:
62+
PREMIUM_KEY: ${{ secrets.PREMIUM_KEY_ENTERPRISE }}
63+
PREMIUM_ENABLED: "true"
64+
SYSTEM_ENABLEANALYTICS: "false"
65+
steps:
66+
- name: Harden Runner
67+
uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1
68+
with:
69+
egress-policy: audit
70+
- name: Checkout repository
71+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
72+
- name: Set up JDK 25
73+
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
74+
with:
75+
java-version: "25"
76+
distribution: "temurin"
77+
- name: Set up Node.js
78+
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
79+
with:
80+
node-version: "22"
81+
cache: "npm"
82+
cache-dependency-path: frontend/package-lock.json
83+
- name: Install Task
84+
uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0
85+
- name: Install Playwright (chromium only)
86+
run: task frontend:test:e2e:install -- chromium
87+
88+
- name: Resolve kubernetes.docker.internal to localhost
89+
# The compose stacks set KC_HOSTNAME=kubernetes.docker.internal so
90+
# Keycloak issues redirect URIs against that host. Docker Desktop
91+
# auto-resolves it; GHA runners don't. Map it to 127.0.0.1 so the
92+
# browser-driven OAuth flow lands back on Stirling-PDF correctly.
93+
run: |
94+
echo "127.0.0.1 kubernetes.docker.internal" | sudo tee -a /etc/hosts
95+
96+
# Helper function used by all phases — boots `:stirling-pdf:bootRun`
97+
# with the React frontend baked in (-PbuildWithFrontend=true) so the
98+
# SPA serves on :8080 and OAuth/SAML callbacks land on the same host
99+
# that the browser is interacting with.
100+
- name: Define helpers
101+
run: |
102+
{
103+
echo 'wait_for_backend() {'
104+
echo ' start=$SECONDS'
105+
echo ' for i in $(seq 1 300); do'
106+
echo ' if curl -fsS http://localhost:8080/api/v1/info/status >/dev/null 2>&1; then'
107+
echo ' echo "Backend up after $((SECONDS - start))s"; return 0'
108+
echo ' fi; sleep 2'
109+
echo ' done'
110+
echo ' tail -200 /tmp/backend.log || true; return 1'
111+
echo '}'
112+
echo 'stop_backend() {'
113+
echo ' if [ -f /tmp/backend.pid ]; then'
114+
echo ' kill "$(cat /tmp/backend.pid)" 2>/dev/null || true'
115+
echo ' rm -f /tmp/backend.pid'
116+
echo ' fi'
117+
echo ' pkill -f "gradlew :stirling-pdf:bootRun" 2>/dev/null || true'
118+
echo ' for i in $(seq 1 30); do'
119+
echo ' curl -fsS http://localhost:8080/api/v1/info/status >/dev/null 2>&1 || return 0'
120+
echo ' sleep 1'
121+
echo ' done'
122+
echo '}'
123+
} > /tmp/helpers.sh
124+
chmod +x /tmp/helpers.sh
125+
126+
# ───────── OAuth round-trip ─────────
127+
- name: Bring up Keycloak (OAuth realm)
128+
working-directory: testing/compose
129+
run: docker compose -f docker-compose-keycloak-oauth.yml up -d --no-deps keycloak-oauth-db keycloak-oauth
130+
- name: Wait for Keycloak (OAuth) ready
131+
working-directory: testing/compose
132+
run: |
133+
for i in $(seq 1 60); do
134+
bash validate-oauth-test.sh 2>/dev/null && exit 0 || true
135+
# validate script also pings stirling on :8080 — accept just the
136+
# keycloak realm as our gate here, stirling boots in the next step
137+
curl -fsS http://localhost:9080/realms/stirling-oauth >/dev/null 2>&1 && exit 0
138+
sleep 5
139+
done
140+
docker compose -f docker-compose-keycloak-oauth.yml logs --tail=200 keycloak-oauth
141+
exit 1
142+
- name: Boot Stirling-PDF (frontend baked in, OAuth env)
143+
env:
144+
SECURITY_ENABLELOGIN: "true"
145+
SECURITY_LOGINMETHOD: "all"
146+
SECURITY_OAUTH2_ENABLED: "true"
147+
SECURITY_OAUTH2_AUTOCREATEUSER: "true"
148+
# Keycloak issues redirect URIs against KC_HOSTNAME, which the
149+
# compose default sets to kubernetes.docker.internal. Match here
150+
# (resolves to localhost via /etc/hosts mapping above).
151+
SECURITY_OAUTH2_CLIENT_KEYCLOAK_ISSUER: "http://kubernetes.docker.internal:9080/realms/stirling-oauth"
152+
SECURITY_OAUTH2_CLIENT_KEYCLOAK_CLIENTID: "stirling-pdf-client"
153+
SECURITY_OAUTH2_CLIENT_KEYCLOAK_CLIENTSECRET: "test-client-secret-change-in-production"
154+
SECURITY_OAUTH2_CLIENT_KEYCLOAK_USEASUSERNAME: "email"
155+
SECURITY_OAUTH2_CLIENT_KEYCLOAK_SCOPES: "openid,profile,email"
156+
run: |
157+
source /tmp/helpers.sh
158+
nohup ./gradlew :stirling-pdf:bootRun -PbuildWithFrontend=true > /tmp/backend.log 2>&1 &
159+
echo $! > /tmp/backend.pid
160+
wait_for_backend
161+
- name: Run enterprise OAuth Playwright tests
162+
id: oauth-tests
163+
run: task frontend:test:e2e -- --project=enterprise --grep "OAuth"
164+
- name: Stop backend + tear down OAuth Keycloak
165+
if: always()
166+
run: |
167+
source /tmp/helpers.sh
168+
stop_backend
169+
(cd testing/compose && docker compose -f docker-compose-keycloak-oauth.yml down -v)
170+
171+
# ───────── SAML round-trip ─────────
172+
- name: Bring up Keycloak (SAML realm)
173+
working-directory: testing/compose
174+
run: docker compose -f docker-compose-keycloak-saml.yml up -d --no-deps keycloak-saml-db keycloak-saml
175+
- name: Wait for Keycloak (SAML) ready
176+
working-directory: testing/compose
177+
run: |
178+
for i in $(seq 1 60); do
179+
curl -fsS http://localhost:9080/realms/stirling-saml >/dev/null 2>&1 && exit 0
180+
sleep 5
181+
done
182+
docker compose -f docker-compose-keycloak-saml.yml logs --tail=200 keycloak-saml
183+
exit 1
184+
- name: Generate SAML SP certs + fetch Keycloak IdP cert
185+
# The .pem/.crt/.key files are gitignored (test-only certs); the
186+
# docker-based start-saml-test.sh generates them at runtime, so do
187+
# the same in CI before bootRun reads them.
188+
working-directory: testing/compose
189+
run: |
190+
openssl req -x509 -newkey rsa:2048 \
191+
-keyout saml-private-key.key \
192+
-out saml-public-cert.crt \
193+
-days 3650 -nodes \
194+
-subj "/CN=stirling-pdf-saml-sp" >/dev/null 2>&1
195+
# Fetch Keycloak's SAML signing cert from the realm descriptor
196+
CERT_BODY=$(curl -sf http://localhost:9080/realms/stirling-saml/protocol/saml/descriptor \
197+
| awk 'BEGIN{RS="<[^>]*X509Certificate>|</[^>]*X509Certificate>"} NR==2{gsub(/[[:space:]]+/,""); print; exit}')
198+
{
199+
echo "-----BEGIN CERTIFICATE-----"
200+
echo "$CERT_BODY"
201+
echo "-----END CERTIFICATE-----"
202+
} > keycloak-saml-cert.pem
203+
test -s saml-private-key.key
204+
test -s saml-public-cert.crt
205+
test -s keycloak-saml-cert.pem
206+
echo "✓ SAML certs prepared"
207+
- name: Boot Stirling-PDF (frontend baked in, SAML env)
208+
env:
209+
SECURITY_ENABLELOGIN: "true"
210+
SECURITY_LOGINMETHOD: "all"
211+
SECURITY_SAML2_ENABLED: "true"
212+
SECURITY_SAML2_AUTOCREATEUSER: "true"
213+
SECURITY_SAML2_PROVIDER: "keycloak"
214+
SECURITY_SAML2_REGISTRATIONID: "keycloak"
215+
SECURITY_SAML2_IDP_ISSUER: "http://localhost:9080/realms/stirling-saml"
216+
SECURITY_SAML2_IDP_ENTITYID: "http://localhost:9080/realms/stirling-saml"
217+
SECURITY_SAML2_IDP_METADATAURI: "http://localhost:9080/realms/stirling-saml/protocol/saml/descriptor"
218+
SECURITY_SAML2_IDPSINGLELOGINURL: "http://localhost:9080/realms/stirling-saml/protocol/saml"
219+
SECURITY_SAML2_IDPSINGLELOGOUTURL: "http://localhost:9080/realms/stirling-saml/protocol/saml"
220+
SECURITY_SAML2_IDP_CERT: "${{ github.workspace }}/testing/compose/keycloak-saml-cert.pem"
221+
SECURITY_SAML2_PRIVATEKEY: "${{ github.workspace }}/testing/compose/saml-private-key.key"
222+
SECURITY_SAML2_SP_CERT: "${{ github.workspace }}/testing/compose/saml-public-cert.crt"
223+
# Realm registers the SP entity as the metadata URL — see
224+
# keycloak-realm-saml.json `clientId`. Match it here so Keycloak
225+
# accepts the AuthnRequest issuer.
226+
SECURITY_SAML2_SP_ENTITYID: "http://localhost:8080/saml2/service-provider-metadata/keycloak"
227+
SECURITY_SAML2_SP_ACS: "http://localhost:8080/login/saml2/sso/keycloak"
228+
SECURITY_SAML2_SP_SLS: "http://localhost:8080/logout/saml2/slo"
229+
run: |
230+
source /tmp/helpers.sh
231+
nohup ./gradlew :stirling-pdf:bootRun -PbuildWithFrontend=true > /tmp/backend.log 2>&1 &
232+
echo $! > /tmp/backend.pid
233+
wait_for_backend
234+
- name: Run enterprise SAML Playwright tests
235+
id: saml-tests
236+
run: task frontend:test:e2e -- --project=enterprise --grep "SAML"
237+
- name: Stop backend + tear down SAML Keycloak
238+
if: always()
239+
run: |
240+
source /tmp/helpers.sh
241+
stop_backend
242+
(cd testing/compose && docker compose -f docker-compose-keycloak-saml.yml down -v)
243+
244+
# ───────── License-gated feature tests (no IdP needed) ─────────
245+
- name: Wipe DB so InitialSecuritySetup re-runs with admin/adminadmin
246+
# Earlier phases (OAuth, SAML) create the default admin/stirling user.
247+
# InitialSecuritySetup only honours SECURITY_INITIALLOGIN_* when the
248+
# admin user doesn't already exist, so the persisted DB has to be
249+
# cleared between phases for the feature env vars to take effect.
250+
run: |
251+
rm -f app/core/configs/stirling-pdf-DB*.mv.db
252+
rm -rf app/core/configs/backup
253+
- name: Boot Stirling-PDF (frontend baked in, premium only)
254+
env:
255+
SECURITY_INITIALLOGIN_USERNAME: admin
256+
SECURITY_INITIALLOGIN_PASSWORD: adminadmin
257+
SECURITY_ENABLELOGIN: "true"
258+
SECURITY_LOGINMETHOD: "all"
259+
run: |
260+
source /tmp/helpers.sh
261+
nohup ./gradlew :stirling-pdf:bootRun -PbuildWithFrontend=true > /tmp/backend.log 2>&1 &
262+
echo $! > /tmp/backend.pid
263+
wait_for_backend
264+
- name: Run enterprise feature Playwright tests
265+
id: feature-tests
266+
run: task frontend:test:e2e -- --project=enterprise --grep "Enterprise license"
267+
- name: Print backend log on failure
268+
if: failure()
269+
run: |
270+
echo "::group::Enterprise backend log"
271+
tail -500 /tmp/backend.log || true
272+
echo "::endgroup::"
273+
- name: Stop backend (final)
274+
if: always()
275+
run: |
276+
source /tmp/helpers.sh
277+
stop_backend
278+
- name: Upload Playwright report
279+
if: always()
280+
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
281+
with:
282+
name: playwright-report-enterprise-${{ github.run_id }}
283+
path: frontend/playwright-report/
284+
retention-days: 7

0 commit comments

Comments
 (0)