Repository navigation
MM-71467 Add Playwright E2E tests: contract-checked fake GitHub on every PR #1072
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
nang2049
wants to merge
2
commits into
master
Choose a base branch
from
MM-71467
base: master
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.
+26,210
−24
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
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,211 @@ | ||
| name: e2e | ||
| on: | ||
| schedule: | ||
| - cron: "0 3 * * *" | ||
| push: | ||
| branches: | ||
| - master | ||
| pull_request: | ||
| workflow_dispatch: | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| playwright: | ||
| name: playwright (${{ matrix.name }}) | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 30 | ||
| strategy: | ||
| fail-fast: false | ||
| matrix: | ||
| include: | ||
| - name: min-server | ||
| image: mattermost/mattermost-enterprise-edition:release-12.0 | ||
| - name: master | ||
| image: mattermostdevelopment/mattermost-enterprise-edition:master | ||
| env: | ||
| CI: true | ||
| MM_IMAGE: ${{ matrix.image }} | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| with: | ||
| persist-credentials: false | ||
|
|
||
| - uses: actions/setup-go@v5 | ||
| with: | ||
| go-version-file: go.mod | ||
| cache: true | ||
|
|
||
| - uses: actions/setup-node@v4 | ||
| with: | ||
| node-version-file: .nvmrc | ||
| cache: npm | ||
| cache-dependency-path: | | ||
| webapp/package-lock.json | ||
| e2e/integration/package-lock.json | ||
|
|
||
| - name: Build E2E plugin bundle | ||
| run: make e2e-dist | ||
| env: | ||
| # Builds only for the runner's OS/arch. | ||
| MM_SERVICESETTINGS_ENABLEDEVELOPER: true | ||
|
|
||
| - name: Install e2e dependencies | ||
| working-directory: e2e/integration | ||
| run: | | ||
| npm ci | ||
| npx playwright install --with-deps chromium | ||
|
|
||
| - name: Type-check e2e | ||
| working-directory: e2e/integration | ||
| run: npm run tsc | ||
|
|
||
| - name: Start Mattermost | ||
| working-directory: e2e/integration | ||
| run: npm run env:up | ||
|
|
||
| - name: Run Playwright | ||
| working-directory: e2e/integration | ||
| run: npm run test:ci | ||
|
|
||
| - name: Collect server logs | ||
| if: failure() | ||
| working-directory: e2e/integration | ||
| run: mkdir -p playwright-report && docker compose logs mattermost > playwright-report/mattermost.log 2>&1 || true | ||
|
|
||
| - uses: actions/upload-artifact@v4 | ||
| if: always() | ||
| with: | ||
| name: playwright-report-${{ matrix.name }} | ||
| path: e2e/integration/playwright-report | ||
| retention-days: 14 | ||
|
|
||
| # Non-blocking: a screenshot diff needs a person to judge whether the change is intended. | ||
| visual: | ||
| name: playwright visual | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 20 | ||
| continue-on-error: true | ||
| env: | ||
| CI: true | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| with: | ||
| persist-credentials: false | ||
|
|
||
| - uses: actions/setup-go@v5 | ||
| with: | ||
| go-version-file: go.mod | ||
| cache: true | ||
|
|
||
| - uses: actions/setup-node@v4 | ||
| with: | ||
| node-version-file: .nvmrc | ||
| cache: npm | ||
| cache-dependency-path: | | ||
| webapp/package-lock.json | ||
| e2e/integration/package-lock.json | ||
|
|
||
| - name: Build E2E plugin bundle | ||
| run: make e2e-dist | ||
| env: | ||
| MM_SERVICESETTINGS_ENABLEDEVELOPER: true | ||
|
|
||
| - name: Install e2e dependencies | ||
| working-directory: e2e/integration | ||
| run: npm ci | ||
|
|
||
| - name: Compare screenshots | ||
| working-directory: e2e/integration | ||
| run: npm run test:visual | ||
|
|
||
| - uses: actions/upload-artifact@v4 | ||
| if: failure() | ||
| with: | ||
| name: playwright-visual-diffs | ||
| path: | | ||
| e2e/integration/playwright-report | ||
| e2e/integration/test-results | ||
| retention-days: 14 | ||
|
|
||
| contract-drift: | ||
| name: fake-github contract drift | ||
| if: github.event_name == 'schedule' | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 5 | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| with: | ||
| persist-credentials: false | ||
|
|
||
| - uses: actions/setup-node@v4 | ||
| with: | ||
| node-version-file: .nvmrc | ||
| cache: npm | ||
| cache-dependency-path: e2e/integration/package-lock.json | ||
|
|
||
| - name: Install e2e dependencies | ||
| working-directory: e2e/integration | ||
| run: npm ci | ||
|
|
||
| - name: Compare committed specs with GitHub's published REST description and GraphQL schema | ||
| working-directory: e2e/integration | ||
| run: npm run contract:check | ||
|
|
||
| real-github: | ||
| name: real github.com journey | ||
| if: (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && vars.REAL_GITHUB_E2E_ENABLED == 'true' | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 30 | ||
| # The fixtures and hook on the test repository are shared between runs. | ||
| concurrency: | ||
| group: real-github-e2e | ||
| cancel-in-progress: false | ||
| env: | ||
| CI: true | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| with: | ||
| persist-credentials: false | ||
|
|
||
| - uses: actions/setup-go@v5 | ||
| with: | ||
| go-version-file: go.mod | ||
| cache: true | ||
|
|
||
| - uses: actions/setup-node@v4 | ||
| with: | ||
| node-version-file: .nvmrc | ||
| cache: npm | ||
| cache-dependency-path: | | ||
| webapp/package-lock.json | ||
| e2e/integration/package-lock.json | ||
|
|
||
| - name: Build plugin bundle | ||
| run: make dist | ||
| env: | ||
| MM_SERVICESETTINGS_ENABLEDEVELOPER: true | ||
|
|
||
| - name: Install e2e dependencies | ||
| working-directory: e2e/integration | ||
| run: | | ||
| npm ci | ||
| npx playwright install --with-deps chromium | ||
|
|
||
| - name: Start Mattermost | ||
| working-directory: e2e/integration | ||
| run: npm run env:up | ||
|
|
||
| # No report upload: it would carry real tokens and page content. | ||
| - name: Run journey | ||
| working-directory: e2e/integration | ||
| run: npm run test:real | ||
| env: | ||
| E2E_GITHUB_OAUTH_CLIENT_ID: ${{ secrets.E2E_GITHUB_OAUTH_CLIENT_ID }} | ||
| E2E_GITHUB_OAUTH_CLIENT_SECRET: ${{ secrets.E2E_GITHUB_OAUTH_CLIENT_SECRET }} | ||
| E2E_GITHUB_REPO: ${{ secrets.E2E_GITHUB_REPO }} | ||
| E2E_GITHUB_MACHINE_TOKEN: ${{ secrets.E2E_GITHUB_MACHINE_TOKEN }} | ||
| E2E_GITHUB_LOGIN_USERNAME: ${{ secrets.E2E_GITHUB_LOGIN_USERNAME }} | ||
| E2E_GITHUB_LOGIN_PASSWORD: ${{ secrets.E2E_GITHUB_LOGIN_PASSWORD }} | ||
| E2E_GITHUB_LOGIN_TOTP_SECRET: ${{ secrets.E2E_GITHUB_LOGIN_TOTP_SECRET }} | ||
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
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,22 @@ | ||
| # Copy to .env. docker compose and the tests both read it. | ||
| # Values for the real lane live in 1Password (search "GitHub plugin E2E"); never paste them into chats, issues or PRs. | ||
|
|
||
| # Stack (optional). Defaults avoid the ports other plugin E2E stacks use. | ||
| # MM_PORT=8069 | ||
| # PW_BROWSER_PORT=3336 | ||
| # MM_IMAGE=mattermostdevelopment/mattermost-enterprise-edition:master | ||
|
|
||
| # Real lane only (npm run test:real). The PR lane needs none of these. | ||
| # A classic OAuth app whose callback URL is http://127.0.0.1:${MM_PORT}/plugins/github/oauth/complete | ||
| E2E_GITHUB_OAUTH_CLIENT_ID= | ||
| E2E_GITHUB_OAUTH_CLIENT_SECRET= | ||
| # owner/name of a throwaway repository in a test organization. The machine account needs admin on it. | ||
| E2E_GITHUB_REPO= | ||
| # OAuth token (or classic personal access token) of the machine account. | ||
| # Scopes: repo, notifications, read:org, admin:org_hook, admin:repo_hook | ||
| E2E_GITHUB_MACHINE_TOKEN= | ||
|
|
||
| # Optional: the real sign-in test skips without these. The account needs 2FA with an authenticator app. | ||
| E2E_GITHUB_LOGIN_USERNAME= | ||
| E2E_GITHUB_LOGIN_PASSWORD= | ||
| E2E_GITHUB_LOGIN_TOTP_SECRET= |
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,6 @@ | ||
| node_modules | ||
| test-results | ||
| playwright-report | ||
| storage_state | ||
| .env | ||
| .auth |
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,59 @@ | ||
| # GitHub plugin Playwright tests | ||
|
|
||
| Two lanes. The fake lane runs on every PR, including forks, with no secrets. The real lane talks to github.com and runs nightly only when enabled. | ||
|
|
||
| The older suite in `e2e/playwright` (and `playwright.yml`) is unchanged and still runs against its own mock. | ||
|
|
||
| ```sh | ||
| make e2e-env-up # Postgres + Mattermost on :8069 | ||
| make e2e # builds the E2E bundle (make e2e-dist), installs the plugin, runs the suite | ||
| make e2e-env-down | ||
|
|
||
| make e2e-real # normal bundle, against github.com. Needs e2e/integration/.env | ||
| ``` | ||
|
|
||
| `make e2e-dist` bakes `http://fake-github:8093` into `testOAuthServerURL`, which now moves the OAuth, REST, GraphQL and upload base URLs together, so the github.com code path is what gets tested. The plugin logs a warning on activation. Never ship that bundle. | ||
|
|
||
| Overrides go in the shell or in `e2e/integration/.env` (copy `.env.example`; docker compose reads it too). Real values live in 1Password or GitHub Actions secrets. | ||
|
|
||
| | Variable | Default | Purpose | | ||
| |---|---|---| | ||
| | `MM_IMAGE` | `mattermostdevelopment/mattermost-enterprise-edition:master` | Server image | | ||
| | `MM_PORT` | `8069` | Host port; must match between `e2e-env-up` and `e2e` | | ||
| | `PW_BROWSER_PORT` | `3336` | Host port of the dockerized visual browser | | ||
| | `PW_HEADLESS` | `true` | `false` to watch the browser | | ||
|
|
||
| ## How GitHub is faked | ||
|
|
||
| - `fake-github/server.ts` runs inside the Playwright process on port 8093. Mattermost reaches it as `fake-github` through `extra_hosts`; Chromium through `--host-resolver-rules`. | ||
| - It is a classic OAuth app: tokens never expire and have no refresh token. Token errors come back as HTTP 200 with an `error` field, as form or JSON depending on `Accept`. Scopes from earlier grants are merged, and consent is skipped when an earlier grant already covers the request. | ||
| - Permissions follow GitHub: private repos need `repo`; repo hooks need repo admin plus `repo` or a `*:repo_hook` scope, otherwise 404; org hooks need org admin plus `admin:org_hook`; notifications need `notifications` or `repo`; issue labels, assignees and milestone are silently dropped without push access; SAML-enforced orgs answer 403 with `X-GitHub-SSO`. | ||
| - Creating an issue fires `issues.opened` to matching hooks and creating a hook sends `ping`, signed with `X-Hub-Signature` (HMAC-SHA1) and `X-Hub-Signature-256`. | ||
| - Tests drive it through `support/fake_github.ts` (`reset`, `createRepo`, `createItem`, `webhook`, `fault`, `sso`, `revoke`, `requests`, ...). | ||
| - Only `connect.spec.ts` drives the browser OAuth flow. Other specs connect with `seedUserInfo()` (`support/kv.ts`), which writes `<userID>_githubtoken` with the plugin's AES-CFB cipher. `installPlugin()` also seeds `mm34646_token_reset_done`, or the plugin resets every stored token a minute after it starts. | ||
| - Don't use `pw.initSetup()`: it resets server config and disables the plugin. | ||
|
|
||
| ## Keeping the fake honest | ||
|
|
||
| Every request and response is checked, and each test fails at teardown if anything broke the contract. | ||
|
|
||
| - **REST:** validated against [github/rest-api-description](https://github.com/github/rest-api-description) (`api.github.com.json`), trimmed to the operations in `fake-github/contract/operations.json`. A route that isn't listed fails the run. | ||
| - **GraphQL:** queries are validated and executed against GitHub's [public schema](https://docs.github.com/en/graphql/overview/public-schema), trimmed to the types the plugin uses. graphql-js rejects `... on Issue { state } ... on PullRequest { state }` as a field conflict while GitHub answers it, so that one message is ignored. The real lane checks GitHub still accepts it. | ||
| - **Webhooks:** validated against the `x-webhooks` schemas in the same REST description. These replaced [octokit/webhooks](https://github.com/octokit/webhooks), which is deprecated and no longer updated. | ||
| - Nightly, `npm run contract:check` fails if GitHub changed the committed specs. `npm run contract:update` refreshes them. | ||
|
|
||
| ## Real GitHub | ||
|
|
||
| `npm run test:real` (or `make e2e-real`) needs the variables in `.env.example`. Global setup: | ||
|
|
||
| - Creates long-lived fixtures once and reuses them: an `e2e-fixture` label, an open issue assigned to the machine account, and an open pull request from the `e2e-fixture` branch. It then polls search until both show up, because the plugin finds them through search and the index lags. | ||
| - Points a repository hook at a public placeholder URL with a fresh secret. GitHub refuses hooks to private addresses, so tests fetch the recorded delivery from the hook deliveries API and post it to the plugin. | ||
| - Seeds the machine account's token into KV, so most tests skip the browser login. | ||
|
|
||
| The sign-in test logs in with password and TOTP, then revokes its grant so the next run starts clean. GitHub skips its emailed device check for accounts with 2FA. | ||
|
|
||
| Writes go through `real-github/rate_limit.ts`, which stays under GitHub's content limits (80 a minute, 500 an hour) across runs on one machine. Retries are off so failures don't repeat writes. Traces, screenshots and video are off when `CI` is set. Don't put account, organization or repository names in this tree. | ||
|
|
||
| ## Screenshots | ||
|
|
||
| `npm run test:visual` compares `@visual` shots in a dockerized browser against a pinned server image, in its own throwaway stack, and does not fail the PR job. Update baselines with `npm run test:visual-update-snapshots` and commit the PNGs. |
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,62 @@ | ||
| name: github-plugin-e2e | ||
|
|
||
| services: | ||
| postgres: | ||
| image: postgres:16-alpine | ||
| environment: | ||
| POSTGRES_USER: mmuser | ||
| POSTGRES_PASSWORD: mostest | ||
| POSTGRES_DB: mattermost_test | ||
| healthcheck: | ||
| test: ["CMD-SHELL", "pg_isready -U mmuser -d mattermost_test"] | ||
| interval: 5s | ||
| timeout: 5s | ||
| retries: 10 | ||
| tmpfs: | ||
| - /var/lib/postgresql/data | ||
|
|
||
| mattermost: | ||
| image: ${MM_IMAGE:-mattermostdevelopment/mattermost-enterprise-edition:master} | ||
| # Server images are published for amd64 only; Apple Silicon runs them under emulation. | ||
| platform: linux/amd64 | ||
| depends_on: | ||
| postgres: | ||
| condition: service_healthy | ||
| ports: | ||
| - "127.0.0.1:${MM_PORT:-8069}:${MM_PORT:-8069}" | ||
| - "127.0.0.1:${PW_BROWSER_PORT:-3336}:3333" | ||
| extra_hosts: | ||
| - "fake-github:host-gateway" | ||
| environment: | ||
| MM_SQLSETTINGS_DRIVERNAME: postgres | ||
| MM_SQLSETTINGS_DATASOURCE: postgres://mmuser:mostest@postgres:5432/mattermost_test?sslmode=disable&connect_timeout=10 | ||
| MM_SERVICESETTINGS_SITEURL: http://127.0.0.1:${MM_PORT:-8069} | ||
| MM_SERVICESETTINGS_LISTENADDRESS: ":${MM_PORT:-8069}" | ||
| MM_SERVICESETTINGS_ENABLELOCALMODE: "true" | ||
| MM_PLUGINSETTINGS_ENABLE: "true" | ||
| MM_PLUGINSETTINGS_ENABLEUPLOADS: "true" | ||
| MM_PLUGINSETTINGS_AUTOMATICPREPACKAGEDPLUGINS: "false" | ||
| MM_TEAMSETTINGS_ENABLEOPENSERVER: "true" | ||
| MM_ANNOUNCEMENTSETTINGS_ADMINNOTICESENABLED: "false" | ||
| MM_ANNOUNCEMENTSETTINGS_USERNOTICESENABLED: "false" | ||
| MM_LOGSETTINGS_CONSOLELEVEL: INFO | ||
| healthcheck: | ||
| test: ["CMD", "/mattermost/bin/mmctl", "--local", "system", "status"] | ||
| interval: 5s | ||
| timeout: 5s | ||
| retries: 60 | ||
|
|
||
| # Shares Mattermost's network so the page origin matches the URL the tests use from the host. | ||
| visual-browser: | ||
| profiles: [visual] | ||
| image: mcr.microsoft.com/playwright:v${PW_VERSION:-1.64.1}-noble | ||
| network_mode: service:mattermost | ||
| # Chromium rasterizes text differently per CPU architecture; CI runners are amd64. | ||
| platform: linux/amd64 | ||
| init: true | ||
| command: npx -y playwright@${PW_VERSION:-1.64.1} run-server --port 3333 --host 0.0.0.0 | ||
| healthcheck: | ||
| test: ["CMD", "node", "-e", "require('net').connect(3333, '127.0.0.1').on('connect', () => process.exit(0)).on('error', () => process.exit(1))"] | ||
| interval: 2s | ||
| timeout: 2s | ||
| retries: 60 |
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.
Uh oh!
There was an error while loading. Please reload this page.