-
Notifications
You must be signed in to change notification settings - Fork 21
Add E2E coverage for sample app sign-in/out, token debug, and profile management #77
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
janithjay
wants to merge
1
commit into
thunder-id:main
Choose a base branch
from
janithjay:test/quickstart-e2e-coverage
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,291 @@ | ||
| name: 🌙 E2E Nightly (latest ThunderID) | ||
|
|
||
| # Runs the same E2E suite as pr-builder.yml's `e2e` job, against the same always-latest ThunderID | ||
| # release. The two differ only in trigger: this one runs on a schedule regardless of PR activity, | ||
| # so a ThunderID release that breaks something surfaces even on a day with no relevant PR open — | ||
| # the per-PR job (see pr-builder.yml) is label-gated and opt-in, not scheduled. | ||
|
|
||
| on: | ||
| schedule: | ||
| - cron: "30 18 * * *" # 18:30 UTC daily | ||
| workflow_dispatch: | ||
|
|
||
| env: | ||
| NODE_VERSION: "lts/*" | ||
|
|
||
| jobs: | ||
| e2e-nightly: | ||
| name: 🌙 E2E (sample apps, latest ThunderID) | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 40 | ||
| # Least privilege: this job never pushes, comments, or writes to the repo — only the default | ||
| # GITHUB_TOKEN's read access is needed for checkout. | ||
| permissions: | ||
| contents: read | ||
| steps: | ||
| - name: 📥 Checkout Code | ||
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | ||
| with: | ||
| persist-credentials: false | ||
|
|
||
| - name: ⚙️ Set up Node.js | ||
| uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 | ||
| with: | ||
| node-version: ${{ env.NODE_VERSION }} | ||
|
|
||
| - name: 📦 Set up pnpm | ||
| uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0 | ||
| with: | ||
| version: latest | ||
| run_install: false | ||
|
|
||
| - name: 🗄️ Cache pnpm store | ||
| uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 | ||
| with: | ||
| path: ~/.local/share/pnpm/store | ||
| key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }} | ||
| restore-keys: | | ||
| ${{ runner.os }}-pnpm- | ||
|
|
||
| - name: 📦 Install Dependencies | ||
| run: pnpm install --frozen-lockfile | ||
|
|
||
| - name: 🔨 Build SDK packages | ||
| run: pnpm build | ||
|
|
||
| - name: 🚀 Install, set up, and start ThunderID (latest, via npx) | ||
| id: install | ||
| working-directory: tests/e2e | ||
| # See the identically-named step in pr-builder.yml's `e2e` job: `npx thunderid` resolves | ||
| # and downloads the latest release itself and runs setup.sh non-interactively, then we | ||
| # start it ourselves with `start.sh` since npx thunderid's own background process doesn't | ||
| # outlive its own (expected) REPL/TTY failure on this runner. | ||
| run: | | ||
| # setsid detaches into a new session with no controlling terminal — see the identical | ||
| # note in pr-builder.yml's `e2e` job / tests/e2e/run-e2e.sh. | ||
| THUNDERID_ADMIN_USERNAME=admin THUNDERID_ADMIN_PASSWORD=admin \ | ||
| setsid npx --yes thunderid --verbose < /dev/null || true | ||
|
|
||
| DIST_HOME=$(find "$PWD/thunderid" -maxdepth 1 -type d -name 'v*' | head -1) | ||
| if [ -z "$DIST_HOME" ]; then | ||
| echo "ERROR: npx thunderid did not produce an installed ThunderID release" && exit 1 | ||
| fi | ||
| echo "dist_home=$DIST_HOME" >> "$GITHUB_OUTPUT" | ||
|
|
||
| chmod +x "$DIST_HOME/start.sh" | ||
| (cd "$DIST_HOME" && setsid ./start.sh &) | ||
| for i in $(seq 1 60); do | ||
| curl -skf https://localhost:8090/health/liveness && exit 0 | ||
| sleep 2 | ||
| done | ||
| echo "ERROR: ThunderID server did not become ready" && exit 1 | ||
|
|
||
| - name: 🔑 Obtain admin token | ||
| id: admin-token | ||
| # See the identically-named step in pr-builder.yml's `e2e` job — the same bash sequence, no | ||
| # reference to thunder-id/thunderid's branch or a pinned commit. | ||
| run: | | ||
| REDIRECT_URI="https://localhost:8090/console" | ||
| CODE_VERIFIER=$(openssl rand -hex 32 | cut -c1-43) | ||
| CODE_CHALLENGE=$(printf '%s' "$CODE_VERIFIER" | openssl dgst -sha256 -binary | openssl base64 -A | tr '+/' '-_' | tr -d '=') | ||
|
|
||
| HEADERS_FILE=$(mktemp) | ||
| curl -sk -o /dev/null -D "$HEADERS_FILE" \ | ||
| -G "https://localhost:8090/oauth2/authorize" \ | ||
| --data-urlencode "client_id=CONSOLE" \ | ||
| --data-urlencode "redirect_uri=$REDIRECT_URI" \ | ||
| --data-urlencode "scope=system" \ | ||
| --data-urlencode "resource=https://localhost:8090/mcp" \ | ||
| --data-urlencode "response_type=code" \ | ||
| --data-urlencode "code_challenge=$CODE_CHALLENGE" \ | ||
| --data-urlencode "code_challenge_method=S256" | ||
|
|
||
| LOCATION=$(grep -i "^location:" "$HEADERS_FILE" | tr -d '\r' | sed 's/^[Ll]ocation: //') | ||
| rm -f "$HEADERS_FILE" | ||
| AUTH_ID=$(echo "$LOCATION" | sed -n 's/.*[?&]authId=\([^&]*\).*/\1/p') | ||
| EXEC_ID=$(echo "$LOCATION" | sed -n 's/.*[?&]executionId=\([^&]*\).*/\1/p') | ||
| [ -n "$AUTH_ID" ] && [ -n "$EXEC_ID" ] || { echo "ERROR: Failed to parse authId/executionId from authorize redirect."; exit 1; } | ||
|
|
||
| PROMPT_RESP=$(curl -sk -X POST "https://localhost:8090/flow/execute" \ | ||
| -H "Content-Type: application/json" -d "{\"executionId\": \"$EXEC_ID\"}") | ||
| CHALLENGE_TOKEN=$(echo "$PROMPT_RESP" | jq -r '.challengeToken // empty') | ||
| [ -n "$CHALLENGE_TOKEN" ] || { echo "ERROR: Flow execution did not return a challenge token: $PROMPT_RESP"; exit 1; } | ||
|
|
||
| FLOW_RESP=$(curl -sk -X POST "https://localhost:8090/flow/execute" \ | ||
| -H "Content-Type: application/json" \ | ||
| -d "$(jq -n --arg executionId "$EXEC_ID" --arg challengeToken "$CHALLENGE_TOKEN" \ | ||
| --arg username "admin" --arg password "admin" --arg action "action_001" \ | ||
| '{executionId: $executionId, challengeToken: $challengeToken, inputs: {username: $username, password: $password}, action: $action}')") | ||
| ASSERTION=$(echo "$FLOW_RESP" | jq -r '.assertion // empty') | ||
| [ -n "$ASSERTION" ] || { echo "ERROR: Admin authentication failed: $FLOW_RESP"; exit 1; } | ||
|
|
||
| CALLBACK_RESP=$(curl -sk -X POST "https://localhost:8090/oauth2/auth/callback" \ | ||
| -H "Content-Type: application/json" -d "{\"authId\": \"$AUTH_ID\", \"assertion\": \"$ASSERTION\"}") | ||
| AUTH_CODE=$(echo "$CALLBACK_RESP" | jq -r '.redirect_uri // empty' | sed 's/.*[?&]code=\([^&]*\).*/\1/') | ||
| [ -n "$AUTH_CODE" ] || { echo "ERROR: OAuth2 callback did not return an authorization code: $CALLBACK_RESP"; exit 1; } | ||
|
|
||
| TOKEN_RESP=$(curl -sk -X POST "https://localhost:8090/oauth2/token" \ | ||
| -H "Content-Type: application/x-www-form-urlencoded" \ | ||
| --data-urlencode "grant_type=authorization_code" \ | ||
| --data-urlencode "code=$AUTH_CODE" \ | ||
| --data-urlencode "redirect_uri=$REDIRECT_URI" \ | ||
| --data-urlencode "client_id=CONSOLE" \ | ||
| --data-urlencode "resource=https://localhost:8090/mcp" \ | ||
| --data-urlencode "code_verifier=$CODE_VERIFIER") | ||
| TOKEN=$(echo "$TOKEN_RESP" | jq -r '.access_token // empty') | ||
| [ -n "$TOKEN" ] || { echo "ERROR: Failed to obtain admin access token: $TOKEN_RESP"; exit 1; } | ||
| echo "::add-mask::$TOKEN" | ||
| echo "token=$TOKEN" >> "$GITHUB_OUTPUT" | ||
|
|
||
| - name: 📝 Import sample app OAuth2 clients | ||
| # See the identically-named step in pr-builder.yml's `e2e` job — the same bash sequence, no | ||
| # reference to thunder-id/thunderid's branch or a pinned commit. | ||
| env: | ||
| ADMIN_TOKEN: ${{ steps.admin-token.outputs.token }} | ||
| run: | | ||
| CONTENT=$(jq -Rs . < tests/e2e/thunderid-config/sample-apps.yaml) | ||
| VARIABLES=$(jq -n '{ | ||
| BROWSER_CLIENT_ID: "JS_SDK_E2E_BROWSER", BROWSER_REDIRECT_URIS: ["http://localhost:5173"], | ||
| REACT_CLIENT_ID: "JS_SDK_E2E_REACT", REACT_REDIRECT_URIS: ["http://localhost:5174"], | ||
| VUE_CLIENT_ID: "JS_SDK_E2E_VUE", VUE_REDIRECT_URIS: ["http://localhost:5175"], | ||
| NEXTJS_CLIENT_ID: "JS_SDK_E2E_NEXTJS", NEXTJS_CLIENT_SECRET: "e2e-nextjs-secret", NEXTJS_REDIRECT_URIS: ["http://localhost:3001"], | ||
| NUXT_CLIENT_ID: "JS_SDK_E2E_NUXT", NUXT_CLIENT_SECRET: "e2e-nuxt-secret", NUXT_REDIRECT_URIS: ["http://localhost:3002/api/auth/callback"], NUXT_POST_LOGOUT_REDIRECT_URIS: ["http://localhost:3002/"], | ||
| EXPRESS_CLIENT_ID: "JS_SDK_E2E_EXPRESS", EXPRESS_CLIENT_SECRET: "e2e-express-secret", EXPRESS_REDIRECT_URIS: ["http://localhost:3000/login"], EXPRESS_POST_LOGOUT_REDIRECT_URIS: ["http://localhost:3000/logout"], | ||
| NODE_CLIENT_ID: "JS_SDK_E2E_NODE", NODE_CLIENT_SECRET: "e2e-node-secret", | ||
| ALL_ORIGINS: ["http://localhost:5173", "http://localhost:5174", "http://localhost:5175", "http://localhost:3000", "http://localhost:3001", "http://localhost:3002"] | ||
| }') | ||
| RESPONSE_FILE=$(mktemp) | ||
| HTTP_STATUS=$(curl -sk -o "$RESPONSE_FILE" -w "%{http_code}" \ | ||
| -X POST "https://localhost:8090/import" \ | ||
| -H "Content-Type: application/json" \ | ||
| -H "Authorization: Bearer $ADMIN_TOKEN" \ | ||
| -d "{\"content\": $CONTENT, \"variables\": $VARIABLES, \"options\": {\"upsert\": true}}") | ||
|
|
||
| if [ "$HTTP_STATUS" != "200" ]; then | ||
| echo "ERROR: import returned HTTP $HTTP_STATUS:"; cat "$RESPONSE_FILE"; echo ""; exit 1 | ||
| fi | ||
| FAILED_COUNT=$(jq -r '.summary.failed // 0' "$RESPONSE_FILE") | ||
| if [ "$FAILED_COUNT" != "0" ]; then | ||
| echo "ERROR: import had $FAILED_COUNT failed resource(s):"; cat "$RESPONSE_FILE"; echo ""; exit 1 | ||
| fi | ||
| rm -f "$RESPONSE_FILE" | ||
|
|
||
| - name: 🔄 Restart the server with security enabled | ||
| working-directory: ${{ steps.install.outputs.dist_home }} | ||
| run: | | ||
| # `pkill -f start.sh` only matches the wrapper script, not the server binary it launches | ||
| # in the foreground (a separate PID that SIGKILL to the wrapper never reaches) — kill by | ||
| # port instead, and confirm the old server is actually gone before starting a new one, | ||
| # since starting a replacement while the old process still holds the port would let the | ||
| # old (pre-import) server keep answering liveness checks and silently pass this step. | ||
| lsof -ti tcp:8090 | xargs -r kill -9 | ||
| for i in $(seq 1 30); do | ||
| curl -skf https://localhost:8090/health/liveness > /dev/null 2>&1 || break | ||
| sleep 1 | ||
| done | ||
| if curl -skf https://localhost:8090/health/liveness > /dev/null 2>&1; then | ||
| echo "ERROR: previous ThunderID server is still reachable after kill" && exit 1 | ||
| fi | ||
|
|
||
| setsid ./start.sh & | ||
| for i in $(seq 1 60); do | ||
| curl -skf https://localhost:8090/health/liveness && exit 0 | ||
| sleep 2 | ||
| done | ||
| echo "ERROR: ThunderID server did not restart" && exit 1 | ||
|
|
||
| - name: 📝 Write sample app .env files | ||
| # See the identically-named step in pr-builder.yml's `e2e` job for why this writes each | ||
| # .env directly instead of using the apps' own `prepare-dev.cjs --flow=redirect`. | ||
| run: | | ||
| NEXTJS_SECRET=$(openssl rand -base64 32) | ||
| NUXT_SECRET=$(openssl rand -base64 32) | ||
|
|
||
| write_env() { | ||
| local file=$1; shift | ||
| printf '%s\n' "$@" > "$file" | ||
| } | ||
|
|
||
| write_env samples/browser/quickstart/.env \ | ||
| "VITE_THUNDERID_BASE_URL=https://localhost:8090" \ | ||
| "VITE_THUNDERID_CLIENT_ID=JS_SDK_E2E_BROWSER" | ||
|
|
||
| write_env samples/react/quickstart/.env \ | ||
| "VITE_THUNDERID_BASE_URL=https://localhost:8090" \ | ||
| "VITE_THUNDERID_CLIENT_ID=JS_SDK_E2E_REACT" | ||
|
|
||
| write_env samples/vue/quickstart/.env \ | ||
| "VITE_THUNDERID_BASE_URL=https://localhost:8090" \ | ||
| "VITE_THUNDERID_CLIENT_ID=JS_SDK_E2E_VUE" | ||
|
|
||
| write_env samples/express/quickstart/.env \ | ||
| "THUNDERID_BASE_URL=https://localhost:8090" \ | ||
| "THUNDERID_CLIENT_ID=JS_SDK_E2E_EXPRESS" \ | ||
| "THUNDERID_CLIENT_SECRET=e2e-express-secret" \ | ||
| "NODE_TLS_REJECT_UNAUTHORIZED=0" | ||
|
|
||
| write_env samples/nextjs/quickstart/.env \ | ||
| "NEXT_PUBLIC_THUNDERID_BASE_URL=https://localhost:8090" \ | ||
| "NEXT_PUBLIC_THUNDERID_CLIENT_ID=JS_SDK_E2E_NEXTJS" \ | ||
| "THUNDERID_CLIENT_SECRET=e2e-nextjs-secret" \ | ||
| "THUNDERID_SECRET=${NEXTJS_SECRET}" \ | ||
| "NODE_TLS_REJECT_UNAUTHORIZED=0" | ||
|
|
||
| write_env samples/nuxt/quickstart/.env \ | ||
| "NUXT_PUBLIC_THUNDERID_BASE_URL=https://localhost:8090" \ | ||
| "NUXT_PUBLIC_THUNDERID_CLIENT_ID=JS_SDK_E2E_NUXT" \ | ||
| "THUNDERID_CLIENT_SECRET=e2e-nuxt-secret" \ | ||
| "THUNDERID_SESSION_SECRET=${NUXT_SECRET}" \ | ||
| "NODE_TLS_REJECT_UNAUTHORIZED=0" | ||
|
|
||
| - name: 🚀 Start sample apps | ||
| run: | | ||
| ( cd samples/browser/quickstart && pnpm exec vite --port 5173 & ) | ||
| ( cd samples/react/quickstart && pnpm exec vite --port 5174 & ) | ||
| ( cd samples/vue/quickstart && pnpm exec vite --port 5175 & ) | ||
| ( cd samples/nextjs/quickstart && pnpm exec next dev -p 3001 & ) | ||
| ( cd samples/nuxt/quickstart && pnpm exec nuxt dev --port 3002 & ) | ||
| ( cd samples/express/quickstart && pnpm exec node --env-file-if-exists=.env index.mjs & ) | ||
| for url in http://localhost:5173 http://localhost:5174 http://localhost:5175 http://localhost:3000 http://localhost:3001 http://localhost:3002; do | ||
| ready=0 | ||
| for i in $(seq 1 60); do | ||
| curl -sf "$url" > /dev/null && ready=1 && break | ||
| sleep 2 | ||
| done | ||
| if [ "$ready" != "1" ]; then | ||
| echo "ERROR: $url did not become ready after 120s" && exit 1 | ||
| fi | ||
| done | ||
|
|
||
| - name: 🎭 Install Playwright browsers | ||
| working-directory: tests/e2e | ||
| run: npx playwright install --with-deps chromium | ||
|
|
||
| - name: 🧪 Run E2E tests | ||
| working-directory: tests/e2e | ||
| env: | ||
| SERVER_URL: https://localhost:8090 | ||
| ADMIN_USERNAME: admin | ||
| ADMIN_PASSWORD: admin | ||
| ADMIN_TOKEN: ${{ steps.admin-token.outputs.token }} | ||
| TEST_USER_USERNAME: e2e-test-user | ||
| TEST_USER_PASSWORD: E2ePassword@123 | ||
| BROWSER_APP_URL: http://localhost:5173 | ||
| REACT_APP_URL: http://localhost:5174 | ||
| VUE_APP_URL: http://localhost:5175 | ||
| NEXTJS_APP_URL: http://localhost:3001 | ||
| NUXT_APP_URL: http://localhost:3002 | ||
| EXPRESS_APP_URL: http://localhost:3000 | ||
| THUNDERID_BASE_URL: https://localhost:8090 | ||
| THUNDERID_CLIENT_ID: JS_SDK_E2E_NODE | ||
| THUNDERID_CLIENT_SECRET: e2e-node-secret | ||
| NODE_TLS_REJECT_UNAUTHORIZED: "0" | ||
| run: npx playwright test | ||
|
|
||
| - name: 📤 Upload Playwright report | ||
| if: ${{ !cancelled() }} | ||
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | ||
| with: | ||
| name: playwright-report-nightly | ||
| path: tests/e2e/playwright-report/ | ||
| retention-days: 30 | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since some of these steps are duplicated in 2 workflows, we can extract them out to a action and re-use.