Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
18 changes: 18 additions & 0 deletions .claude/docs/quality-assurance.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
# Quality Assurance

## CI Pipeline

Tests run automatically via GitHub Actions on every PR and push to `main` (`.github/workflows/ci.yml`).

| Job | Runner | What it runs |
|-----|--------|-------------|
| `backend-tests` | ubuntu-latest + Postgres 17 | `swift test` in `tmbr-web/` — all AppTests and CoreTests |
| `api-kit-tests` | macos-15 + Xcode 16.2 | `swift test --package-path api-kit` |
| `e2e-tests` | ubuntu-latest + Postgres 17 | Playwright against a live server booted with `--env testing` |

**E2E session in CI:** The server boots in `.testing` mode, which registers `POST /__test/login`. The workflow POSTs `{}` to create a fresh test user, extracts the `vapor_session` cookie from the response, and passes it to Playwright via `E2E_SESSION_COOKIE`. No Apple Sign In credentials are needed.

**No secrets required** — the server gracefully skips Apple JWT setup when `SIWA_APP_ID` is absent.

When a job fails: check the Actions tab on the PR. For E2E failures, download the `playwright-report` artifact (uploaded on failure) for screenshots and traces.

---

## Scope

| Package | Priority | Current state |
Expand Down
4 changes: 2 additions & 2 deletions .claude/incidents/001-note-textarea-layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ Not properly fixed. A reminder was added to CLAUDE.md noting the exact inclusion
## Prevention

- [ ] **Long-term fix**: Replace Leaf with a Swift templating library that supports proper component scoping (e.g., `swift-html` or a structured macro system). See `leaf-limitations.md` for known Leaf issues.
- [ ] **E2E test**: Add a Playwright test that loads every page containing a note section and verifies the access checkbox is: (a) visible, (b) inside the form, (c) functional. This would catch regressions immediately if the template is incorrectly included on a new page.
- [x] **E2E test**: `catalogue-editor.spec.ts` — "note access checkbox: visible, inside notes section, and interactive". Verifies (a) visible, (b) inside `#notes-section` bounds, (c) toggleable. Run for `/albums/new`; extend to other note-bearing pages as they're added.
- [ ] **Process**: Any PR that adds a note section to a new page must include a screenshot or Playwright test verifying correct layout.

## Tests added

