修改问题
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const root = path.join(__dirname, '..');
|
||||
const appContractPath = path.join(root, 'APP.openapi.json');
|
||||
const obsoleteRecordPaths = [
|
||||
'docs/api-page-integration-2026-07-09.md',
|
||||
'docs/api-page-integration-tracker-2026-07-09.md'
|
||||
];
|
||||
const forbiddenPatterns = [
|
||||
/\/genealogy\/app/,
|
||||
/\bAPP_(?:LOGIN|REGISTER|PHONE_CHANGE|FORGOT_PASSWORD|SMS_CODE|ACCOUNT_DEACTIVATE)\b/,
|
||||
/APP\.openapi\.json/
|
||||
];
|
||||
const checkedFiles = [
|
||||
'utils/ApiClient.js',
|
||||
'public/js/auth-pages.js',
|
||||
'public/js/captcha-pages.js',
|
||||
'login.html',
|
||||
'register.html',
|
||||
'forgot-password.html',
|
||||
'app.html',
|
||||
'download.html',
|
||||
'notice-detail.html',
|
||||
'profile-album.html',
|
||||
'profile-data.html',
|
||||
'profile-gift.html',
|
||||
'profile-security.html',
|
||||
'profile-services.html',
|
||||
'profile-tree.html',
|
||||
'genealogy-pc-openapi.yaml',
|
||||
'docs/pc-api-page-integration-tracker-2026-07-09.md',
|
||||
'docs/superpowers/plans/2026-07-09-pc-api-replan.md',
|
||||
'docs/superpowers/plans/2026-07-09-pc-auth-three-pages.md',
|
||||
'docs/superpowers/plans/2026-07-09-tac-captcha-auth.md',
|
||||
'docs/superpowers/plans/2026-07-10-pc-tracker-reverification.md',
|
||||
'public/js/album-pages.js',
|
||||
'public/js/article-pages.js',
|
||||
'public/js/ceremony-pages.js',
|
||||
'public/js/feed-pages.js',
|
||||
'public/js/genealogy-pages.js',
|
||||
'public/js/growth-pages.js',
|
||||
'public/js/lineage-pages.js',
|
||||
'public/js/member-admin-pages.js',
|
||||
'public/js/memo-pages.js',
|
||||
'public/js/vip-pages.js'
|
||||
];
|
||||
|
||||
function read(relativePath) {
|
||||
return fs.readFileSync(path.join(root, relativePath), 'utf8');
|
||||
}
|
||||
|
||||
function run() {
|
||||
const apiClient = read('utils/ApiClient.js');
|
||||
const authPages = read('public/js/auth-pages.js');
|
||||
|
||||
assert.strictEqual(fs.existsSync(appContractPath), false);
|
||||
obsoleteRecordPaths.forEach((relativePath) => {
|
||||
assert.strictEqual(fs.existsSync(path.join(root, relativePath)), false, `${relativePath} must be removed`);
|
||||
});
|
||||
assert.match(apiClient, /\/genealogy\/pc\/auth\/login\/sms/);
|
||||
assert.match(authPages, /captchaScene: 'WEB_H5_LOGIN'/);
|
||||
|
||||
checkedFiles.forEach((relativePath) => {
|
||||
const content = read(relativePath);
|
||||
|
||||
forbiddenPatterns.forEach((pattern) => {
|
||||
assert.doesNotMatch(content, pattern, `${relativePath} must not retain APP API contracts`);
|
||||
});
|
||||
});
|
||||
|
||||
console.log('pc-api-contract tests passed');
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -0,0 +1,82 @@
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const root = path.join(__dirname, '..');
|
||||
const openApi = fs.readFileSync(path.join(root, 'genealogy-pc-openapi.yaml'), 'utf8');
|
||||
const apiClient = fs.readFileSync(path.join(root, 'utils', 'ApiClient.js'), 'utf8');
|
||||
const authPages = fs.readFileSync(path.join(root, 'public', 'js', 'auth-pages.js'), 'utf8');
|
||||
const captchaPages = fs.readFileSync(path.join(root, 'public', 'js', 'captcha-pages.js'), 'utf8');
|
||||
const profilePages = fs.readFileSync(path.join(root, 'public', 'js', 'profile-pages.js'), 'utf8');
|
||||
const securityPages = fs.readFileSync(path.join(root, 'public', 'js', 'security-pages.js'), 'utf8');
|
||||
const regionPages = fs.readFileSync(path.join(root, 'public', 'js', 'region-pages.js'), 'utf8');
|
||||
const uploadPages = fs.readFileSync(path.join(root, 'public', 'js', 'upload-pages.js'), 'utf8');
|
||||
|
||||
const ENDPOINTS = {
|
||||
'GET /captcha/require': { source: captchaPages, needle: 'api.captchaRequire', status: 'complete' },
|
||||
'POST /captcha/challenge': { source: captchaPages, needle: "api.buildApiUrl('/captcha/challenge')", status: 'complete' },
|
||||
'POST /captcha/verify': { source: captchaPages, needle: "api.buildApiUrl('/captcha/verify')", status: 'complete' },
|
||||
'GET /auth/code': { status: 'blocked', reason: 'legacy captcha has no PC request field' },
|
||||
'POST /genealogy/pc/auth/register': { source: authPages, needle: 'api.register', status: 'complete' },
|
||||
'POST /genealogy/pc/auth/login': { source: authPages, needle: 'api.login', status: 'complete' },
|
||||
'POST /genealogy/pc/auth/login/sms': { source: authPages, needle: 'api.loginBySms', status: 'complete' },
|
||||
'POST /genealogy/pc/auth/sms/code': { source: authPages, needle: 'api.sendSmsCode', status: 'complete' },
|
||||
'GET /genealogy/pc/auth/profile': { source: profilePages, needle: 'api.currentProfile', status: 'complete' },
|
||||
'PUT /genealogy/pc/auth/profile': { source: profilePages, needle: 'api.updateProfile', status: 'complete' },
|
||||
'PUT /genealogy/pc/auth/password': { source: securityPages, needle: 'api.changePassword', status: 'complete' },
|
||||
'PUT /genealogy/pc/auth/password/reset': { source: authPages, needle: 'api.resetPassword', status: 'complete' },
|
||||
'PUT /genealogy/pc/auth/phone': { status: 'blocked', reason: 'PC/H5 phone SMS scene is not documented' },
|
||||
'POST /genealogy/pc/auth/account/deactivate': { source: securityPages, needle: 'api.deactivateAccount', status: 'complete' },
|
||||
'DELETE /genealogy/pc/auth/logout': { source: securityPages, needle: 'api.logout', status: 'complete' },
|
||||
'POST /genealogy/pc/files/upload': { source: uploadPages, needle: 'api.uploadFile', status: 'complete' },
|
||||
'POST /genealogy/pc/files/resumable/init': { source: uploadPages, needle: 'api.initResumableUpload', status: 'complete' },
|
||||
'POST /genealogy/pc/files/resumable/chunk': { source: uploadPages, needle: 'api.uploadChunk', status: 'complete' },
|
||||
'POST /genealogy/pc/files/resumable/complete': { source: uploadPages, needle: 'api.completeResumableUpload', status: 'complete' },
|
||||
'POST /genealogy/pc/files/reference': { status: 'blocked', reason: 'PC business table and ID contract is not documented' },
|
||||
'DELETE /genealogy/pc/files/reference': { status: 'blocked', reason: 'PC business table and ID contract is not documented' },
|
||||
'GET /genealogy/region/children': { source: regionPages, needle: 'api.regionChildren', status: 'complete' },
|
||||
'GET /genealogy/region/path/{regionCode}': { source: regionPages, needle: 'api.regionPath', apiNeedle: '/genealogy/region/path/', status: 'complete' },
|
||||
'GET /genealogy/region/search': { source: regionPages, needle: 'api.regionSearch', status: 'complete' },
|
||||
'GET /genealogy/region/{regionCode}': { source: regionPages, needle: 'api.regionDetail', apiNeedle: '/genealogy/region/', status: 'complete' }
|
||||
};
|
||||
|
||||
function getDocumentOperations(documentText) {
|
||||
let currentPath = '';
|
||||
const operations = [];
|
||||
|
||||
documentText.split(/\r?\n/).forEach((line) => {
|
||||
const pathMatch = line.match(/^ (\/[^:]+):$/);
|
||||
const methodMatch = line.match(/^ (get|post|put|delete):$/);
|
||||
|
||||
if (pathMatch) currentPath = pathMatch[1];
|
||||
if (methodMatch && currentPath) operations.push(`${methodMatch[1].toUpperCase()} ${currentPath}`);
|
||||
});
|
||||
|
||||
return operations;
|
||||
}
|
||||
|
||||
function run() {
|
||||
const operations = getDocumentOperations(openApi);
|
||||
|
||||
assert.strictEqual(operations.length, 25);
|
||||
assert.deepStrictEqual(Object.keys(ENDPOINTS).sort(), operations.slice().sort());
|
||||
|
||||
operations.forEach((operation) => {
|
||||
const descriptor = ENDPOINTS[operation];
|
||||
const route = operation.slice(operation.indexOf(' ') + 1);
|
||||
|
||||
assert.match(apiClient, new RegExp((descriptor.apiNeedle || route).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
|
||||
assert.ok(descriptor, `${operation} must have an owner`);
|
||||
|
||||
if (descriptor.status === 'blocked') {
|
||||
assert.ok(descriptor.reason, `${operation} must name its backend block`);
|
||||
return;
|
||||
}
|
||||
|
||||
assert.ok(descriptor.source.includes(descriptor.needle), `${operation} must be used by its page module`);
|
||||
});
|
||||
|
||||
console.log('pc-endpoint-coverage tests passed');
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -1,4 +1,6 @@
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const ProfilePages = require('../public/js/profile-pages.js');
|
||||
|
||||
function run() {
|
||||
@@ -77,6 +79,16 @@ function run() {
|
||||
getToken: () => 'token-x'
|
||||
}, { status: 500 }), false);
|
||||
|
||||
const profileData = fs.readFileSync(path.join(__dirname, '..', 'profile-data.html'), 'utf8');
|
||||
assert.match(profileData, /data-upload-target="#profileAvatarOssId"/);
|
||||
assert.match(profileData, /data-upload-mode="auto"/);
|
||||
assert.match(profileData, /data-upload-biz-type="avatar"/);
|
||||
assert.match(profileData, /<script src="public\/js\/md5\.js"><\/script>/);
|
||||
assert.match(profileData, /<script src="public\/js\/upload-pages\.js"><\/script>/);
|
||||
assert.match(profileData, /data-region-search-form/);
|
||||
assert.match(profileData, /data-region-search-results/);
|
||||
assert.match(profileData, /data-region-search-path/);
|
||||
|
||||
console.log('profile-pages tests passed');
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,26 @@ function run() {
|
||||
districtCode: undefined
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(RegionPages.buildRegionSelection([
|
||||
{ regionCode: '510000', regionName: '四川省', level: 1 },
|
||||
{ regionCode: '510100', regionName: '成都市', level: 2 },
|
||||
{ regionCode: '510104', regionName: '锦江区', level: 3 }
|
||||
]), {
|
||||
provinceCode: '510000',
|
||||
cityCode: '510100',
|
||||
districtCode: '510104'
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(RegionPages.buildRegionSelection([
|
||||
{ regionCode: '110000' },
|
||||
{ regionCode: '110100' },
|
||||
{ regionCode: '110105' }
|
||||
]), {
|
||||
provinceCode: '110000',
|
||||
cityCode: '110100',
|
||||
districtCode: '110105'
|
||||
});
|
||||
|
||||
console.log('region-pages tests passed');
|
||||
}
|
||||
|
||||
|
||||
+106
-11
@@ -1,11 +1,58 @@
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const vm = require('vm');
|
||||
const SecurityPages = require('../public/js/security-pages.js');
|
||||
|
||||
const securityPagePath = path.join(__dirname, '..', 'profile-security.html');
|
||||
const securityPagesPath = path.join(__dirname, '..', 'public', 'js', 'security-pages.js');
|
||||
|
||||
function hash(value) {
|
||||
return `hashed:${value}`;
|
||||
}
|
||||
|
||||
function run() {
|
||||
function loadSecurityPages(root) {
|
||||
const source = fs.readFileSync(securityPagesPath, 'utf8');
|
||||
const sandbox = {
|
||||
module: { exports: {} },
|
||||
exports: {},
|
||||
globalThis: root,
|
||||
window: root
|
||||
};
|
||||
|
||||
vm.runInNewContext(source, sandbox, { filename: securityPagesPath });
|
||||
return sandbox.module.exports;
|
||||
}
|
||||
|
||||
function createPageRoot(api) {
|
||||
const handlers = {};
|
||||
const document = {
|
||||
addEventListener(type, handler) {
|
||||
handlers[type] = handler;
|
||||
},
|
||||
querySelector(selector) {
|
||||
return selector === '[data-security-page]' ? {} : null;
|
||||
},
|
||||
querySelectorAll() {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
document,
|
||||
handlers,
|
||||
GenealogyApi: { defaultClient: api },
|
||||
confirm: () => true,
|
||||
alert: () => {},
|
||||
location: { href: '' }
|
||||
};
|
||||
}
|
||||
|
||||
function waitForAsyncAction() {
|
||||
return new Promise((resolve) => setImmediate(resolve));
|
||||
}
|
||||
|
||||
async function run() {
|
||||
assert.deepStrictEqual(SecurityPages.buildPasswordChangeBody({
|
||||
oldPassword: '123456',
|
||||
newPassword: 'abcdef'
|
||||
@@ -24,15 +71,8 @@ function run() {
|
||||
validToken: 'ticket'
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(SecurityPages.buildSmsLoginBody({
|
||||
phone: '13900000000',
|
||||
smsCode: '123456',
|
||||
validToken: ''
|
||||
}), {
|
||||
phone: '13900000000',
|
||||
smsCode: '123456',
|
||||
validToken: undefined
|
||||
});
|
||||
assert.strictEqual(SecurityPages.buildSmsLoginBody, undefined);
|
||||
assert.doesNotMatch(fs.readFileSync(securityPagePath, 'utf8'), /data-security-form="sms-login"/);
|
||||
|
||||
assert.deepStrictEqual(SecurityPages.buildDeactivateBody({
|
||||
password: '123456',
|
||||
@@ -45,7 +85,62 @@ function run() {
|
||||
assert.strictEqual(SecurityPages.isDangerConfirmed('确认注销'), true);
|
||||
assert.strictEqual(SecurityPages.isDangerConfirmed('注销'), false);
|
||||
|
||||
const logoutRoot = createPageRoot({
|
||||
logout() {
|
||||
return Promise.reject(new Error('network'));
|
||||
}
|
||||
});
|
||||
const LogoutPages = loadSecurityPages(logoutRoot);
|
||||
LogoutPages.init();
|
||||
logoutRoot.handlers.click({
|
||||
target: {
|
||||
closest(selector) {
|
||||
return selector === '[data-security-logout]' ? this : null;
|
||||
}
|
||||
},
|
||||
preventDefault() {}
|
||||
});
|
||||
await waitForAsyncAction();
|
||||
assert.strictEqual(logoutRoot.location.href, 'index.html');
|
||||
|
||||
const deactivateRoot = createPageRoot({
|
||||
deactivateAccount() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
});
|
||||
const DeactivatePages = loadSecurityPages(deactivateRoot);
|
||||
const deactivateForm = {
|
||||
getAttribute(name) {
|
||||
return name === 'data-security-form' ? 'deactivate' : null;
|
||||
},
|
||||
querySelectorAll(selector) {
|
||||
if (selector !== '[name]') return [];
|
||||
return [
|
||||
{ name: 'password', value: '123456' },
|
||||
{ name: 'reason', value: '' },
|
||||
{ name: 'confirmText', value: '确认注销' }
|
||||
];
|
||||
},
|
||||
querySelector() {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
DeactivatePages.init();
|
||||
deactivateRoot.handlers.submit({
|
||||
target: {
|
||||
closest(selector) {
|
||||
return selector === '[data-security-form]' ? deactivateForm : null;
|
||||
}
|
||||
},
|
||||
preventDefault() {}
|
||||
});
|
||||
await waitForAsyncAction();
|
||||
assert.strictEqual(deactivateRoot.location.href, 'index.html');
|
||||
|
||||
console.log('security-pages tests passed');
|
||||
}
|
||||
|
||||
run();
|
||||
run().catch((error) => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
|
||||
+114
-2
@@ -1,7 +1,7 @@
|
||||
const assert = require('assert');
|
||||
const UploadPages = require('../public/js/upload-pages.js');
|
||||
|
||||
function run() {
|
||||
async function run() {
|
||||
assert.deepStrictEqual(UploadPages.normalizeUploadResult({
|
||||
ossId: 2060001,
|
||||
fileName: 'cover.jpg',
|
||||
@@ -27,7 +27,119 @@ function run() {
|
||||
assert.strictEqual(UploadPages.mergeOssIds('2060001', '2060001', true), '2060001');
|
||||
assert.strictEqual(UploadPages.buildUploadStatus('cover.jpg', '2060001'), 'cover.jpg 上传完成,OSS ID:2060001');
|
||||
|
||||
assert.strictEqual(UploadPages.getUploadMode(4194304), 'single');
|
||||
assert.strictEqual(UploadPages.getUploadMode(4194305), 'resumable');
|
||||
assert.deepStrictEqual(UploadPages.buildResumableInitBody({
|
||||
name: 'avatar.jpg',
|
||||
size: 5242880,
|
||||
type: 'image/jpeg'
|
||||
}, 'file-md5', 4194304, {
|
||||
bizType: 'avatar',
|
||||
usageScene: 'profile_avatar'
|
||||
}), {
|
||||
fileName: 'avatar.jpg',
|
||||
fileSize: 5242880,
|
||||
fileMd5: 'file-md5',
|
||||
contentType: 'image/jpeg',
|
||||
chunkSize: 4194304,
|
||||
totalChunks: 2,
|
||||
bizType: 'avatar',
|
||||
usageScene: 'profile_avatar'
|
||||
});
|
||||
|
||||
const calls = [];
|
||||
const file = {
|
||||
name: 'avatar.jpg',
|
||||
size: 8,
|
||||
type: 'image/jpeg',
|
||||
slice(start, end) {
|
||||
return { start, end, size: end - start };
|
||||
}
|
||||
};
|
||||
const api = {
|
||||
initResumableUpload(body) {
|
||||
calls.push({ type: 'init', body });
|
||||
return Promise.resolve({ uploadId: 'upload-1' });
|
||||
},
|
||||
uploadChunk(body) {
|
||||
calls.push({ type: 'chunk', body });
|
||||
return Promise.resolve();
|
||||
},
|
||||
completeResumableUpload(body) {
|
||||
calls.push({ type: 'complete', body });
|
||||
return Promise.resolve({ ossId: 2060001 });
|
||||
}
|
||||
};
|
||||
const result = await UploadPages.uploadResumable(api, file, {
|
||||
chunkSize: 4,
|
||||
bizType: 'avatar',
|
||||
usageScene: 'profile_avatar',
|
||||
hashBlob(blob) {
|
||||
return Promise.resolve(blob === file ? 'file-md5' : `chunk-${blob.start}`);
|
||||
}
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(result, { ossId: 2060001 });
|
||||
assert.deepStrictEqual(calls, [
|
||||
{
|
||||
type: 'init',
|
||||
body: {
|
||||
fileName: 'avatar.jpg',
|
||||
fileSize: 8,
|
||||
fileMd5: 'file-md5',
|
||||
contentType: 'image/jpeg',
|
||||
chunkSize: 4,
|
||||
totalChunks: 2,
|
||||
bizType: 'avatar',
|
||||
usageScene: 'profile_avatar'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'chunk',
|
||||
body: {
|
||||
uploadId: 'upload-1',
|
||||
chunkIndex: 0,
|
||||
chunkMd5: 'chunk-0',
|
||||
file: { start: 0, end: 4, size: 4 }
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'chunk',
|
||||
body: {
|
||||
uploadId: 'upload-1',
|
||||
chunkIndex: 1,
|
||||
chunkMd5: 'chunk-4',
|
||||
file: { start: 4, end: 8, size: 4 }
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'complete',
|
||||
body: {
|
||||
uploadId: 'upload-1',
|
||||
fileMd5: 'file-md5',
|
||||
fileSize: 8
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
let singleUploadCalled = false;
|
||||
const singleResult = await UploadPages.uploadFileForPage({
|
||||
uploadFile(value) {
|
||||
singleUploadCalled = value.size === 4;
|
||||
return Promise.resolve({ ossId: 2060002 });
|
||||
}
|
||||
}, { size: 4 }, {
|
||||
mode: 'auto',
|
||||
chunkSize: 4
|
||||
});
|
||||
|
||||
assert.strictEqual(singleUploadCalled, true);
|
||||
assert.deepStrictEqual(singleResult, { ossId: 2060002 });
|
||||
|
||||
console.log('upload-pages tests passed');
|
||||
}
|
||||
|
||||
run();
|
||||
run().catch((error) => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user