完成80%
This commit is contained in:
@@ -223,7 +223,7 @@ foreach ($contract in @(
|
||||
'const togglePasswordVisibility = () =>',
|
||||
'<AppToast :visible="feedbackVisible" :message="feedbackMessage" />',
|
||||
'<TacVerification',
|
||||
'AUTH_TAC_SCENE.SMS_LOGIN',
|
||||
'AUTH_VERIFICATION_OPERATION.SMS_LOGIN',
|
||||
'normalizeTacSuccess',
|
||||
'appApi.loginWithPassword',
|
||||
'appApi.loginWithSms',
|
||||
|
||||
@@ -20,10 +20,12 @@ const createHarnessFactory = (
|
||||
goRootResult = true,
|
||||
goRootError = null,
|
||||
initialSessionToken = "",
|
||||
captchaRequired = true,
|
||||
) => new Function(
|
||||
"goRootResult",
|
||||
"goRootError",
|
||||
"initialSessionToken",
|
||||
"captchaRequired",
|
||||
`
|
||||
"use strict";
|
||||
const calls = [];
|
||||
@@ -56,10 +58,10 @@ const createHarnessFactory = (
|
||||
async getCaptchaRequirement(payload) {
|
||||
calls.push({ type: "require", payload: { ...payload } });
|
||||
return {
|
||||
required: true,
|
||||
required: captchaRequired,
|
||||
providerCode: "TIANAI",
|
||||
captchaType: "SLIDER",
|
||||
sceneCode: payload.sceneCode,
|
||||
sceneCode: "APP_PASSWORD_LOGIN",
|
||||
ttlSeconds: 300,
|
||||
};
|
||||
},
|
||||
@@ -76,11 +78,14 @@ const createHarnessFactory = (
|
||||
return { access_token: "token-sms" };
|
||||
},
|
||||
};
|
||||
const AUTH_TAC_SCENE = Object.freeze({ SMS_LOGIN: "APP_SMS_LOGIN" });
|
||||
const AUTH_VERIFICATION_OPERATION = Object.freeze({
|
||||
PASSWORD_LOGIN: "password-login",
|
||||
SMS_LOGIN: "sms-login",
|
||||
});
|
||||
const createTacRenderContext = (value) => ({ ...value });
|
||||
const isAuthPhone = (value) => /^1[3-9]\\d{9}$/.test(value);
|
||||
const normalizeCaptchaRequirement = (value, expectedScene) => {
|
||||
if (value?.sceneCode !== expectedScene || value?.required !== true) {
|
||||
const normalizeCaptchaRequirement = (value) => {
|
||||
if (typeof value?.required !== "boolean") {
|
||||
throw new Error("requirement invalid");
|
||||
}
|
||||
return value;
|
||||
@@ -141,13 +146,14 @@ const createHarnessFactory = (
|
||||
},
|
||||
};
|
||||
`,
|
||||
)(goRootResult, goRootError, initialSessionToken);
|
||||
)(goRootResult, goRootError, initialSessionToken, captchaRequired);
|
||||
|
||||
const createHarness = (options = {}) =>
|
||||
createHarnessFactory(
|
||||
options.goRootResult ?? true,
|
||||
options.goRootError ?? null,
|
||||
options.initialSessionToken ?? "",
|
||||
options.captchaRequired ?? true,
|
||||
);
|
||||
|
||||
const countCalls = (harness, type) =>
|
||||
@@ -193,6 +199,7 @@ const run = async () => {
|
||||
assert.deepStrictEqual(success.calls[1].payload, {
|
||||
phone: "13800138000",
|
||||
passwordHash: "md5:secret-1",
|
||||
validToken: "ticket-1",
|
||||
});
|
||||
assert.strictEqual(success.calls[2].pageId, "G01");
|
||||
assert.strictEqual(countCalls(success, "require"), 1);
|
||||
@@ -200,6 +207,18 @@ const run = async () => {
|
||||
assert.strictEqual(success.tacVisible.value, false);
|
||||
assert.strictEqual(success.tacContext.value, null);
|
||||
|
||||
const noCaptcha = createHarness({ captchaRequired: false });
|
||||
noCaptcha.phone.value = "13800138000";
|
||||
noCaptcha.password.value = "secret-1";
|
||||
noCaptcha.agreed.value = true;
|
||||
await noCaptcha.submitLogin();
|
||||
assert.deepStrictEqual(
|
||||
noCaptcha.calls.map((call) => call.type),
|
||||
["require", "password-login", "go-root"],
|
||||
"服务端关闭 password-login TAC 时必须直接走已发布密码登录请求",
|
||||
);
|
||||
assert.strictEqual(noCaptcha.tacVisible.value, false);
|
||||
|
||||
const navigationRejected = createHarness({ goRootResult: false });
|
||||
await preparePassword(navigationRejected);
|
||||
await completePasswordTac(navigationRejected);
|
||||
|
||||
@@ -47,7 +47,7 @@ foreach ($required in @(
|
||||
'const submitRegister = async () =>',
|
||||
'<AppToast :visible="feedbackVisible" :message="feedbackMessage" />',
|
||||
'<TacVerification',
|
||||
'AUTH_TAC_SCENE.REGISTER',
|
||||
'AUTH_VERIFICATION_OPERATION.REGISTER',
|
||||
'appApi.registerWithPassword',
|
||||
'if (!/^\d{4}$/.test(verificationCode.value))',
|
||||
'if (!isAuthPhone(phone.value))',
|
||||
|
||||
@@ -46,7 +46,7 @@ foreach ($required in @(
|
||||
'if (!isAuthPhone(phone.value))',
|
||||
'if (!/^\d{4}$/.test(verificationCode.value))',
|
||||
'<TacVerification',
|
||||
'AUTH_TAC_SCENE.FORGOT_PASSWORD',
|
||||
'AUTH_VERIFICATION_OPERATION.FORGOT_PASSWORD',
|
||||
'await appApi.resetPassword',
|
||||
'validatePassword(password.value)',
|
||||
'PASSWORD_POLICY_MESSAGE',
|
||||
|
||||
@@ -16,7 +16,7 @@ const routeEnd = Number(process.env.PAGE_AUDIT_END || 0);
|
||||
const wait = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
|
||||
|
||||
const pageExpectations = {
|
||||
"pages/auth/a01-entry": { title: "", noReload: true, skipTitle: true },
|
||||
"pages/auth/a01-entry": { title: "", noReload: true, skipTitle: true, allowAuthenticatedRedirect: true },
|
||||
"pages/auth/a04-register": { title: "注册账号" },
|
||||
"pages/auth/a05-reset-password": { title: "重设密码" },
|
||||
"pages/genealogy/g01-my-genealogies": { title: "我的家谱", capture: "01-genealogies.png" },
|
||||
@@ -47,7 +47,7 @@ const pageExpectations = {
|
||||
"pages/family/f10-video-list": { title: "家族视频", query: `genealogyId=${genealogyId}` },
|
||||
"pages/records/r01-people-list": { title: "人物录", query: `genealogyId=${genealogyId}` },
|
||||
"pages/records/r02-person-detail": { title: "人物详情", query: `genealogyId=${genealogyId}&state=error` },
|
||||
"pages/records/r03-gift-list": { title: "亲友往来", query: `genealogyId=${genealogyId}`, capture: "05-gifts.png" },
|
||||
"pages/records/r03-gift-list": { title: "贺礼簿", query: `genealogyId=${genealogyId}`, capture: "05-gifts.png" },
|
||||
"pages/records/r04-gift-editor": { title: "新建往来记录", query: `genealogyId=${genealogyId}&mode=create` },
|
||||
"pages/records/r05-ritual-list": { title: "礼仪活动", query: `genealogyId=${genealogyId}` },
|
||||
"pages/records/r06-ritual-detail": { title: "礼仪详情", query: `genealogyId=${genealogyId}&ceremonyId=0` },
|
||||
@@ -111,13 +111,24 @@ const waitFor = async (send, expression, message) => {
|
||||
};
|
||||
|
||||
const open = async (send, route, noReload) => {
|
||||
const expectation = pageExpectations[route];
|
||||
const url = `${origin}/#/${route}${pageExpectations[route].query ? `?${pageExpectations[route].query}` : ""}`;
|
||||
await send("Page.navigate", { url });
|
||||
await waitFor(send, `location.href === ${JSON.stringify(url)}`, `navigation failed: ${route}`);
|
||||
const authenticatedRedirect = `${origin}/#/pages/genealogy/g01-my-genealogies`;
|
||||
const expectedLocation = pageExpectations[route].allowAuthenticatedRedirect
|
||||
? `location.href === ${JSON.stringify(url)} || location.href === ${JSON.stringify(authenticatedRedirect)}`
|
||||
: `location.href === ${JSON.stringify(url)}`;
|
||||
await waitFor(send, expectedLocation, `navigation failed: ${route}`);
|
||||
if (!expectation.skipTitle) {
|
||||
await waitFor(send, `document.body?.innerText.includes(${JSON.stringify(expectation.title)})`, `title did not render: ${route}`);
|
||||
}
|
||||
if (!noReload) {
|
||||
const previousTimeOrigin = await evaluate(send, "performance.timeOrigin");
|
||||
await send("Page.reload");
|
||||
send("Page.reload").catch(() => {});
|
||||
await waitFor(send, `performance.timeOrigin !== ${JSON.stringify(previousTimeOrigin)}`, `reload failed: ${route}`);
|
||||
if (!expectation.skipTitle) {
|
||||
await waitFor(send, `document.body?.innerText.includes(${JSON.stringify(expectation.title)})`, `title did not render after reload: ${route}`);
|
||||
}
|
||||
}
|
||||
await waitFor(send, "document.body && document.body.innerText.length > 0", `page did not render: ${route}`);
|
||||
await wait(700);
|
||||
@@ -131,7 +142,7 @@ const capture = async (send, filename) => {
|
||||
|
||||
const run = async () => {
|
||||
const allRoutes = require("../pages.json").pages.map((page) => page.path);
|
||||
assert.deepStrictEqual(allRoutes.sort(), Object.keys(pageExpectations).sort(), "route coverage drifted from pages.json");
|
||||
assert.deepStrictEqual([...allRoutes].sort(), Object.keys(pageExpectations).sort(), "route coverage drifted from pages.json");
|
||||
const routes = routeEnd > routeStart ? allRoutes.slice(routeStart, routeEnd) : allRoutes;
|
||||
const { socket, send } = await connect();
|
||||
const results = [];
|
||||
|
||||
@@ -34,10 +34,11 @@ const run = async () => {
|
||||
};
|
||||
const hasRemoteConfig = () => true;
|
||||
const resolveRuntimeMode = () => "remote";
|
||||
const AUTH_TAC_SCENE = Object.freeze({
|
||||
SMS_LOGIN: "APP_SMS_LOGIN",
|
||||
REGISTER: "APP_REGISTER",
|
||||
FORGOT_PASSWORD: "APP_FORGOT_PASSWORD",
|
||||
const AUTH_VERIFICATION_OPERATION = Object.freeze({
|
||||
PASSWORD_LOGIN: "password-login",
|
||||
SMS_LOGIN: "sms-login",
|
||||
REGISTER: "register",
|
||||
FORGOT_PASSWORD: "forgot-password",
|
||||
});
|
||||
const assertSmsCode = (value) => {
|
||||
if (typeof value !== "string" || !/^\\d{4}$/.test(value)) throw new Error("请输入 4 位短信验证码");
|
||||
@@ -84,7 +85,7 @@ const run = async () => {
|
||||
nextResponse = response;
|
||||
};
|
||||
const sendSms = () => appApi.sendSmsCode({
|
||||
sceneCode: "APP_REGISTER",
|
||||
operationCode: "register",
|
||||
phone: "13800138000",
|
||||
validToken: "ticket-1",
|
||||
});
|
||||
@@ -114,6 +115,26 @@ const run = async () => {
|
||||
assert.strictEqual(await sendSms(), null, "RVoid 未声明 data 必填,省略 data 仍必须解析为 null");
|
||||
assert.strictEqual(requests.at(-1).header.clientid, "client-1");
|
||||
assert.strictEqual(requests.at(-1).header.tenantId, "000000");
|
||||
assert.strictEqual(
|
||||
requests.at(-1).url,
|
||||
"https://backend-api.ddxcjp.cn/genealogy/app/auth/sms/register/code",
|
||||
);
|
||||
assert.deepStrictEqual(requests.at(-1).data, {
|
||||
tenantId: "000000",
|
||||
grantType: "sms",
|
||||
phone: "13800138000",
|
||||
validToken: "ticket-1",
|
||||
});
|
||||
respond({ statusCode: 200, data: { code: 200 } });
|
||||
await appApi.sendSmsCode({
|
||||
operationCode: "register",
|
||||
phone: "13800138000",
|
||||
});
|
||||
assert.deepStrictEqual(requests.at(-1).data, {
|
||||
tenantId: "000000",
|
||||
grantType: "sms",
|
||||
phone: "13800138000",
|
||||
}, "策略关闭时发码请求不得伪造 validToken");
|
||||
|
||||
// 已鉴权读取收到业务 401 时,只清理失效的本地会话;请求仍向调用方失败返回。
|
||||
respond({ statusCode: 200, data: { code: 401, msg: "认证失败", data: null } });
|
||||
@@ -144,6 +165,26 @@ const run = async () => {
|
||||
"找回密码的 RVoid 省略 data 时仍必须解析为 null",
|
||||
);
|
||||
|
||||
respond({
|
||||
statusCode: 200,
|
||||
data: { code: 200, msg: "操作成功", data: { access_token: "token-1" } },
|
||||
});
|
||||
respond({ statusCode: 200, data: { code: 200 } });
|
||||
await appApi.sendLegacySmsCode({
|
||||
phone: "13800138000",
|
||||
validToken: "ticket-legacy",
|
||||
});
|
||||
assert.strictEqual(
|
||||
requests.at(-1).url,
|
||||
"https://backend-api.ddxcjp.cn/genealogy/app/auth/sms/code",
|
||||
);
|
||||
assert.deepStrictEqual(requests.at(-1).data, {
|
||||
tenantId: "000000",
|
||||
grantType: "sms",
|
||||
phone: "13800138000",
|
||||
validToken: "ticket-legacy",
|
||||
});
|
||||
|
||||
respond({
|
||||
statusCode: 200,
|
||||
data: { code: 200, msg: "操作成功", data: { access_token: "token-1" } },
|
||||
@@ -159,6 +200,7 @@ const run = async () => {
|
||||
const passwordLogin = await appApi.loginWithPassword({
|
||||
phone: "13800138000",
|
||||
passwordHash: "a".repeat(32),
|
||||
validToken: "ticket-password",
|
||||
});
|
||||
assert.strictEqual(passwordLogin.access_token, "token-password");
|
||||
const passwordRequest = requests.at(-1);
|
||||
@@ -167,11 +209,11 @@ const run = async () => {
|
||||
assert.strictEqual(passwordRequest.header.clientid, "client-1");
|
||||
assert.strictEqual(passwordRequest.header.Authorization, undefined);
|
||||
assert.deepStrictEqual(passwordRequest.data, {
|
||||
clientId: "client-1",
|
||||
tenantId: "000000",
|
||||
grantType: "password",
|
||||
phone: "13800138000",
|
||||
password: "a".repeat(32),
|
||||
validToken: "ticket-password",
|
||||
});
|
||||
assert.deepStrictEqual(savedTokens, ["token-1", "token-password"]);
|
||||
|
||||
@@ -224,7 +266,7 @@ const run = async () => {
|
||||
const requestController = createRequestController();
|
||||
const cancelled = appApi.sendSmsCode(
|
||||
{
|
||||
sceneCode: "APP_REGISTER",
|
||||
operationCode: "register",
|
||||
phone: "13800138000",
|
||||
validToken: "ticket-1",
|
||||
},
|
||||
|
||||
@@ -26,7 +26,7 @@ const run = async () => {
|
||||
|
||||
const firstValues = [];
|
||||
const first = createAuthSmsCooldown({
|
||||
sceneCode: "APP_SMS_LOGIN",
|
||||
operationCode: "sms-login",
|
||||
onChange: (value) => firstValues.push(value),
|
||||
now: () => now,
|
||||
setIntervalFn,
|
||||
@@ -43,7 +43,7 @@ const run = async () => {
|
||||
|
||||
const restoredValues = [];
|
||||
const restored = createAuthSmsCooldown({
|
||||
sceneCode: "APP_SMS_LOGIN",
|
||||
operationCode: "sms-login",
|
||||
onChange: (value) => restoredValues.push(value),
|
||||
now: () => now,
|
||||
setIntervalFn,
|
||||
@@ -56,7 +56,7 @@ const run = async () => {
|
||||
);
|
||||
|
||||
const other = createAuthSmsCooldown({
|
||||
sceneCode: "APP_REGISTER",
|
||||
operationCode: "register",
|
||||
onChange: () => {},
|
||||
now: () => now,
|
||||
setIntervalFn,
|
||||
@@ -72,10 +72,10 @@ const run = async () => {
|
||||
assert.throws(
|
||||
() =>
|
||||
createAuthSmsCooldown({
|
||||
sceneCode: "",
|
||||
operationCode: "",
|
||||
onChange: () => {},
|
||||
}),
|
||||
/sceneCode/,
|
||||
/operationCode/,
|
||||
);
|
||||
|
||||
process.stdout.write("AUTH-SMS-COOLDOWN-RUNTIME-SMOKE PASS\n");
|
||||
|
||||
@@ -63,7 +63,7 @@ $authApiMatch = [regex]::Match($api, '(?s)async getCaptchaRequirement.*?(?=\s+as
|
||||
if (-not $authApiMatch.Success) { throw '无法定位唯一认证 API 区段' }
|
||||
Reject-Text -Content $authApiMatch.Value -Text "return { success: true }" -Label '认证短信 mock'
|
||||
Reject-Text -Content $authApiMatch.Value -Text "mock-session-token" -Label '认证会话 mock'
|
||||
Require-Text -Content $api -Text 'sceneCode, phone, validToken' -Label '短信票据请求'
|
||||
Require-Text -Content $api -Text 'operationCode, phone, validToken' -Label '短信票据请求'
|
||||
foreach ($token in @('const requestAuth =', 'strictEnvelope: true', 'expectedStatus: 200', "hasOwnProperty.call(data, 'data')")) {
|
||||
Require-Text -Content $api -Text $token -Label '认证严格响应 owner'
|
||||
}
|
||||
@@ -115,17 +115,17 @@ foreach ($token in @('const blockBusyAction = () =>', 'if (blockBusyAction()) re
|
||||
Require-Text -Content $a01 -Text $token -Label 'A01 忙碌动作门禁'
|
||||
}
|
||||
|
||||
foreach ($token in @('AUTH_TAC_SCENE.SMS_LOGIN', 'appApi.loginWithPassword', 'appApi.loginWithSms', 'calcMD5(password.value)', 'const preparePasswordLogin = async () =>', '/^\d{4}$/')) {
|
||||
foreach ($token in @('AUTH_VERIFICATION_OPERATION.PASSWORD_LOGIN', 'AUTH_VERIFICATION_OPERATION.SMS_LOGIN', 'appApi.loginWithPassword', 'appApi.loginWithSms', 'calcMD5(password.value)', 'const preparePasswordLogin = async () =>', '/^\d{4}$/')) {
|
||||
Require-Text -Content $a01 -Text $token -Label 'A01'
|
||||
}
|
||||
Reject-Text -Content $a01 -Text 'PASSWORD_TAC_BLOCKED_MESSAGE' -Label 'A01 旧密码登录硬关闭'
|
||||
Reject-Text -Content $a01 -Text '/^\d{6}$/' -Label 'A01 六位短信码'
|
||||
|
||||
foreach ($token in @('AUTH_TAC_SCENE.REGISTER', 'v-model.trim="verificationCode"', 'appApi.registerWithPassword', 'calcMD5(password.value)', '/^\d{4}$/', 'goRoot("G01")')) {
|
||||
foreach ($token in @('AUTH_VERIFICATION_OPERATION.REGISTER', 'v-model.trim="verificationCode"', 'appApi.registerWithPassword', 'calcMD5(password.value)', '/^\d{4}$/', 'goRoot("G01")')) {
|
||||
Require-Text -Content $a04 -Text $token -Label 'A04'
|
||||
}
|
||||
|
||||
foreach ($token in @('AUTH_TAC_SCENE.FORGOT_PASSWORD', 'appApi.resetPassword', 'calcMD5(password.value)', '/^\d{4}$/', 'await appApi.resetPassword')) {
|
||||
foreach ($token in @('AUTH_VERIFICATION_OPERATION.FORGOT_PASSWORD', 'appApi.resetPassword', 'calcMD5(password.value)', '/^\d{4}$/', 'await appApi.resetPassword')) {
|
||||
Require-Text -Content $a05 -Text $token -Label 'A05'
|
||||
}
|
||||
Reject-Text -Content $a05 -Text '/^\d{6}$/' -Label 'A05 六位短信码'
|
||||
|
||||
@@ -15,23 +15,23 @@ foreach ($path in @($apiPath, $a01Path, $runtimePath)) {
|
||||
if ($issues.Count -eq 0) {
|
||||
$api = Get-Content -Raw -Encoding UTF8 -LiteralPath $apiPath
|
||||
$a01 = Get-Content -Raw -Encoding UTF8 -LiteralPath $a01Path
|
||||
$passwordOwner = [regex]::Match($api, '(?s)async loginWithPassword\(\{ phone, passwordHash \}.*?(?=\s+async loginWithSms)')
|
||||
$passwordOwner = [regex]::Match($api, '(?s)async loginWithPassword\(\{ phone, passwordHash, validToken \}.*?(?=\s+async loginWithSms)')
|
||||
if (-not $passwordOwner.Success) {
|
||||
$issues.Add('missing bounded password login owner')
|
||||
} else {
|
||||
foreach ($required in @("url: '/genealogy/app/auth/login'", "grantType: 'password'", 'password: assertPasswordHash(passwordHash)')) {
|
||||
foreach ($required in @("url: '/genealogy/app/auth/login'", "tenantId: runtimeConfig.tenantId", "grantType: 'password'", 'password: assertPasswordHash(passwordHash)', 'const normalizedValidToken = normalizeOptionalValidToken(validToken)', '...(normalizedValidToken ? { validToken: normalizedValidToken } : {})')) {
|
||||
if (-not $passwordOwner.Value.Contains($required)) { $issues.Add("password login owner missing: $required") }
|
||||
}
|
||||
if ($passwordOwner.Value.Contains('validToken')) {
|
||||
$issues.Add('password login must not upload validToken')
|
||||
if ($passwordOwner.Value.Contains('authPayload(')) {
|
||||
$issues.Add('password login must not put clientId in the body')
|
||||
}
|
||||
}
|
||||
foreach ($required in @('const preparePasswordLogin = async () =>', 'appApi.loginWithPassword', 'TAC')) {
|
||||
if (-not $a01.Contains($required)) { $issues.Add("A01 missing password TAC precondition: $required") }
|
||||
}
|
||||
$smsOwner = [regex]::Match($api, '(?s)async sendSmsCode\(\{ sceneCode, phone, validToken \}.*?(?=\s+async loginWithPassword)')
|
||||
if (-not $smsOwner.Success -or -not $smsOwner.Value.Contains('validToken: assertValidToken(validToken)')) {
|
||||
$issues.Add('SMS owner must remain the sole validToken consumer')
|
||||
$smsOwner = [regex]::Match($api, '(?s)async sendSmsCode\(\{ operationCode, phone, validToken \}.*?(?=\s+async loginWithPassword)')
|
||||
if (-not $smsOwner.Success -or -not $smsOwner.Value.Contains('const normalizedValidToken = normalizeOptionalValidToken(validToken)') -or -not $smsOwner.Value.Contains('...(normalizedValidToken ? { validToken: normalizedValidToken } : {})')) {
|
||||
$issues.Add('SMS owner must remain the sole validToken consumer and omit it only when the server policy closes TAC')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ if ($issues.Count -eq 0) {
|
||||
if ($issues.Count -gt 0) {
|
||||
Write-Output 'AUTH-TAC-OPENAPI-CONTRACT BLOCKED'
|
||||
foreach ($issue in $issues) { Write-Output "- $issue" }
|
||||
Write-Output '- Password login is gated by native TAC on the client and must not send validToken. SMS operations consume their own validToken only.'
|
||||
Write-Output '- Password login and SMS operations send a validToken only after the server marks that operation as verification-required.'
|
||||
exit 1
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ const run = async () => {
|
||||
"utf8",
|
||||
);
|
||||
const {
|
||||
AUTH_TAC_SCENE,
|
||||
AUTH_VERIFICATION_OPERATION,
|
||||
isAuthPhone,
|
||||
assertSmsCode,
|
||||
normalizeCaptchaRequirement,
|
||||
@@ -23,13 +23,14 @@ const run = async () => {
|
||||
} = await import(toDataModuleUrl(source));
|
||||
|
||||
assert.deepStrictEqual(
|
||||
{ ...AUTH_TAC_SCENE },
|
||||
{ ...AUTH_VERIFICATION_OPERATION },
|
||||
{
|
||||
SMS_LOGIN: "APP_SMS_LOGIN",
|
||||
REGISTER: "APP_REGISTER",
|
||||
FORGOT_PASSWORD: "APP_FORGOT_PASSWORD",
|
||||
PASSWORD_LOGIN: "password-login",
|
||||
SMS_LOGIN: "sms-login",
|
||||
REGISTER: "register",
|
||||
FORGOT_PASSWORD: "forgot-password",
|
||||
},
|
||||
"认证 TAC 场景必须由受保护 OpenAPI 的唯一枚举拥有",
|
||||
"认证动作必须由受保护 OpenAPI 的唯一枚举拥有",
|
||||
);
|
||||
|
||||
assert.strictEqual(isAuthPhone("13800138000"), true);
|
||||
@@ -47,32 +48,28 @@ const run = async () => {
|
||||
required: true,
|
||||
providerCode: "TIANAI",
|
||||
captchaType: "SLIDER",
|
||||
sceneCode: AUTH_TAC_SCENE.REGISTER,
|
||||
sceneCode: "APP_REGISTER",
|
||||
ttlSeconds: 300,
|
||||
},
|
||||
AUTH_TAC_SCENE.REGISTER,
|
||||
);
|
||||
assert.deepStrictEqual(requirement, {
|
||||
required: true,
|
||||
providerCode: "TIANAI",
|
||||
captchaType: "SLIDER",
|
||||
sceneCode: AUTH_TAC_SCENE.REGISTER,
|
||||
sceneCode: "APP_REGISTER",
|
||||
ttlSeconds: 300,
|
||||
});
|
||||
assert.throws(
|
||||
() => normalizeCaptchaRequirement({ ...requirement, required: false }, AUTH_TAC_SCENE.REGISTER),
|
||||
/未要求行为验证|票据/,
|
||||
assert.deepStrictEqual(
|
||||
normalizeCaptchaRequirement({ ...requirement, required: false }),
|
||||
{ required: false, sceneCode: "APP_REGISTER" },
|
||||
"策略关闭时必须保留服务端绑定场景并跳过 TAC 渲染",
|
||||
);
|
||||
assert.throws(
|
||||
() => normalizeCaptchaRequirement({ ...requirement, sceneCode: AUTH_TAC_SCENE.SMS_LOGIN }, AUTH_TAC_SCENE.REGISTER),
|
||||
/场景/,
|
||||
);
|
||||
assert.throws(
|
||||
() => normalizeCaptchaRequirement({ ...requirement, providerCode: "OTHER" }, AUTH_TAC_SCENE.REGISTER),
|
||||
() => normalizeCaptchaRequirement({ ...requirement, providerCode: "OTHER" }),
|
||||
/TIANAI/,
|
||||
);
|
||||
assert.throws(
|
||||
() => normalizeCaptchaRequirement({ ...requirement, captchaType: "math" }, AUTH_TAC_SCENE.REGISTER),
|
||||
() => normalizeCaptchaRequirement({ ...requirement, captchaType: "math" }),
|
||||
/验证码类型/,
|
||||
);
|
||||
|
||||
@@ -81,18 +78,18 @@ const run = async () => {
|
||||
baseUrl: "https://backend-api.ddxcjp.cn/",
|
||||
clientId: "client-1",
|
||||
tenantId: "000000",
|
||||
sceneCode: AUTH_TAC_SCENE.REGISTER,
|
||||
operationCode: AUTH_VERIFICATION_OPERATION.REGISTER,
|
||||
subject: "13800138000",
|
||||
requirement,
|
||||
});
|
||||
assert.deepStrictEqual(context, {
|
||||
requestId: "register-1",
|
||||
baseUrl: "https://backend-api.ddxcjp.cn",
|
||||
challengeUrl: "https://backend-api.ddxcjp.cn/captcha/challenge",
|
||||
verifyUrl: "https://backend-api.ddxcjp.cn/captcha/verify",
|
||||
challengeUrl: "https://backend-api.ddxcjp.cn/genealogy/app/auth/verification/register/challenge",
|
||||
verifyUrl: "https://backend-api.ddxcjp.cn/genealogy/app/auth/verification/register/verify",
|
||||
clientId: "client-1",
|
||||
tenantId: "000000",
|
||||
sceneCode: AUTH_TAC_SCENE.REGISTER,
|
||||
operationCode: AUTH_VERIFICATION_OPERATION.REGISTER,
|
||||
subject: "13800138000",
|
||||
providerCode: "TIANAI",
|
||||
captchaType: "SLIDER",
|
||||
@@ -105,6 +102,14 @@ const run = async () => {
|
||||
() => createTacRenderContext({ ...context, subject: "1380013800", requirement }),
|
||||
/手机号/,
|
||||
);
|
||||
assert.throws(
|
||||
() => createTacRenderContext({ ...context, operationCode: "APP_REGISTER", requirement }),
|
||||
/认证动作/,
|
||||
);
|
||||
assert.throws(
|
||||
() => createTacRenderContext({ ...context, requirement: { ...requirement, required: false } }),
|
||||
/无需行为验证/,
|
||||
);
|
||||
|
||||
assert.deepStrictEqual(
|
||||
normalizeTacSuccess(
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
"use strict";
|
||||
|
||||
const assert = require("assert");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const toDataModuleUrl = (source) =>
|
||||
`data:text/javascript;base64,${Buffer.from(source).toString("base64")}`;
|
||||
|
||||
const run = async () => {
|
||||
const rawSource = fs.readFileSync(path.join(__dirname, "../utils/api.js"), "utf8");
|
||||
const moduleBody = rawSource.slice(rawSource.indexOf("const successCodes"));
|
||||
const prelude = `
|
||||
const currentUser = {};
|
||||
const genealogies = [];
|
||||
const publicGenealogies = [];
|
||||
const treeMembers = [];
|
||||
const notifications = [];
|
||||
const joinApplications = [];
|
||||
const listFamilyFeedFixtures = () => [];
|
||||
const listFamilyArticleFixtures = () => [];
|
||||
const listFamilyAlbumFixtures = () => [];
|
||||
const listCeremonyFixtures = () => [];
|
||||
const listGrowthRecordFixtures = () => [];
|
||||
const listNotificationFixtures = () => [];
|
||||
const runtimeConfig = { mode: "remote", baseUrl: "https://backend-api.ddxcjp.cn", clientId: "client-1", tenantId: "000000" };
|
||||
const hasRemoteConfig = () => true;
|
||||
const resolveRuntimeMode = () => "remote";
|
||||
const AUTH_VERIFICATION_OPERATION = Object.freeze({});
|
||||
const assertSmsCode = (value) => value;
|
||||
const GENEALOGY_ACCESS_PRESET = Object.freeze({ MEMBER_ONLY: "MEMBER_ONLY" });
|
||||
const fromApiGenealogyAccess = () => null;
|
||||
const session = { getToken: () => "session-1", saveToken() {} };
|
||||
`;
|
||||
const requests = [];
|
||||
let nextResponse;
|
||||
globalThis.uni = {
|
||||
request(options) {
|
||||
requests.push(options);
|
||||
queueMicrotask(() => options.success(nextResponse));
|
||||
return { abort() {} };
|
||||
},
|
||||
};
|
||||
const { appApi } = await import(toDataModuleUrl(`${prelude}\n${moduleBody}`));
|
||||
|
||||
nextResponse = { statusCode: 200, data: { code: 200, data: [{ invitationId: "9001", inviteStatus: "PENDING" }] } };
|
||||
assert.deepStrictEqual(await appApi.getCeremonyInvitations("1001", "2001"), [{ invitationId: "9001", inviteStatus: "PENDING" }]);
|
||||
assert.strictEqual(requests.at(-1).url, "https://backend-api.ddxcjp.cn/genealogy/app/genealogies/1001/ceremonies/2001/invitations");
|
||||
assert.strictEqual(requests.at(-1).method, "GET");
|
||||
|
||||
nextResponse = { statusCode: 200, data: { code: 200, data: { invitationId: "9001", inviteStatus: "ACCEPTED" } } };
|
||||
await appApi.respondToCeremonyInvitation("1001", "2001", { inviteStatus: "ACCEPTED" });
|
||||
assert.strictEqual(requests.at(-1).url, "https://backend-api.ddxcjp.cn/genealogy/app/genealogies/1001/ceremonies/2001/invitations/me");
|
||||
assert.strictEqual(requests.at(-1).method, "PUT");
|
||||
assert.deepStrictEqual(requests.at(-1).data, { inviteStatus: "ACCEPTED" });
|
||||
|
||||
nextResponse = { statusCode: 200, data: { code: 200, data: [] } };
|
||||
await appApi.replaceCeremonyInvitees("1001", "2001", { inviteeUserIds: [] });
|
||||
assert.strictEqual(requests.at(-1).url, "https://backend-api.ddxcjp.cn/genealogy/app/genealogies/1001/ceremonies/2001/invitees");
|
||||
assert.deepStrictEqual(requests.at(-1).data, { inviteeUserIds: [] });
|
||||
|
||||
await assert.rejects(
|
||||
appApi.replaceCeremonyInvitees("1001", "2001", { inviteeUserIds: [7, 7] }),
|
||||
/重复标识/,
|
||||
);
|
||||
await assert.rejects(
|
||||
appApi.replaceCeremonyInvitees("1001", "2001", { inviteeUserIds: ["2081232520259612673"] }),
|
||||
(error) => error?.code === "CEREMONY_INVITEE_ID_UNSAFE",
|
||||
);
|
||||
|
||||
nextResponse = { statusCode: 200, data: { code: 200, data: [] } };
|
||||
await appApi.getMyCeremonyInvitations();
|
||||
assert.strictEqual(requests.at(-1).url, "https://backend-api.ddxcjp.cn/genealogy/app/genealogies/ceremony-invitations/mine");
|
||||
assert.strictEqual(requests.at(-1).method, "GET");
|
||||
delete globalThis.uni;
|
||||
process.stdout.write("CEREMONY-INVITATIONS-API-RUNTIME-SMOKE PASS\n");
|
||||
};
|
||||
|
||||
run().catch((error) => {
|
||||
process.stderr.write(`${error.stack || error.message}\n`);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -26,7 +26,7 @@ const run = async () => {
|
||||
const runtimeConfig = { baseUrl: "https://backend-api.ddxcjp.cn", clientId: "client-1", tenantId: "000000" };
|
||||
const hasRemoteConfig = () => true;
|
||||
const resolveRuntimeMode = () => "remote";
|
||||
const AUTH_TAC_SCENE = Object.freeze({});
|
||||
const AUTH_VERIFICATION_OPERATION = Object.freeze({});
|
||||
const assertSmsCode = (value) => value;
|
||||
const GENEALOGY_ACCESS_PRESET = Object.freeze({ MEMBER_ONLY: "MEMBER_ONLY" });
|
||||
const fromApiGenealogyAccess = () => null;
|
||||
@@ -79,16 +79,16 @@ const run = async () => {
|
||||
contentType: "image/png",
|
||||
};
|
||||
const completePayload = {
|
||||
uploadId: basePayload.uploadId,
|
||||
uploadId: "upload-1",
|
||||
fileName: basePayload.fileName,
|
||||
fileMd5: basePayload.fileMd5,
|
||||
totalSize: basePayload.totalSize,
|
||||
totalChunks: basePayload.totalChunks,
|
||||
};
|
||||
|
||||
response = { statusCode: 200, data: { code: 200, data: { uploadId: "upload-1", instant: true, ossId: 900001 } } };
|
||||
response = { statusCode: 200, data: { code: 200, data: { uploadId: null, instant: true, ossId: 900001 } } };
|
||||
assert.deepStrictEqual(await appApi.initializeResumableUpload(basePayload), {
|
||||
uploadId: "upload-1",
|
||||
uploadId: null,
|
||||
instant: true,
|
||||
ossId: "900001",
|
||||
url: "",
|
||||
@@ -97,6 +97,12 @@ const run = async () => {
|
||||
assert.deepStrictEqual(requests.at(-1).data, basePayload);
|
||||
assert.strictEqual(requests.at(-1).url, "https://backend-api.ddxcjp.cn/genealogy/app/files/resumable/init");
|
||||
|
||||
response = { statusCode: 200, data: { code: 200, data: { uploadId: null, instant: false, ossId: null } } };
|
||||
await assert.rejects(
|
||||
appApi.initializeResumableUpload(basePayload),
|
||||
/uploadId/,
|
||||
);
|
||||
|
||||
await appApi.uploadResumableChunk({
|
||||
uploadId: "upload-1",
|
||||
chunkIndex: 0,
|
||||
|
||||
@@ -197,6 +197,54 @@ const run = async () => {
|
||||
(error) => error?.code === "GENEALOGY_RESPONSE_INVALID",
|
||||
);
|
||||
|
||||
nextResponse = {
|
||||
statusCode: 200,
|
||||
data: { code: 200, data: 7 },
|
||||
};
|
||||
assert.strictEqual(await appApi.getUnreadNotificationCount(), 7);
|
||||
assert.strictEqual(
|
||||
requests.at(-1).url,
|
||||
"https://backend-api.ddxcjp.cn/genealogy/app/notifications/unread-count",
|
||||
);
|
||||
|
||||
nextResponse = {
|
||||
statusCode: 200,
|
||||
data: { code: 200, data: -1 },
|
||||
};
|
||||
await assert.rejects(
|
||||
appApi.getUnreadNotificationCount(),
|
||||
(error) => error?.code === "NOTIFICATION_COUNT_RESPONSE_INVALID",
|
||||
);
|
||||
|
||||
const quota = {
|
||||
createUsed: 1,
|
||||
createLimit: 3,
|
||||
createRemaining: 2,
|
||||
canCreate: true,
|
||||
joinUsed: 2,
|
||||
joinLimit: 10,
|
||||
joinRemaining: 8,
|
||||
canJoin: true,
|
||||
};
|
||||
nextResponse = {
|
||||
statusCode: 200,
|
||||
data: { code: 200, data: quota },
|
||||
};
|
||||
assert.deepStrictEqual(await appApi.getGenealogyQuota(), quota);
|
||||
assert.strictEqual(
|
||||
requests.at(-1).url,
|
||||
"https://backend-api.ddxcjp.cn/genealogy/app/genealogies/quota",
|
||||
);
|
||||
|
||||
nextResponse = {
|
||||
statusCode: 200,
|
||||
data: { code: 200, data: { ...quota, canJoin: undefined } },
|
||||
};
|
||||
await assert.rejects(
|
||||
appApi.getGenealogyQuota(),
|
||||
(error) => error?.code === "GENEALOGY_QUOTA_RESPONSE_INVALID",
|
||||
);
|
||||
|
||||
process.stdout.write("G01-MY-GENEALOGIES-RUNTIME-SMOKE PASS\n");
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
const sleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
|
||||
const cdpPort = process.env.CDP_PORT || "9222";
|
||||
|
||||
const connect = async () => {
|
||||
const pages = await (await fetch("http://127.0.0.1:9222/json/list")).json();
|
||||
const pages = await (await fetch(`http://127.0.0.1:${cdpPort}/json/list`)).json();
|
||||
const page = pages.find((item) => item.type === "page" && item.url.startsWith("http://localhost:5173"));
|
||||
if (!page) throw new Error("Chrome debugging has no localhost:5173 page");
|
||||
|
||||
@@ -49,7 +50,7 @@ const waitFor = async (send, expression, message) => {
|
||||
const origin = process.argv[2] || "http://localhost:5173";
|
||||
|
||||
const openG03 = async (send, suffix = "") => {
|
||||
const url = `${origin}/?g03CreateAudit=${Date.now()}#${"/pages/genealogy/g03-create-genealogy"}${suffix}`;
|
||||
const url = `${origin}/?g03CreateAudit=${Date.now()}${suffix}#${"/pages/genealogy/g03-create-genealogy"}`;
|
||||
await send("Page.navigate", { url });
|
||||
await waitFor(send, `location.href === ${JSON.stringify(url)}`, "G03 navigation failed");
|
||||
await waitFor(send, "Boolean(document.querySelector('.create-card'))", "G03 create form did not render");
|
||||
@@ -77,6 +78,21 @@ const run = async () => {
|
||||
const fakeControl = await valueOf(send, "Boolean(document.querySelector('.flow-success-dialog'))");
|
||||
if (fakeControl) throw new Error("G03 must not expose the retired local bootstrap flow");
|
||||
|
||||
await valueOf(send, `(() => {
|
||||
const input = document.querySelector('.create-card input');
|
||||
const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set;
|
||||
setter.call(input, 'discard-check');
|
||||
input.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
document.querySelector('.header-back')?.click();
|
||||
return true;
|
||||
})()`);
|
||||
await waitFor(send, "Boolean(document.querySelector('.app-dialog-layer'))", "G03 dirty back did not require discard confirmation");
|
||||
await valueOf(send, "document.querySelector('.app-dialog__actions .app-button:first-child')?.click()");
|
||||
await waitFor(send, "!document.querySelector('.app-dialog-layer')", "G03 discard cancellation did not close the dialog");
|
||||
if ((await valueOf(send, "document.querySelector('.create-card input')?.value")) !== "discard-check") {
|
||||
throw new Error("G03 discard cancellation lost the draft");
|
||||
}
|
||||
|
||||
for (const size of [{ width: 320, height: 568 }, { width: 360, height: 800 }, { width: 412, height: 915 }]) {
|
||||
await send("Emulation.setDeviceMetricsOverride", { ...size, deviceScaleFactor: 1, mobile: true });
|
||||
await openG03(send, `&width=${size.width}`);
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Captures the current native MuMu rendering of every app route. It uses the
|
||||
* installed app's WebView and ADB; no browser emulation, fixture, or mock data.
|
||||
*
|
||||
* Usage:
|
||||
* $env:ADB_PATH='...\\adb.exe'; node tests/mumu-page-visual-audit.js
|
||||
*/
|
||||
const { execFileSync } = require("node:child_process");
|
||||
const { mkdirSync } = require("node:fs");
|
||||
const { join } = require("node:path");
|
||||
|
||||
const adb = process.env.ADB_PATH || "adb";
|
||||
const device = process.env.MUMU_DEVICE || "emulator-5554";
|
||||
const outputDir = process.env.MUMU_AUDIT_DIR || join(process.cwd(), "tmp", "mumu-visual-audit");
|
||||
const genealogyId = process.env.MUMU_GENEALOGY_ID || "2081191846772518914";
|
||||
const start = Number(process.env.MUMU_AUDIT_START || 0);
|
||||
const end = Number(process.env.MUMU_AUDIT_END || 52);
|
||||
const routes = [
|
||||
["a01", "/pages/auth/a01-entry"], ["a04", "/pages/auth/a04-register"], ["a05", "/pages/auth/a05-reset-password"],
|
||||
["g01", "/pages/genealogy/g01-my-genealogies"], ["g03", "/pages/genealogy/g03-create-genealogy"], ["g05", `/pages/genealogy/g05-genealogy-overview?genealogyId=${genealogyId}`], ["g06", "/pages/genealogy/g06-search-genealogies"], ["g08", `/pages/genealogy/g08-join-application?genealogyId=${genealogyId}`], ["g09", "/pages/genealogy/g09-my-applications"], ["g10", `/pages/genealogy/g10-application-review?genealogyId=${genealogyId}`], ["g11", `/pages/genealogy/g11-genealogy-settings?genealogyId=${genealogyId}`], ["g12", `/pages/genealogy/g12-generation-poems?genealogyId=${genealogyId}`],
|
||||
["t01", `/pages/tree/t01-tree-overview?genealogyId=${genealogyId}`], ["t03", `/pages/tree/t03-member-profile?genealogyId=${genealogyId}`], ["t04", `/pages/tree/t04-add-relative?genealogyId=${genealogyId}`], ["t05", `/pages/tree/t05-edit-member?genealogyId=${genealogyId}`], ["t06", `/pages/tree/t06-edit-relationship?genealogyId=${genealogyId}`], ["t07", `/pages/tree/t07-member-directory?genealogyId=${genealogyId}`], ["t08", `/pages/tree/t08-member-states?genealogyId=${genealogyId}`],
|
||||
["f01", `/pages/family/f01-family-feed?genealogyId=${genealogyId}`], ["f02", `/pages/family/f02-publish-feed?genealogyId=${genealogyId}`], ["f03", `/pages/family/f03-feed-detail?genealogyId=${genealogyId}&feedId=2081197770778128385`], ["f04", `/pages/family/f04-article-list?genealogyId=${genealogyId}`], ["f05", `/pages/family/f05-article-detail?genealogyId=${genealogyId}&articleId=2081200381052891138`], ["f06", `/pages/family/f06-article-editor?genealogyId=${genealogyId}`], ["f07", `/pages/family/f07-album-list?genealogyId=${genealogyId}`], ["f08", `/pages/family/f08-album-detail?genealogyId=${genealogyId}&albumId=2081202236751392769`], ["f09", `/pages/family/f09-media-upload?genealogyId=${genealogyId}&albumId=2081202236751392769`], ["f10", `/pages/family/f10-video-list?genealogyId=${genealogyId}`],
|
||||
["r01", `/pages/records/r01-people-list?genealogyId=${genealogyId}`], ["r02", `/pages/records/r02-person-detail?genealogyId=${genealogyId}`], ["r03", `/pages/records/r03-gift-list?genealogyId=${genealogyId}`], ["r04", `/pages/records/r04-gift-editor?genealogyId=${genealogyId}`], ["r05", `/pages/records/r05-ritual-list?genealogyId=${genealogyId}`], ["r06", `/pages/records/r06-ritual-detail?genealogyId=${genealogyId}`], ["r07", `/pages/records/r07-ritual-editor?genealogyId=${genealogyId}`], ["r08", `/pages/records/r08-growth-journal?genealogyId=${genealogyId}`], ["r09", `/pages/records/r09-life-events?genealogyId=${genealogyId}`], ["r10", `/pages/records/r10-memo-list?genealogyId=${genealogyId}`], ["r11", `/pages/records/r11-merit-records?genealogyId=${genealogyId}`],
|
||||
["n01", "/pages/notification/n01-message-center"], ["n02", "/pages/notification/n02-message-detail"],
|
||||
["m01", "/pages/profile/m01-profile-home"], ["m02", "/pages/profile/m02-edit-profile"], ["m03", "/pages/profile/m03-security-settings"], ["m04", "/pages/profile/m04-change-password"], ["m05", "/pages/profile/m05-change-phone"], ["m06", "/pages/profile/m06-help-center"], ["m07", "/pages/profile/m07-feedback"], ["m08", "/pages/profile/m08-promotion"], ["m09", "/pages/profile/m09-vip-orders"], ["m10", "/pages/profile/m10-about-settings"],
|
||||
];
|
||||
|
||||
const run = (...args) => execFileSync(adb, ["-s", device, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
||||
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
async function currentTarget() {
|
||||
const targets = await (await fetch("http://127.0.0.1:9223/json/list")).json();
|
||||
const pages = targets.filter((item) => item.type === "page" && item.title !== "View");
|
||||
if (!pages.length) throw new Error("MuMu 中没有可操作的应用页面 WebView");
|
||||
return pages.at(-1);
|
||||
}
|
||||
|
||||
async function evaluate(expression) {
|
||||
const target = await currentTarget();
|
||||
return new Promise((resolve, reject) => {
|
||||
const socket = new WebSocket(target.webSocketDebuggerUrl);
|
||||
socket.onopen = () => socket.send(JSON.stringify({ id: 1, method: "Runtime.evaluate", params: { expression, awaitPromise: true, returnByValue: true } }));
|
||||
socket.onmessage = (event) => {
|
||||
const result = JSON.parse(event.data);
|
||||
socket.close();
|
||||
if (result.error || result.result?.exceptionDetails) reject(new Error(JSON.stringify(result.error || result.result.exceptionDetails)));
|
||||
else resolve(result.result?.result?.value);
|
||||
};
|
||||
socket.onerror = () => reject(new Error("无法连接 MuMu WebView 调试端口"));
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
mkdirSync(outputDir, { recursive: true });
|
||||
run("forward", "tcp:9223", "localabstract:webview_devtools_remote_2728");
|
||||
try {
|
||||
for (const [name, route] of routes.slice(start, end)) {
|
||||
await evaluate(`uni.reLaunch({ url: ${JSON.stringify(route)} }); true`);
|
||||
await delay(1800);
|
||||
run("shell", "screencap", "-p", "/sdcard/jiapu-visual-audit.png");
|
||||
run("pull", "/sdcard/jiapu-visual-audit.png", join(outputDir, `${name}.png`));
|
||||
process.stdout.write(`captured ${name}\n`);
|
||||
}
|
||||
} finally {
|
||||
try { run("forward", "--remove", "tcp:9223"); } catch {}
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => { console.error(error.stack || error.message); process.exitCode = 1; });
|
||||
@@ -547,7 +547,7 @@ foreach ($contract in @(
|
||||
@{ Key = 'M04'; ParamToken = 'kind: "flow"'; SourceToken = 'allowedSources: ["M03"]' },
|
||||
@{ Key = 'M05'; ParamToken = 'kind: "flow"'; SourceToken = 'allowedSources: ["M03"]' },
|
||||
@{ Key = 'M06'; ParamToken = 'kind: "page"'; SourceToken = 'allowedSources: ["M01"]' },
|
||||
@{ Key = 'M07'; ParamToken = 'kind: "flow"'; SourceToken = 'allowedSources: ["M06"]' },
|
||||
@{ Key = 'M07'; ParamToken = 'kind: "flow"'; SourceToken = 'allowedSources: ["M01", "M06"]' },
|
||||
@{ Key = 'M08'; ParamToken = 'kind: "page"'; SourceToken = 'allowedSources: ["M01"]' },
|
||||
@{ Key = 'M09'; ParamToken = 'kind: "page"'; SourceToken = 'allowedSources: ["M01"]' },
|
||||
@{ Key = 'M10'; ParamToken = 'kind: "page"'; SourceToken = 'allowedSources: ["M01"]' }
|
||||
|
||||
@@ -1411,6 +1411,39 @@ process.exitCode = 1;
|
||||
assert.strictEqual(harness.calls[0].payload.delta, 2, "必须返回最近的既有目标实例");
|
||||
}
|
||||
|
||||
{
|
||||
const harness = await createHarness({
|
||||
routes: ROUTES,
|
||||
stack: [
|
||||
createPage(ROUTES, "F01"),
|
||||
createPage(ROUTES, "F03", { genealogyId: "g1", feedId: "feed-1" }),
|
||||
],
|
||||
});
|
||||
assert.strictEqual(
|
||||
await harness.navigation.returnTo("F01", { genealogyId: "g1" }),
|
||||
true,
|
||||
"根页未在 URL 保留可选上下文时,子页可按自身同一上下文返回",
|
||||
);
|
||||
assert.strictEqual(harness.calls[0].method, "navigateBack");
|
||||
assert.strictEqual(harness.calls[0].payload.delta, 1);
|
||||
}
|
||||
|
||||
{
|
||||
const harness = await createHarness({
|
||||
routes: ROUTES,
|
||||
stack: [
|
||||
createPage(ROUTES, "F01"),
|
||||
createPage(ROUTES, "F03", { genealogyId: "g1", feedId: "feed-1" }),
|
||||
],
|
||||
});
|
||||
await assert.rejects(
|
||||
harness.navigation.returnTo("F01", { genealogyId: "g2" }),
|
||||
/genealogyId.*不一致|最近实例/,
|
||||
"根页隐式上下文仍不得接受与当前子页不同的家谱",
|
||||
);
|
||||
assert.strictEqual(harness.calls.length, 0);
|
||||
}
|
||||
|
||||
{
|
||||
const harness = await createHarness({
|
||||
routes: ROUTES,
|
||||
|
||||
@@ -17,13 +17,31 @@ const run = async () => {
|
||||
const GENEALOGY_ACCESS_PRESET = Object.freeze({ MEMBER_ONLY: "MEMBER_ONLY" }); const fromApiGenealogyAccess = () => null; const session = { getToken: () => "session-1", saveToken() {} };
|
||||
`;
|
||||
const requests = [];
|
||||
globalThis.uni = { request(options) { requests.push(options); queueMicrotask(() => options.success({ statusCode: 200, data: { code: 200, data: [{ regionCode: "11", label: "Beijing", parentCode: "0", leaf: false, regionLevel: 1 }] } })); return { abort() {} }; } };
|
||||
let responseData = [{ regionCode: "11", label: "Beijing", parentCode: "0", leaf: false, regionLevel: 1 }];
|
||||
globalThis.uni = { request(options) { requests.push(options); queueMicrotask(() => options.success({ statusCode: 200, data: { code: 200, data: responseData } })); return { abort() {} }; } };
|
||||
const { appApi } = await import(toDataModuleUrl(`${prelude}\n${moduleBody}`));
|
||||
assert.deepStrictEqual(await appApi.getRegionChildren(), [{ regionCode: "11", label: "Beijing", parentCode: "0", leaf: false, regionLevel: 1 }]);
|
||||
assert.strictEqual(requests.at(-1).url, "https://backend-api.ddxcjp.cn/genealogy/app/region/children");
|
||||
assert.strictEqual(requests.at(-1).url, "https://backend-api.ddxcjp.cn/genealogy/region/children");
|
||||
assert.strictEqual(requests.at(-1).method, "GET");
|
||||
assert.deepStrictEqual(requests.at(-1).data, { parentCode: "0" });
|
||||
await assert.rejects(appApi.getRegionChildren(" "), /地区父级/);
|
||||
responseData = [{ regionCode: "110000", label: "Beijing" }];
|
||||
assert.deepStrictEqual(await appApi.getRegionPath("110000"), responseData);
|
||||
assert.strictEqual(requests.at(-1).url, "https://backend-api.ddxcjp.cn/genealogy/region/path/110000");
|
||||
await assert.rejects(appApi.getRegionPath(" "), /行政区划编码/);
|
||||
|
||||
assert.deepStrictEqual(
|
||||
await appApi.searchRegions({ keyword: " Beijing ", level: 2, limit: 20 }),
|
||||
responseData,
|
||||
);
|
||||
assert.strictEqual(requests.at(-1).url, "https://backend-api.ddxcjp.cn/genealogy/region/search");
|
||||
assert.deepStrictEqual(requests.at(-1).data, { keyword: "Beijing", level: 2, limit: 20 });
|
||||
await assert.rejects(appApi.searchRegions({ keyword: "" }), /行政区划搜索关键词/);
|
||||
await assert.rejects(appApi.searchRegions({ keyword: "Beijing", level: 6 }), /行政区划级别/);
|
||||
|
||||
responseData = { regionCode: "110000", label: "Beijing" };
|
||||
assert.deepStrictEqual(await appApi.getRegion("110000"), responseData);
|
||||
assert.strictEqual(requests.at(-1).url, "https://backend-api.ddxcjp.cn/genealogy/region/110000");
|
||||
delete globalThis.uni;
|
||||
process.stdout.write("REGION-API-RUNTIME-SMOKE PASS\n");
|
||||
};
|
||||
|
||||
@@ -15,8 +15,7 @@ assert(adapter, "TAC 供应商适配器必须暴露唯一全局 owner");
|
||||
|
||||
const context = {
|
||||
tenantId: "000000",
|
||||
clientId: "client-1",
|
||||
sceneCode: "APP_REGISTER",
|
||||
operationCode: "register",
|
||||
subject: "13800138000",
|
||||
providerCode: "TIANAI",
|
||||
captchaType: "SLIDER",
|
||||
@@ -72,8 +71,6 @@ const verifyBody = adapter.buildVerifyBody(
|
||||
);
|
||||
assert.deepStrictEqual(JSON.parse(JSON.stringify(verifyBody)), {
|
||||
tenantId: "000000",
|
||||
clientId: "client-1",
|
||||
sceneCode: "APP_REGISTER",
|
||||
subject: "13800138000",
|
||||
challengeId: "challenge-1",
|
||||
providerCode: "TIANAI",
|
||||
|
||||
@@ -149,7 +149,7 @@ for (const [name, method] of Object.entries(definition.methods)) {
|
||||
|
||||
const strictRequest = () => instance.sendStrictRequest({
|
||||
method: "POST",
|
||||
url: "https://backend-api.ddxcjp.cn/captcha/verify",
|
||||
url: "https://backend-api.ddxcjp.cn/genealogy/app/auth/verification/register/verify",
|
||||
headers: { clientid: "client-1" },
|
||||
data: { challengeId: "challenge-1" },
|
||||
});
|
||||
@@ -180,11 +180,11 @@ const run = async () => {
|
||||
const context = {
|
||||
visible: true,
|
||||
requestId: "request-1",
|
||||
challengeUrl: "https://backend-api.ddxcjp.cn/captcha/challenge",
|
||||
verifyUrl: "https://backend-api.ddxcjp.cn/captcha/verify",
|
||||
challengeUrl: "https://backend-api.ddxcjp.cn/genealogy/app/auth/verification/register/challenge",
|
||||
verifyUrl: "https://backend-api.ddxcjp.cn/genealogy/app/auth/verification/register/verify",
|
||||
clientId: "client-1",
|
||||
tenantId: "000000",
|
||||
sceneCode: "APP_REGISTER",
|
||||
operationCode: "register",
|
||||
subject: "13800138000",
|
||||
};
|
||||
instance.requestContext = context;
|
||||
|
||||
Reference in New Issue
Block a user