- fix: provider-pool-manager: 移除 if(true) 占位符,改为读取凭据文件真实过期时间
- fix: provider-pool-manager: Math.min 展开大数组改为 reduce,防止栈溢出
- fix: provider-pool-manager: forceRefreshToken 调用前检查方法是否实现,不存在则 fallback
- fix: provider-api: handleAddProvider 默认路径统一为 configs/provider_pools.json
- fix: config-api: handleGetConfig 改为白名单字段过滤,REQUIRED_API_KEY 脱敏返回
- fix: api-server: 启动日志中 API Key 遮码处理
- fix: utils: generateUUID 改用 crypto.randomUUID() 替代 Math.random()
- fix: config-manager: renderProviderTags innerHTML 加 escHtml 防 XSS 注入
- fix: config-manager: PROVIDER_POOLS_FILE_PATH 未定义时加 || '' 兜底
- fix: section-config.css: white 改为 var(--bg-primary, white) 支持暗黑模式
- chore: .gitignore 添加 AGENTS.md
- chore: docker-compose.yml 添加代理环境变量
diff --git a/.gitignore b/.gitignore
index f752bb4..c375cbc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,4 +15,5 @@ api-potluck-keys.json
api-potluck-data.json
# Codex credentials
configs/codex/
+AGENTS.md
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index 6977d13..8c08ef3 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -14,6 +14,12 @@ services:
- ./configs:/app/configs
environment:
- ARGS=
+ - HTTP_PROXY=http://host.docker.internal:10801
+ - http_proxy=http://host.docker.internal:10801
+ - HTTPS_PROXY=http://host.docker.internal:10801
+ - https_proxy=http://host.docker.internal:10801
+ - NO_PROXY=localhost,127.0.0.1,host.docker.internal
+ - no_proxy=localhost,127.0.0.1,host.docker.internal
healthcheck:
test: ["CMD", "node", "healthcheck.js"]
interval: 30s
diff --git a/src/providers/provider-pool-manager.js b/src/providers/provider-pool-manager.js
index f039e48..5875f1f 100644
--- a/src/providers/provider-pool-manager.js
+++ b/src/providers/provider-pool-manager.js
@@ -112,7 +112,12 @@ export class ProviderPoolManager {
if (configPath && fs.existsSync(configPath)) {
try {
- if (true) {
+ const fileContent = fs.readFileSync(configPath, 'utf-8');
+ const credData = JSON.parse(fileContent);
+ const expiryTime = credData.expiry_date || credData.expiry || credData.expires_at;
+ const nearExpiryMs = (currentConfig?.CRON_NEAR_MINUTES || 10) * 60 * 1000;
+ const isNearExpiry = expiryTime && (expiryTime - Date.now()) < nearExpiryMs;
+ if (isNearExpiry) {
this._log('warn', `Node ${providerStatus.uuid} (${providerType}) is near expiration. Enqueuing refresh...`);
this._enqueueRefresh(providerType, providerStatus);
}
@@ -389,7 +394,16 @@ export class ProviderPoolManager {
// 调用适配器的 refreshToken 方法(内部封装了具体的刷新逻辑)
if (typeof serviceAdapter.refreshToken === 'function') {
const startTime = Date.now();
- force ? await serviceAdapter.forceRefreshToken() : await serviceAdapter.refreshToken()
+ if (force) {
+ if (typeof serviceAdapter.forceRefreshToken === 'function') {
+ await serviceAdapter.forceRefreshToken();
+ } else {
+ this._log('warn', `forceRefreshToken not implemented for ${providerType}, falling back to refreshToken`);
+ await serviceAdapter.refreshToken();
+ }
+ } else {
+ await serviceAdapter.refreshToken();
+ }
const duration = Date.now() - startTime;
this._log('info', `Token refresh successful for node ${providerStatus.uuid} (Duration: ${duration}ms)`);
@@ -452,7 +466,7 @@ export class ProviderPoolManager {
const lastSelectionSeq = config._lastSelectionSeq || 0;
if (minSeqInPool === -1) {
const pool = this.providerStatus[providerStatus.type] || [];
- minSeqInPool = Math.min(...pool.map(p => p.config._lastSelectionSeq || 0));
+ minSeqInPool = pool.reduce((min, p) => Math.min(min, p.config._lastSelectionSeq || 0), Infinity);
}
const relativeSeq = Math.max(0, lastSelectionSeq - minSeqInPool);
const cappedRelativeSeq = Math.min(relativeSeq, 100);
@@ -1819,14 +1833,14 @@ export class ProviderPoolManager {
continue;
}
- const checkStartTime = Date.now();
+ const providerCheckStart = Date.now();
const checkModelName = provider.config.checkModelName || ProviderPoolManager.DEFAULT_HEALTH_CHECK_MODELS[providerType] || 'unknown';
const displayName = customName || uuid.substring(0, 8);
-
+
try {
// Perform health check (health check is based on providerTypes configuration, not per-provider checkHealth flag)
const result = await this._checkProviderHealth(providerType, provider.config);
- const checkDuration = Date.now() - checkStartTime;
+ const checkDuration = Date.now() - providerCheckStart;
if (!result.success) {
// Provider is unhealthy
@@ -1840,7 +1854,7 @@ export class ProviderPoolManager {
this.markProviderHealthy(providerType, provider.config, false, result.modelName);
}
} catch (error) {
- const checkDuration = Date.now() - checkStartTime;
+ const checkDuration = Date.now() - providerCheckStart;
failCount++;
this._log('error', `[ScheduledHealthCheck] ${displayName} (${providerType}) EXCEPTION: ${error.message} (${checkDuration}ms)`);
this.markProviderUnhealthyImmediately(providerType, provider.config, error.message);
diff --git a/src/services/api-server.js b/src/services/api-server.js
index ad9bdc9..c1a850f 100644
--- a/src/services/api-server.js
+++ b/src/services/api-server.js
@@ -313,7 +313,7 @@ async function startServer() {
logger.info(` System Prompt Mode: ${CONFIG.SYSTEM_PROMPT_MODE}`);
logger.info(` Host: ${CONFIG.HOST}`);
logger.info(` Port: ${CONFIG.SERVER_PORT}`);
- logger.info(` Required API Key: ${CONFIG.REQUIRED_API_KEY}`);
+ logger.info(` Required API Key: ${CONFIG.REQUIRED_API_KEY ? CONFIG.REQUIRED_API_KEY.slice(0, 4) + '****' : '(none)'}`);
logger.info(` Prompt Logging: ${CONFIG.PROMPT_LOG_MODE}${CONFIG.PROMPT_LOG_FILENAME ? ` (to ${CONFIG.PROMPT_LOG_FILENAME})` : ''}`);
logger.info(`------------------------------------------`);
logger.info(`\nUnified API Server running on http://${CONFIG.HOST}:${CONFIG.SERVER_PORT}`);
@@ -355,14 +355,14 @@ async function startServer() {
setInterval(heartbeatAndRefreshToken, CONFIG.CRON_NEAR_MINUTES * 60 * 1000);
}
// 服务器完全启动后,执行初始健康检查
- const poolManager = getProviderPoolManager();
- if (poolManager) {
- logger.info('[Initialization] Performing initial health checks for provider pools...');
- poolManager.performHealthChecks();
- }
+ const poolManager = getProviderPoolManager();
+ if (poolManager) {
+ logger.info('[Initialization] Performing initial health checks for provider pools...');
+ poolManager.performHealthChecks();
+ }
// 定时健康检查
- const scheduledConfig = CONFIG.SCHEDULED_HEALTH_CHECK;
+ const scheduledConfig = CONFIG.SCHEDULED_HEALTH_CHECK;
if (scheduledConfig?.enabled) {
// 设计决策:只验证最小值 60000ms,不设最大值。
// 前端有 max=3600000 (1小时) 的 UI 限制,但后端允许更大值以支持特殊需求。
diff --git a/src/ui-modules/config-api.js b/src/ui-modules/config-api.js
index 92c4dac..f6f2b0e 100644
--- a/src/ui-modules/config-api.js
+++ b/src/ui-modules/config-api.js
@@ -57,11 +57,48 @@ export async function handleGetConfig(req, res, currentConfig) {
}
}
+ // 白名单过滤:只返回前端需要的字段,避免泄露凭据路径、内部状态等敏感信息
+ const safeConfig = {
+ HOST: currentConfig.HOST,
+ SERVER_PORT: currentConfig.SERVER_PORT,
+ MODEL_PROVIDER: currentConfig.MODEL_PROVIDER,
+ SYSTEM_PROMPT_FILE_PATH: currentConfig.SYSTEM_PROMPT_FILE_PATH,
+ SYSTEM_PROMPT_MODE: currentConfig.SYSTEM_PROMPT_MODE,
+ PROMPT_LOG_BASE_NAME: currentConfig.PROMPT_LOG_BASE_NAME,
+ PROMPT_LOG_MODE: currentConfig.PROMPT_LOG_MODE,
+ REQUEST_MAX_RETRIES: currentConfig.REQUEST_MAX_RETRIES,
+ REQUEST_BASE_DELAY: currentConfig.REQUEST_BASE_DELAY,
+ CREDENTIAL_SWITCH_MAX_RETRIES: currentConfig.CREDENTIAL_SWITCH_MAX_RETRIES,
+ CRON_NEAR_MINUTES: currentConfig.CRON_NEAR_MINUTES,
+ CRON_REFRESH_TOKEN: currentConfig.CRON_REFRESH_TOKEN,
+ LOGIN_EXPIRY: currentConfig.LOGIN_EXPIRY,
+ PROVIDER_POOLS_FILE_PATH: currentConfig.PROVIDER_POOLS_FILE_PATH,
+ MAX_ERROR_COUNT: currentConfig.MAX_ERROR_COUNT,
+ WARMUP_TARGET: currentConfig.WARMUP_TARGET,
+ REFRESH_CONCURRENCY_PER_PROVIDER: currentConfig.REFRESH_CONCURRENCY_PER_PROVIDER,
+ providerFallbackChain: currentConfig.providerFallbackChain,
+ modelFallbackMapping: currentConfig.modelFallbackMapping,
+ PROXY_URL: currentConfig.PROXY_URL,
+ PROXY_ENABLED_PROVIDERS: currentConfig.PROXY_ENABLED_PROVIDERS,
+ TLS_SIDECAR_ENABLED: currentConfig.TLS_SIDECAR_ENABLED,
+ TLS_SIDECAR_ENABLED_PROVIDERS: currentConfig.TLS_SIDECAR_ENABLED_PROVIDERS,
+ TLS_SIDECAR_PORT: currentConfig.TLS_SIDECAR_PORT,
+ TLS_SIDECAR_PROXY_URL: currentConfig.TLS_SIDECAR_PROXY_URL,
+ LOG_ENABLED: currentConfig.LOG_ENABLED,
+ LOG_OUTPUT_MODE: currentConfig.LOG_OUTPUT_MODE,
+ LOG_LEVEL: currentConfig.LOG_LEVEL,
+ LOG_DIR: currentConfig.LOG_DIR,
+ LOG_INCLUDE_REQUEST_ID: currentConfig.LOG_INCLUDE_REQUEST_ID,
+ LOG_INCLUDE_TIMESTAMP: currentConfig.LOG_INCLUDE_TIMESTAMP,
+ LOG_MAX_FILE_SIZE: currentConfig.LOG_MAX_FILE_SIZE,
+ LOG_MAX_FILES: currentConfig.LOG_MAX_FILES,
+ SCHEDULED_HEALTH_CHECK: currentConfig.SCHEDULED_HEALTH_CHECK,
+ // 脱敏:只返回是否设置了 API Key,不返回原文
+ REQUIRED_API_KEY: currentConfig.REQUIRED_API_KEY ? '******' : '',
+ systemPrompt,
+ };
res.writeHead(200, { 'Content-Type': 'application/json' });
- res.end(JSON.stringify({
- ...currentConfig,
- systemPrompt
- }));
+ res.end(JSON.stringify(safeConfig));
return true;
}
@@ -129,8 +166,10 @@ export async function handleUpdateConfig(req, res, currentConfig) {
providerTypes: Array.isArray(incoming?.providerTypes) ? incoming.providerTypes : []
};
- // 如果定时器已存在且 enabled,重新加载 timer(interval 变化时)
- if (globalThis.reloadHealthCheckTimer && currentConfig.SCHEDULED_HEALTH_CHECK.enabled) {
+ // 如果定时器已存在且 enabled,仅在 interval 实际变化时重新加载 timer
+ const previousInterval = currentConfig.SCHEDULED_HEALTH_CHECK._activeInterval;
+ if (globalThis.reloadHealthCheckTimer && currentConfig.SCHEDULED_HEALTH_CHECK.enabled && newInterval !== previousInterval) {
+ currentConfig.SCHEDULED_HEALTH_CHECK._activeInterval = newInterval;
globalThis.reloadHealthCheckTimer(newInterval);
}
}
diff --git a/src/ui-modules/provider-api.js b/src/ui-modules/provider-api.js
index 2789de3..a2a02fc 100644
--- a/src/ui-modules/provider-api.js
+++ b/src/ui-modules/provider-api.js
@@ -115,7 +115,7 @@ export async function handleAddProvider(req, res, currentConfig, providerPoolMan
providerConfig.errorCount = providerConfig.errorCount || 0;
providerConfig.lastErrorTime = providerConfig.lastErrorTime || null;
- const filePath = currentConfig.PROVIDER_POOLS_FILE_PATH || 'provider_pools.json';
+ const filePath = currentConfig.PROVIDER_POOLS_FILE_PATH || 'configs/provider_pools.json';
let providerPools = {};
// Load existing pools
diff --git a/src/utils/provider-utils.js b/src/utils/provider-utils.js
index 905b8e8..d7e9134 100644
--- a/src/utils/provider-utils.js
+++ b/src/utils/provider-utils.js
@@ -97,11 +97,7 @@ export const PROVIDER_MAPPINGS = [
* @returns {string} UUID 字符串
*/
export function generateUUID() {
- return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
- const r = Math.random() * 16 | 0;
- const v = c === 'x' ? r : (r & 0x3 | 0x8);
- return v.toString(16);
- });
+ return crypto.randomUUID();
}
/**
diff --git a/static/app/config-manager.js b/static/app/config-manager.js
index 130bfdc..36ac1cb 100644
--- a/static/app/config-manager.js
+++ b/static/app/config-manager.js
@@ -53,10 +53,11 @@ function renderProviderTags(container, configs, isRequired) {
// 过滤掉不可见的提供商
const visibleConfigs = configs.filter(c => c.visible !== false);
+ const escHtml = s => String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
container.innerHTML = visibleConfigs.map(c => `
- <button type="button" class="provider-tag" data-value="${c.id}">
- <i class="fas ${c.icon || 'fa-server'}"></i>
- <span>${c.name}</span>
+ <button type="button" class="provider-tag" data-value="${escHtml(c.id)}">
+ <i class="fas ${escHtml(c.icon || 'fa-server')}"></i>
+ <span>${escHtml(c.name)}</span>
</button>
`).join('');
@@ -157,7 +158,7 @@ async function loadConfiguration() {
if (cronNearMinutesEl) cronNearMinutesEl.value = data.CRON_NEAR_MINUTES || 1;
if (cronRefreshTokenEl) cronRefreshTokenEl.checked = data.CRON_REFRESH_TOKEN || false;
if (loginExpiryEl) loginExpiryEl.value = data.LOGIN_EXPIRY || 3600;
- if (providerPoolsFilePathEl) providerPoolsFilePathEl.value = data.PROVIDER_POOLS_FILE_PATH;
+ if (providerPoolsFilePathEl) providerPoolsFilePathEl.value = data.PROVIDER_POOLS_FILE_PATH || '';
if (maxErrorCountEl) maxErrorCountEl.value = data.MAX_ERROR_COUNT || 10;
if (warmupTargetEl) warmupTargetEl.value = data.WARMUP_TARGET || 0;
if (refreshConcurrencyPerProviderEl) refreshConcurrencyPerProviderEl.value = data.REFRESH_CONCURRENCY_PER_PROVIDER || 1;
@@ -248,7 +249,7 @@ async function loadConfiguration() {
const scheduledHealthCheckIntervalEl = document.getElementById('scheduledHealthCheckInterval');
if (data.SCHEDULED_HEALTH_CHECK) {
- if (scheduledHealthCheckEnabledEl) scheduledHealthCheckEnabledEl.checked = data.SCHEDULED_HEALTH_CHECK.enabled !== false;
+ if (scheduledHealthCheckEnabledEl) scheduledHealthCheckEnabledEl.checked = data.SCHEDULED_HEALTH_CHECK.enabled === true;
if (scheduledHealthCheckStartupRunEl) scheduledHealthCheckStartupRunEl.checked = data.SCHEDULED_HEALTH_CHECK.startupRun !== false;
if (scheduledHealthCheckIntervalEl) scheduledHealthCheckIntervalEl.value = data.SCHEDULED_HEALTH_CHECK.interval || 600000;
} else {
diff --git a/static/components/section-config.css b/static/components/section-config.css
index e16338b..aebf6c4 100644
--- a/static/components/section-config.css
+++ b/static/components/section-config.css
@@ -173,7 +173,7 @@ textarea.form-control {
width: 18px;
left: 3px;
bottom: 2px;
- background-color: white;
+ background-color: var(--bg-primary, white);
transition: var(--transition);
border-radius: 50%;
box-shadow: 0 1px 3px var(--neutral-shadow-30);
325 lines
12 KiB
JavaScript
325 lines
12 KiB
JavaScript
/**
|
|
* Security Fixes Integration Test Suite
|
|
*
|
|
* 测试最近修复的安全问题:
|
|
* 1. XSS 防护 - sanitizeProviderData
|
|
* 2. 路径遍历防护 - 路径验证逻辑
|
|
* 3. 文件锁超时机制
|
|
* 4. 健康检查方法调用
|
|
*/
|
|
|
|
import { describe, test, expect } from '@jest/globals';
|
|
import { fetch } from 'undici';
|
|
|
|
const TEST_SERVER_BASE_URL = process.env.TEST_SERVER_BASE_URL || 'http://localhost:3000';
|
|
const TEST_API_KEY = process.env.TEST_API_KEY || '123456';
|
|
|
|
describe('Security Fixes Integration Tests', () => {
|
|
|
|
describe('XSS Protection', () => {
|
|
test('should remove script tags from customName', async () => {
|
|
const maliciousName = '<script>alert("XSS")</script>TestProvider';
|
|
const response = await fetch(`${TEST_SERVER_BASE_URL}/api/providers`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${TEST_API_KEY}`
|
|
},
|
|
body: JSON.stringify({
|
|
providerType: 'openai-custom',
|
|
providerConfig: {
|
|
customName: maliciousName,
|
|
OPENAI_CUSTOM_BASE_URL: 'https://api.example.com',
|
|
OPENAI_CUSTOM_API_KEY: 'test-key'
|
|
}
|
|
})
|
|
});
|
|
|
|
const data = await response.json();
|
|
expect(data.provider.customName).not.toContain('<script>');
|
|
expect(data.provider.customName).not.toContain('</script>');
|
|
expect(data.provider.customName).toContain('TestProvider');
|
|
});
|
|
|
|
test('should reject javascript: protocol', async () => {
|
|
const maliciousName = 'javascript:alert("XSS")';
|
|
const response = await fetch(`${TEST_SERVER_BASE_URL}/api/providers`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${TEST_API_KEY}`
|
|
},
|
|
body: JSON.stringify({
|
|
providerType: 'openai-custom',
|
|
providerConfig: {
|
|
customName: maliciousName,
|
|
OPENAI_CUSTOM_BASE_URL: 'https://api.example.com',
|
|
OPENAI_CUSTOM_API_KEY: 'test-key'
|
|
}
|
|
})
|
|
});
|
|
|
|
const data = await response.json();
|
|
expect(data.provider.customName).toBe('');
|
|
});
|
|
|
|
test('should reject data: protocol', async () => {
|
|
const maliciousName = 'data:text/html,<script>alert(1)</script>';
|
|
const response = await fetch(`${TEST_SERVER_BASE_URL}/api/providers`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${TEST_API_KEY}`
|
|
},
|
|
body: JSON.stringify({
|
|
providerType: 'openai-custom',
|
|
providerConfig: {
|
|
customName: maliciousName,
|
|
OPENAI_CUSTOM_BASE_URL: 'https://api.example.com',
|
|
OPENAI_CUSTOM_API_KEY: 'test-key'
|
|
}
|
|
})
|
|
});
|
|
|
|
const data = await response.json();
|
|
expect(data.provider.customName).toBe('');
|
|
});
|
|
|
|
test('should remove HTML event handlers', async () => {
|
|
const maliciousName = '<img src=x onerror="alert(1)">';
|
|
const response = await fetch(`${TEST_SERVER_BASE_URL}/api/providers`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${TEST_API_KEY}`
|
|
},
|
|
body: JSON.stringify({
|
|
providerType: 'openai-custom',
|
|
providerConfig: {
|
|
customName: maliciousName,
|
|
OPENAI_CUSTOM_BASE_URL: 'https://api.example.com',
|
|
OPENAI_CUSTOM_API_KEY: 'test-key'
|
|
}
|
|
})
|
|
});
|
|
|
|
const data = await response.json();
|
|
expect(data.provider.customName).not.toContain('onerror');
|
|
expect(data.provider.customName).not.toContain('<img');
|
|
});
|
|
|
|
test('should remove HTML entities', async () => {
|
|
const maliciousName = '<script>alert(1)</script>';
|
|
const response = await fetch(`${TEST_SERVER_BASE_URL}/api/providers`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${TEST_API_KEY}`
|
|
},
|
|
body: JSON.stringify({
|
|
providerType: 'openai-custom',
|
|
providerConfig: {
|
|
customName: maliciousName,
|
|
OPENAI_CUSTOM_BASE_URL: 'https://api.example.com',
|
|
OPENAI_CUSTOM_API_KEY: 'test-key'
|
|
}
|
|
})
|
|
});
|
|
|
|
const data = await response.json();
|
|
expect(data.provider.customName).not.toContain('<');
|
|
expect(data.provider.customName).not.toContain('>');
|
|
});
|
|
|
|
test('should preserve normal text', async () => {
|
|
const normalName = 'My Test Provider 123';
|
|
const response = await fetch(`${TEST_SERVER_BASE_URL}/api/providers`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${TEST_API_KEY}`
|
|
},
|
|
body: JSON.stringify({
|
|
providerType: 'openai-custom',
|
|
providerConfig: {
|
|
customName: normalName,
|
|
OPENAI_CUSTOM_BASE_URL: 'https://api.example.com',
|
|
OPENAI_CUSTOM_API_KEY: 'test-key'
|
|
}
|
|
})
|
|
});
|
|
|
|
const data = await response.json();
|
|
expect(data.provider.customName).toBe(normalName);
|
|
});
|
|
});
|
|
|
|
describe('Path Traversal Protection', () => {
|
|
test('should reject paths with ..', async () => {
|
|
const maliciousPath = '../../../etc/passwd';
|
|
const response = await fetch(`${TEST_SERVER_BASE_URL}/api/config`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${TEST_API_KEY}`
|
|
},
|
|
body: JSON.stringify({
|
|
SYSTEM_PROMPT_FILE_PATH: maliciousPath
|
|
})
|
|
});
|
|
|
|
expect(response.status).toBe(200);
|
|
|
|
const getResponse = await fetch(`${TEST_SERVER_BASE_URL}/api/config`, {
|
|
headers: {
|
|
'Authorization': `Bearer ${TEST_API_KEY}`
|
|
}
|
|
});
|
|
const config = await getResponse.json();
|
|
expect(config.SYSTEM_PROMPT_FILE_PATH).not.toBe(maliciousPath);
|
|
});
|
|
|
|
test('should accept valid paths within working directory', async () => {
|
|
const validPath = 'configs/my_prompt.txt';
|
|
const response = await fetch(`${TEST_SERVER_BASE_URL}/api/config`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${TEST_API_KEY}`
|
|
},
|
|
body: JSON.stringify({
|
|
SYSTEM_PROMPT_FILE_PATH: validPath
|
|
})
|
|
});
|
|
|
|
expect(response.status).toBe(200);
|
|
|
|
const getResponse = await fetch(`${TEST_SERVER_BASE_URL}/api/config`, {
|
|
headers: {
|
|
'Authorization': `Bearer ${TEST_API_KEY}`
|
|
}
|
|
});
|
|
const config = await getResponse.json();
|
|
expect(config.SYSTEM_PROMPT_FILE_PATH).toBe(validPath);
|
|
});
|
|
});
|
|
|
|
describe('Health Check Configuration', () => {
|
|
test('should save scheduled health check settings', async () => {
|
|
const response = await fetch(`${TEST_SERVER_BASE_URL}/api/config`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${TEST_API_KEY}`
|
|
},
|
|
body: JSON.stringify({
|
|
SCHEDULED_HEALTH_CHECK: {
|
|
enabled: true,
|
|
startupRun: true,
|
|
interval: 300000,
|
|
providerTypes: ['openai-custom']
|
|
}
|
|
})
|
|
});
|
|
|
|
expect(response.status).toBe(200);
|
|
|
|
const getResponse = await fetch(`${TEST_SERVER_BASE_URL}/api/config`, {
|
|
headers: {
|
|
'Authorization': `Bearer ${TEST_API_KEY}`
|
|
}
|
|
});
|
|
const config = await getResponse.json();
|
|
expect(config.SCHEDULED_HEALTH_CHECK).toBeDefined();
|
|
expect(config.SCHEDULED_HEALTH_CHECK.enabled).toBe(true);
|
|
expect(config.SCHEDULED_HEALTH_CHECK.interval).toBe(300000);
|
|
});
|
|
|
|
test('should enforce minimum interval', async () => {
|
|
const response = await fetch(`${TEST_SERVER_BASE_URL}/api/config`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${TEST_API_KEY}`
|
|
},
|
|
body: JSON.stringify({
|
|
SCHEDULED_HEALTH_CHECK: {
|
|
enabled: true,
|
|
interval: 30000
|
|
}
|
|
})
|
|
});
|
|
|
|
expect(response.status).toBe(200);
|
|
|
|
const getResponse = await fetch(`${TEST_SERVER_BASE_URL}/api/config`, {
|
|
headers: {
|
|
'Authorization': `Bearer ${TEST_API_KEY}`
|
|
}
|
|
});
|
|
const config = await getResponse.json();
|
|
expect(config.SCHEDULED_HEALTH_CHECK.interval).toBeGreaterThanOrEqual(60000);
|
|
});
|
|
});
|
|
|
|
describe('Configuration Validation', () => {
|
|
test('should reject invalid port numbers', async () => {
|
|
const response = await fetch(`${TEST_SERVER_BASE_URL}/api/config`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${TEST_API_KEY}`
|
|
},
|
|
body: JSON.stringify({
|
|
SERVER_PORT: 99999
|
|
})
|
|
});
|
|
|
|
expect(response.status).toBe(200);
|
|
|
|
const getResponse = await fetch(`${TEST_SERVER_BASE_URL}/api/config`, {
|
|
headers: {
|
|
'Authorization': `Bearer ${TEST_API_KEY}`
|
|
}
|
|
});
|
|
const config = await getResponse.json();
|
|
expect(config.SERVER_PORT).not.toBe(99999);
|
|
});
|
|
|
|
test('should reject excessive retry counts', async () => {
|
|
const response = await fetch(`${TEST_SERVER_BASE_URL}/api/config`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${TEST_API_KEY}`
|
|
},
|
|
body: JSON.stringify({
|
|
REQUEST_MAX_RETRIES: 999
|
|
})
|
|
});
|
|
|
|
expect(response.status).toBe(200);
|
|
|
|
const getResponse = await fetch(`${TEST_SERVER_BASE_URL}/api/config`, {
|
|
headers: {
|
|
'Authorization': `Bearer ${TEST_API_KEY}`
|
|
}
|
|
});
|
|
const config = await getResponse.json();
|
|
expect(config.REQUEST_MAX_RETRIES).toBeLessThanOrEqual(100);
|
|
});
|
|
|
|
test('should mask API key in response', async () => {
|
|
const response = await fetch(`${TEST_SERVER_BASE_URL}/api/config`, {
|
|
headers: {
|
|
'Authorization': `Bearer ${TEST_API_KEY}`
|
|
}
|
|
});
|
|
const config = await response.json();
|
|
|
|
if (config.REQUIRED_API_KEY) {
|
|
expect(config.REQUIRED_API_KEY).toContain('*');
|
|
}
|
|
});
|
|
});
|
|
});
|