None yet — blocked on Playwright E2E setup (tracked in QA plan).
`tmbr-web/Tests/E2E/catalogue-editor.spec.ts` — "note access checkbox: visible, inside notes section, and interactive"
204 changes: 204 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
# ─── Path change detection (PRs only) ────────────────────────────────────────
changes:
name: Detect Changes
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
outputs:
backend: ${{ steps.filter.outputs.backend }}
api-kit: ${{ steps.filter.outputs.api-kit }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
backend:
- 'tmbr-web/**'
- '.github/workflows/**'
api-kit:
- 'api-kit/**'
- '.github/workflows/**'

# ─── Swift unit & integration tests (tmbr-web) ───────────────────────────────
backend-tests:
name: Backend Tests
needs: [changes]
if: |
always() && (
github.event_name == 'push' ||
needs.changes.outputs.backend == 'true'
)
runs-on: ubuntu-latest
services:
postgres:
image: postgres:17
env:
POSTGRES_DB: tmbr_test
POSTGRES_USER: tmbr
POSTGRES_PASSWORD: password
options: >-
--health-cmd "pg_isready -U tmbr"
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
env:
DATABASE_HOST: localhost
DATABASE_PORT: 5432
DATABASE_NAME: tmbr_test
DATABASE_USERNAME: tmbr
DATABASE_PASSWORD: password
steps:
- uses: actions/checkout@v4
# Release mode avoids a Linux compiler crash (SIGSEGV in CreateCast) that
# affects debug builds of complex cross-module generic expressions.
- uses: swift-actions/setup-swift@v2
with:
swift-version: '6.1'
- name: Cache Swift build
uses: actions/cache@v4
with:
path: tmbr-web/.build
key: ${{ runner.os }}-swift-backend-${{ hashFiles('tmbr-web/Package.resolved') }}
restore-keys: ${{ runner.os }}-swift-backend-
- name: Build
working-directory: tmbr-web
run: swift build -c release
- name: Test
working-directory: tmbr-web
run: swift test -c release

# ─── Swift unit tests (api-kit, requires Xcode toolchain) ────────────────────
api-kit-tests:
name: API Kit Tests
needs: [changes]
if: |
always() && (
github.event_name == 'push' ||
needs.changes.outputs.api-kit == 'true'
)
runs-on: macos-15
steps:
- uses: actions/checkout@v4
- name: Select Xcode 16.2
run: sudo xcode-select -s /Applications/Xcode_16.2.app
- name: Cache Swift build
uses: actions/cache@v4
with:
path: api-kit/.build
key: ${{ runner.os }}-swift-apikit-${{ hashFiles('api-kit/Package.resolved') }}
restore-keys: ${{ runner.os }}-swift-apikit-
- name: Test
run: DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer swift test --package-path api-kit

# ─── Playwright E2E tests against a live server ───────────────────────────────
e2e-tests:
name: E2E Tests
needs: [changes]
if: |
always() && (
github.event_name == 'push' ||
needs.changes.outputs.backend == 'true'
)
runs-on: ubuntu-latest
services:
postgres:
image: postgres:17
env:
POSTGRES_DB: tmbr_test
POSTGRES_USER: tmbr
POSTGRES_PASSWORD: password
options: >-
--health-cmd "pg_isready -U tmbr"
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
env:
DATABASE_HOST: localhost
DATABASE_PORT: 5432
DATABASE_NAME: tmbr_test
DATABASE_USERNAME: tmbr
DATABASE_PASSWORD: password
BASE_URL: http://localhost:8080
steps:
- uses: actions/checkout@v4
- uses: swift-actions/setup-swift@v2
with:
swift-version: '6.1'
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Cache Swift build
uses: actions/cache@v4
with:
path: tmbr-web/.build
key: ${{ runner.os }}-swift-e2e-${{ hashFiles('tmbr-web/Package.resolved') }}
restore-keys: ${{ runner.os }}-swift-e2e-
- name: Cache node modules
uses: actions/cache@v4
with:
path: tmbr-web/node_modules
key: ${{ runner.os }}-node-${{ hashFiles('tmbr-web/package-lock.json') }}
restore-keys: ${{ runner.os }}-node-
- name: Install npm dependencies
working-directory: tmbr-web
run: npm ci
- name: Install Playwright browsers
working-directory: tmbr-web
run: npx playwright install --with-deps chromium
- name: Build server
working-directory: tmbr-web
# -DVAPOR_TESTING enables /__test/login in release builds (test routes are
# guarded by #if DEBUG || VAPOR_TESTING and never compiled into production).
run: swift build -c release -Xswiftc -DVAPOR_TESTING
- name: Run database migrations
working-directory: tmbr-web
run: .build/release/Backend migrate --yes
- name: Start server
working-directory: tmbr-web
run: .build/release/Backend serve --env testing &
- name: Wait for server to be ready
run: |
for i in $(seq 1 30); do
curl -sf http://localhost:8080/signin -o /dev/null && break || true
sleep 2
done
- name: Create test session
id: session
run: |
SESSION=$(curl -si -X POST http://localhost:8080/__test/login \
-H "Content-Type: application/json" \
-d '{}' \
| grep -i 'set-cookie:' \
| grep vapor_session \
| sed 's/.*vapor_session=//;s/;.*//' \
| tr -d '\r')
echo "cookie=$SESSION" >> "$GITHUB_OUTPUT"
- name: Run Playwright tests
working-directory: tmbr-web
env:
E2E_SESSION_COOKIE: ${{ steps.session.outputs.cookie }}
run: npx playwright test
- name: Upload Playwright report
uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report
path: tmbr-web/playwright-report/
retention-days: 7
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Tests/LinuxMain.swift
.vscode
.bash_history
.cache/
node_modules/

# API Docs Generation
generate-package-api-docs.swift
Expand Down
2 changes: 1 addition & 1 deletion api-kit/Sources/ApiKit/AuthProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ public actor AuthProvider {

private var refreshTask: Task<String, Error>?

private let refresher: (@Sendable () async throws -> String)?
private let refresher: @Sendable () async throws -> String

public init(
token: String? = nil,
Expand Down
10 changes: 6 additions & 4 deletions tmbr-web/Sources/App/Modules/Notifications/Notifications.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,12 @@ struct Notifications: Module {
}

func configure(_ app: Vapor.Application) async throws {
await app.storage.setWithAsyncShutdown(
ServiceKey.self,
to: try NotificationService(app: app)
)
do {
let service = try NotificationService(app: app)
await app.storage.setWithAsyncShutdown(ServiceKey.self, to: service)
} catch {
app.logger.warning("Push notifications disabled: \(error)")
}
try await app.permissions.add(scope: permissions)
try await app.commands.add(collection: commands)
}
Expand Down
38 changes: 38 additions & 0 deletions tmbr-web/Sources/App/Testing/TestRoutes.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#if DEBUG || VAPOR_TESTING
import Vapor
import AuthKit
import Fluent

// MARK: - Test-only routes

/// Registers routes available only in the .testing environment, so the live server can issue
/// sessions without Apple Sign In. Compiled out of release builds entirely.
func registerTestOnlyRoutes(_ app: Application) {
guard app.environment == .testing else { return }
// POST /__test/login
// Body: { "userID": Int } — logs in an existing user
// Body: {} or absent userID — creates a fresh test user and logs them in
app.post("__test", "login") { req async throws -> Response in
struct LoginPayload: Content { let userID: Int? }
let payload = try req.content.decode(LoginPayload.self)
let user: User
if let userID = payload.userID,
let found = try await User.find(userID, on: req.db) {
user = found
} else {
let newUser = User(
appleID: "ci-\(UUID().uuidString)",
email: "ci@test.example",
firstName: "CI",
lastName: "Test",
role: .standard
)
try await newUser.save(on: req.db)
user = newUser
}
req.auth.login(user)
req.session.authenticate(user)
return Response(status: .ok)
}
}
#endif
5 changes: 5 additions & 0 deletions tmbr-web/Sources/App/configure.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,9 @@ func configure(_ app: Application) async throws {
)
try await registry.configure(app)
try await registry.boot(app.routes)
#if DEBUG || VAPOR_TESTING
if app.environment == .testing {
registerTestOnlyRoutes(app)
}
#endif
}
Loading
Loading