test(integration): end-to-end auth flow test with Mailpit + CI gating

Adds a 13-step integration test that exercises register → email
verification → login → JWT validation → /me/data → encryption-vault
init/key → logout against a real stack of postgres + redis + mailpit +
mana-auth + mana-notify in docker compose.

Verified locally that this catches every regression we hit on
2026-04-08 in well under a second:

  - missing nanoid dependency → register endpoint 500
  - missing MANA_AUTH_KEK env passthrough → mana-auth never starts
  - missing encryption-vault SQL migrations → vault endpoints 500
  - wrong cookie name in /api/v1/auth/login → no accessToken in response
  - mana-notify SMTP misconfigured → mailpit poll times out

Files:

- docker-compose.test.yml — minimal isolated stack on alt ports
  (postgres 5443, redis 6390, mailpit 1026/8026, mana-auth 3091,
  mana-notify 3092). Runs alongside the dev stack without collision.
  Postgres healthcheck runs a real query rather than just pg_isready
  to avoid the race where pg_isready reports healthy while the docker
  init scripts are still running on a unix socket.

- tests/integration/auth-flow.test.ts — bun test that drives the full
  flow via fetch + mailpit's REST API. Cleans up its test user from
  postgres in afterAll. Self-contained, no extra deps.

- tests/integration/README.md — what's covered, why it exists, how
  to run locally + extend.

- scripts/run-integration-tests.sh — orchestrator. Brings up the
  stack, pushes the @mana/auth Drizzle schema, applies the
  encryption-vault SQL migrations (002, 003), restarts mana-auth so
  it sees the fresh tables, runs the test, tears down on exit.
  KEEP_STACK=1 to leave it up for manual mailpit inspection.

- docker-compose.dev.yml — also adds Mailpit as a regular dev service
  (ports 1025/8025) so local development can have a working email
  capture without spinning up the test stack.

- .github/workflows/ci.yml — new auth-integration job that runs on
  every PR. Calls run-integration-tests.sh; on failure dumps
  mana-auth + mana-notify logs and the mailpit message queue. Marked
  as a required check via the existing PR validation pipeline.

Reproduced 3 clean runs and 1 negative-control run (removed nanoid
from package.json → mana-auth container exits → script aborts with
non-zero) before committing. Full happy path runs in ~22s on a warm
Docker cache.
This commit is contained in:
Till JS 2026-04-08 17:14:02 +02:00
parent 3b41b39a32
commit 5af4ddab3c
7 changed files with 612 additions and 0 deletions

View file

