diff --git a/tests/e2e/40-compose-pilot.e2e.ts b/tests/e2e/40-compose-pilot.e2e.ts new file mode 100644 index 0000000..466662c --- /dev/null +++ b/tests/e2e/40-compose-pilot.e2e.ts @@ -0,0 +1,56 @@ +// Статическая проверка deploy/pilot/compose.yaml (аналог tests/unit/deployment.test.ts +// для корневого compose.yaml). Лежит в tests/e2e, а не в tests/unit: у исполнителя +// теста нет права менять tests/unit, а красные пункты (открытые дефекты deploy/) +// должны идти через known-defects.json, не ломая `npm run check`. +import { readFileSync } from 'node:fs'; +import path from 'node:path'; +import { describe, expect, it } from 'vitest'; +import { parse } from 'yaml'; + +const composePath = path.resolve(import.meta.dirname, '../../deploy/pilot/compose.yaml'); +const raw = readFileSync(composePath, 'utf8'); +const compose = parse(raw) as { + services: Record | string[] }>; +}; + +function environmentOf(service: string): Record { + const env = compose.services[service]?.environment ?? {}; + if (Array.isArray(env)) { + return Object.fromEntries( + env.map((entry) => { + const index = entry.indexOf('='); + return index < 0 ? [entry, ''] : [entry.slice(0, index), entry.slice(index + 1)]; + }), + ); + } + return Object.fromEntries(Object.entries(env).map(([key, value]) => [key, String(value)])); +} + +describe('deploy/pilot/compose.yaml', () => { + it('[PILOT-1] все подставляемые переменные обязательны: ${VAR:?сообщение}', () => { + const interpolations = [...raw.matchAll(/\$\{([^}]*)\}/g)].map((match) => match[1]!); + expect(interpolations.length).toBeGreaterThan(0); + const optional = interpolations.filter((inner) => !/^[A-Z0-9_]+:\?.+$/.test(inner)); + expect(optional, 'переменные без :? — пустое значение молча попадёт в контейнер').toEqual([]); + }); + + it('[PILOT-2] ни один сервис не запускается с NODE_ENV=development', () => { + const offenders = Object.keys(compose.services).filter( + (service) => environmentOf(service).NODE_ENV === 'development', + ); + expect(offenders).toEqual([]); + }); + + it('[PILOT-3] у сервиса app нет учётных данных мигратора', () => { + const env = environmentOf('app'); + const keys = Object.keys(env).filter((key) => + ['DATABASE_MIGRATION_URL', 'POSTGRES_MIGRATION_PASSWORD', 'POSTGRES_MIGRATION_USER'].includes( + key, + ), + ); + const values = Object.entries(env) + .filter(([, value]) => /POSTGRES_MIGRATION_PASSWORD|krona_migrator/.test(value)) + .map(([key]) => key); + expect({ keys, values }).toEqual({ keys: [], values: [] }); + }); +}); diff --git a/tests/e2e/known-defects.json b/tests/e2e/known-defects.json index 49a3fc1..39086ed 100644 --- a/tests/e2e/known-defects.json +++ b/tests/e2e/known-defects.json @@ -25,6 +25,8 @@ "SYNC-H": "регистрация устройства по cookie без проверки Origin", "SYNC-I": "без токенов анонимно регистрируется устройство", "SYNC-J": "sync принимает отметки вне окна времени", - "SYNC-K": "ack принимает курсор больше выданного сервером" + "SYNC-K": "ack принимает курсор больше выданного сервером", + "PILOT-1": "deploy/pilot/compose.yaml: 14 подстановок без :? (пустые токены/пароли молча проходят)", + "PILOT-2": "deploy/pilot/compose.yaml: app с NODE_ENV=development" } }