92 lines
3.5 KiB
TypeScript
92 lines
3.5 KiB
TypeScript
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',
|
||
)
|
||
})
|
||
})
|