import test from 'node:test' import assert from 'node:assert/strict' import { openFixedActor, requireFixedActors } from './fixed-test-actors.mjs' import { AcceptanceRun } from '../admin-test-env-acceptance.mjs' import { readFileSync } from 'node:fs' function fixture(fault = '') { const calls = [], actor = { membershipId: '101', roleId: '201', roleCode: 'EXISTING_READER', username: 'existing-reader', password: 'private-fixed-password' } const context = { token: 'admin', options: { fixedActors: { reader: actor } }, async request(method, path, body, token) { calls.push({ method, path, body, token }) let data if (path.endsWith('/roles')) data = [{ id: '201', roleCode: actor.roleCode, status: fault === 'inactive role' ? 'DISABLED' : 'ACTIVE' }] else if (path.includes('/users?')) { const keyword = new URL(path, 'https://fixture.invalid').searchParams.get('keyword') // The service does not search login names; its keyword filters profile fields/member numbers. assert.equal(keyword, null) data = { page: 1, size: 100, total: 1, records: [{ membershipId: '101', username: actor.username, tenantId: fault === 'foreign user' ? '9' : '0', status: fault === 'inactive user' ? 'DISABLED' : 'ACTIVE', roleIds: fault === 'extra role' ? ['201', '999'] : ['201'] }] } } else if (path.endsWith('/login')) data = { accessToken: 'reader-session', refreshToken: 'reader-refresh' } else if (path.endsWith('/authorization/permissions')) data = fault === 'extra permissions' ? ['read', 'write'] : ['read'] else if (path.endsWith('/logout')) return { http: 200, body: { code: fault === 'logout fails' ? 'S0503' : '200' } } else assert.fail(path) return { http: 200, body: { code: '200', data } } } } return { context, calls, actor } } test('repeated runs reuse the same configured membership and role, and logout is idempotent', async () => { const { context, calls } = fixture() for (let run = 0; run < 3; run++) { const actor = await openFixedActor(context, 'reader', ['read']) assert.equal(actor.membershipId, '101'); assert.equal(actor.roleId, '201'); assert.equal(actor.reused, true) assert.equal(actor.password, undefined) await actor.close(); const count = calls.length; await actor.close(); assert.equal(calls.length, count) } assert.ok(calls.every(call => call.method === 'GET' || ['/api/auth/login', '/api/auth/logout'].includes(call.path))) assert.equal(calls.filter(call => call.path.endsWith('/logout')).length, 3) }) for (const fault of ['inactive role', 'foreign user', 'inactive user', 'extra role', 'extra permissions']) test(`fixed actor rejects ${fault} without changing resources`, async () => { const { context, calls } = fixture(fault) await assert.rejects(openFixedActor(context, 'reader', ['read']), /不匹配/) assert.equal(calls.some(call => ['PUT', 'PATCH', 'DELETE'].includes(call.method)), false) assert.equal(calls.some(call => call.method === 'POST' && !call.path.startsWith('/api/auth/')), false) if (fault === 'extra permissions') assert.equal(calls.at(-1).path, '/api/auth/logout') }) test('missing configuration or reusing one account for two isolated actors stops before HTTP', async () => { const { context, actor, calls } = fixture() await assert.rejects(openFixedActor(context, 'missing', []), /禁止自动创建/) assert.throws(() => requireFixedActors({ fixedActors: { a: actor, b: actor } }, ['a', 'b']), /不同的固定账号/) assert.equal(calls.length, 0) }) test('membership lookup finds a configured user on later pages without treating the login name as a searchable profile field', async () => { const { context, calls } = fixture(), original = context.request context.request = async (method, path, ...args) => { if (path.includes('/users?')) { const url = new URL(path, 'https://fixture.invalid') assert.equal(url.searchParams.has('keyword'), false) if (url.searchParams.get('page') === '1') return { http: 200, body: { code: '200', data: { page: 1, size: 100, total: 101, records: Array.from({ length: 100 }, (_, index) => ({ membershipId: String(1000 + index), username: 'unrelated' })) } } } const response = await original(method, path, ...args) response.body.data.page = 2; response.body.data.total = 101 return response } return original(method, path, ...args) } const actor = await openFixedActor(context, 'reader', ['read']) assert.equal(actor.membershipId, '101'); await actor.close() assert.equal(calls.some(call => call.path.includes('page=2')), true) }) test('logout failures remain visible and can be checked again without creating accounts', async () => { const { context, calls } = fixture('logout fails') const actor = await openFixedActor(context, 'reader', ['read']) await assert.rejects(actor.close(), /退出未确认/) assert.equal(calls.filter(call => call.path.endsWith('/logout')).length, 1) }) test('all Admin HTTP transports reject creation of roles, users, permissions and suppliers regardless of environment', async () => { for (const environment of [undefined, 'test', 'dev', 'prod']) { const context = new AcceptanceRun({ environment }) for (const path of ['/api/platform/v1/iam/roles', '/api/platform/v1/iam/users', '/api/platform/v1/iam/permissions', '/api/platform/v1/suppliers?tenantId=0']) for (const entry of ['request', 'requestWithBody', 'requestMultipart']) await assert.rejects(context[entry]('POST', path, new FormData()), /禁止新增角色/) } }) test('local transport and F33 handoff have no master provisioning paths', () => { for (const file of ['local-transfer-pickup-acceptance.mjs', 'local-dispatch-transport-acceptance.mjs', 'fba-remaining-nodes-acceptance-pda-operator.mjs']) { const source = readFileSync(new URL(`../${file}`, import.meta.url), 'utf8') assert.doesNotMatch(source, /['"]POST['"]\s*,\s*['"](?:\/api)?\/platform\/v1\/iam\/(roles|users|permissions)['"]/, file) assert.doesNotMatch(source, /['"]PUT['"]\s*,\s*['"](?:\/api)?\/platform\/v1\/iam\/roles\/permissions['"]/, file) } }) test('report security scans fixed actor passwords even if hidden under ordinary evidence keys', async () => { const context = new AcceptanceRun({ environment: 'test', fixedCustomerId: '123', quiet: true, modules: [], fixedActors: { reader: { password: 'fixed-sensitive-password' } } }) context.request = async () => ({ http: 200, body: { code: 'A0401' } }) context.createdResources.evidence = { note: 'fixed-sensitive-password' } const report = await context.run() assert.equal(report.steps.find(step => step.module === 'report-security').passed, false) })