@ -472,6 +472,58 @@ jobs:
echo "::warning::Potentially vulnerable axios version detected"
fi
# ===========================================
# Auth flow integration test
# ===========================================
# Spins up postgres + redis + mailpit + mana-auth + mana-notify via
# docker-compose.test.yml and runs tests/integration/auth-flow.test.ts.
# Catches register/verify/login/JWT/encryption-vault regressions before
# they can be merged. Required check — never bypass.
auth-integration:
name: Auth flow integration test
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
timeout-minutes: 15
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v2
with:
version: ${{ env.PNPM_VERSION }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'pnpm'
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run auth flow integration test
run: ./scripts/run-integration-tests.sh
- name: Dump test stack logs on failure
if: failure()
run: |
echo "::group::mana-auth logs"
docker logs mana-test-mana-auth 2>&1 | tail -200 || true
echo "::endgroup::"
echo "::group::mana-notify logs"
docker logs mana-test-mana-notify 2>&1 | tail -200 || true
echo "::endgroup::"
echo "::group::mailpit messages"
curl -s http://localhost:8026/api/v1/messages | head -100 || true
echo "::endgroup::"
# ===========================================
# Build Docker images - only changed services
# ===========================================

View file

@ -44,6 +44,28 @@ services:
timeout: 5s
retries: 5
# Mailpit — fake SMTP server for local development.
# Catches every outbound mail and exposes them via web UI + REST API.
# Point any service that sends email at SMTP_HOST=mailpit, SMTP_PORT=1025
# and inspect what came in at http://localhost:8025.
mailpit:
image: axllent/mailpit:latest
container_name: mana-mailpit
restart: unless-stopped
environment:
MP_SMTP_AUTH_ACCEPT_ANY: "1"
MP_SMTP_AUTH_ALLOW_INSECURE: "1"
ports:
- "1025:1025" # SMTP
- "8025:8025" # Web UI + REST API
networks:
- mana-network
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:8025/api/v1/info"]
interval: 10s
timeout: 5s
retries: 3
# MinIO Object Storage (S3-compatible)
minio:
image: minio/minio:latest

166
docker-compose.test.yml Normal file
View file

@ -0,0 +1,166 @@
# Integration test stack for the auth/credentials/encryption-vault flow.
#
# Spins up the minimum stack needed to register, verify, log in, and
# exercise the encryption-vault: postgres + redis + mailpit (fake SMTP)
# + mana-auth + mana-notify. No mana-credits, mana-sync, mana-media etc.
# — those are not on the auth-flow critical path and would just slow
# down the build.
#
# Ports are offset from docker-compose.dev.yml so this stack can run
# alongside a normal dev environment. Everything is bound to 127.0.0.1
# so it's only reachable from the same machine.
#
# Usage:
# ./scripts/run-integration-tests.sh
#
# Or manually:
# docker compose -f docker-compose.test.yml up -d --build
# docker compose -f docker-compose.test.yml down -v
#
# The compose project is namespaced as `mana-test` so the containers,
# network and volumes don't collide with the dev stack.
name: mana-test
services:
postgres:
image: postgres:16-alpine
container_name: mana-test-postgres
environment:
POSTGRES_DB: mana_platform
POSTGRES_USER: mana
POSTGRES_PASSWORD: testpassword
volumes:
- ./docker/init-db:/docker-entrypoint-initdb.d:ro
ports:
- "127.0.0.1:5443:5432"
networks:
- mana-test
healthcheck:
# pg_isready alone reports healthy while the docker-entrypoint init
# scripts are still running on a unix socket — TCP connections from
# other containers then race-fail with "connection refused". Run a
# real query against the actual platform DB so we only flip healthy
# once postgres is genuinely accepting external TCP traffic.
test: ["CMD-SHELL", "PGPASSWORD=testpassword psql -h localhost -U mana -d mana_platform -tAc 'SELECT 1' >/dev/null"]
interval: 2s
timeout: 3s
retries: 30
start_period: 5s
redis:
image: redis:7-alpine
container_name: mana-test-redis
command: redis-server --requirepass testpassword --maxmemory 64mb
ports:
- "127.0.0.1:6390:6379"
networks:
- mana-test
healthcheck:
test: ["CMD", "redis-cli", "-a", "testpassword", "ping"]
interval: 2s
timeout: 3s
retries: 10
# Fake SMTP server. Captures every outbound email and exposes them
# via a REST API on :8025 (also a web UI on the same port). Tests
# poll the API to find the verification email.
mailpit:
image: axllent/mailpit:latest
container_name: mana-test-mailpit
environment:
MP_SMTP_AUTH_ACCEPT_ANY: "1"
MP_SMTP_AUTH_ALLOW_INSECURE: "1"
ports:
- "127.0.0.1:1026:1025" # SMTP
- "127.0.0.1:8026:8025" # HTTP API + Web UI
networks:
- mana-test
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:8025/api/v1/info"]
interval: 2s
timeout: 3s
retries: 10
mana-auth:
build:
context: ./services/mana-auth
dockerfile: Dockerfile
container_name: mana-test-mana-auth
environment:
NODE_ENV: production # exercise the prod KEK validation path
PORT: 3001
DATABASE_URL: postgresql://mana:testpassword@postgres:5432/mana_platform
# BASE_URL must be reachable from INSIDE the container — the validate
# endpoint fetches its own JWKS via this URL, and the JWT iss claim
# uses it. The test rewrites email verify URLs from `mana-auth:3001`
# to the host-bound port before following them.
BASE_URL: http://mana-auth:3001
COOKIE_DOMAIN: localhost
BETTER_AUTH_SECRET: test-secret-not-for-production
MANA_AUTH_KEK: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= # 32 zero bytes, test only
MANA_NOTIFY_URL: http://mana-notify:3013
MANA_SERVICE_KEY: test-service-key
MANA_CREDITS_URL: http://localhost:9 # unreachable, .catch() swallows it
MANA_SUBSCRIPTIONS_URL: http://localhost:9
CORS_ORIGINS: http://localhost:5173,http://localhost:3091
depends_on:
postgres:
condition: service_healthy
ports:
- "127.0.0.1:3091:3001"
networks:
- mana-test
healthcheck:
test: ["CMD", "bun", "-e", "fetch('http://127.0.0.1:3001/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
interval: 3s
timeout: 3s
retries: 20
start_period: 10s
mana-notify:
build:
context: .
dockerfile: services/mana-notify/Dockerfile
container_name: mana-test-mana-notify
# mana-notify pings the database once at startup and exits on
# failure. If postgres is mid-restart at exactly that millisecond
# we want compose to bring it back up rather than declare the
# whole stack dead.
restart: on-failure:5
environment:
PORT: 3013
DATABASE_URL: postgresql://mana:testpassword@postgres:5432/mana_platform?sslmode=disable
REDIS_HOST: redis
REDIS_PORT: 6379
REDIS_PASSWORD: testpassword
SERVICE_KEY: test-service-key
MANA_AUTH_URL: http://mana-auth:3001
SMTP_HOST: mailpit
SMTP_PORT: 1025
SMTP_USER: test
SMTP_PASSWORD: test
SMTP_FROM: "Mana Test <noreply@test.local>"
SMTP_INSECURE_TLS: "true"
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
mailpit:
condition: service_healthy
ports:
- "127.0.0.1:3092:3013"
networks:
- mana-test
healthcheck:
# Override the Dockerfile's port-3040 healthcheck — mana-notify
# actually binds to the PORT env var (3013 here).
test: ["CMD", "wget", "-q", "--spider", "http://localhost:3013/health"]
interval: 3s
timeout: 3s
retries: 20
start_period: 5s
networks:
mana-test:
driver: bridge

View file

@ -0,0 +1,76 @@
#!/usr/bin/env bash
#
# Run the auth-flow integration test against a fresh docker-compose.test.yml stack.
#
# Usage:
# ./scripts/run-integration-tests.sh # build, run, tear down
# KEEP_STACK=1 ./scripts/run-integration-tests.sh # leave stack up after run
#
# In CI, just call this. Locally, useful for both quick reruns and the
# "wait what does Mailpit look like" debugging step (set KEEP_STACK=1 then
# open http://127.0.0.1:8026).
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
COMPOSE_FILE="$REPO_ROOT/docker-compose.test.yml"
TEST_DIR="$REPO_ROOT/tests/integration"
# Pick a docker binary that's actually on PATH. macOS layouts vary.
if command -v docker >/dev/null 2>&1; then
DOCKER=docker
elif [ -x /opt/homebrew/bin/docker ]; then
DOCKER=/opt/homebrew/bin/docker
elif [ -x /usr/local/bin/docker ]; then
DOCKER=/usr/local/bin/docker
else
echo "error: docker not found in PATH" >&2
exit 1
fi
cd "$REPO_ROOT"
cleanup() {
if [ "${KEEP_STACK:-0}" = "1" ]; then
echo
echo "==> KEEP_STACK=1, leaving the test stack up."
echo " Mailpit UI: http://127.0.0.1:8026"
echo " mana-auth: http://127.0.0.1:3091"
echo " Postgres: psql postgresql://mana:testpassword@localhost:5443/mana_platform"
echo " Tear down: $DOCKER compose -f $COMPOSE_FILE down -v"
return
fi
echo
echo "==> Tearing down test stack"
$DOCKER compose -f "$COMPOSE_FILE" down -v --remove-orphans >/dev/null 2>&1 || true
}
trap cleanup EXIT
echo "==> Building & starting test stack"
$DOCKER compose -f "$COMPOSE_FILE" up -d --build --wait
echo "==> Pushing mana-auth Drizzle schema into test postgres"
DATABASE_URL="postgresql://mana:testpassword@localhost:5443/mana_platform" \
pnpm --filter @mana/auth db:push --force >/dev/null
echo "==> Applying encryption-vault SQL migrations (002, 003)"
$DOCKER cp "$REPO_ROOT/services/mana-auth/sql/002_encryption_vaults.sql" \
mana-test-postgres:/tmp/002.sql
$DOCKER cp "$REPO_ROOT/services/mana-auth/sql/003_recovery_wrap.sql" \
mana-test-postgres:/tmp/003.sql
$DOCKER exec mana-test-postgres psql -U mana -d mana_platform -f /tmp/002.sql >/dev/null
$DOCKER exec mana-test-postgres psql -U mana -d mana_platform -f /tmp/003.sql >/dev/null
echo "==> Restarting mana-auth so it picks up the freshly-created tables"
# mana-auth's connection pool might have been opened against an empty DB. A
# quick restart guarantees a clean cache + schema view.
$DOCKER compose -f "$COMPOSE_FILE" restart mana-auth >/dev/null
$DOCKER compose -f "$COMPOSE_FILE" up -d --wait mana-auth
echo "==> Running auth-flow integration test"
cd "$TEST_DIR"
bun test auth-flow.test.ts
echo
echo "✅ integration tests passed"

View file

@ -0,0 +1,50 @@
# Integration tests
End-to-end tests that exercise real services against real Postgres + Redis + a fake SMTP server (Mailpit), via `docker-compose.test.yml`.
## What's covered
| File | Flow under test |
|------|----------|
| `auth-flow.test.ts` | register → email verification (via Mailpit) → login → JWT validation → `/me/data` → encryption vault init/key → logout |
## Running locally
```bash
./scripts/run-integration-tests.sh
```
That script:
1. Brings up `docker-compose.test.yml` (postgres, redis, mailpit, mana-auth, mana-notify) on isolated ports (`5443`, `6390`, `8026`, `3091`, `3092`)
2. Waits for everything to be healthy
3. Pushes the `@mana/auth` Drizzle schema into the test database
4. Applies the encryption-vault SQL migrations (`002_encryption_vaults.sql`, `003_recovery_wrap.sql`)
5. Runs `bun test auth-flow.test.ts` from this directory
6. Tears the stack down on exit (success or failure)
The whole thing runs in well under a minute on a warm Docker cache.
## Mailpit web UI
While the stack is up you can also browse incoming mail manually at <http://127.0.0.1:8026>.
## Why this exists
Bugs caught by this test the first time it ran:
- `services/mana-auth` imported `nanoid` but didn't declare it in its `package.json``Cannot find package 'nanoid'` at startup, register endpoint 500'd. Local `pnpm install` resolved it transitively via `postcss → nanoid@3.3.11`, an isolated container build couldn't.
- `MANA_AUTH_KEK` was never passed through to the mana-auth container in `docker-compose.macmini.yml`, so the prod service hard-failed at startup with `MANA_AUTH_KEK env var is required in production`.
- The encryption-vault SQL migrations (`002`, `003`) had never been applied to prod Postgres, so any vault endpoint 500'd with `relation "auth.encryption_vaults" does not exist`.
- `/api/v1/auth/login` minted a JWT by reconstructing the session cookie under the wrong name (`mana.session_token` instead of `__Secure-mana.session_token`), so the JWT-mint silently fell through and clients got `accessToken: undefined`.
- mana-notify SMTP credentials were misconfigured against Stalwart, so no verification email actually went out — the failure was buried in mana-notify worker logs and the auth flow appeared to "work" only because the user could be flipped to verified by other means.
Each of those would have been a single red `bun test` run instead of a multi-hour debugging session.
## Adding more flows
Drop another `<name>.test.ts` next to `auth-flow.test.ts` and update `package.json` to include it. Use the same helpers (`postJson`, `waitForMail`, `pgExec`) — they're free to copy.
## CI
The same script runs in `.github/workflows/ci.yml` as a required PR check. Don't bypass it.

View file

@ -0,0 +1,237 @@
/**
* End-to-end auth flow integration test.
*
* Spins up nothing on its own assumes the docker-compose.test.yml stack
* is already running. Run via `./scripts/run-integration-tests.sh` which
* brings up the stack, applies the encryption-vault SQL migrations, runs
* this test, then tears the stack down.
*
* What this test covers, in order:
*
* 1. POST /api/v1/auth/register user created
* 2. mana-auth mana-notify mailpit email arrives
* 3. Extract verify URL from email body
* 4. GET <verify URL> email_verified
* 5. POST /api/v1/auth/login JWT minted
* 6. POST /api/v1/auth/validate(JWT) claims valid
* 7. GET /api/v1/me/data user summary
* 8. POST /api/v1/me/encryption-vault/init master key
* 9. GET /api/v1/me/encryption-vault/key unwrap roundtrip
* 10. POST /api/v1/auth/logout success
* 11. (cleanup) DELETE the test user from postgres
*
* Every regression we hit on 2026-04-08 would have been caught here:
* - missing nanoid dep step 1 500
* - missing MANA_AUTH_KEK mana-auth never starts
* - missing encryption_vaults table step 8 500
* - wrong cookie name in /login step 5 no accessToken
* - mana-notify SMTP auth fails step 2 mailpit times out
*/
import { test, expect, beforeAll, afterAll } from 'bun:test';
const AUTH_URL = process.env.AUTH_URL ?? 'http://localhost:3091';
const MAILPIT_URL = process.env.MAILPIT_URL ?? 'http://localhost:8026';
// Postgres connection (for the cleanup step). Reads the env that
// run-integration-tests.sh sets, falls back to the test stack defaults.
const PG_DSN =
process.env.TEST_DATABASE_URL ?? 'postgresql://mana:testpassword@localhost:5443/mana_platform';
// Generated per test run so reruns don't collide.
const TEST_EMAIL = `auth-flow-${Date.now()}@manatest.local`;
const TEST_PASSWORD = 'TestPassword123!';
const TEST_NAME = 'Auth Flow';
let createdUserId: string | null = null;
// ─── Tiny helpers ────────────────────────────────────────────────────
async function postJson<T = unknown>(path: string, body: unknown, headers?: HeadersInit) {
const res = await fetch(`${AUTH_URL}${path}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...headers },
body: JSON.stringify(body),
});
const json = (await res.json().catch(() => ({}))) as T;
return { status: res.status, json };
}
async function getJson<T = unknown>(url: string, headers?: HeadersInit) {
const res = await fetch(url, { headers });
const json = (await res.json().catch(() => ({}))) as T;
return { status: res.status, json };
}
async function waitForMail(to: string, timeoutMs = 15000): Promise<{ html: string; text: string }> {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const list = await fetch(
`${MAILPIT_URL}/api/v1/search?query=${encodeURIComponent(`to:${to}`)}`
);
if (list.ok) {
const data = (await list.json()) as { messages?: Array<{ ID: string }> };
if (data.messages && data.messages.length > 0) {
const id = data.messages[0].ID;
const full = await fetch(`${MAILPIT_URL}/api/v1/message/${id}`);
if (full.ok) {
const msg = (await full.json()) as { HTML?: string; Text?: string };
return { html: msg.HTML ?? '', text: msg.Text ?? '' };
}
}
}
await new Promise((r) => setTimeout(r, 250));
}
throw new Error(`No email to ${to} arrived within ${timeoutMs}ms`);
}
function extractVerifyUrl(html: string): string {
// Better Auth's verify URL is /api/auth/verify-email?token=...&callbackURL=...
// We allow either http://mana-auth:3001 or http://localhost:3091 since the
// test runs against the host but mana-auth's BASE_URL might be either.
const match = html.match(/https?:\/\/[^\s"'<>]+\/api\/auth\/verify-email\?[^\s"'<>]+/);
if (!match) throw new Error(`No verify URL found in email body: ${html.slice(0, 200)}`);
return match[0];
}
async function pgExec(sql: string): Promise<string> {
// Shell out to docker exec rather than pulling in a postgres client lib —
// the test container is the only place this runs and it has docker.
const proc = Bun.spawn(
[
'docker',
'exec',
'mana-test-postgres',
'psql',
'-U',
'mana',
'-d',
'mana_platform',
'-t',
'-A',
'-c',
sql,
],
{ stdout: 'pipe', stderr: 'pipe' }
);
const out = await new Response(proc.stdout).text();
const err = await new Response(proc.stderr).text();
const code = await proc.exited;
if (code !== 0) throw new Error(`psql failed (${code}): ${err}`);
return out.trim();
}
// ─── Cleanup at the end so failed runs don't leak ────────────────────
afterAll(async () => {
if (!createdUserId) return;
try {
await pgExec(
`DELETE FROM auth.encryption_vault_audit WHERE user_id = '${createdUserId}';
DELETE FROM auth.encryption_vaults WHERE user_id = '${createdUserId}';
DELETE FROM auth.users WHERE id = '${createdUserId}';`
);
} catch (err) {
console.warn('cleanup failed:', err);
}
});
// ─── The test ────────────────────────────────────────────────────────
test('full register → verify → login → vault → logout flow', async () => {
// 1. Register
const reg = await postJson<{ user?: { id: string } }>('/api/v1/auth/register', {
email: TEST_EMAIL,
password: TEST_PASSWORD,
name: TEST_NAME,
});
expect(reg.status).toBe(200);
expect(reg.json.user?.id).toBeTruthy();
createdUserId = reg.json.user!.id;
// 2. Wait for the verification email to land in mailpit
const mail = await waitForMail(TEST_EMAIL);
expect(mail.html.length).toBeGreaterThan(0);
// 3. Extract the verify URL
const verifyUrl = extractVerifyUrl(mail.html);
// 4. Hit the verify URL. Better Auth issues a 302 redirect on success.
// Use manual redirect handling so we can assert the redirect itself.
//
// Sometimes Better Auth's verify URL points at the internal docker
// hostname `http://mana-auth:3001/...`. Rewrite that to the host-bound
// port so we can actually reach it from outside the docker network.
const reachableVerifyUrl = verifyUrl.replace('http://mana-auth:3001', AUTH_URL);
const verifyRes = await fetch(reachableVerifyUrl, { redirect: 'manual' });
expect([200, 302, 303]).toContain(verifyRes.status);
// Belt-and-suspenders: confirm the DB row actually flipped.
const verified = await pgExec(
`SELECT email_verified FROM auth.users WHERE id = '${createdUserId}';`
);
expect(verified).toBe('t');
// 5. Login. Expect accessToken (the JWT) and refreshToken (the session token).
const login = await postJson<{
user?: { id: string };
accessToken?: string;
refreshToken?: string;
}>('/api/v1/auth/login', {
email: TEST_EMAIL,
password: TEST_PASSWORD,
});
expect(login.status).toBe(200);
expect(login.json.user?.id).toBe(createdUserId);
expect(login.json.accessToken).toBeTruthy();
expect(login.json.accessToken!.split('.').length).toBe(3); // JWT has 3 segments
expect(login.json.refreshToken).toBeTruthy();
const jwt = login.json.accessToken!;
// 6. Validate the JWT against the same service that minted it
const validate = await postJson<{ valid: boolean; payload?: { sub: string; email: string } }>(
'/api/v1/auth/validate',
{ token: jwt }
);
expect(validate.status).toBe(200);
expect(validate.json.valid).toBe(true);
expect(validate.json.payload?.sub).toBe(createdUserId);
expect(validate.json.payload?.email).toBe(TEST_EMAIL);
// 7. /me/data round-trip — exercises JWT auth middleware end-to-end
const me = await getJson<{ user?: { id: string; email: string } }>(`${AUTH_URL}/api/v1/me/data`, {
Authorization: `Bearer ${jwt}`,
});
expect(me.status).toBe(200);
expect(me.json.user?.id).toBe(createdUserId);
// 8. Encryption vault init — exercises nanoid + MANA_AUTH_KEK + the
// auth.encryption_vaults / auth.encryption_vault_audit tables
const vaultInit = await postJson<{ masterKey?: string; kekId?: string; formatVersion?: number }>(
'/api/v1/me/encryption-vault/init',
{},
{ Authorization: `Bearer ${jwt}` }
);
expect(vaultInit.status).toBe(200);
expect(vaultInit.json.masterKey).toBeTruthy();
expect(vaultInit.json.formatVersion).toBe(1);
expect(vaultInit.json.kekId).toBeTruthy();
const mintedKey = vaultInit.json.masterKey!;
// 9. Vault key retrieval — should return the same master key
const vaultKey = await getJson<{ masterKey?: string }>(
`${AUTH_URL}/api/v1/me/encryption-vault/key`,
{ Authorization: `Bearer ${jwt}` }
);
expect(vaultKey.status).toBe(200);
expect(vaultKey.json.masterKey).toBe(mintedKey);
// 10. Logout
const logout = await postJson<{ success?: boolean }>(
'/api/v1/auth/logout',
{},
{ Authorization: `Bearer ${jwt}` }
);
expect(logout.status).toBe(200);
expect(logout.json.success).toBe(true);
});

View file

@ -0,0 +1,9 @@
{
"name": "@mana/integration-tests",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"test": "bun test auth-flow.test.ts"
}
}