diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d61c830b1..d8c24bf34 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 # =========================================== diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 7525d975d..ffbc52c7e 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -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 diff --git a/docker-compose.test.yml b/docker-compose.test.yml new file mode 100644 index 000000000..6301c8024 --- /dev/null +++ b/docker-compose.test.yml @@ -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 " + 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 diff --git a/scripts/run-integration-tests.sh b/scripts/run-integration-tests.sh new file mode 100755 index 000000000..c42aea5c8 --- /dev/null +++ b/scripts/run-integration-tests.sh @@ -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" diff --git a/tests/integration/README.md b/tests/integration/README.md new file mode 100644 index 000000000..41e1be3bd --- /dev/null +++ b/tests/integration/README.md @@ -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 . + +## 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 `.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. diff --git a/tests/integration/auth-flow.test.ts b/tests/integration/auth-flow.test.ts new file mode 100644 index 000000000..ee73a4311 --- /dev/null +++ b/tests/integration/auth-flow.test.ts @@ -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 → 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(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(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 { + // 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); +}); diff --git a/tests/integration/package.json b/tests/integration/package.json new file mode 100644 index 000000000..7c1b18a10 --- /dev/null +++ b/tests/integration/package.json @@ -0,0 +1,9 @@ +{ + "name": "@mana/integration-tests", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "test": "bun test auth-flow.test.ts" + } +}