import { readFileSync } from 'node:fs'; import { describe, expect, it } from 'vitest'; const workflow = readFileSync(new URL('../../.gitea/workflows/deploy.yml', import.meta.url), 'utf8'); const compose = readFileSync(new URL('../../docker-compose.deploy.yml', import.meta.url), 'utf8'); describe('deployment workflow', () => { it('maps main and experimental to isolated stacks and VPN ports', () => { expect(workflow).toContain('ASSESSOR_ENV_FILE: ${{ secrets.PROD_ENV_FILE }}'); expect(workflow).toContain('ASSESSOR_ENV_FILE: ${{ secrets.STAGING_ENV_FILE }}'); expect(workflow).toContain('ASSESSOR_STACK=assessor_prod'); expect(workflow).toContain('ASSESSOR_STACK=assessor_experimental'); expect(workflow).toContain('ASSESSOR_PUBLISH_PORT=4173'); expect(workflow).toContain('ASSESSOR_PUBLISH_PORT=4174'); expect(workflow.match(/ASSESSOR_BIND=10\.0\.0\.154/g)).toHaveLength(2); expect(workflow).not.toContain('assessor.soc-ops.de'); }); it('validates all database settings without printing secret values', () => { expect(workflow.match(/if \[ -z "\$ASSESSOR_ENV_FILE" \]/g)).toHaveLength(2); expect(workflow.match(/for key in ASSESSOR_POSTGRES_DB ASSESSOR_POSTGRES_USER ASSESSOR_POSTGRES_PASSWORD/g)).toHaveLength(2); expect(workflow).toContain('secret values were not printed.'); expect(workflow).not.toMatch(/echo "\$ASSESSOR_ENV_FILE"/); expect(workflow).not.toContain('printf \'%s\\n\' "${{ secrets.'); }); it('tests, builds, validates and serializes each branch deployment', () => { expect(workflow).toContain('run: npm test'); expect(workflow).toContain('run: npm run build'); expect(workflow).toMatch(/docker compose[^\n]*config --quiet/); expect(workflow).toContain('publish=$ASSESSOR_PUBLISH_PORT'); expect(workflow).toContain('project="$(docker inspect'); expect(workflow).toContain('if [ "$project" != "$ASSESSOR_STACK" ]'); expect(workflow).toContain('name: Acquire environment deployment lock'); expect(workflow).toContain('name: Release environment deployment lock'); expect(workflow).toContain('${ASSESSOR_STACK}_deployment_lock'); expect(workflow).toContain('ASSESSOR_LOCK_ACQUIRED=true'); expect(workflow).toContain('if: always()'); expect(workflow).not.toMatch(/^concurrency:/m); }); it('creates separate pre-deployment database and artifact backups', () => { expect(workflow).toContain('backup_volume=assessor_deployment_backups'); expect(workflow).toContain('pg_dump --format=custom --no-owner'); expect(workflow).toContain('artifacts.tar.gz'); expect(workflow).toContain('SHA256SUMS'); expect(workflow).not.toContain('down --volumes'); expect(workflow).not.toContain('docker image prune'); expect(workflow).not.toMatch(/docker compose[^\n]*logs/); }); it('checks application health inside the app container', () => { expect(workflow).toMatch(/docker compose[^\n]*exec -T app/); expect(workflow).toContain("fetch('http://127.0.0.1:4173/api/v1/health')"); expect(workflow).not.toMatch(/curl[^\n]*127\.0\.0\.1:4173/); }); }); describe('deployment Compose stack', () => { it('requires an explicit bind, port and database configuration', () => { expect(compose).toContain('${ASSESSOR_BIND:?set ASSESSOR_BIND}'); expect(compose).toContain('${ASSESSOR_PUBLISH_PORT:?set ASSESSOR_PUBLISH_PORT}'); expect(compose).toContain('${ASSESSOR_POSTGRES_DB:?set ASSESSOR_POSTGRES_DB}'); expect(compose).toContain('${ASSESSOR_POSTGRES_USER:?set ASSESSOR_POSTGRES_USER}'); expect(compose).toContain('${ASSESSOR_POSTGRES_PASSWORD:?set ASSESSOR_POSTGRES_PASSWORD}'); }); it('keeps PostgreSQL internal and scopes persistent volumes through Compose', () => { const postgresSection = compose.slice(compose.indexOf(' postgres:')); expect(postgresSection).not.toMatch(/\n ports:/); expect(compose).toContain('assessor-artifacts:/var/lib/assessor/artifacts'); expect(compose).toContain('assessor-postgres-data:/var/lib/postgresql/data'); expect(compose.match(/restart: unless-stopped/g)).toHaveLength(2); }); });