Files
console-dev/tests/review-auth-hardening.e2e.test.mjs
T

222 lines
8.2 KiB
JavaScript

/**
* 修复前记录的失败场景:
* - 实名认证 HTML 应保留二维码刷新脚本,但不能读写控制台父页面。
* - 收到二维码地址不等于图片已加载;图片等待或损坏时不得开始登录轮询。
* - 图片损坏时应提供可访问的错误提示和重试入口;新图片加载成功后仍应允许正常登录。
* 所有场景均进入实际构建的页面,仅替换外部 HTTP 服务或浏览器时钟。
*/
import assert from "node:assert/strict";
import { test } from "node:test";
import {
appOrigin,
backendOrigin,
eventually,
login,
scenario,
} from "./helpers/console-browser.mjs";
const qrImage = "R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==";
function reply(route, data) {
return route.fulfill({
json: { status: 200, msg: "", data },
headers: { "Access-Control-Allow-Origin": "*" },
});
}
test(
"certification QR refresh scripts run without console-parent privileges",
{ timeout: 30000 },
(t) =>
scenario(t, {}, async (page) => {
await page.addInitScript(() => {
document.addEventListener(
"DOMContentLoaded",
() => {
document.documentElement.dataset.auditIsolationSeed =
"synthetic-local-marker";
},
{ once: true },
);
});
await page
.context()
.route(`${backendOrigin}/console/v1/certification/**`, (route) => {
const path = new URL(route.request().url()).pathname;
if (path.endsWith("/info"))
return reply(route, {
certification_open: 1,
certification_company_open: 0,
certification_upload: 0,
is_certification: 0,
person: { status: 4 },
});
if (path.endsWith("/auth"))
return reply(route, {
html: `<!doctype html><html><body><div id="qrcode"><img src="data:image/gif;base64,${qrImage}"></div><script>
document.documentElement.dataset.auditPluginScript = "ran";
try {
parent.document.documentElement.dataset.auditFromCertification =
parent.document.documentElement.dataset.auditIsolationSeed;
document.documentElement.dataset.auditParentAccess = "allowed";
} catch {
document.documentElement.dataset.auditParentAccess = "blocked";
}
setTimeout(() => {
const replacement = document.createElement("img");
replacement.src = "data:image/gif;base64,${qrImage}";
replacement.dataset.auditDynamicQr = "updated";
document.querySelector("#qrcode").replaceChildren(replacement);
}, 30);
</script></body></html>`,
});
if (path.endsWith("/status"))
return reply(route, { code: 2, refresh: 0 });
return route.fallback();
});
await login(page, "/account/realname");
await page
.getByRole("button", { name: "继续扫码核验", exact: true })
.click();
const element = await page.locator("iframe.auth-frame").elementHandle();
const frame = await element.contentFrame();
await frame
.locator('[data-certification-qr][data-audit-dynamic-qr="updated"]')
.waitFor();
const observed = {
refreshedQrVisible: await frame
.locator("[data-certification-qr]")
.isVisible(),
pluginScriptRan: await frame.evaluate(
() => document.documentElement.dataset.auditPluginScript === "ran",
),
parentAccessBlocked: await frame.evaluate(
() =>
document.documentElement.dataset.auditParentAccess === "blocked",
),
parentMarkerUntouched: await page.evaluate(
() =>
document.documentElement.dataset.auditFromCertification ===
undefined,
),
};
assert.equal(observed.refreshedQrVisible, true);
assert.equal(observed.pluginScriptRan, true);
assert.equal(observed.parentAccessBlocked, true);
assert.equal(observed.parentMarkerUntouched, true);
return observed;
}),
);
test(
"a broken WeChat image stops its scan and a fresh image recovers login",
{ timeout: 35000 },
(t) =>
scenario(t, {}, async (page) => {
await page.clock.install();
let acquisitions = 0;
let firstScanPolls = 0;
let secondScanPolls = 0;
let releaseImage;
const imageRelease = new Promise((resolve) => {
releaseImage = resolve;
});
let imageRequested;
const imageStarted = new Promise((resolve) => {
imageRequested = resolve;
});
await page.context().route(`${backendOrigin}/**`, async (route) => {
const url = new URL(route.request().url());
if (url.pathname === "/audit-qr/1.gif") {
imageRequested();
await imageRelease;
return route.fulfill({
contentType: "image/gif",
body: "invalid-image",
});
}
if (url.pathname === "/audit-qr/2.gif")
return route.fulfill({
contentType: "image/gif",
body: Buffer.from(qrImage, "base64"),
});
if (url.pathname === "/console/v1/common")
return reply(route, {
website_name: "本地扫码验收",
login_phone_password: 1,
login_email_password: 1,
plugin_configuration: { mp_weixin_notice: { scan_login: 1 } },
});
if (url.pathname === "/console/v1/mp_weixin_notice/qrcode") {
acquisitions++;
return reply(route, {
img_url: `${backendOrigin}/audit-qr/${acquisitions}.gif`,
ticket: `local-qr-ticket-${acquisitions}`,
token: "local-qr-token",
});
}
if (url.pathname === "/console/v1/mp_weixin_notice/qrcode/status") {
if (url.searchParams.get("ticket") === "local-qr-ticket-1") {
firstScanPolls++;
return reply(route, { status: "Wait" });
}
secondScanPolls++;
return reply(route, { status: "Success", jwt: "e2e-session-token" });
}
return route.fallback();
});
try {
await page.goto(`${appOrigin}/login?redirect=/security/ssh-keys`);
await page
.getByRole("button", { name: "获取二维码", exact: true })
.click();
await imageStarted;
await page.clock.runFor(5000);
const pollsBeforeImage = firstScanPolls;
releaseImage();
await eventually(
() =>
page.evaluate(() => {
const image = document.querySelector(".qr-img");
return !image || image.complete;
}),
Boolean,
"The invalid image must finish decoding",
);
await page.clock.runFor(5000);
const failureState = {
accessibleError: await page
.locator(".qr-status")
.getByRole("alert")
.isVisible(),
brokenImageRemoved: (await page.locator(".qr-img").count()) === 0,
pollsBeforeImage,
pollsAfterImageFailure: firstScanPolls,
};
await page
.getByRole("button", { name: "刷新二维码", exact: true })
.click();
await eventually(
() =>
page.evaluate(() => {
const image = document.querySelector(".qr-img");
return Boolean(image?.complete && image.naturalWidth > 0);
}),
Boolean,
"Retry must display a real decoded QR image",
);
await page.clock.runFor(2100);
await page.waitForURL((url) => url.pathname === "/security/ssh-keys");
await page.getByText("audit-01", { exact: true }).waitFor();
assert.equal(failureState.accessibleError, true);
assert.equal(failureState.brokenImageRemoved, true);
assert.equal(failureState.pollsBeforeImage, 0);
assert.equal(failureState.pollsAfterImageFailure, 0);
assert.equal(secondScanPolls, 1);
return { ...failureState, retryCompletedNormalLogin: true };
} finally {
releaseImage();
}
}),
);