VAPID private key를 Compose secret으로 주입하고 Web Push를 기본 비활성으로 전달한다. 활성화 입력과 Compose 모델 검증, 운영 README를 함께 추가한다.
170 lines
5.7 KiB
JavaScript
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')));
|
|
});
|