Files
core2026_docker/test/validate-compose-model.test.mjs
T
Hide_D 663e66b8f9 feat: Web Push 비활성 운영 설정을 준비한다
VAPID private key를 Compose secret으로 주입하고 Web Push를 기본 비활성으로 전달한다. 활성화 입력과 Compose 모델 검증, 운영 README를 함께 추가한다.
2026-08-23 13:16:21 +00:00

170 lines
5.7 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { validateComposeModel } from '../runtime/validate-compose-model.mjs';
const safeRuntime = {
environment: {
CORE_SOURCE_MODE: 'clone',
NODE_OPTIONS: '--max-old-space-size=1536',
TURN_DAEMON_NODE_OPTIONS: '--max-old-space-size=3072',
RAYON_NUM_THREADS: '2',
GATEWAY_IMAGE_UPLOAD_URL: 'https://sam-image.hided.net',
GATEWAY_IMAGE_UPLOAD_SECRET_FILE: '/run/secrets/image_upload_core2026_secret',
GATEWAY_SHARED_ICON_PUBLIC_URL: 'https://sam-image.hided.net/icons',
GATEWAY_USER_ICON_PUBLIC_URL: 'https://sam-image.hided.net/icons',
GAME_IMAGE_UPLOAD_URL: 'https://sam-image.hided.net',
GAME_IMAGE_UPLOAD_SECRET_FILE: '/run/secrets/image_upload_core2026_secret',
GAME_CONTENT_IMAGE_PUBLIC_URL: 'https://sam-image.hided.net/uploads/core2026',
IMAGE_SYNC_URL: 'https://sam-image.hided.net',
IMAGE_SYNC_SECRET_FILE: '/run/secrets/image_sync_core2026_secret',
VITE_IMAGE_PUBLIC_URL: 'https://sam-image.hided.net',
VITE_GATEWAY_USER_ICON_BASE_URL: 'https://sam-image.hided.net/icons',
WEB_PUSH_ENABLED: 'false',
WEB_PUSH_VAPID_PRIVATE_KEY_FILE: '/run/secrets/web_push_vapid_private_key',
FRONTEND_SERVE_MODE: 'static',
FRONTEND_SHARED_ASSET_PUBLIC_PATH: '/gateway/profile-assets',
RELEASE_BUILDER_URL: 'http://builder:15100',
GATEWAY_ACTIVE_RELEASE_GIT_REF: 'refs/sammo/active-gateway',
},
volumes: [
{
type: 'bind',
source: '/secrets/image_upload_core2026_secret',
target: '/run/secrets/image_upload_core2026_secret',
read_only: true,
},
{
type: 'volume',
source: 'frontend-artifacts',
target: '/srv/frontend-artifacts',
read_only: false,
},
{
type: 'bind',
source: '/secrets/image_sync_core2026_secret',
target: '/run/secrets/image_sync_core2026_secret',
read_only: true,
},
],
secrets: [
{
source: 'web_push_vapid_private_key',
target: '/run/secrets/web_push_vapid_private_key',
},
],
restart: 'unless-stopped',
mem_limit: String(4 * 1024 * 1024 * 1024),
memswap_limit: String(4 * 1024 * 1024 * 1024),
cpus: 4,
pids_limit: 256,
};
const safeBuilder = {
environment: {
NODE_OPTIONS: '--max-old-space-size=3072',
RAYON_NUM_THREADS: '2',
},
restart: 'unless-stopped',
mem_limit: String(4 * 1024 * 1024 * 1024),
memswap_limit: String(4 * 1024 * 1024 * 1024),
cpus: 4,
pids_limit: 256,
};
const safeCaddy = {
restart: 'unless-stopped',
volumes: [
{
type: 'volume',
source: 'frontend-artifacts',
target: '/srv/frontend-artifacts',
read_only: true,
},
],
};
const safeServices = () => ({
runtime: structuredClone(safeRuntime),
builder: structuredClone(safeBuilder),
caddy: structuredClone(safeCaddy),
});
test('accepts a bounded production runtime', () => {
assert.deepEqual(validateComposeModel({ services: safeServices() }, 'production'), []);
});
test('accepts a literal, non-restarting development model', () => {
const services = safeServices();
services.postgres = { restart: 'no' };
services.redis = { restart: 'no' };
services.builder.restart = 'no';
services.caddy.restart = 'no';
services.runtime = {
...safeRuntime,
restart: 'no',
environment: {
...safeRuntime.environment,
CORE_SOURCE_MODE: 'bind',
RUNTIME_MODE: 'development',
},
};
assert.deepEqual(validateComposeModel({ services }, 'development'), []);
});
test('accepts a bounded, non-restarting production smoke model', () => {
const services = safeServices();
services.postgres = { restart: 'no' };
services.redis = { restart: 'no' };
services.builder.restart = 'no';
services.caddy.restart = 'no';
services.runtime = { ...safeRuntime, restart: 'no' };
assert.deepEqual(validateComposeModel({ services }, 'smoke'), []);
});
test('rejects an unbounded or production-mode development runtime', () => {
const services = safeServices();
services.postgres = { restart: 'unless-stopped' };
services.redis = { restart: 'unless-stopped' };
services.runtime = {
environment: { CORE_SOURCE_MODE: 'bind', RUNTIME_MODE: 'production' },
restart: 'unless-stopped',
mem_limit: 0,
memswap_limit: 0,
cpus: 0,
pids_limit: 0,
};
const errors = validateComposeModel({ services }, 'development');
assert.ok(errors.some((error) => error.includes('memory limit must be positive')));
assert.ok(errors.some((error) => error.includes('literal development')));
assert.ok(errors.some((error) => error.includes('restart policy must be no')));
});
test('rejects missing or excessive build memory and parallelism limits', () => {
const runtime = {
...safeRuntime,
environment: {
CORE_SOURCE_MODE: 'clone',
NODE_OPTIONS: '--max-old-space-size=4096',
TURN_DAEMON_NODE_OPTIONS: '--max-old-space-size=8192',
RAYON_NUM_THREADS: '8',
},
};
const services = safeServices();
services.runtime = runtime;
const errors = validateComposeModel({ services }, 'production');
assert.ok(errors.some((error) => error.includes('Node heap limit')));
assert.ok(errors.some((error) => error.includes('turn daemon Node heap limit')));
assert.ok(errors.some((error) => error.includes('Rayon thread count')));
});
test('requires Web Push activation to keep the private key in a mounted secret', () => {
const services = safeServices();
services.runtime.environment.WEB_PUSH_ENABLED = 'true';
services.runtime.secrets = [];
const errors = validateComposeModel({ services }, 'production');
assert.ok(errors.some((error) => error.includes('VAPID private key')));
assert.ok(errors.some((error) => error.includes('WEB_PUSH_VAPID_SUBJECT')));
assert.ok(errors.some((error) => error.includes('WEB_PUSH_VAPID_PUBLIC_KEY')));
});