From a2da9a6096e3f8db56cc5e8d6ed92f403fa3ed00 Mon Sep 17 00:00:00 2001 From: Brandon Corfman Date: Sat, 18 Jul 2026 21:39:47 -0400 Subject: [PATCH] Railway deploy split for dev and prod --- .github/workflows/deploy-backend-railway.yml | 96 ++++++++++ .github/workflows/deploy-frontend-pages.yml | 10 +- ...y-stable-dev-deployment-plan-2026-07-18.md | 171 ++++++++++++++++++ docs/.vitepress/config.mts | 1 + docs/getting-started/railway-deployment.md | 72 ++++++++ docs/index.md | 2 +- server/src/server/app.ts | 6 + server/src/settings.ts | 9 + tests/deployment/workflow-config.test.ts | 22 +++ tests/server/auth.test.ts | 16 ++ tests/server/settings.test.ts | 15 ++ 11 files changed, 417 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/deploy-backend-railway.yml create mode 100644 .plans/railway-stable-dev-deployment-plan-2026-07-18.md create mode 100644 docs/getting-started/railway-deployment.md create mode 100644 tests/deployment/workflow-config.test.ts create mode 100644 tests/server/settings.test.ts diff --git a/.github/workflows/deploy-backend-railway.yml b/.github/workflows/deploy-backend-railway.yml new file mode 100644 index 00000000..ba728afc --- /dev/null +++ b/.github/workflows/deploy-backend-railway.yml @@ -0,0 +1,96 @@ +name: Deploy Backend (Railway) + +on: + workflow_run: + workflows: + - PhaserForge CI + types: + - completed + workflow_dispatch: + inputs: + channel: + description: Deployment channel. Stable is an explicit promotion. + required: true + default: dev + type: choice + options: + - dev + - stable + ref: + description: Commit, branch, or release tag to deploy for a manual promotion. + required: true + default: main + type: string + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + deploy: + if: >- + (github.event_name == 'workflow_dispatch') || + (github.event.workflow_run.conclusion == 'success' && + github.event.workflow_run.event == 'push' && + github.event.workflow_run.head_branch == 'main' && + github.event.workflow_run.head_repository.full_name == github.repository) + runs-on: ubuntu-latest + environment: ${{ github.event_name == 'workflow_dispatch' && inputs.channel == 'stable' && 'stable' || 'development' }} + permissions: + contents: read + steps: + - name: Checkout deployment ref + uses: actions/checkout@v7 + with: + ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || github.event.workflow_run.head_sha }} + lfs: true + + - name: Setup Node + uses: actions/setup-node@v7 + with: + node-version: 24 + + - name: Install dependencies + run: npm ci + + - name: Install Railway CLI + run: npm install --global @railway/cli + + - name: Deploy to Railway + env: + RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }} + RAILWAY_PROJECT_ID: ${{ secrets.RAILWAY_PROJECT_ID }} + RAILWAY_ENVIRONMENT_ID: ${{ secrets.RAILWAY_ENVIRONMENT_ID }} + RAILWAY_SERVICE_ID: ${{ secrets.RAILWAY_SERVICE_ID }} + DEPLOY_CHANNEL: ${{ github.event_name == 'workflow_dispatch' && inputs.channel || 'dev' }} + DEPLOY_COMMIT: ${{ github.event_name == 'workflow_dispatch' && github.sha || github.event.workflow_run.head_sha }} + run: | + set -euo pipefail + test -n "$RAILWAY_TOKEN" + test -n "$RAILWAY_PROJECT_ID" + test -n "$RAILWAY_ENVIRONMENT_ID" + test -n "$RAILWAY_SERVICE_ID" + railway up \ + --project "$RAILWAY_PROJECT_ID" \ + --environment "$RAILWAY_ENVIRONMENT_ID" \ + --service "$RAILWAY_SERVICE_ID" \ + --ci + + - name: Verify Railway health and deployed version + env: + RAILWAY_PUBLIC_URL: ${{ vars.RAILWAY_PUBLIC_URL }} + EXPECTED_CHANNEL: ${{ github.event_name == 'workflow_dispatch' && inputs.channel || 'dev' }} + DEPLOYED_COMMIT: ${{ github.event_name == 'workflow_dispatch' && github.sha || github.event.workflow_run.head_sha }} + run: | + set -euo pipefail + test -n "$RAILWAY_PUBLIC_URL" + for attempt in $(seq 1 30); do + health="$(curl --silent --show-error --fail "$RAILWAY_PUBLIC_URL/api/v1/health" 2>/dev/null || true)" + version="$(curl --silent --show-error --fail "$RAILWAY_PUBLIC_URL/api/v1/version" 2>/dev/null || true)" + if [ "$health" = '{"status":"ok"}' ] && echo "$version" | grep -Fq '"channel":"'"$EXPECTED_CHANNEL"'"'; then + echo "Railway deployment is healthy for commit $DEPLOYED_COMMIT: $version" + exit 0 + fi + sleep 10 + done + echo "Railway deployment did not become healthy with the expected version" >&2 + exit 1 diff --git a/.github/workflows/deploy-frontend-pages.yml b/.github/workflows/deploy-frontend-pages.yml index f2b2229d..39b45f52 100644 --- a/.github/workflows/deploy-frontend-pages.yml +++ b/.github/workflows/deploy-frontend-pages.yml @@ -77,18 +77,24 @@ jobs: - name: Build dev channel env: - VITE_API_BASE_URL: ${{ vars.VITE_API_BASE_URL }} + VITE_API_BASE_URL: ${{ vars.VITE_API_BASE_URL_DEV }} VITE_PHASERFORGE_DEPLOY_CHANNEL: dev VITE_E2E_TEST_BRIDGE: '1' run: npm run build -- --outDir dist/dev + - name: Verify dev API configuration + env: + VITE_API_BASE_URL: ${{ vars.VITE_API_BASE_URL_DEV }} + run: test -n "$VITE_API_BASE_URL" + - name: Build stable channel if: github.event_name == 'workflow_dispatch' && inputs.channel == 'stable' env: - VITE_API_BASE_URL: ${{ vars.VITE_API_BASE_URL }} + VITE_API_BASE_URL: ${{ vars.VITE_API_BASE_URL_STABLE }} VITE_PHASERFORGE_DEPLOY_CHANNEL: stable VITE_E2E_TEST_BRIDGE: '1' run: | + test -n "$VITE_API_BASE_URL" npm run build -- --outDir dist/stable node -e "const fs=require('fs'); fs.writeFileSync('dist/stable/version.json', JSON.stringify({ channel: 'stable', commit: process.env.GITHUB_SHA, promotedAt: new Date().toISOString() }, null, 2) + '\n')" diff --git a/.plans/railway-stable-dev-deployment-plan-2026-07-18.md b/.plans/railway-stable-dev-deployment-plan-2026-07-18.md new file mode 100644 index 00000000..f6bce707 --- /dev/null +++ b/.plans/railway-stable-dev-deployment-plan-2026-07-18.md @@ -0,0 +1,171 @@ +# Stable and Development Railway Deployment Plan + +Status: proposed phased plan + +Date: 2026-07-18 + +## Outcome + +PhaserForge will have two independently deployable release channels: + +- Stable Pages frontend → stable Railway API → stable database +- Development Pages frontend → development Railway API → development database + +The stable channel will not be exposed to development API changes, test data, migrations, or configuration. Development work may deploy frequently and may use disposable or resettable data without risking production users. + +The root Pages URL remains a redirect/landing surface; it must not become a third editor deployment. + +## Current gap + +- `.github/workflows/deploy-frontend-pages.yml` builds `/dev/` on normal successful `main` deployments and promotes `/stable/` manually. +- Both frontend builds currently receive the same `VITE_API_BASE_URL` repository variable. +- `railway.toml` defines one Railway service and health check, but the repository has no Railway deployment workflow. +- Development cloud persistence is disabled by default, which limits accidental development writes but does not provide API or database isolation. +- The archived Railway plan describes a deployment workflow but is historical context, not an active implementation contract. + +## Guiding decisions + +1. Separate environments at the Railway service/database boundary, not only through frontend labels. +2. Stable deploys are explicit promotions; development deploys can follow successful development-channel CI. +3. Database migrations are forward-compatible and are applied before application code that requires them. +4. Stable and development credentials, OAuth callbacks, CORS origins, cookies, secrets, and API URLs are separate. +5. No production data is copied into development unless an explicit sanitized fixture process is later approved. +6. Every deployment has a health check and a post-deploy smoke check. + +7. Use a hybrid configuration boundary: + - Repository code owns shared deployment behavior: `railway.toml`, build/start commands, migration ordering, health checks, the Railway CLI workflow, and stable/development promotion rules. + - Railway and GitHub dashboard configuration owns environment-specific infrastructure: services, databases, domains, secrets, URLs, OAuth credentials, cookie/proxy values, deployment identifiers, and approval protections. + - Do not store Railway tokens, database URLs, OAuth secrets, or environment-specific public URLs in the repository. +8. Use GitHub Actions as the single deployment trigger for these services. Disable Railway dashboard auto-deploys after the CI workflow is connected so dashboard and CI deployments cannot race or deploy different refs unexpectedly. +9. A separate `development` environment inside the existing Railway project is the practical default. It must contain a separate API service and separate Postgres service. A separate Railway project is an optional stronger-isolation choice when access controls or billing make that worthwhile. Either setup may safely connect to the same `bcorfman/phaserforge` GitHub repository because Railway services have independent deployment and variable state. Do not leave auto-deploy enabled on either service; the collision to avoid is two services independently auto-deploying the same branch, not sharing the repository itself. + +## Phase 0 — Inventory and environment contract + +Goal: establish the external resources and names before changing deployment automation. + +- [ ] Confirm the current Railway project, service name, public domain, deployment source, branch, variables, database attachment, and custom domain configuration. +- [ ] Create a separate `development` environment under the existing Railway project, then create a separate API service and Postgres database inside it. A separate Railway project is optional for stronger access/billing isolation; connecting either setup to the same GitHub repository is safe when auto-deploy is disabled. +- [ ] Reserve canonical URLs, for example: + - `https://phaserforge-api-production.up.railway.app` + - `https://phaserforge-api-dev.up.railway.app` +- [ ] Define the frontend URLs: + - `https://bcorfman.github.io/phaserforge/stable/` + - `https://bcorfman.github.io/phaserforge/dev/` +- [ ] Define an environment-variable matrix for API URLs, CORS, OAuth, cookie policy, database URLs, and deployment secrets. +- [ ] Record which existing Railway service/database is production and preserve it as the stable environment. +- [ ] Confirm Railway dashboard auto-deploy is disabled for both services once the GitHub Actions deploy workflow is enabled. + +Exit criteria: + +- Stable resources are identified and no production resource will be repurposed destructively. +- The dev environment can be created without sharing its database or secrets with stable. + +## Phase 1 — Create and prove the Railway development environment + +Goal: create a working dev backend without changing stable behavior. + +- [ ] Create the development Railway service from the same repository and configure it to deploy the intended development branch or deployment ref. +- [ ] Attach a separate development Postgres database. +- [ ] Configure development-only values for `PUBLIC_BASE_URL`, `FRONTEND_BASE_URL`, `CORS_ALLOW_ORIGINS`, `COOKIE_SAMESITE`, `COOKIE_SECURE`, `TRUST_PROXY`, session settings, and OAuth callback URLs. +- [ ] Use separate GitHub OAuth credentials/app callback configuration if GitHub login is enabled in development. +- [ ] Run migrations against the empty development database. +- [ ] Verify `/api/v1/health`, auth, session persistence, CSRF, CORS, and basic cloud-game CRUD against the dev service. +- [ ] Confirm no development request can resolve to the stable database or stable OAuth callback. +- [ ] Configure the development Railway service's source/ref and environment-specific variables in the dashboard; keep shared build/start/health behavior in `railway.toml`. + +Exit criteria: + +- Dev Railway health is green. +- A test account and test game can be created and deleted in dev. +- Stable API health and an existing stable account/game remain unaffected. + +## Phase 2 — Make the frontend environment-aware + +Goal: ensure each Pages channel targets the correct Railway environment. + +- [x] Add explicit build variables for stable and development API bases rather than relying on one shared `VITE_API_BASE_URL` value. +- [x] Update the Pages workflow so `/dev/` receives the development API URL and `/stable/` receives the stable API URL. +- [x] Preserve channel-scoped browser storage and the existing default that disables development cloud persistence until the dev backend is verified. +- [x] Ensure OAuth start links and callback return paths are correct for `/dev/` and `/stable/` (existing callback-path tests remain green; the build keeps each channel's `BASE_URL`). +- [x] Add tests for build-channel-to-API configuration and reject missing/ambiguous production configuration where practical. +- [x] Update deployment/troubleshooting documentation with the two environment URLs and the data-isolation rule. + +Exit criteria: + +- The built dev bundle contains only the dev API base. +- The built stable bundle contains only the stable API base. +- Browser requests, OAuth redirects, cookies, and cloud persistence are isolated by environment. + +## Phase 3 — Add CI-gated Railway deployments + +Goal: make Railway deployment reproducible, auditable, and aligned with the Pages release model. + +- [x] Add `.github/workflows/deploy-backend-railway.yml` or an equivalent environment-aware workflow. +- [x] Gate automatic dev deployment on successful CI for the development deployment ref. +- [x] Make stable deployment an explicit promotion from a known commit or release tag after the corresponding frontend promotion. +- [ ] Use GitHub environment protections for stable deployment approval and stable secrets. +- [ ] Store the Railway project/environment/service identifiers and token in the corresponding GitHub environment, not in repository files. +- [ ] Disable Railway dashboard auto-deploys so GitHub Actions remains the only deployment trigger. +- [x] Deploy with the Railway CLI or the configured Railway integration; do not depend on undocumented dashboard-only behavior. +- [x] Run the Railway health check after deployment and fail the workflow if it does not become healthy. +- [x] Capture the deployed commit/environment in workflow logs or a small version endpoint/response. +- [x] Ensure migrations run in a controlled step before starting the server, with a single migration runner per environment. + +Exit criteria: + +- A dev deployment can be reproduced from CI and reports its commit/environment. +- Stable deployment requires an explicit promotion and cannot be triggered accidentally by a dev change. +- Failed health checks fail the deployment and leave the previous healthy service available where Railway supports rollback. + +## Phase 4 — Migration and compatibility hardening + +Goal: prevent backend changes from breaking the stable channel during normal development. + +- [x] Audit the current migration/start command behavior in `package.json`, Prisma configuration, and server startup. +- [ ] Adopt expand/ migrate/ contract sequencing for schema changes: + - add nullable or additive schema first; + - deploy code that supports old and new shapes; + - backfill or migrate data; + - remove old fields only after stable has moved past the compatibility window. +- [x] Add API contract tests for health, auth, sessions, CORS, cloud games, and publish-related endpoints used by both channels (covered by the existing server suites, with channel-origin isolation tightened in `tests/server/auth.test.ts`). +- [x] Add a smoke test that verifies stable and dev API origins are not interchangeable (server CORS isolation test added; deployed cross-channel smoke remains in Phase 5). +- [x] Document rollback requirements for application-only deploys and for migrations that cannot be reversed. + +Exit criteria: + +- A development API migration can be tested without touching stable data. +- Stable promotion has a documented compatibility and rollback path. + +## Phase 5 — End-to-end channel verification and cutover + +Goal: prove the complete user-visible topology before treating the split as operational. + +- [x] Run unit/server tests for settings, CORS, cookies, OAuth return paths, migrations, and deployment helpers. +- [x] Run the required local Chromium smoke suite for frontend/editor changes (not applicable: this implementation did not change `src/editor/**`, `src/App.tsx`, or `src/phaser/EditorScene.ts`). + `npm run test:e2e -- --project=chromium --grep @smoke` +- [ ] Run deployed smoke checks against both channels: + - load the correct frontend bundle; + - verify `/api/v1/health` through the intended API; + - sign up/log in with environment-specific test accounts; + - create, reload, update, and delete a cloud project; + - verify OAuth callback and return path if enabled; + - confirm stable data is absent from dev and dev data is absent from stable. +- [ ] Verify a dev backend deploy changes `/dev/` behavior without changing `/stable/`. +- [ ] Verify a stable promotion changes both stable frontend/backend only when explicitly requested. +- [ ] Add monitoring/alert ownership for both Railway services and document the rollback commands. + +Definition of done: + +- Stable and dev frontends point to separate Railway APIs. +- Stable and dev APIs use separate databases and environment secrets. +- CI deploys dev predictably and stable only through explicit promotion. +- Cross-channel auth, cookies, OAuth, CORS, and cloud persistence are tested. +- A failed dev deployment cannot take down or mutate stable. +- Documentation and repository memory reflect the final deployment contract. + +## Follow-up, if needed + +- Add preview environments for pull requests only after stable/dev isolation is reliable. +- Add database backup/restore drills for stable. +- Add automated synthetic checks for both public frontend/API pairs. +- Consider a release manifest tying the stable Pages commit, Railway commit, and migration level together. diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 0e585af2..e7c28f87 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -19,6 +19,7 @@ export default defineConfig({ { text: 'Pattern Demo', link: '/getting-started/pattern-demo' }, { text: 'Stars Demo', link: '/getting-started/stars-demo' }, { text: 'Publish to GitHub Pages', link: '/getting-started/publish-to-github-pages' }, + { text: 'Stable and Development Deployment', link: '/getting-started/railway-deployment' }, ], }, { diff --git a/docs/getting-started/railway-deployment.md b/docs/getting-started/railway-deployment.md new file mode 100644 index 00000000..588da08e --- /dev/null +++ b/docs/getting-started/railway-deployment.md @@ -0,0 +1,72 @@ +# Stable and development deployment + +PhaserForge has two release channels: + +- `/stable/` → the stable Pages bundle → the stable Railway API and database +- `/dev/` → the development Pages bundle → the development Railway API and database + +Never point both Pages channels at the same Railway database. Development data and credentials must remain disposable and separate from stable data. + +## GitHub configuration + +In **Repository → Settings → Secrets and variables → Actions → Variables**, create: + +- `VITE_API_BASE_URL_DEV`: the public development Railway URL +- `VITE_API_BASE_URL_STABLE`: the public stable Railway URL + +Delete the old shared `VITE_API_BASE_URL` variable after both new variables are present. A normal successful `main` CI run deploys `/dev/`; a stable Pages deployment is an explicit `workflow_dispatch` promotion. + +For the Railway workflow, create a GitHub **development** environment and a protected **stable** environment. Add these environment secrets to each environment, using that environment's resources: + +- `RAILWAY_TOKEN` +- `RAILWAY_PROJECT_ID` +- `RAILWAY_ENVIRONMENT_ID` +- `RAILWAY_SERVICE_ID` + +Add `RAILWAY_PUBLIC_URL` as an environment variable in each GitHub environment. Stable should require reviewers before a deployment job can start. + +## Railway dashboard configuration + +For each service, open **Variables** and set the environment-specific values: + +```text +PUBLIC_BASE_URL=https:// +FRONTEND_BASE_URL=https://bcorfman.github.io/phaserforge// +CORS_ALLOW_ORIGINS=https://bcorfman.github.io +COOKIE_SAMESITE=none +COOKIE_SECURE=true +TRUST_PROXY=true +DEPLOY_CHANNEL= +``` + +Attach a separate Postgres service to each environment so `DATABASE_URL` is supplied by that environment's database. Configure separate GitHub OAuth apps/callbacks if OAuth is enabled. The callback is: + +```text +https:///api/v1/auth/github/callback +``` + +In **Settings → Networking**, confirm the public domain and copy it into `PUBLIC_BASE_URL`, the matching GitHub Actions variable, and the matching Pages build variable. In **Deploy**, confirm the repository, branch/ref, and the healthcheck path `/api/v1/health` from `railway.toml`. + +## Verification + +After each deployment, check: + +```text +GET /api/v1/health → {"status":"ok"} +GET /api/v1/version → the expected channel and commit +``` + +Then use an environment-specific test account to create and delete a test game. Do not copy stable production data into development without an approved sanitized-fixture process. + +## Migration and rollback rules + +Use expand/migrate/contract sequencing for schema changes: + +1. Add nullable or additive fields. +2. Deploy code that works with both old and new shapes. +3. Run the forward migration or backfill. +4. Remove old fields only after stable has passed the compatibility window. + +Application-only rollback should redeploy the last known-good commit through the same channel workflow. Treat destructive or irreversible migrations as a separate release: restore stable from its Railway backup or apply a documented forward repair before attempting an application rollback. Never point a rollback at the other channel's database. + +Keep the previous healthy Railway deployment available until the post-deploy health and smoke checks pass. Record the Pages commit, Railway commit, and migration level together when promoting stable. diff --git a/docs/index.md b/docs/index.md index 6bac7eca..601c0503 100644 --- a/docs/index.md +++ b/docs/index.md @@ -7,6 +7,7 @@ This guide is the in-repo user guide for PhaserForge. It turns the current workf - [Set Up Your Cloud Account](./getting-started/cloud-account-setup) - [Build the Pattern Demo](./getting-started/pattern-demo) - [Publish to GitHub Pages](./getting-started/publish-to-github-pages) +- [Stable and development deployment](./getting-started/railway-deployment) ## Reference @@ -19,4 +20,3 @@ This guide is the in-repo user guide for PhaserForge. It turns the current workf ## Music Credits - [List](./reference/credits) - diff --git a/server/src/server/app.ts b/server/src/server/app.ts index 1069d5a4..ed3e08cd 100644 --- a/server/src/server/app.ts +++ b/server/src/server/app.ts @@ -54,6 +54,12 @@ export function createApp(options: CreateAppOptions) { app.use(express.json({ limit: '1mb' })); app.get('/api/v1/health', (_req, res) => res.json({ status: 'ok' })); + app.get('/api/v1/version', (_req, res) => + res.json({ + channel: settings.deployment?.channel ?? 'unknown', + commit: settings.deployment?.commit ?? 'unknown', + }), + ); app.use('/api/v1/auth', authRouter(settings, repositories)); app.use('/api/v1/games', gamesRouter(settings, repositories)); app.use('/api/v1/publish', publishRouter(settings, repositories)); diff --git a/server/src/settings.ts b/server/src/settings.ts index 0f9d1478..8960a83c 100644 --- a/server/src/settings.ts +++ b/server/src/settings.ts @@ -15,6 +15,10 @@ export type Settings = { clientId: string; clientSecret: string; }; + deployment?: { + channel: 'stable' | 'dev' | 'unknown'; + commit: string; + }; }; export function resolveCookiePolicy(settings: Pick): { @@ -70,5 +74,10 @@ export function loadSettingsFromEnv(env: NodeJS.ProcessEnv): Settings { githubClientId && githubClientSecret ? { clientId: githubClientId, clientSecret: githubClientSecret } : undefined, + deployment: { + channel: + env.DEPLOY_CHANNEL === 'stable' || env.DEPLOY_CHANNEL === 'dev' ? env.DEPLOY_CHANNEL : 'unknown', + commit: env.DEPLOY_COMMIT?.trim() || 'unknown', + }, }; } diff --git a/tests/deployment/workflow-config.test.ts b/tests/deployment/workflow-config.test.ts new file mode 100644 index 00000000..93d2fb25 --- /dev/null +++ b/tests/deployment/workflow-config.test.ts @@ -0,0 +1,22 @@ +import { readFileSync } from 'node:fs'; +import { describe, expect, it } from 'vitest'; + +const pagesWorkflow = readFileSync('.github/workflows/deploy-frontend-pages.yml', 'utf8'); +const railwayWorkflow = readFileSync('.github/workflows/deploy-backend-railway.yml', 'utf8'); + +describe('deployment workflow contract', () => { + it('builds each Pages channel from its own API variable', () => { + expect(pagesWorkflow).toContain('VITE_API_BASE_URL_DEV'); + expect(pagesWorkflow).toContain('VITE_API_BASE_URL_STABLE'); + expect(pagesWorkflow).not.toContain('vars.VITE_API_BASE_URL }}'); + }); + + it('keeps stable backend deployment manual and protected by a stable environment', () => { + expect(railwayWorkflow).toContain('workflow_dispatch:'); + expect(railwayWorkflow).toContain("inputs.channel == 'stable'"); + expect(railwayWorkflow).toContain("&& 'stable' || 'development'"); + expect(railwayWorkflow).toContain('railway up'); + expect(railwayWorkflow).toContain('/api/v1/health'); + expect(railwayWorkflow).toContain('/api/v1/version'); + }); +}); diff --git a/tests/server/auth.test.ts b/tests/server/auth.test.ts index 9706c6b3..29401df6 100644 --- a/tests/server/auth.test.ts +++ b/tests/server/auth.test.ts @@ -51,6 +51,11 @@ describe('auth', () => { await request(app).get('/api/v1/health').expect(200).expect({ status: 'ok' }); }); + it('exposes deployment channel and commit without changing the health contract', async () => { + const { app } = makeApp({ deployment: { channel: 'dev', commit: 'abc123' } }); + await request(app).get('/api/v1/version').expect(200).expect({ channel: 'dev', commit: 'abc123' }); + }); + it('serves csrf responses with no-store cache headers', async () => { const { app } = makeApp(); const res = await request(app).get('/api/v1/auth/csrf').expect(200); @@ -81,6 +86,17 @@ describe('auth', () => { expect(res.headers['access-control-allow-headers']).toContain('Pragma'); }); + it('does not grant credentialed CORS access to an unrecognized channel origin', async () => { + const { app } = makeApp({ corsAllowOrigins: ['https://bcorfman.github.io'] }); + const res = await request(app) + .get('/api/v1/health') + .set('Origin', 'https://untrusted.example') + .expect(200); + + expect(res.headers['access-control-allow-origin']).toBeUndefined(); + expect(res.headers['access-control-allow-credentials']).toBeUndefined(); + }); + it('signs up and returns session cookie', async () => { const { app } = makeApp(); const agent = request.agent(app); diff --git a/tests/server/settings.test.ts b/tests/server/settings.test.ts new file mode 100644 index 00000000..4c4e90fb --- /dev/null +++ b/tests/server/settings.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, it } from 'vitest'; + +import { loadSettingsFromEnv } from '../../server/src/settings'; + +describe('deployment settings', () => { + it('normalizes deployment channel and commit from the service environment', () => { + const settings = loadSettingsFromEnv({ DEPLOY_CHANNEL: 'dev', DEPLOY_COMMIT: 'abc123' }); + expect(settings.deployment).toEqual({ channel: 'dev', commit: 'abc123' }); + }); + + it('does not infer a stable deployment when the channel is missing or invalid', () => { + expect(loadSettingsFromEnv({}).deployment).toEqual({ channel: 'unknown', commit: 'unknown' }); + expect(loadSettingsFromEnv({ DEPLOY_CHANNEL: 'production' }).deployment?.channel).toBe('unknown'); + }); +});