mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-14 10:34:06 +02:00
playwright (#6025)
This commit is contained in:
@@ -63,4 +63,23 @@ licenses-frontend: &licenses-frontend
|
||||
|
||||
licenses-backend: &licenses-backend
|
||||
- ".github/workflows/frontend-backend-licenses-update.yml"
|
||||
- *build
|
||||
- *build
|
||||
|
||||
# Files that can affect premium / enterprise behaviour. Gate the enterprise
|
||||
# Playwright job on changes to any of these on PRs.
|
||||
proprietary: &proprietary
|
||||
- app/proprietary/**
|
||||
- frontend/src/proprietary/**
|
||||
- frontend/src/core/tests/enterprise/**
|
||||
- testing/compose/docker-compose-keycloak-oauth.yml
|
||||
- testing/compose/docker-compose-keycloak-saml.yml
|
||||
- testing/compose/keycloak-realm-oauth.json
|
||||
- testing/compose/keycloak-realm-saml.json
|
||||
- testing/compose/start-oauth-test.sh
|
||||
- testing/compose/start-saml-test.sh
|
||||
- testing/compose/validate-oauth-test.sh
|
||||
- testing/compose/validate-saml-test.sh
|
||||
- configs/settings.yml.template
|
||||
- build.gradle
|
||||
- app/proprietary/build.gradle
|
||||
- .github/workflows/build-enterprise.yml
|
||||
@@ -0,0 +1,284 @@
|
||||
name: Enterprise E2E (Playwright)
|
||||
|
||||
# Enterprise Playwright suite — exercises premium-key gated features (audit,
|
||||
# teams, analytics) plus full OAuth + SAML logins via the Keycloak compose
|
||||
# stacks under testing/compose. Slow and secret-gated, so it runs in three
|
||||
# situations:
|
||||
#
|
||||
# - PRs that touch proprietary / premium / SSO compose / enterprise tests
|
||||
# (path-filtered against .github/config/.files.yaml `proprietary`),
|
||||
# - every push to main (post-merge safety net),
|
||||
# - on a nightly cron schedule (catches Keycloak image drift, license
|
||||
# expiry, upstream proprietary changes),
|
||||
# - manual workflow_dispatch.
|
||||
#
|
||||
# Auto-skipped when secrets.PREMIUM_KEY_ENTERPRISE is missing (forks, dependabot).
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
pull_request:
|
||||
branches: ["main"]
|
||||
schedule:
|
||||
- cron: "0 4 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref_name || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
files-changed:
|
||||
name: detect what files changed
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 3
|
||||
outputs:
|
||||
proprietary: ${{ steps.changes.outputs.proprietary }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Check for file changes
|
||||
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
id: changes
|
||||
with:
|
||||
filters: .github/config/.files.yaml
|
||||
|
||||
playwright-e2e-enterprise:
|
||||
# Run on PRs only if relevant files changed; always run on push-to-main,
|
||||
# cron, and manual dispatch. Fork PRs without the secret will fail at
|
||||
# the compose step (PREMIUM_KEY empty) — that's intentional, not silent.
|
||||
if: github.event_name != 'pull_request' || needs.files-changed.outputs.proprietary == 'true'
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
env:
|
||||
PREMIUM_KEY: ${{ secrets.PREMIUM_KEY_ENTERPRISE }}
|
||||
PREMIUM_ENABLED: "true"
|
||||
SYSTEM_ENABLEANALYTICS: "false"
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Set up JDK 25
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
java-version: "25"
|
||||
distribution: "temurin"
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: "npm"
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0
|
||||
- name: Install Playwright (chromium only)
|
||||
run: task frontend:test:e2e:install -- chromium
|
||||
|
||||
- name: Resolve kubernetes.docker.internal to localhost
|
||||
# The compose stacks set KC_HOSTNAME=kubernetes.docker.internal so
|
||||
# Keycloak issues redirect URIs against that host. Docker Desktop
|
||||
# auto-resolves it; GHA runners don't. Map it to 127.0.0.1 so the
|
||||
# browser-driven OAuth flow lands back on Stirling-PDF correctly.
|
||||
run: |
|
||||
echo "127.0.0.1 kubernetes.docker.internal" | sudo tee -a /etc/hosts
|
||||
|
||||
# Helper function used by all phases — boots `:stirling-pdf:bootRun`
|
||||
# with the React frontend baked in (-PbuildWithFrontend=true) so the
|
||||
# SPA serves on :8080 and OAuth/SAML callbacks land on the same host
|
||||
# that the browser is interacting with.
|
||||
- name: Define helpers
|
||||
run: |
|
||||
{
|
||||
echo 'wait_for_backend() {'
|
||||
echo ' start=$SECONDS'
|
||||
echo ' for i in $(seq 1 300); do'
|
||||
echo ' if curl -fsS http://localhost:8080/api/v1/info/status >/dev/null 2>&1; then'
|
||||
echo ' echo "Backend up after $((SECONDS - start))s"; return 0'
|
||||
echo ' fi; sleep 2'
|
||||
echo ' done'
|
||||
echo ' tail -200 /tmp/backend.log || true; return 1'
|
||||
echo '}'
|
||||
echo 'stop_backend() {'
|
||||
echo ' if [ -f /tmp/backend.pid ]; then'
|
||||
echo ' kill "$(cat /tmp/backend.pid)" 2>/dev/null || true'
|
||||
echo ' rm -f /tmp/backend.pid'
|
||||
echo ' fi'
|
||||
echo ' pkill -f "gradlew :stirling-pdf:bootRun" 2>/dev/null || true'
|
||||
echo ' for i in $(seq 1 30); do'
|
||||
echo ' curl -fsS http://localhost:8080/api/v1/info/status >/dev/null 2>&1 || return 0'
|
||||
echo ' sleep 1'
|
||||
echo ' done'
|
||||
echo '}'
|
||||
} > /tmp/helpers.sh
|
||||
chmod +x /tmp/helpers.sh
|
||||
|
||||
# ───────── OAuth round-trip ─────────
|
||||
- name: Bring up Keycloak (OAuth realm)
|
||||
working-directory: testing/compose
|
||||
run: docker compose -f docker-compose-keycloak-oauth.yml up -d --no-deps keycloak-oauth-db keycloak-oauth
|
||||
- name: Wait for Keycloak (OAuth) ready
|
||||
working-directory: testing/compose
|
||||
run: |
|
||||
for i in $(seq 1 60); do
|
||||
bash validate-oauth-test.sh 2>/dev/null && exit 0 || true
|
||||
# validate script also pings stirling on :8080 — accept just the
|
||||
# keycloak realm as our gate here, stirling boots in the next step
|
||||
curl -fsS http://localhost:9080/realms/stirling-oauth >/dev/null 2>&1 && exit 0
|
||||
sleep 5
|
||||
done
|
||||
docker compose -f docker-compose-keycloak-oauth.yml logs --tail=200 keycloak-oauth
|
||||
exit 1
|
||||
- name: Boot Stirling-PDF (frontend baked in, OAuth env)
|
||||
env:
|
||||
SECURITY_ENABLELOGIN: "true"
|
||||
SECURITY_LOGINMETHOD: "all"
|
||||
SECURITY_OAUTH2_ENABLED: "true"
|
||||
SECURITY_OAUTH2_AUTOCREATEUSER: "true"
|
||||
# Keycloak issues redirect URIs against KC_HOSTNAME, which the
|
||||
# compose default sets to kubernetes.docker.internal. Match here
|
||||
# (resolves to localhost via /etc/hosts mapping above).
|
||||
SECURITY_OAUTH2_CLIENT_KEYCLOAK_ISSUER: "http://kubernetes.docker.internal:9080/realms/stirling-oauth"
|
||||
SECURITY_OAUTH2_CLIENT_KEYCLOAK_CLIENTID: "stirling-pdf-client"
|
||||
SECURITY_OAUTH2_CLIENT_KEYCLOAK_CLIENTSECRET: "test-client-secret-change-in-production"
|
||||
SECURITY_OAUTH2_CLIENT_KEYCLOAK_USEASUSERNAME: "email"
|
||||
SECURITY_OAUTH2_CLIENT_KEYCLOAK_SCOPES: "openid,profile,email"
|
||||
run: |
|
||||
source /tmp/helpers.sh
|
||||
nohup ./gradlew :stirling-pdf:bootRun -PbuildWithFrontend=true > /tmp/backend.log 2>&1 &
|
||||
echo $! > /tmp/backend.pid
|
||||
wait_for_backend
|
||||
- name: Run enterprise OAuth Playwright tests
|
||||
id: oauth-tests
|
||||
run: task frontend:test:e2e -- --project=enterprise --grep "OAuth"
|
||||
- name: Stop backend + tear down OAuth Keycloak
|
||||
if: always()
|
||||
run: |
|
||||
source /tmp/helpers.sh
|
||||
stop_backend
|
||||
(cd testing/compose && docker compose -f docker-compose-keycloak-oauth.yml down -v)
|
||||
|
||||
# ───────── SAML round-trip ─────────
|
||||
- name: Bring up Keycloak (SAML realm)
|
||||
working-directory: testing/compose
|
||||
run: docker compose -f docker-compose-keycloak-saml.yml up -d --no-deps keycloak-saml-db keycloak-saml
|
||||
- name: Wait for Keycloak (SAML) ready
|
||||
working-directory: testing/compose
|
||||
run: |
|
||||
for i in $(seq 1 60); do
|
||||
curl -fsS http://localhost:9080/realms/stirling-saml >/dev/null 2>&1 && exit 0
|
||||
sleep 5
|
||||
done
|
||||
docker compose -f docker-compose-keycloak-saml.yml logs --tail=200 keycloak-saml
|
||||
exit 1
|
||||
- name: Generate SAML SP certs + fetch Keycloak IdP cert
|
||||
# The .pem/.crt/.key files are gitignored (test-only certs); the
|
||||
# docker-based start-saml-test.sh generates them at runtime, so do
|
||||
# the same in CI before bootRun reads them.
|
||||
working-directory: testing/compose
|
||||
run: |
|
||||
openssl req -x509 -newkey rsa:2048 \
|
||||
-keyout saml-private-key.key \
|
||||
-out saml-public-cert.crt \
|
||||
-days 3650 -nodes \
|
||||
-subj "/CN=stirling-pdf-saml-sp" >/dev/null 2>&1
|
||||
# Fetch Keycloak's SAML signing cert from the realm descriptor
|
||||
CERT_BODY=$(curl -sf http://localhost:9080/realms/stirling-saml/protocol/saml/descriptor \
|
||||
| awk 'BEGIN{RS="<[^>]*X509Certificate>|</[^>]*X509Certificate>"} NR==2{gsub(/[[:space:]]+/,""); print; exit}')
|
||||
{
|
||||
echo "-----BEGIN CERTIFICATE-----"
|
||||
echo "$CERT_BODY"
|
||||
echo "-----END CERTIFICATE-----"
|
||||
} > keycloak-saml-cert.pem
|
||||
test -s saml-private-key.key
|
||||
test -s saml-public-cert.crt
|
||||
test -s keycloak-saml-cert.pem
|
||||
echo "✓ SAML certs prepared"
|
||||
- name: Boot Stirling-PDF (frontend baked in, SAML env)
|
||||
env:
|
||||
SECURITY_ENABLELOGIN: "true"
|
||||
SECURITY_LOGINMETHOD: "all"
|
||||
SECURITY_SAML2_ENABLED: "true"
|
||||
SECURITY_SAML2_AUTOCREATEUSER: "true"
|
||||
SECURITY_SAML2_PROVIDER: "keycloak"
|
||||
SECURITY_SAML2_REGISTRATIONID: "keycloak"
|
||||
SECURITY_SAML2_IDP_ISSUER: "http://localhost:9080/realms/stirling-saml"
|
||||
SECURITY_SAML2_IDP_ENTITYID: "http://localhost:9080/realms/stirling-saml"
|
||||
SECURITY_SAML2_IDP_METADATAURI: "http://localhost:9080/realms/stirling-saml/protocol/saml/descriptor"
|
||||
SECURITY_SAML2_IDPSINGLELOGINURL: "http://localhost:9080/realms/stirling-saml/protocol/saml"
|
||||
SECURITY_SAML2_IDPSINGLELOGOUTURL: "http://localhost:9080/realms/stirling-saml/protocol/saml"
|
||||
SECURITY_SAML2_IDP_CERT: "${{ github.workspace }}/testing/compose/keycloak-saml-cert.pem"
|
||||
SECURITY_SAML2_PRIVATEKEY: "${{ github.workspace }}/testing/compose/saml-private-key.key"
|
||||
SECURITY_SAML2_SP_CERT: "${{ github.workspace }}/testing/compose/saml-public-cert.crt"
|
||||
# Realm registers the SP entity as the metadata URL — see
|
||||
# keycloak-realm-saml.json `clientId`. Match it here so Keycloak
|
||||
# accepts the AuthnRequest issuer.
|
||||
SECURITY_SAML2_SP_ENTITYID: "http://localhost:8080/saml2/service-provider-metadata/keycloak"
|
||||
SECURITY_SAML2_SP_ACS: "http://localhost:8080/login/saml2/sso/keycloak"
|
||||
SECURITY_SAML2_SP_SLS: "http://localhost:8080/logout/saml2/slo"
|
||||
run: |
|
||||
source /tmp/helpers.sh
|
||||
nohup ./gradlew :stirling-pdf:bootRun -PbuildWithFrontend=true > /tmp/backend.log 2>&1 &
|
||||
echo $! > /tmp/backend.pid
|
||||
wait_for_backend
|
||||
- name: Run enterprise SAML Playwright tests
|
||||
id: saml-tests
|
||||
run: task frontend:test:e2e -- --project=enterprise --grep "SAML"
|
||||
- name: Stop backend + tear down SAML Keycloak
|
||||
if: always()
|
||||
run: |
|
||||
source /tmp/helpers.sh
|
||||
stop_backend
|
||||
(cd testing/compose && docker compose -f docker-compose-keycloak-saml.yml down -v)
|
||||
|
||||
# ───────── License-gated feature tests (no IdP needed) ─────────
|
||||
- name: Wipe DB so InitialSecuritySetup re-runs with admin/adminadmin
|
||||
# Earlier phases (OAuth, SAML) create the default admin/stirling user.
|
||||
# InitialSecuritySetup only honours SECURITY_INITIALLOGIN_* when the
|
||||
# admin user doesn't already exist, so the persisted DB has to be
|
||||
# cleared between phases for the feature env vars to take effect.
|
||||
run: |
|
||||
rm -f app/core/configs/stirling-pdf-DB*.mv.db
|
||||
rm -rf app/core/configs/backup
|
||||
- name: Boot Stirling-PDF (frontend baked in, premium only)
|
||||
env:
|
||||
SECURITY_INITIALLOGIN_USERNAME: admin
|
||||
SECURITY_INITIALLOGIN_PASSWORD: adminadmin
|
||||
SECURITY_ENABLELOGIN: "true"
|
||||
SECURITY_LOGINMETHOD: "all"
|
||||
run: |
|
||||
source /tmp/helpers.sh
|
||||
nohup ./gradlew :stirling-pdf:bootRun -PbuildWithFrontend=true > /tmp/backend.log 2>&1 &
|
||||
echo $! > /tmp/backend.pid
|
||||
wait_for_backend
|
||||
- name: Run enterprise feature Playwright tests
|
||||
id: feature-tests
|
||||
run: task frontend:test:e2e -- --project=enterprise --grep "Enterprise license"
|
||||
- name: Print backend log on failure
|
||||
if: failure()
|
||||
run: |
|
||||
echo "::group::Enterprise backend log"
|
||||
tail -500 /tmp/backend.log || true
|
||||
echo "::endgroup::"
|
||||
- name: Stop backend (final)
|
||||
if: always()
|
||||
run: |
|
||||
source /tmp/helpers.sh
|
||||
stop_backend
|
||||
- name: Upload Playwright report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: playwright-report-enterprise-${{ github.run_id }}
|
||||
path: frontend/playwright-report/
|
||||
retention-days: 7
|
||||
@@ -340,6 +340,7 @@ jobs:
|
||||
retention-days: 3
|
||||
|
||||
playwright-e2e:
|
||||
# Backend-free stubbed suite — fast, no Spring Boot required.
|
||||
if: needs.files-changed.outputs.frontend == 'true'
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
@@ -360,13 +361,101 @@ jobs:
|
||||
uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0
|
||||
- name: Install Playwright (chromium only)
|
||||
run: task frontend:test:e2e:install -- chromium
|
||||
- name: Run E2E tests (chromium)
|
||||
run: task frontend:test:e2e -- --project=chromium
|
||||
- name: Run stubbed E2E tests (chromium)
|
||||
run: task frontend:test:e2e -- --project=stubbed --workers=3
|
||||
- name: Upload Playwright report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: playwright-report-pr-${{ github.run_id }}
|
||||
name: playwright-report-stubbed-${{ github.run_id }}
|
||||
path: frontend/playwright-report/
|
||||
retention-days: 7
|
||||
|
||||
playwright-e2e-live:
|
||||
# Live-backend suite — boots Spring Boot and runs auth + real tool round-trips.
|
||||
if: needs.files-changed.outputs.frontend == 'true'
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Set up JDK 25
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
java-version: "25"
|
||||
distribution: "temurin"
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: "npm"
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0
|
||||
- name: Install Playwright (chromium only)
|
||||
run: task frontend:test:e2e:install -- chromium
|
||||
- name: Start Spring Boot backend (background)
|
||||
env:
|
||||
# Suppress the analytics opt-in modal that fires on first admin login when
|
||||
# enableAnalytics is null (see Onboarding.tsx). The modal renders a Mantine
|
||||
# overlay that intercepts pointer events on every tool page until dismissed,
|
||||
# which causes every "click run button" assertion in the live suite to fail.
|
||||
SYSTEM_ENABLEANALYTICS: "false"
|
||||
# NOTE: SECURITY_INITIALLOGIN_USERNAME/PASSWORD are intentionally NOT set.
|
||||
# The live-setup project's bootstrap spec performs the real first-login
|
||||
# flow against the backend's default admin/stirling user, exercising the
|
||||
# forced-password-change UI and leaving the DB at admin/adminadmin for
|
||||
# the rest of the live suite. This is both real coverage of the first-
|
||||
# login flow and a stronger seed than env-var-driven user creation.
|
||||
run: |
|
||||
nohup ./gradlew :stirling-pdf:bootRun > /tmp/backend.log 2>&1 &
|
||||
echo $! > /tmp/backend.pid
|
||||
- name: Wait for backend to become ready
|
||||
run: |
|
||||
start=$SECONDS
|
||||
# 300 iterations × 2s = 10 minute ceiling
|
||||
for i in $(seq 1 300); do
|
||||
if curl -fsS http://localhost:8080/api/v1/info/status >/dev/null 2>&1; then
|
||||
echo "Backend up after $((SECONDS - start))s"
|
||||
exit 0
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
echo "Backend did not become ready in $((SECONDS - start))s"
|
||||
tail -200 /tmp/backend.log || true
|
||||
exit 1
|
||||
- name: Run live E2E tests (chromium)
|
||||
id: live-tests
|
||||
run: task frontend:test:e2e -- --project=live
|
||||
- name: Print backend log on failure
|
||||
if: failure() && steps.live-tests.conclusion == 'failure'
|
||||
run: |
|
||||
echo "::group::Spring Boot backend log (last 500 lines)"
|
||||
tail -500 /tmp/backend.log || echo "no backend log found"
|
||||
echo "::endgroup::"
|
||||
- name: Stop backend
|
||||
if: always()
|
||||
run: |
|
||||
if [ -f /tmp/backend.pid ]; then
|
||||
kill "$(cat /tmp/backend.pid)" 2>/dev/null || true
|
||||
fi
|
||||
- name: Upload backend log
|
||||
if: always()
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: backend-log-live-${{ github.run_id }}
|
||||
path: /tmp/backend.log
|
||||
retention-days: 7
|
||||
- name: Upload Playwright report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: playwright-report-live-${{ github.run_id }}
|
||||
path: frontend/playwright-report/
|
||||
retention-days: 7
|
||||
|
||||
|
||||
Reference in New Issue
Block a user