import { readFileSync } from 'node:fs' import { resolve } from 'node:path' import { describe, expect, it } from 'vitest' const root = resolve(import.meta.dirname, '../../..') const read = (p: string) => readFileSync(resolve(root, p), 'utf-8') const dockerfile = read('deploy/docker/authportal/Dockerfile') const composes = { 'docker-compose.yml': read('docker-compose.yml'), 'deploy/docker-compose.traefik.yml': read('deploy/docker-compose.traefik.yml'), } describe('Dockerfile', () => { it('graceful stop и ограничение ресурсов Node', () => { expect(dockerfile).toMatch(/^STOPSIGNAL SIGTERM$/m) expect(dockerfile).toMatch(/NODE_OPTIONS=--max-old-space-size=\d+/) expect(dockerfile).toMatch(/UV_THREADPOOL_SIZE=4/) }) it('healthcheck без запуска node', () => { const hc = dockerfile.split('HEALTHCHECK')[1]!.split('\n').slice(0, 2).join(' ') expect(hc).toContain('wget') expect(hc).not.toContain('"node"') }) it('запуск не от root, рантайм без тулчейна и pnpm deploy', () => { expect(dockerfile).toMatch(/^USER node$/m) const runtime = dockerfile.split('AS runtime')[1]! expect(runtime).not.toMatch(/apk add/) expect(dockerfile).not.toMatch(/pnpm .*deploy/) }) it('статика сжимается только brotli', () => { expect(dockerfile).toContain('brotli') expect(dockerfile).not.toMatch(/gzip -k/) }) it('preview-маршруты дизайна вырезаются до сборки web', () => { const rm = dockerfile.indexOf('rm -f apps/web/src/routes/design-preview*.tsx') const build = dockerfile.indexOf('pnpm turbo build') expect(rm).toBeGreaterThan(0) expect(rm).toBeLessThan(build) }) it('базовый образ закреплён по digest', () => { expect(dockerfile).toMatch(/ARG BASE_NODE=\S+@sha256:[0-9a-f]{64}/) expect(read('deploy/docker/docker-bake.hcl')).toMatch(/node:22-alpine@sha256:[0-9a-f]{64}/) }) it('в образ копируется весь dist API, а tsup не дробит бандл на чанки', () => { expect(dockerfile).toContain('cp -r apps/api/dist /out/dist') expect(read('apps/api/tsup.config.ts')).toMatch(/splitting:\s*false/) }) it('нативные модули проверяются при сборке', () => { expect(dockerfile).toContain('native modules ok') }) }) describe.each(Object.entries(composes))('%s', (_name, text) => { it('init, grace period и лимиты ресурсов', () => { expect(text).toMatch(/init: true/) expect(text).toMatch(/stop_grace_period: 15s/) expect(text).toMatch(/mem_limit: 256m/) expect(text).toMatch(/pids_limit: \d+/) expect(text).toMatch(/read_only: true/) expect(text).toMatch(/cap_drop:\s*\n\s*- ALL/) expect(text).toMatch(/no-new-privileges:true/) }) it('healthcheck через wget', () => { expect(text).toMatch(/"wget", "-q"/) expect(text).not.toMatch(/fetch\('http:\/\/127\.0\.0\.1:8080\/health'\)/) }) }) describe('traefik', () => { it('app ждёт healthy traefik, у traefik есть healthcheck', () => { const t = composes['deploy/docker-compose.traefik.yml'] expect(t).toMatch(/condition: service_healthy/) expect(t).toContain('traefik", "healthcheck", "--ping') expect(t).toContain('--ping=true') }) it('включено сжатие ответов', () => { expect(composes['deploy/docker-compose.traefik.yml']).toContain( 'middlewares.auth-portal-compress.compress=true', ) }) })