import assert from 'node:assert/strict'; import { readFile } from 'node:fs/promises'; import http from 'node:http'; import test from 'node:test'; import { once } from 'node:events'; import { verifyKnowledgePlatform } from '../verify-knowledge-platform.mjs'; test('真实HTTP验证器覆盖正例、跨租户反例、伪造身份和限流', async (t) => { const rateLimit = 3; let rateProbes = 0; const server = http.createServer(async (request, response) => { const chunks = []; for await (const chunk of request) chunks.push(chunk); const body = chunks.length ? JSON.parse(Buffer.concat(chunks).toString('utf8')) : {}; const token = String(request.headers.authorization || '').replace(/^Bearer\s+/i, ''); let code = 200; let data = null; if (request.url === '/api/knowledge/admin/spaces') { data = token === 'admin-token' ? ['yc_public_policy', 'yc_property_sop', 'yc_management_ops'].map((value) => ({ code: value })) : null; code = data ? 200 : 401; } else if (token.endsWith('x') || !['employee-token', 'supervisor-token', 'meitu-token'].includes(token)) { code = 401; } else if (body.queryText === 'rate-limit-probe') { rateProbes += 1; code = rateProbes > rateLimit ? 429 : 403; } else if (request.url === '/api/open/knowledge/query' && body.toolCode) { code = 403; } else if (body.toolCode === 'TEAM_PRACTICE_SUMMARY' && token === 'employee-token') { code = 403; } else if (body.toolCode) { data = { citations: [{ sourceType: 'DATA_TOOL', docId: body.toolCode }], data: { scope: token === 'employee-token' ? 'SELF' : 'TEAM' } }; } else { const codeRequested = body.spaceCodes?.[0]; const isMeitu = token === 'meitu-token'; const allowed = isMeitu ? codeRequested === 'mt_customer_service' : ['yc_public_policy', 'yc_property_sop', 'yc_management_ops'].includes(codeRequested); const roleAllowed = codeRequested !== 'yc_management_ops' || token === 'supervisor-token'; if (!allowed || !roleAllowed) { code = 403; } else { data = { usedSpaceCodes: [codeRequested], citations: [{ sourceType: 'DOCUMENT', spaceCode: codeRequested }] }; } } response.writeHead(200, { 'Content-Type': 'application/json' }); response.end(JSON.stringify({ code, msg: code === 200 ? '操作成功' : 'rejected', data })); }); server.listen(0, '127.0.0.1'); await once(server, 'listening'); t.after(() => server.close()); const address = server.address(); const logs = []; const results = await verifyKnowledgePlatform({ AIHR_BASE_URL: `http://127.0.0.1:${address.port}`, AIHR_SILVER_ADMIN_TOKEN: 'admin-token', AIHR_SILVER_EMPLOYEE_TOKEN: 'employee-token', AIHR_SILVER_SUPERVISOR_TOKEN: 'supervisor-token', AIHR_MEITU_APP_TOKEN: 'meitu-token', AIHR_SHARED_DOC_QUERY: 'shared-marker', AIHR_MEITU_RATE_LIMIT: rateLimit, AIHR_VERIFY_RATE_LIMIT: true, AIHR_VERIFY_TIMEOUT_MS: 2_000 }, { log: (line) => logs.push(line) }); assert.equal(results.length, 15); assert.equal(results.every((item) => item.status === 'PASS'), true); assert.equal(logs.some((line) => /token/.test(line)), false); assert.equal(rateProbes, rateLimit + 1); }); test('服务端安全合同在RAG前拒绝空范围,且身份只取认证上下文', async () => { const root = new URL('../../', import.meta.url); const queryService = await readFile(new URL( 'backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/knowledge/service/AihrKnowledgeQueryService.java', root), 'utf8'); const queryDto = await readFile(new URL( 'backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/knowledge/domain/AihrKnowledgeQueryDto.java', root), 'utf8'); const accessService = await readFile(new URL( 'backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/knowledge/service/AihrKnowledgeAccessService.java', root), 'utf8'); const appService = await readFile(new URL( 'backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/knowledge/service/AihrKnowledgeAppService.java', root), 'utf8'); assert.doesNotMatch(queryDto, /tenantId|userId|extPartyId/); assert.match(queryService, /principalResolver\.current\(\)/); assert.match(queryService, /TenantHelper\.dynamic\(app\.tenantId\(\)/); assert.match(queryService, /resolveInternalSpaceIds[\s\S]*queryDocuments/); assert.match(accessService, /if \(effective\.isEmpty\(\)\)[\s\S]*throw forbidden/); assert.match(accessService, /where s\.tenant_id = \? and s\.app_id = \?/); assert.match(accessService, /where g\.tenant_id = \?/); assert.match(appService, /expiresTime\(\) == null \|\| row\.expiresTime\(\)\.isAfter/); assert.match(appService, /RateType\.OVERALL, rate, 60/); }); test('验证器从内部JWT自动携带clientid且不要求额外密钥配置', async () => { const source = await readFile(new URL('../verify-knowledge-platform.mjs', import.meta.url), 'utf8'); assert.match(source, /payload\.clientid/); assert.match(source, /clientid: clientId/); assert.doesNotMatch(source, /AIHR_.*CLIENT_ID/); }); test('破坏性解绑验证默认关闭且本地运行器最终撤销临时令牌', async () => { const verifier = await readFile(new URL('../verify-knowledge-platform.mjs', import.meta.url), 'utf8'); const localRunner = await readFile(new URL('../verify-knowledge-platform-local.mjs', import.meta.url), 'utf8'); assert.match(verifier, /AIHR_VERIFY_UNBIND: env\.AIHR_VERIFY_UNBIND === 'true'/); assert.match(verifier, /removed\.data\?\.ossDeleted !== false/); assert.match(verifier, /解绑后其他空间检索/); assert.match(localRunner, /finally \{[\s\S]*disableTemporaryMeituToken\(\)/); assert.doesNotMatch(localRunner, /console\.log\([^\n]*externalToken/); });