Repository navigation
feat: Add integration tests #195
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
hhvrc
wants to merge
29
commits into
develop
Choose a base branch
from
feat/integration-tests
base: develop
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
Show all changes
29 commits
Select commit
Hold shift + click to select a range
3fc329a
Add more integration tests
hhvrc 393b2a8
more stuff
hhvrc 6012cf0
more stuff, idk
hhvrc b6c5dd0
fix pnpm check
hhvrc 5a7af59
additional changes
hhvrc e1c2451
lint and fix pr comments
hhvrc 380ddb0
ci stuff
hhvrc 89c0a42
more fixes
hhvrc c684dd1
more stuff, idk
hhvrc 65cd7e6
giving up
hhvrc cd5bf45
Merge branch 'develop' into feat/integration-tests
hhvrc 10b7a3e
Fix tests
hhvrc b940ee3
yeah
hhvrc a1886a4
Update dev-integration.mjs
hhvrc 71a3557
fix comments
hhvrc a70073b
Refactor integration stack into standalone script
hhvrc b024bda
Merge develop into feat/integration-tests
hhvrc cf13db1
Merge develop into feat/integration-tests
hhvrc 769107b
test(integration): serve integration stack over plain HTTP
hhvrc 317bb98
Merge branch 'develop' into feat/integration-tests
hhvrc 9b920f6
test: adapt unit tests to current API surface and svelte-core split
hhvrc be25347
style: fix lint failures inherited from develop
hhvrc 5ae87e8
chore: merge develop into integration tests branch
hhvrc 0b251bf
chore: merge develop into feat/integration-tests
hhvrc beac190
fix(ci): check out submodules in the integration workflow
hhvrc a77bb97
fix(ci): address review findings on the integration workflow
hhvrc ccd2b43
feat(env): allow a plain-HTTP loopback API URL for the integration stack
hhvrc a6f240b
test(integration): make the flagged specs able to fail
hhvrc b27767b
fix(ci): do not run mkcert in integration mode
hhvrc 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,7 @@ | ||
| PUBLIC_SITE_URL=http://localhost:5173 | ||
| PUBLIC_SITE_SHORT_URL=http://localhost:5173 | ||
| # Browser & SSR API calls go through the Vite proxy (same origin — no CORS) | ||
| PUBLIC_BACKEND_API_URL=http://localhost:5173 | ||
| PUBLIC_GATEWAY_CSP_WILDCARD=http://localhost:* | ||
| # Vite dev server proxies /1/* and /2/* to this target (plain HTTP, no cert trust) | ||
| VITE_API_PROXY_TARGET=http://localhost:5001 |
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,92 @@ | ||
| on: | ||
| push: | ||
| branches: | ||
| - develop | ||
| - master | ||
| pull_request: | ||
| branches: | ||
| - develop | ||
| types: [opened, reopened, synchronize] | ||
| workflow_dispatch: | ||
|
|
||
| name: ci-integration | ||
|
|
||
| jobs: | ||
| test: | ||
| if: | | ||
| github.event_name == 'push' || | ||
| github.event_name == 'workflow_dispatch' || | ||
| ( | ||
| github.event_name == 'pull_request' && | ||
| github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name | ||
| ) | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 30 | ||
| permissions: | ||
| contents: read | ||
| packages: read | ||
|
|
||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | ||
| with: | ||
| submodules: recursive | ||
| # The steps below run dependency code (pnpm install, Playwright, the | ||
| # test stack); none of them needs an authenticated git remote. | ||
| persist-credentials: false | ||
|
|
||
| - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 | ||
| name: Install pnpm | ||
| with: | ||
| run_install: false | ||
|
|
||
| - name: Install Node.js | ||
| uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | ||
| with: | ||
| node-version-file: .nvmrc | ||
| cache: 'pnpm' | ||
|
|
||
| - name: Install dependencies | ||
| shell: bash | ||
| run: pnpm install --frozen-lockfile --strict-peer-dependencies | ||
|
|
||
| - name: Get Playwright version | ||
| id: playwright-version | ||
| shell: bash | ||
| run: echo "version=$(pnpm list @playwright/test --json | jq -r '.[0].devDependencies["@playwright/test"].version')" >> $GITHUB_OUTPUT | ||
|
|
||
| - name: Cache Playwright browsers | ||
| uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 | ||
| id: playwright-cache | ||
| with: | ||
| path: ~/.cache/ms-playwright | ||
| key: playwright-${{ steps.playwright-version.outputs.version }} | ||
|
|
||
| - name: Install Playwright browsers | ||
| if: steps.playwright-cache.outputs.cache-hit != 'true' | ||
| shell: bash | ||
| run: pnpm exec playwright install --with-deps chromium | ||
|
|
||
| - name: Install Playwright system deps | ||
| if: steps.playwright-cache.outputs.cache-hit == 'true' | ||
| shell: bash | ||
| run: pnpm exec playwright install-deps chromium | ||
|
|
||
| - name: Log in to GHCR | ||
| uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 | ||
| with: | ||
| registry: ghcr.io | ||
| username: ${{ github.actor }} | ||
| password: ${{ secrets.GITHUB_TOKEN }} | ||
|
|
||
| - name: Run integration tests | ||
| shell: bash | ||
| run: pnpm test:integration | ||
|
|
||
| - name: Upload Playwright report | ||
| if: failure() | ||
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 | ||
| with: | ||
| name: playwright-report | ||
| path: playwright-report/ | ||
| retention-days: 7 | ||
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,38 @@ | ||
| // Minimal API client used only for test teardown (account deletion). | ||
| // Account creation happens through the browser UI in full E2E tests. | ||
| import { BACKEND_URL } from './env'; | ||
|
|
||
| export type AuthCookies = string[]; | ||
|
|
||
| async function readBody(res: Response): Promise<string> { | ||
| try { | ||
| return await res.text(); | ||
| } catch { | ||
| return ''; | ||
| } | ||
| } | ||
|
|
||
| function joinCookieHeader(cookies: AuthCookies): string { | ||
| return cookies.map((c) => c.split(';', 1)[0]).join('; '); | ||
| } | ||
|
|
||
| /** Delete the account that owns the given auth cookies. */ | ||
| export async function deleteSelf(cookies: AuthCookies): Promise<void> { | ||
| const res = await fetch(`${BACKEND_URL}/1/account`, { | ||
| method: 'DELETE', | ||
| headers: { Cookie: joinCookieHeader(cookies) }, | ||
| }); | ||
| if (!res.ok && res.status !== 404) { | ||
| throw new Error( | ||
| `account-delete failed: ${res.status} ${res.statusText} — ${await readBody(res)}` | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| /** Log out (invalidates the session server-side). */ | ||
| export async function logout(cookies: AuthCookies): Promise<void> { | ||
| await fetch(`${BACKEND_URL}/1/account/logout`, { | ||
| method: 'POST', | ||
| headers: { Cookie: joinCookieHeader(cookies) }, | ||
| }); | ||
| } |
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,13 @@ | ||
| // Full E2E tests. | ||
| // Local dev: TEST_FRONTEND_URL=https://local.openshock.dev (pnpm dev) | ||
| // Staging: TEST_FRONTEND_URL=https://next.openshock.dev (no captcha enforcement) | ||
| export const FRONTEND_URL = process.env.TEST_FRONTEND_URL ?? 'https://next.openshock.dev'; | ||
| export const BACKEND_URL = process.env.TEST_BACKEND_URL ?? 'https://api.openshock.dev'; | ||
|
|
||
| // MailPit captures test emails and exposes an HTTP API for reading them. | ||
| // In local dev, MailPit is included in Dev/docker-compose.yml and listens on | ||
| // localhost:8025 (HTTP UI) and localhost:1025 (SMTP). | ||
| // Set TEST_MAILPIT_URL to enable email-verification tests; leave empty to skip them. | ||
| // Local default: http://localhost:8025 | ||
| // CI/staging: set explicitly or leave empty to skip | ||
| export const MAILPIT_URL = process.env.TEST_MAILPIT_URL ?? ''; |
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,84 @@ | ||
| // MailPit API client for reading test emails. | ||
| // MailPit docs: https://mailpit.axllent.org/docs/api-v1/ | ||
|
|
||
| export interface MailpitSummary { | ||
| ID: string; | ||
| Subject: string; | ||
| To: Array<{ Address: string; Name: string }>; | ||
| Date: string; | ||
| } | ||
|
|
||
| export interface MailpitMessage extends MailpitSummary { | ||
| HTML: string; | ||
| Text: string; | ||
| } | ||
|
|
||
| async function fetchJson<T>(url: string): Promise<T> { | ||
| const res = await fetch(url); | ||
| if (!res.ok) throw new Error(`MailPit request failed: ${res.status} ${res.statusText}`); | ||
| return res.json() as Promise<T>; | ||
| } | ||
|
|
||
| /** List the most recent messages addressed to `to`. Returns newest-first. */ | ||
| async function listMessagesTo(mailpitUrl: string, to: string): Promise<MailpitSummary[]> { | ||
| const query = encodeURIComponent(`to:"${to}"`); | ||
| const data = await fetchJson<{ messages: MailpitSummary[] | null }>( | ||
| `${mailpitUrl}/api/v1/messages?query=${query}&limit=10` | ||
| ); | ||
| return data.messages ?? []; | ||
| } | ||
|
|
||
| /** Fetch the full body of a message. */ | ||
| async function getMessage(mailpitUrl: string, id: string): Promise<MailpitMessage> { | ||
| return fetchJson<MailpitMessage>(`${mailpitUrl}/api/v1/message/${id}`); | ||
| } | ||
|
|
||
| /** Delete a message (cleanup). */ | ||
| export async function deleteMessage(mailpitUrl: string, id: string): Promise<void> { | ||
| await fetch(`${mailpitUrl}/api/v1/message/${id}`, { method: 'DELETE' }); | ||
| } | ||
|
|
||
| const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); | ||
|
|
||
| /** Poll MailPit until an email to `to` arrives, then return its full content. */ | ||
| export async function waitForEmailTo( | ||
| mailpitUrl: string, | ||
| to: string, | ||
| { timeoutMs = 30_000, pollMs = 2_000 } = {} | ||
| ): Promise<MailpitMessage> { | ||
| const deadline = Date.now() + timeoutMs; | ||
| while (Date.now() < deadline) { | ||
| const summaries = await listMessagesTo(mailpitUrl, to); | ||
| if (summaries.length > 0) { | ||
| return getMessage(mailpitUrl, summaries[0].ID); | ||
| } | ||
| await sleep(pollMs); | ||
| } | ||
| throw new Error( | ||
| `No email to "${to}" found in MailPit (${mailpitUrl}) within ${timeoutMs / 1000}s` | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Extract the first URL from the email that matches `pattern` and rewrite its | ||
| * origin to `targetOrigin` so navigation works against the test frontend. | ||
| */ | ||
| export function extractAndRewriteLink( | ||
| msg: MailpitMessage, | ||
| pattern: RegExp, | ||
| targetOrigin: string | ||
| ): string | null { | ||
| const body = msg.HTML || msg.Text; | ||
| const match = body.match(pattern); | ||
| if (!match) return null; | ||
| try { | ||
| const original = new URL(match[0].replace(/&/g, '&')); | ||
| const target = new URL(targetOrigin); | ||
| original.protocol = target.protocol; | ||
| original.hostname = target.hostname; | ||
| original.port = target.port; | ||
| return original.toString(); | ||
| } catch { | ||
| return null; | ||
| } | ||
| } |
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.