295 lines
7.7 KiB
JavaScript
295 lines
7.7 KiB
JavaScript
"use strict";
|
|
|
|
const assert = require("assert");
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
const vm = require("vm");
|
|
|
|
const componentSource = fs.readFileSync(
|
|
path.join(__dirname, "../components/TacVerification.vue"),
|
|
"utf8",
|
|
);
|
|
const renderScript = componentSource.match(
|
|
/<script module="tacRenderer" lang="renderjs">([\s\S]*?)<\/script>/,
|
|
);
|
|
assert(renderScript, "必须保留 TacVerification 的 renderjs 桥接入口");
|
|
|
|
let latestXhr = null;
|
|
class FakeXMLHttpRequest {
|
|
static DONE = 4;
|
|
|
|
constructor() {
|
|
this.readyState = 0;
|
|
this.status = 0;
|
|
this.responseText = "";
|
|
this.headers = {};
|
|
this.aborted = false;
|
|
latestXhr = this;
|
|
}
|
|
|
|
open(method, url) {
|
|
this.method = method;
|
|
this.url = url;
|
|
}
|
|
|
|
setRequestHeader(name, value) {
|
|
this.headers[name] = value;
|
|
}
|
|
|
|
send(payload) {
|
|
this.payload = payload;
|
|
}
|
|
|
|
abort() {
|
|
this.aborted = true;
|
|
}
|
|
|
|
respond(status, body) {
|
|
this.status = status;
|
|
this.responseText = body;
|
|
this.readyState = FakeXMLHttpRequest.DONE;
|
|
if (this.onreadystatechange) this.onreadystatechange();
|
|
}
|
|
|
|
timeoutNow() {
|
|
if (this.ontimeout) this.ontimeout();
|
|
}
|
|
}
|
|
|
|
let latestTac = null;
|
|
class FakeCaptchaConfig {
|
|
constructor(options) {
|
|
this.options = options;
|
|
}
|
|
|
|
addRequestChain(chain) {
|
|
this.chain = chain;
|
|
}
|
|
}
|
|
|
|
class FakeTac {
|
|
constructor(config) {
|
|
this.config = config;
|
|
this.destroyCount = 0;
|
|
this.reloadCount = 0;
|
|
latestTac = this;
|
|
}
|
|
|
|
init() {
|
|
this.initialized = true;
|
|
}
|
|
|
|
destroyWindow() {
|
|
this.destroyCount += 1;
|
|
}
|
|
|
|
reloadCaptcha() {
|
|
this.reloadCount += 1;
|
|
}
|
|
}
|
|
|
|
const host = { innerHTML: "rendered" };
|
|
const loadedStyle = { dataset: { jiapuState: "loaded" }, sheet: {} };
|
|
const backgroundControl = {
|
|
focusCount: 0,
|
|
focus() {
|
|
this.focusCount += 1;
|
|
document.activeElement = this;
|
|
},
|
|
};
|
|
const refreshControl = {
|
|
offsetParent: {},
|
|
focus() {
|
|
document.activeElement = this;
|
|
},
|
|
};
|
|
const closeControl = {
|
|
offsetParent: {},
|
|
focus() {
|
|
document.activeElement = this;
|
|
},
|
|
};
|
|
const dialog = {
|
|
offsetParent: {},
|
|
listener: null,
|
|
focus() {
|
|
document.activeElement = this;
|
|
},
|
|
querySelector(selector) {
|
|
return selector === ".tac-tool--refresh" ? refreshControl : null;
|
|
},
|
|
querySelectorAll() {
|
|
return [refreshControl, closeControl];
|
|
},
|
|
addEventListener(name, listener) {
|
|
if (name === "keydown") this.listener = listener;
|
|
},
|
|
removeEventListener(name, listener) {
|
|
if (name === "keydown" && this.listener === listener) this.listener = null;
|
|
},
|
|
};
|
|
const document = {
|
|
activeElement: backgroundControl,
|
|
head: { appendChild() {} },
|
|
querySelector(selector) {
|
|
if (selector === "#jiapu-tac-host") return host;
|
|
if (selector === "#jiapu-tac-dialog") return dialog;
|
|
if (selector.startsWith("link[data-jiapu-tac=")) return loadedStyle;
|
|
return null;
|
|
},
|
|
createElement() {
|
|
throw new Error("本测试中的本地 TAC 资源应已就绪");
|
|
},
|
|
};
|
|
const window = {
|
|
CaptchaConfig: FakeCaptchaConfig,
|
|
TAC: FakeTac,
|
|
JiapuTacAdapter: {
|
|
buildVerifyBody: (data) => data,
|
|
normalizeChallengeResponse: () => ({ challenge: {}, sdkResponse: {} }),
|
|
normalizeVerifyResponse: (response) => response,
|
|
toSdkFailure: (error) => ({ code: 500, msg: error.message }),
|
|
},
|
|
};
|
|
const sandbox = {
|
|
console,
|
|
document,
|
|
window,
|
|
XMLHttpRequest: FakeXMLHttpRequest,
|
|
setTimeout: (callback) => {
|
|
callback();
|
|
return 1;
|
|
},
|
|
clearTimeout() {},
|
|
Promise,
|
|
Error,
|
|
Object,
|
|
};
|
|
sandbox.globalThis = sandbox;
|
|
vm.runInNewContext(
|
|
renderScript[1].replace("export default", "globalThis.__renderModule ="),
|
|
sandbox,
|
|
{ filename: "TacVerification.renderjs.js" },
|
|
);
|
|
|
|
const definition = sandbox.__renderModule;
|
|
assert(definition && definition.methods, "renderjs 必须导出可执行桥接模块");
|
|
const ownerCalls = [];
|
|
const instance = {
|
|
...definition.data(),
|
|
$ownerInstance: {
|
|
callMethod(name, payload) {
|
|
ownerCalls.push({ name, payload });
|
|
},
|
|
},
|
|
};
|
|
for (const [name, method] of Object.entries(definition.methods)) {
|
|
instance[name] = method.bind(instance);
|
|
}
|
|
|
|
const strictRequest = () => instance.sendStrictRequest({
|
|
method: "POST",
|
|
url: "https://backend-api.ddxcjp.cn/captcha/verify",
|
|
headers: { clientid: "client-1" },
|
|
data: { challengeId: "challenge-1" },
|
|
});
|
|
|
|
const run = async () => {
|
|
let pending = strictRequest();
|
|
latestXhr.respond(500, "");
|
|
assert.strictEqual((await pending).code, 500, "HTTP 500 空体不得被供应商 SDK 当作成功");
|
|
|
|
pending = strictRequest();
|
|
latestXhr.respond(200, "");
|
|
assert.strictEqual((await pending).code, 502, "HTTP 200 空体必须失败关闭");
|
|
|
|
pending = strictRequest();
|
|
latestXhr.respond(200, "null");
|
|
assert.strictEqual((await pending).code, 502, "HTTP 200 非对象 JSON 必须失败关闭");
|
|
|
|
pending = strictRequest();
|
|
latestXhr.timeoutNow();
|
|
assert.strictEqual((await pending).code, 504, "验证码请求超时必须返回可识别失败码");
|
|
|
|
strictRequest();
|
|
const abortedXhr = latestXhr;
|
|
instance.destroyTac();
|
|
assert.strictEqual(abortedXhr.aborted, true, "关闭验证层必须中止在途请求");
|
|
assert.strictEqual(instance.activeXhr, null);
|
|
|
|
const context = {
|
|
visible: true,
|
|
requestId: "request-1",
|
|
challengeUrl: "https://backend-api.ddxcjp.cn/captcha/challenge",
|
|
verifyUrl: "https://backend-api.ddxcjp.cn/captcha/verify",
|
|
clientId: "client-1",
|
|
tenantId: "000000",
|
|
sceneCode: "APP_REGISTER",
|
|
subject: "13800138000",
|
|
};
|
|
instance.context = context;
|
|
instance.previousFocus = backgroundControl;
|
|
instance.generation += 1;
|
|
instance.createTac();
|
|
assert.strictEqual(latestTac.initialized, true);
|
|
assert.strictEqual(typeof latestTac.config.doSendRequest, "function");
|
|
assert.strictEqual(document.activeElement, refreshControl, "打开 TAC 后必须把焦点移入对话框");
|
|
document.activeElement = closeControl;
|
|
let tabPrevented = false;
|
|
dialog.listener({
|
|
key: "Tab",
|
|
shiftKey: false,
|
|
preventDefault() {
|
|
tabPrevented = true;
|
|
},
|
|
});
|
|
assert.strictEqual(tabPrevented, true, "TAC 末项 Tab 必须被焦点环截获");
|
|
assert.strictEqual(document.activeElement, refreshControl);
|
|
|
|
const firstSuccess = latestTac.config.options.validSuccess;
|
|
firstSuccess(
|
|
{ data: { validToken: "ticket-1", expireSeconds: 300 } },
|
|
null,
|
|
latestTac,
|
|
);
|
|
firstSuccess(
|
|
{ data: { validToken: "ticket-duplicate", expireSeconds: 300 } },
|
|
null,
|
|
latestTac,
|
|
);
|
|
assert.strictEqual(
|
|
ownerCalls.filter(({ name }) => name === "handleTacSuccess").length,
|
|
1,
|
|
"同一轮供应商重复成功回调只能向逻辑层提交一次",
|
|
);
|
|
|
|
const staleSuccess = firstSuccess;
|
|
instance.generation += 1;
|
|
instance.context = { ...context, requestId: "request-2" };
|
|
staleSuccess({ data: { validToken: "stale", expireSeconds: 300 } }, null, latestTac);
|
|
assert.strictEqual(ownerCalls.length, 1, "换轮后的陈旧回调不得污染新页面状态");
|
|
|
|
instance.createTac();
|
|
const reopenedTac = latestTac;
|
|
reopenedTac.config.options.btnCloseFun(null, reopenedTac);
|
|
reopenedTac.config.options.btnCloseFun(null, reopenedTac);
|
|
assert.strictEqual(
|
|
ownerCalls.filter(({ name }) => name === "handleTacCancel").length,
|
|
1,
|
|
"关闭按钮重复回调只能提交一次取消",
|
|
);
|
|
|
|
instance.context = { ...context, requestId: "request-3" };
|
|
await instance.onContextChange({ visible: false });
|
|
assert.strictEqual(instance.tac, null, "隐藏或卸载验证层必须销毁 SDK 实例");
|
|
assert.strictEqual(host.innerHTML, "", "隐藏或卸载验证层必须清空宿主节点");
|
|
assert.strictEqual(backgroundControl.focusCount, 1, "关闭 TAC 后必须恢复进入前焦点");
|
|
|
|
console.log("TAC-RENDERJS-RUNTIME-SMOKE PASS");
|
|
};
|
|
|
|
run().catch((error) => {
|
|
console.error(error);
|
|
process.exitCode = 1;
|
|
});
|