Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 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
e433592
chore: merge develop into feat/integration-tests
hhvrc Oct 28, 2026
4b2e8dd
chore: merge develop into feat/integration-tests
hhvrc Oct 28, 2026
eec3161
fix: regenerate lockfile against the pinned svelte-core
hhvrc Oct 28, 2026
45f209c
fix(ci): run the Cron host in the integration stack
hhvrc Oct 28, 2026
2d67595
chore: merge develop into feat/integration-tests
hhvrc Nov 12, 2026
cf10938
fix(e2e): update integration specs for the new token page and list la…
hhvrc Nov 12, 2026
2c0f91e
fix(e2e): address review feedback on the integration and e2e suites
hhvrc Nov 12, 2026
9521ffc
chore: merge develop into feat/integration-tests
hhvrc Nov 13, 2026
61ffa28
fix(test): mock paraglide in live-control test and drop TLS bypass fr…
hhvrc Nov 13, 2026
86a09b8
fix(ci): repair update-toolchain and bump Docker base image to Node 2…
hhvrc Nov 13, 2026
01c7359
chore: merge develop into feat/integration-tests
hhvrc Nov 13, 2026
eb071c1
Merge branch 'develop' into feat/integration-tests
hhvrc Nov 13, 2026
fe31e8e
fix(e2e): run the integration stack on Dragonfly and Postgres 18, sto…
hhvrc Nov 13, 2026
98a4d66
test: address remaining CodeRabbit findings on the e2e and integratio…
hhvrc Nov 13, 2026
ad7f13d
fix(e2e): delete the test account even when the test ended its session
hhvrc Nov 13, 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.
Resolved
retention-days: 7
54 changes: 34 additions & 20 deletions .github/workflows/update-toolchain.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,11 @@ jobs:
exit 1
fi

# Absolute latest Node release across all lines (not locked to the current
# major) — nodejs.org's index is already sorted newest-first.
new_node="$(curl -fsSL https://nodejs.org/dist/index.json | jq -r '.[0].version' | sed 's/^v//')"
# Latest stable Node release across all lines (not locked to the current major).
# nodejs.org's index is sorted newest-first but also lists prereleases such as
# v27.0.0-alpha.0, which have no Docker image, so only plain x.y.z versions count.
new_node="$(curl -fsSL https://nodejs.org/dist/index.json \
| jq -r '[.[].version | select(test("^v[0-9]+\\.[0-9]+\\.[0-9]+$"))][0]' | sed 's/^v//')"

new_pnpm="$(curl -fsSL https://registry.npmjs.org/pnpm | jq -r '."dist-tags".latest')"

Expand All @@ -50,17 +52,23 @@ jobs:
| grep -E "^${new_node}-alpine[0-9]+\.[0-9]+$" \
| sed -E 's/^.*-alpine//' \
| sort -V | tail -n1)"
new_tag="${new_node}-alpine${new_base}"
else
# Debian codenames aren't lexically ordered, so resolve via digest: the plain
# "-slim" tag is kept aliased upstream to whichever codename is current stable.
slim_digest="$(jq -r --arg t "${new_node}-slim" '.results[] | select(.name == $t) | .digest' <<<"$tags_json")"
new_base="$(jq -r --arg d "$slim_digest" --arg plain "${new_node}-slim" \
'.results[] | select(.name | endswith("-slim")) | select(.name != $plain) | select(.digest == $d) | .name' <<<"$tags_json" \
| sed -E 's/^.*-([a-z]+)-slim$/\1/' | head -n1)"
new_tag="${new_node}-${new_base}-slim"
fi

if [ -z "$new_node" ] || [ -z "$new_pnpm" ] || [ -z "$new_base" ]; then
echo "::error::Failed to resolve one or more versions (node=$new_node pnpm=$new_pnpm base=$new_base)"
# The Dockerfile pins the base image by digest as well as by tag; this is the
# multi-arch index digest, the same one `docker buildx imagetools inspect` reports.
new_digest="$(jq -r --arg t "$new_tag" '.results[] | select(.name == $t) | .digest' <<<"$tags_json")"

if [ -z "$new_node" ] || [ -z "$new_pnpm" ] || [ -z "$new_base" ] || [[ "$new_digest" != sha256:* ]]; then
echo "::error::Failed to resolve one or more versions (node=$new_node pnpm=$new_pnpm base=$new_base digest=$new_digest)"
exit 1
fi

Expand All @@ -69,31 +77,27 @@ jobs:
echo "pnpm=$new_pnpm"
echo "base_family=$base_family"
echo "base_version=$new_base"
echo "image=node:${new_tag}@${new_digest}"
} >> "$GITHUB_OUTPUT"

- name: Apply version updates
shell: bash
env:
NEW_NODE: ${{ steps.resolve.outputs.node }}
NEW_PNPM: ${{ steps.resolve.outputs.pnpm }}
BASE_FAMILY: ${{ steps.resolve.outputs.base_family }}
NEW_BASE: ${{ steps.resolve.outputs.base_version }}
NEW_IMAGE: ${{ steps.resolve.outputs.image }}
run: |
set -euo pipefail

