106 lines
6.5 KiB
JavaScript
106 lines
6.5 KiB
JavaScript
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)
|
||
|
|
})
|