Skip to content
Open
Show file tree
Hide file tree
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 Apr 28, 2026
393b2a8
more stuff
hhvrc Apr 28, 2026
6012cf0
more stuff, idk
hhvrc Apr 28, 2026
b6c5dd0
fix pnpm check
hhvrc Apr 29, 2026
5a7af59
additional changes
hhvrc Apr 29, 2026
e1c2451
lint and fix pr comments
hhvrc Apr 29, 2026
380ddb0
ci stuff
hhvrc Apr 29, 2026
89c0a42
more fixes
hhvrc Apr 29, 2026
c684dd1
more stuff, idk
hhvrc Apr 29, 2026
65cd7e6
giving up
hhvrc Apr 29, 2026
cd5bf45
Merge branch 'develop' into feat/integration-tests
hhvrc May 6, 2026
10b7a3e
Fix tests
hhvrc May 6, 2026
b940ee3
yeah
hhvrc May 6, 2026
a1886a4
Update dev-integration.mjs
hhvrc May 6, 2026
71a3557
fix comments
hhvrc May 6, 2026
a70073b
Refactor integration stack into standalone script
hhvrc Jun 18, 2026
b024bda
Merge develop into feat/integration-tests
hhvrc Jun 19, 2026
cf13db1
Merge develop into feat/integration-tests
hhvrc Jun 23, 2026
769107b
test(integration): serve integration stack over plain HTTP
hhvrc Jun 23, 2026
317bb98
Merge branch 'develop' into feat/integration-tests
hhvrc Aug 4, 2026
9b920f6
test: adapt unit tests to current API surface and svelte-core split
hhvrc Aug 4, 2026
be25347
style: fix lint failures inherited from develop
hhvrc Aug 4, 2026
5ae87e8
chore: merge develop into integration tests branch
hhvrc Oct 14, 2026
0b251bf
chore: merge develop into feat/integration-tests
hhvrc Oct 14, 2026
beac190
fix(ci): check out submodules in the integration workflow
hhvrc Oct 14, 2026
a77bb97
fix(ci): address review findings on the integration workflow
hhvrc Oct 14, 2026
ccd2b43
feat(env): allow a plain-HTTP loopback API URL for the integration stack
hhvrc Oct 14, 2026
a6f240b
test(integration): make the flagged specs able to fail
hhvrc Oct 14, 2026
b27767b
fix(ci): do not run mkcert in integration mode
hhvrc Oct 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .env.integration
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
92 changes: 92 additions & 0 deletions .github/workflows/ci-integration.yml
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/
Comment thread
coderabbitai[bot] marked this conversation as resolved.
retention-days: 7
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@
<tr>
<td>master</td>
<td><a href="https://github.com/OpenShock/Frontend/actions/workflows/ci-build.yml"><img src="https://github.com/OpenShock/Frontend/actions/workflows/ci-build.yml/badge.svg?branch=master" alt="Build Status" /></a></td>
<td><a href="https://github.com/OpenShock/Frontend/actions/workflows/ci-integration.yml"><img src="https://github.com/OpenShock/Frontend/actions/workflows/ci-integration.yml/badge.svg?branch=master" alt="Integration Tests" /></a></td>
<td><a href="https://github.com/OpenShock/Frontend/actions/workflows/codeql.yml"><img src="https://github.com/OpenShock/Frontend/actions/workflows/codeql.yml/badge.svg?branch=master" alt="CodeQL Status" /></a></td>
</tr>
<tr>
<td>develop</td>
<td><a href="https://github.com/OpenShock/Frontend/actions/workflows/ci-build.yml"><img src="https://github.com/OpenShock/Frontend/actions/workflows/ci-build.yml/badge.svg?branch=develop" alt="Build Status" /></a></td>
<td><a href="https://github.com/OpenShock/Frontend/actions/workflows/ci-integration.yml"><img src="https://github.com/OpenShock/Frontend/actions/workflows/ci-integration.yml/badge.svg?branch=develop" alt="Integration Tests" /></a></td>
<td><a href="https://github.com/OpenShock/Frontend/actions/workflows/codeql.yml"><img src="https://github.com/OpenShock/Frontend/actions/workflows/codeql.yml/badge.svg?branch=develop" alt="CodeQL Status" /></a></td>
</tr>
</table>
Expand Down
38 changes: 38 additions & 0 deletions e2e/e2e/lib/api-client.ts
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) },
});
}
13 changes: 13 additions & 0 deletions e2e/e2e/lib/env.ts
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 ?? '';
84 changes: 84 additions & 0 deletions e2e/e2e/lib/mailpit.ts
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(/&amp;/g, '&'));
const target = new URL(targetOrigin);
original.protocol = target.protocol;
original.hostname = target.hostname;
original.port = target.port;
return original.toString();
} catch {
return null;
}
}
Loading
Loading