sed -i "s/^ARG NODE_VERSION=.*/ARG NODE_VERSION=${NEW_NODE}/" Dockerfile
sed -i "s/^ARG PNPM_VERSION=.*/ARG PNPM_VERSION=${NEW_PNPM}/" Dockerfile
# The base image is a literal `FROM node:<tag>@<digest>` line, so Node version, base
# OS and digest all change together by replacing the image reference on it.
sed -i -E "s|^FROM node:[^[:space:]]+|FROM ${NEW_IMAGE}|" Dockerfile
if ! grep -qxF "FROM ${NEW_IMAGE}" Dockerfile; then
echo "::error::Dockerfile has no 'FROM node:...' line to update"
exit 1
fi

case "$BASE_FAMILY" in
alpine)
sed -i "s/^ARG ALPINE_VERSION=.*/ARG ALPINE_VERSION=${NEW_BASE}/" Dockerfile
;;
debian)
# Convention: if/when the Dockerfile switches to a Debian-slim base, name its
# codename ARG `DEBIAN_VERSION` (e.g. `trixie`) so this step keeps working as-is.
sed -i "s/^ARG DEBIAN_VERSION=.*/ARG DEBIAN_VERSION=${NEW_BASE}/" Dockerfile
;;
esac
sed -i "s/^ARG PNPM_VERSION=.*/ARG PNPM_VERSION=${NEW_PNPM}/" Dockerfile

echo "$NEW_NODE" > .nvmrc

Expand Down Expand Up @@ -154,6 +158,15 @@ jobs:
shell: bash
run: docker build --pull -f Dockerfile -t toolchain-update-check:latest .

# open-pr-from-diff pushes with --force-with-lease when the branch already exists on the
# remote (an open PR, or a branch left behind by a merged one). The checkout above only
# fetched the default branch, and a lease with no remote-tracking ref to compare against
# is rejected as "stale info", so make the ref known first.
- name: Fetch existing update branch
if: steps.diff.outputs.changed == 'true'
shell: bash
run: git fetch origin '+refs/heads/chore/update-toolchain-versions:refs/remotes/origin/chore/update-toolchain-versions' || true

- name: Open pull request
if: steps.diff.outputs.changed == 'true'
uses: OpenShock/actions/open-pr-from-diff@21ca2d511bca92cf24fb502fa302f82700822584 # v1.1.1
Expand All @@ -166,9 +179,10 @@ jobs:

| Component | New version |
|---|---|
| Node (`NODE_VERSION` / `.nvmrc` / `engines.node`) | `${{ steps.resolve.outputs.node }}` |
| Node (`.nvmrc` / `engines.node`) | `${{ steps.resolve.outputs.node }}` |
| pnpm (`PNPM_VERSION` / `packageManager`) | `${{ steps.resolve.outputs.pnpm }}` |
| Base OS (`${{ steps.resolve.outputs.base_family }}`) | `${{ steps.resolve.outputs.base_version }}` |
| Base image (Dockerfile `FROM`) | `${{ steps.resolve.outputs.image }}` |

`pnpm run check`, `pnpm test:unit`, and a container build were run
against these new versions before this PR was opened.
5 changes: 3 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# Define versions as build arguments for easy updates
# The pnpm version and the base image line below (tag and digest) are rewritten by
# .github/workflows/update-toolchain.yml - keep their shape if you edit them by hand.
ARG PNPM_VERSION=12.15.0

FROM node:26.8.2-trixie-slim@sha256:f7bb8247fdb16250dbec7fd0e24f091c6f5f0a29d256f3aef5816a7a369166b2
FROM node:26.9.0-trixie-slim@sha256:3a771f83944bb763050c23c0225c260638c4b7899e7a72485ef75e5e570499e5

ARG PNPM_VERSION

Expand Down
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
68 changes: 68 additions & 0 deletions e2e/e2e/lib/api-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Minimal API client used only for test teardown (activation, login and account deletion).
// Account creation happens through the browser UI in full E2E tests.
import { BACKEND_URL } from './env';

export type AuthCookies = string[];

// The targets this suite runs against do not enforce the captcha, so any value passes.
const TURNSTILE_RESPONSE = 'e2e-bypass';

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('; ');
}

/** Activate an account with the token from its activation mail. */
export async function activate(token: string): Promise<void> {
const res = await fetch(`${BACKEND_URL}/1/account/activate?token=${encodeURIComponent(token)}`, {
method: 'POST',
});
if (!res.ok) {
throw new Error(`activate failed: ${res.status} ${res.statusText} — ${await readBody(res)}`);
}
}

/** Log in and return the session cookies. */
export async function login(email: string, password: string): Promise<AuthCookies> {
const res = await fetch(`${BACKEND_URL}/2/account/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
usernameOrEmail: email,
password,
turnstileResponse: TURNSTILE_RESPONSE,
}),
});
if (!res.ok) {
throw new Error(`login failed: ${res.status} ${res.statusText} — ${await readBody(res)}`);
}
return res.headers.getSetCookie?.() ?? [];
}

/** 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 ?? '';
Loading
Loading