95 lines
3.7 KiB
JavaScript
95 lines
3.7 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { readFile } from 'node:fs/promises';
|
|
import test from 'node:test';
|
|
import vm from 'node:vm';
|
|
|
|
const [appSource, indexSource] = await Promise.all([
|
|
readFile(new URL('./app.js', import.meta.url), 'utf8'),
|
|
readFile(new URL('./index.html', import.meta.url), 'utf8')
|
|
]);
|
|
|
|
function loadNamedFunction(name) {
|
|
const start = appSource.indexOf(`function ${name}(`);
|
|
assert.notEqual(start, -1, `${name} must exist`);
|
|
const signatureEnd = appSource.indexOf(') {', start);
|
|
assert.notEqual(signatureEnd, -1, `${name} must have a complete signature`);
|
|
const bodyStart = signatureEnd + 2;
|
|
let depth = 0;
|
|
let end = -1;
|
|
for (let index = bodyStart; index < appSource.length; index += 1) {
|
|
if (appSource[index] === '{') depth += 1;
|
|
if (appSource[index] === '}') {
|
|
depth -= 1;
|
|
if (depth === 0) {
|
|
end = index + 1;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
assert.notEqual(end, -1, `${name} must have a complete body`);
|
|
const context = {};
|
|
vm.runInNewContext(`${appSource.slice(start, end)}; globalThis.loaded = ${name};`, context);
|
|
return context.loaded;
|
|
}
|
|
|
|
test('account creation accepts any non-empty password and blocks an empty password before the request', () => {
|
|
const validate = loadNamedFunction('validateAccountCreationValues');
|
|
const empty = validate({
|
|
username: 'operator',
|
|
password: '',
|
|
role: 'user',
|
|
erpAccount: 'erp-operator'
|
|
});
|
|
assert.equal(empty.ok, false);
|
|
assert.equal(empty.field, 'accountPassword');
|
|
assert.equal(empty.message, '请输入初始密码。');
|
|
const oneCharacter = validate({
|
|
username: 'operator',
|
|
password: '1',
|
|
role: 'user',
|
|
erpAccount: 'erp-operator'
|
|
});
|
|
assert.equal(oneCharacter.ok, true);
|
|
const createStart = appSource.indexOf('async function createAccountFromForm()');
|
|
const createEnd = appSource.indexOf('\n}\n\nasync function updateManagedAccount', createStart);
|
|
assert.notEqual(createStart, -1);
|
|
assert.notEqual(createEnd, -1);
|
|
const createSource = appSource.slice(createStart, createEnd);
|
|
assert.ok(createSource.indexOf('if (!validation.ok)') < createSource.indexOf("apiRequest('/api/accounts'"));
|
|
assert.match(createSource, /if \(!validation\.ok\) \{[\s\S]+return;[\s\S]+apiRequest\('\/api\/accounts'/u);
|
|
});
|
|
|
|
test('account creation normalizes valid form values into the server contract', () => {
|
|
const validate = loadNamedFunction('validateAccountCreationValues');
|
|
const result = validate({
|
|
username: ' TeamLead ',
|
|
password: '123456',
|
|
role: 'team_lead',
|
|
erpAccount: ' ERP-TeamLead '
|
|
});
|
|
assert.equal(result.ok, true);
|
|
assert.equal(result.body.username, 'TeamLead');
|
|
assert.equal(result.body.password, '123456');
|
|
assert.equal(result.body.role, 'team_lead');
|
|
assert.equal(result.body.erp_account, 'ERP-TeamLead');
|
|
assert.equal(Object.hasOwn(result.body, 'must_change_password'), false);
|
|
assert.equal(Array.isArray(result.body.business_route_ids), true);
|
|
assert.equal(result.body.business_route_ids.length, 0);
|
|
});
|
|
|
|
test('account creation translates backend password validation into an actionable message', () => {
|
|
const messageFor = loadNamedFunction('accountCreationErrorMessage');
|
|
const message = messageFor({
|
|
errorCode: 'invalid_request',
|
|
details: ['password'],
|
|
message: '请求参数不符合要求。'
|
|
});
|
|
assert.equal(message, '请输入初始密码。');
|
|
});
|
|
|
|
test('account forms expose no length rule or first-login forced-password flow', () => {
|
|
assert.match(indexSource, /初始密码<input id="accountPassword"[^>]+required>/u);
|
|
assert.doesNotMatch(indexSource, /accountMustChangePassword|首次登录必须修改密码|minlength="12"|12—512/u);
|
|
assert.doesNotMatch(appSource, /passwordChangeForced|must_change_password/u);
|
|
});
|