d824d16eed
Compose de prod docker-compose.prod.yml (GitOps, sans env_file, réseau proxy réel, certresolver letsencrypt) + runbook docs/deploy.md (Phase 1, users chlova restreints Portainer/n8n). Surface Telegram rendue optionnelle pour un déploiement UI-only ; garde assertHasSurface fail-closed. Typecheck + 78 tests verts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016w5jRe87MGdd6AMvXQcHNi
102 lines
3.1 KiB
TypeScript
102 lines
3.1 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import {
|
|
loadConfig,
|
|
assertReadOnlyPhase,
|
|
assertHasSurface,
|
|
redactedConfig,
|
|
} from "../src/config.js";
|
|
|
|
const fullEnv = (): NodeJS.ProcessEnv => ({
|
|
OLLAMA_BASE_URL: "http://ollama:11434",
|
|
OLLAMA_API_KEY: "secret-ollama",
|
|
OLLAMA_MODEL: "qwen3:cloud",
|
|
MCP_N8N_URL: "http://mcp-n8n:3000",
|
|
MCP_N8N_AUTH_TOKEN: "secret-n8n",
|
|
MCP_PORTAINER_URL: "http://mcp-portainer:3000",
|
|
PORTAINER_MCP_AUTH_TOKEN: "secret-portainer",
|
|
PORTAINER_READ_ONLY: "true",
|
|
TELEGRAM_BOT_TOKEN: "secret-tg",
|
|
TELEGRAM_ALLOWED_USER_IDS: "111, 222",
|
|
});
|
|
|
|
describe("config fail-closed", () => {
|
|
it("charge une config complète", () => {
|
|
const cfg = loadConfig(fullEnv());
|
|
expect(cfg.ollamaModel).toBe("qwen3:cloud");
|
|
expect(cfg.telegramAllowedUserIds).toEqual(["111", "222"]);
|
|
});
|
|
|
|
it("refuse de démarrer si un secret manque", () => {
|
|
const env = fullEnv();
|
|
delete env.OLLAMA_API_KEY;
|
|
expect(() => loadConfig(env)).toThrow(/fail-closed/);
|
|
});
|
|
|
|
it("refuse une URL invalide", () => {
|
|
const env = fullEnv();
|
|
env.MCP_N8N_URL = "pas-une-url";
|
|
expect(() => loadConfig(env)).toThrow();
|
|
});
|
|
});
|
|
|
|
describe("verrou lecture seule Phase 1", () => {
|
|
it("accepte PORTAINER_READ_ONLY=true", () => {
|
|
expect(() => assertReadOnlyPhase(loadConfig(fullEnv()))).not.toThrow();
|
|
});
|
|
|
|
it("refuse PORTAINER_READ_ONLY=false en Phase 1", () => {
|
|
const env = fullEnv();
|
|
env.PORTAINER_READ_ONLY = "false";
|
|
expect(() => assertReadOnlyPhase(loadConfig(env))).toThrow(/lecture seule/);
|
|
});
|
|
|
|
it("Phase 2 autorise PORTAINER_READ_ONLY=false", () => {
|
|
const env = fullEnv();
|
|
env.CHLOVA_PHASE = "2";
|
|
env.PORTAINER_READ_ONLY = "false";
|
|
const cfg = loadConfig(env);
|
|
expect(cfg.phase).toBe(2);
|
|
expect(() => assertReadOnlyPhase(cfg)).not.toThrow();
|
|
});
|
|
|
|
it("phase invalide retombe sur 1 (fail-safe)", () => {
|
|
const env = fullEnv();
|
|
env.CHLOVA_PHASE = "9";
|
|
expect(loadConfig(env).phase).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe("garde de surface (fail-closed)", () => {
|
|
it("Telegram seul suffit", () => {
|
|
expect(() => assertHasSurface(loadConfig(fullEnv()))).not.toThrow();
|
|
});
|
|
|
|
it("API/UI seule suffit (sans Telegram)", () => {
|
|
const env = fullEnv();
|
|
delete env.TELEGRAM_BOT_TOKEN;
|
|
env.CHLOVA_ADMIN_USER = "kantin";
|
|
env.CHLOVA_ADMIN_PASSWORD_HASH = "hash";
|
|
env.CHLOVA_TOTP_SECRET = "totp";
|
|
env.CHLOVA_JWT_SECRET = "jwt";
|
|
expect(() => assertHasSurface(loadConfig(env))).not.toThrow();
|
|
});
|
|
|
|
it("refuse de démarrer sans aucune surface", () => {
|
|
const env = fullEnv();
|
|
delete env.TELEGRAM_BOT_TOKEN;
|
|
expect(() => assertHasSurface(loadConfig(env))).toThrow(/surface/i);
|
|
});
|
|
});
|
|
|
|
describe("redactedConfig masque les secrets", () => {
|
|
it("ne révèle aucun secret", () => {
|
|
const red = redactedConfig(loadConfig(fullEnv()));
|
|
expect(red.ollamaApiKey).toBe("[REDACTED]");
|
|
expect(red.telegramBotToken).toBe("[REDACTED]");
|
|
expect(red.mcpN8nAuthToken).toBe("[REDACTED]");
|
|
expect(red.portainerMcpAuthToken).toBe("[REDACTED]");
|
|
// les non-secrets restent visibles
|
|
expect(red.ollamaModel).toBe("qwen3:cloud");
|
|
});
|
|
});
|