forked from wangziqi/gongxue-base
feat: enforce api production safety limits
This commit is contained in:
@@ -109,6 +109,8 @@ async function startServerIfNeeded() {
|
||||
...process.env,
|
||||
PORT: String(port),
|
||||
DATABASE_URL: process.env.DATABASE_URL || DEFAULT_DATABASE_URL,
|
||||
MAX_JSON_BODY_BYTES: process.env.MAX_JSON_BODY_BYTES || '8192',
|
||||
MAX_IMPORT_JSON_BODY_BYTES: process.env.MAX_IMPORT_JSON_BODY_BYTES || '65536',
|
||||
},
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
windowsHide: true,
|
||||
@@ -124,6 +126,56 @@ async function startServerIfNeeded() {
|
||||
await waitForHealth();
|
||||
}
|
||||
|
||||
async function waitForProcessExit(child, timeoutMs = 5000) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
child.kill();
|
||||
reject(new Error('process did not exit before timeout'));
|
||||
}, timeoutMs);
|
||||
|
||||
child.on('exit', (code, signal) => {
|
||||
clearTimeout(timer);
|
||||
resolve({ code, signal });
|
||||
});
|
||||
child.on('error', error => {
|
||||
clearTimeout(timer);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function testProductionConfigFailFast() {
|
||||
const port = await getFreePort();
|
||||
const child = spawn(process.execPath, ['apps/api/dist/apps/api/src/server.js'], {
|
||||
cwd: process.cwd(),
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_ENV: 'production',
|
||||
PORT: String(port),
|
||||
DATABASE_URL: process.env.DATABASE_URL || DEFAULT_DATABASE_URL,
|
||||
CORS_ORIGIN: '*',
|
||||
AUTH_SMS_PROVIDER: 'mock',
|
||||
AUTH_CODE_PEPPER: 'development-code-pepper-change-me',
|
||||
AUTH_SESSION_SECRET: 'development-session-secret-change-me',
|
||||
PLATFORM_ADMIN_API_KEY: 'local-platform-admin-key',
|
||||
},
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
windowsHide: true,
|
||||
});
|
||||
|
||||
let logs = '';
|
||||
child.stdout.on('data', chunk => {
|
||||
logs += chunk.toString();
|
||||
});
|
||||
child.stderr.on('data', chunk => {
|
||||
logs += chunk.toString();
|
||||
});
|
||||
|
||||
const result = await waitForProcessExit(child);
|
||||
assert.notEqual(result.code, 0, 'production server with unsafe defaults should fail to start');
|
||||
assert.match(logs, /Invalid production API configuration/, 'production fail-fast should explain unsafe config');
|
||||
}
|
||||
|
||||
function stopServer() {
|
||||
if (serverProcess && !serverProcess.killed) {
|
||||
serverProcess.kill();
|
||||
@@ -818,6 +870,38 @@ async function testTenantContentAssetsAndImports() {
|
||||
});
|
||||
assert.equal(deniedImport.code, 'TENANT_CONTENT_EDITOR_REQUIRED', 'student should not preview content import');
|
||||
|
||||
const oversizedNormalJson = await request('/api/auth/sms/send', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
phone: '13800000009',
|
||||
purpose: 'login',
|
||||
padding: 'x'.repeat(9000),
|
||||
},
|
||||
expectStatus: 413,
|
||||
});
|
||||
assert.equal(oversizedNormalJson.code, 'JSON_BODY_TOO_LARGE', 'ordinary JSON endpoints should enforce body size limit');
|
||||
|
||||
const largeImportPreview = await request('/api/tenant-content/imports/preview/questions', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'POST',
|
||||
body: {
|
||||
sourceName: 'large-but-allowed-question-import.json',
|
||||
subjectId: ids.subject,
|
||||
categoryId: ids.category,
|
||||
regionId: ids.region,
|
||||
items: [
|
||||
{
|
||||
legacyId: 'integration-large-preview-001',
|
||||
type: 'choice',
|
||||
content: `大体积导入预览:${'企业级导入需要受控上限。'.repeat(500)}`,
|
||||
options: ['正确', '错误'],
|
||||
correctOptionIndices: [0],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
assert.equal(largeImportPreview.job?.errorCount, 0, 'import preview should use larger bounded body limit');
|
||||
|
||||
const invalidPreview = await request('/api/tenant-content/imports/preview/questions', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'POST',
|
||||
@@ -1680,6 +1764,7 @@ async function testReferralAndCrmGrowth() {
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
await check('production config fail-fast', testProductionConfigFailFast);
|
||||
await startServerIfNeeded();
|
||||
console.log(`[INFO] API integration target: ${apiBase}`);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user