feat: 完成全部接口替换为新后端接口
This commit is contained in:
@@ -0,0 +1,513 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { resolve, dirname } from 'node:path';
|
||||
import { test } from 'node:test';
|
||||
import vm from 'node:vm';
|
||||
import ts from 'typescript';
|
||||
|
||||
const root = resolve('miniprogram');
|
||||
const user = (id = 'server-user') => ({
|
||||
id,
|
||||
nickName: null,
|
||||
avatarUrl: null,
|
||||
avatarUrlExpiresAt: null,
|
||||
createdAt: '2026-10-19T00:00:00Z',
|
||||
});
|
||||
const result = (token, id) => ({
|
||||
accessToken: token,
|
||||
tokenType: 'Bearer',
|
||||
expiresIn: 86400,
|
||||
expiresAt: '2026-10-20T00:00:00Z',
|
||||
user: { ...user(id), openid: 'must-not-persist', role: 'admin' },
|
||||
});
|
||||
const failure = (reason, statusCode = 401) => ({
|
||||
statusCode,
|
||||
raw: { code: statusCode, reason },
|
||||
});
|
||||
const deferred = () => {
|
||||
let resolve;
|
||||
const promise = new Promise((done) => {
|
||||
resolve = done;
|
||||
});
|
||||
return { promise, resolve };
|
||||
};
|
||||
|
||||
/** 隔离微信与时间;登录、缓存、HTTP 包装都加载实际源码,不使用真实凭据。 */
|
||||
function fixture(initialStorage = new Map()) {
|
||||
let now = Date.now();
|
||||
const storage = initialStorage;
|
||||
const app = { globalData: {} };
|
||||
let loginCalls = 0;
|
||||
let handler = async (options) =>
|
||||
options.url.endsWith('/auth/wechat/login')
|
||||
? { data: result(`token-${loginCalls}`) }
|
||||
: { data: user() };
|
||||
const requests = [];
|
||||
const uploads = [];
|
||||
const wx = {
|
||||
getAccountInfoSync: () => ({
|
||||
miniProgram: { appId: 'wx-test', envVersion: 'develop' },
|
||||
}),
|
||||
getStorageSync: (key) => storage.get(key),
|
||||
setStorageSync(key, value) {
|
||||
storage.set(key, JSON.parse(JSON.stringify(value)));
|
||||
},
|
||||
login(options) {
|
||||
loginCalls++;
|
||||
options.success({ code: `one-use-code-${loginCalls}` });
|
||||
},
|
||||
uploadFile(options) {
|
||||
uploads.push(options);
|
||||
Promise.resolve()
|
||||
.then(() => handler({ ...options, data: options.formData }))
|
||||
.then((value) => {
|
||||
if (value.networkError) options.fail({});
|
||||
else
|
||||
options.success({
|
||||
statusCode: value.statusCode ?? 200,
|
||||
data: JSON.stringify(
|
||||
value.raw ?? { code: 0, data: value.data },
|
||||
),
|
||||
});
|
||||
});
|
||||
},
|
||||
request(options) {
|
||||
requests.push(options);
|
||||
Promise.resolve()
|
||||
.then(() => handler(options))
|
||||
.then((value) => {
|
||||
if (value.networkError) options.fail({});
|
||||
else
|
||||
options.success({
|
||||
statusCode: value.statusCode ?? 200,
|
||||
data: value.raw ?? { code: 0, data: value.data },
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
class Clock extends Date {
|
||||
static now() {
|
||||
return now;
|
||||
}
|
||||
}
|
||||
const context = vm.createContext({
|
||||
wx,
|
||||
getApp: () => app,
|
||||
Date: Clock,
|
||||
console,
|
||||
});
|
||||
const modules = new Map();
|
||||
function load(file) {
|
||||
file = resolve(root, file);
|
||||
if (!file.endsWith('.ts')) file += '.ts';
|
||||
if (modules.has(file)) return modules.get(file);
|
||||
const exports = {};
|
||||
modules.set(file, exports);
|
||||
const source = readFileSync(file, 'utf8');
|
||||
const code = ts.transpileModule(source, {
|
||||
compilerOptions: {
|
||||
module: ts.ModuleKind.CommonJS,
|
||||
target: ts.ScriptTarget.ES2020,
|
||||
},
|
||||
}).outputText;
|
||||
const run = vm.runInContext(
|
||||
`(function(require,exports){${code}\n})`,
|
||||
context,
|
||||
);
|
||||
run((name) => load(resolve(dirname(file), name)), exports);
|
||||
return exports;
|
||||
}
|
||||
return {
|
||||
auth: load('utils/auth'),
|
||||
session: load('services/auth-session'),
|
||||
api: load('services/api-request'),
|
||||
member: load('services/member-api'),
|
||||
admin: load('services/admin-api'),
|
||||
site: load('services/site-api'),
|
||||
config: load('config/api').apiConfig,
|
||||
wx,
|
||||
requests,
|
||||
uploads,
|
||||
storage,
|
||||
app,
|
||||
get loginCalls() {
|
||||
return loginCalls;
|
||||
},
|
||||
handle(fn) {
|
||||
handler = fn;
|
||||
},
|
||||
advance(ms) {
|
||||
now += ms;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const loginRequests = (f) =>
|
||||
f.requests.filter((r) => r.url.endsWith('/auth/wechat/login'));
|
||||
|
||||
test('静默登录:首次 start,并发合并、缓存复用,过期后 resume 且不缓存微信身份/角色', async () => {
|
||||
const f = fixture();
|
||||
await Promise.all(Array.from({ length: 20 }, () => f.auth.getUser()));
|
||||
assert.equal(f.loginCalls, 1);
|
||||
assert.equal(loginRequests(f)[0].data.mode, 'start');
|
||||
assert.equal(f.app.globalData.user.id, 'server-user');
|
||||
assert.ok(
|
||||
!JSON.stringify([...f.storage.values()]).includes('must-not-persist'),
|
||||
);
|
||||
assert.ok(!JSON.stringify([...f.storage.values()]).includes('admin'));
|
||||
assert.equal(f.auth.getCachedUser().id, 'server-user');
|
||||
const reload = fixture(f.storage);
|
||||
await reload.auth.getUser();
|
||||
assert.equal(reload.loginCalls, 0);
|
||||
f.advance(86400 * 1000);
|
||||
await f.auth.getUser();
|
||||
assert.equal(f.loginCalls, 2);
|
||||
assert.equal(loginRequests(f)[1].data.mode, 'resume');
|
||||
assert.notEqual(
|
||||
loginRequests(f)[0].data.code,
|
||||
loginRequests(f)[1].data.code,
|
||||
);
|
||||
});
|
||||
|
||||
test('公开请求不登录不带凭证;受保护请求登录并 GET /me 刷新用户', async () => {
|
||||
const f = fixture();
|
||||
await f.api.apiRequest('/categories', {
|
||||
auth: 'none',
|
||||
header: { authorization: 'do-not-forward' },
|
||||
});
|
||||
assert.equal(f.loginCalls, 0);
|
||||
assert.equal(
|
||||
Object.keys(f.requests[0].header).some(
|
||||
(k) => k.toLowerCase() === 'authorization',
|
||||
),
|
||||
false,
|
||||
);
|
||||
const refreshed = await f.auth.refreshUser();
|
||||
assert.equal(refreshed.id, 'server-user');
|
||||
assert.equal(f.requests.at(-1).header.Authorization, 'Bearer token-1');
|
||||
await f.api.apiRequest('/categories', { auth: 'none' });
|
||||
assert.equal(f.requests.at(-1).header.Authorization, undefined);
|
||||
});
|
||||
|
||||
test('多个并发 401 共用一次 resume,迟到的旧 401 复用新 token', async () => {
|
||||
const f = fixture();
|
||||
await f.auth.getUser();
|
||||
const late = deferred();
|
||||
const receivedLate = deferred();
|
||||
f.handle(async (options) => {
|
||||
if (options.url.endsWith('/auth/wechat/login'))
|
||||
return { data: result('token-2') };
|
||||
if (options.header.Authorization === 'Bearer token-1') {
|
||||
if (options.url.endsWith('/late')) {
|
||||
receivedLate.resolve();
|
||||
await late.promise;
|
||||
}
|
||||
return failure('AUTH_EXPIRED');
|
||||
}
|
||||
return { data: 'ok' };
|
||||
});
|
||||
const delayed = f.api.apiRequest('/late');
|
||||
await receivedLate.promise;
|
||||
const values = await Promise.all(
|
||||
Array.from({ length: 8 }, () => f.api.apiRequest('/me')),
|
||||
);
|
||||
assert.ok(values.every((v) => v === 'ok'));
|
||||
assert.equal(f.loginCalls, 2);
|
||||
late.resolve();
|
||||
assert.equal(await delayed, 'ok');
|
||||
assert.equal(f.loginCalls, 2);
|
||||
});
|
||||
|
||||
test('第二次 401 停止恢复;403、429、503 和网络错误不自动重登', async () => {
|
||||
for (const status of [401, 403, 429, 503, 0]) {
|
||||
const f = fixture();
|
||||
await f.auth.getUser();
|
||||
f.handle(async (options) =>
|
||||
options.url.endsWith('/auth/wechat/login')
|
||||
? { data: result('token-2') }
|
||||
: status
|
||||
? failure('AUTH_EXPIRED', status)
|
||||
: { networkError: true },
|
||||
);
|
||||
await assert.rejects(f.api.apiRequest('/me'));
|
||||
assert.equal(f.loginCalls, status === 401 ? 2 : 1);
|
||||
const business = f.requests.filter((r) => r.url.endsWith('/me'));
|
||||
assert.equal(business.length, status === 401 ? 2 : 1);
|
||||
}
|
||||
});
|
||||
|
||||
test('账户被删除或禁用时阻止自动重建,标记重启后保留;用户可主动重新开始', async () => {
|
||||
const f = fixture();
|
||||
await f.auth.getUser();
|
||||
f.handle(async (options) =>
|
||||
options.url.endsWith('/auth/wechat/login')
|
||||
? failure('ACCOUNT_NOT_FOUND')
|
||||
: failure('AUTH_EXPIRED'),
|
||||
);
|
||||
await assert.rejects(f.api.apiRequest('/me'));
|
||||
await assert.rejects(f.auth.getUser(), /重新确认/);
|
||||
assert.equal(f.loginCalls, 2);
|
||||
assert.equal(loginRequests(f)[1].data.mode, 'resume');
|
||||
const reload = fixture(f.storage);
|
||||
await assert.rejects(reload.auth.getUser(), /重新确认/);
|
||||
assert.equal(reload.loginCalls, 0);
|
||||
await reload.auth.startAccount();
|
||||
assert.equal(loginRequests(reload)[0].data.mode, 'start');
|
||||
});
|
||||
|
||||
test('恢复后身份变化中止原业务写入,不把请求补发到另一个账户', async () => {
|
||||
const f = fixture();
|
||||
await f.auth.getUser();
|
||||
f.handle(async (options) =>
|
||||
options.url.endsWith('/auth/wechat/login')
|
||||
? { data: result('token-other', 'another-user') }
|
||||
: failure('AUTH_EXPIRED'),
|
||||
);
|
||||
await assert.rejects(
|
||||
f.api.apiRequest('/me/action', { method: 'POST', data: { value: 1 } }),
|
||||
(e) => e.reason === 'ACCOUNT_CHANGED',
|
||||
);
|
||||
assert.equal(
|
||||
f.requests.filter((r) => r.url.endsWith('/me/action')).length,
|
||||
1,
|
||||
);
|
||||
assert.equal(f.auth.getCachedUser(), null);
|
||||
});
|
||||
|
||||
test('微信无效 code 只补试一次且使用新 code,网络失败不重试登录', async () => {
|
||||
const f = fixture();
|
||||
let count = 0;
|
||||
f.handle(async () =>
|
||||
++count === 1
|
||||
? failure('WECHAT_CODE_INVALID')
|
||||
: { data: result('token-2') },
|
||||
);
|
||||
await f.auth.getUser();
|
||||
assert.equal(f.loginCalls, 2);
|
||||
assert.notEqual(
|
||||
loginRequests(f)[0].data.code,
|
||||
loginRequests(f)[1].data.code,
|
||||
);
|
||||
const g = fixture();
|
||||
g.handle(async () => ({ networkError: true }));
|
||||
await assert.rejects(g.auth.getUser());
|
||||
assert.equal(g.loginCalls, 1);
|
||||
});
|
||||
|
||||
test('接口环境与 AppID 隔离;清理或切换环境后迟到登录不得恢复原会话', async () => {
|
||||
const f = fixture();
|
||||
await f.auth.getUser();
|
||||
f.config.localBaseUrl = 'http://127.0.0.1:8801/api/v1';
|
||||
assert.equal(f.auth.getCachedUser(), null);
|
||||
await f.auth.getUser();
|
||||
assert.equal(loginRequests(f)[1].data.mode, 'start');
|
||||
assert.equal(f.storage.size, 2);
|
||||
f.wx.getAccountInfoSync = () => ({
|
||||
miniProgram: { appId: 'wx-other', envVersion: 'develop' },
|
||||
});
|
||||
assert.equal(f.auth.getCachedUser(), null);
|
||||
const pending = deferred();
|
||||
const received = deferred();
|
||||
f.handle(async () => {
|
||||
received.resolve();
|
||||
return pending.promise;
|
||||
});
|
||||
const login = f.auth.getUser();
|
||||
await received.promise;
|
||||
f.auth.clearUser();
|
||||
pending.resolve({ data: result('late') });
|
||||
await assert.rejects(login, /状态已变化/);
|
||||
assert.equal(f.auth.getCachedUser(), null);
|
||||
});
|
||||
|
||||
test('畸形登录响应不保存凭证;账户禁用不会触发新的自动注册', async () => {
|
||||
const f = fixture();
|
||||
f.handle(async () => ({
|
||||
data: { ...result('invalid'), tokenType: 'Basic' },
|
||||
}));
|
||||
await assert.rejects(f.auth.getUser(), /格式异常/);
|
||||
assert.equal(f.auth.getCachedUser(), null);
|
||||
assert.equal(f.storage.size, 0);
|
||||
const g = fixture();
|
||||
await g.auth.getUser();
|
||||
g.handle(async () => failure('ACCOUNT_DISABLED'));
|
||||
await assert.rejects(g.api.apiRequest('/me'));
|
||||
assert.equal(g.loginCalls, 1);
|
||||
await assert.rejects(g.auth.getUser(), /重新确认/);
|
||||
assert.equal(g.loginCalls, 1);
|
||||
});
|
||||
|
||||
test('切换环境后原业务的 401 不向旧接口发起新登录', async () => {
|
||||
const f = fixture();
|
||||
await f.auth.getUser();
|
||||
const pending = deferred();
|
||||
const received = deferred();
|
||||
f.handle(async () => {
|
||||
received.resolve();
|
||||
return pending.promise;
|
||||
});
|
||||
const request = f.api.apiRequest('/me');
|
||||
await received.promise;
|
||||
f.config.localBaseUrl = 'http://127.0.0.1:8801/api/v1';
|
||||
pending.resolve(failure('AUTH_EXPIRED'));
|
||||
await assert.rejects(request, /环境已变化/);
|
||||
assert.equal(f.loginCalls, 1);
|
||||
});
|
||||
|
||||
test('提前续登后身份变化时,原页面发起的业务请求也不会首次发送到新账户', async () => {
|
||||
const f = fixture();
|
||||
await f.auth.getUser();
|
||||
f.advance(86400 * 1000);
|
||||
f.handle(async () => ({ data: result('another-token', 'another-user') }));
|
||||
await assert.rejects(
|
||||
f.api.apiRequest('/me/action', { method: 'POST' }),
|
||||
(e) => e.reason === 'ACCOUNT_CHANGED',
|
||||
);
|
||||
assert.equal(
|
||||
f.requests.filter((r) => r.url.endsWith('/me/action')).length,
|
||||
0,
|
||||
);
|
||||
});
|
||||
|
||||
test('七牛头像/预览上传沿用统一鉴权;401 重试沿用 uploadId,不自行设置 multipart boundary', async () => {
|
||||
const f = fixture();
|
||||
await f.auth.getUser();
|
||||
f.handle(async (options) => {
|
||||
if (options.url.endsWith('/auth/wechat/login'))
|
||||
return { data: result('token-2') };
|
||||
if (options.header.Authorization === 'Bearer token-1')
|
||||
return failure('AUTH_EXPIRED');
|
||||
return {
|
||||
data: {
|
||||
id: 'avatar-resource',
|
||||
url: 'https://cdn.test/avatar.png',
|
||||
mimeType: 'image/png',
|
||||
sizeBytes: 100,
|
||||
},
|
||||
};
|
||||
});
|
||||
const uploaded = await f.member.uploadAsset(
|
||||
'wxfile://chosen-avatar.png',
|
||||
'avatar',
|
||||
);
|
||||
assert.equal(uploaded.id, 'avatar-resource');
|
||||
assert.equal(f.uploads.length, 2);
|
||||
assert.equal(
|
||||
f.uploads[0].formData.uploadId,
|
||||
f.uploads[1].formData.uploadId,
|
||||
);
|
||||
assert.equal(f.uploads[0].header['Content-Type'], undefined);
|
||||
assert.equal(f.uploads[1].header.Authorization, 'Bearer token-2');
|
||||
assert.ok(f.uploads[0].url.endsWith('/me/avatar-upload'));
|
||||
await f.member.uploadAsset('wxfile://preview.jpg', 'preview');
|
||||
assert.ok(f.uploads.at(-1).url.endsWith('/admin/assets/preview-upload'));
|
||||
});
|
||||
|
||||
test('资料更新只提交资源 ID,服务端响应更新当前用户摘要', async () => {
|
||||
const f = fixture();
|
||||
await f.auth.getUser();
|
||||
f.handle(async () => ({
|
||||
data: {
|
||||
...user(),
|
||||
nickName: '新名字',
|
||||
avatarUrl: 'https://cdn.test/avatar.png',
|
||||
avatarAssetId: 'avatar-resource',
|
||||
role: 'admin',
|
||||
},
|
||||
}));
|
||||
const updated = await f.auth.updateUserProfile({
|
||||
nickName: '新名字',
|
||||
avatarAssetId: 'avatar-resource',
|
||||
});
|
||||
const request = f.requests.at(-1);
|
||||
assert.ok(request.url.endsWith('/me/profile'));
|
||||
assert.equal(request.method, 'PUT');
|
||||
assert.equal(request.data.avatarAssetId, 'avatar-resource');
|
||||
assert.equal(request.data.avatarUrl, undefined);
|
||||
assert.equal(updated.avatarAssetId, 'avatar-resource');
|
||||
assert.equal(f.auth.getCachedUser().nickName, '新名字');
|
||||
assert.equal(f.auth.getCachedUser().role, undefined);
|
||||
});
|
||||
|
||||
test('管理目录完整分页,真实计数和展示数分开;批次与 PUT 状态映射至新接口', async () => {
|
||||
const f = fixture();
|
||||
const rows = Array.from({ length: 121 }, (_, index) => ({
|
||||
id: `item-${index}`,
|
||||
likes: '2',
|
||||
downloads: '3',
|
||||
likesDisplay: '22',
|
||||
downloadsDisplay: '33',
|
||||
status: 'active',
|
||||
}));
|
||||
f.handle(async (options) => {
|
||||
if (options.url.endsWith('/auth/wechat/login'))
|
||||
return { data: result('token-1') };
|
||||
if (options.url.endsWith('/admin/catalog/worksheets')) {
|
||||
const { page } = options.data;
|
||||
return {
|
||||
data: {
|
||||
items: rows.slice((page - 1) * 100, page * 100),
|
||||
total: rows.length,
|
||||
page,
|
||||
pageSize: 100,
|
||||
hasNext: page * 100 < rows.length,
|
||||
},
|
||||
};
|
||||
}
|
||||
return { data: {} };
|
||||
});
|
||||
const display = await f.admin.managementRequest('worksheets.list');
|
||||
assert.equal(display.data.length, 121);
|
||||
assert.equal(display.data[0]._id, 'item-0');
|
||||
assert.equal(display.data[0].downloads, '33');
|
||||
const raw = await f.admin.managementRequest('worksheets.list', {
|
||||
rawStats: true,
|
||||
});
|
||||
assert.equal(raw.data[0].downloads, '3');
|
||||
await f.admin.managementRequest('worksheets.batch', {
|
||||
patches: [{ id: 'item-0', likes_seed: 10, downloads_seed: 20 }],
|
||||
dryRun: false,
|
||||
});
|
||||
assert.ok(f.requests.at(-1).url.endsWith('/admin/worksheets/batch'));
|
||||
assert.equal(f.requests.at(-1).data.patches[0].likesSeed, 10);
|
||||
assert.equal(f.requests.at(-1).data.patches[0].likes_seed, undefined);
|
||||
await f.admin.managementRequest('worksheets.status', {
|
||||
id: 'item-0',
|
||||
status: 'hidden',
|
||||
});
|
||||
assert.equal(f.requests.at(-1).method, 'PUT');
|
||||
await f.admin.managementRequest('categories.update', {
|
||||
id: 'cat-0',
|
||||
name: '分类',
|
||||
});
|
||||
assert.ok(f.requests.at(-1).url.endsWith('/admin/categories/cat-0'));
|
||||
assert.equal(f.requests.at(-1).data.id, undefined);
|
||||
});
|
||||
|
||||
test('首页兜底与分龄推荐不携带个人凭证,也不触发登录', async () => {
|
||||
const f = fixture();
|
||||
f.handle(async () => ({ data: { home: {}, items: [] } }));
|
||||
await f.site.fetchPageConfig();
|
||||
await f.site.fetchAgeRecommendations('5-6');
|
||||
assert.equal(f.loginCalls, 0);
|
||||
assert.ok(f.requests[0].url.endsWith('/page-config'));
|
||||
assert.ok(f.requests[1].url.endsWith('/recommendations/age'));
|
||||
assert.equal(f.requests[1].data.ageBand, '5-6');
|
||||
assert.ok(f.requests.every((r) => !r.header.Authorization));
|
||||
});
|
||||
|
||||
test('清理账户后迟到的成功响应不能回填旧账户视图', async () => {
|
||||
const f = fixture();
|
||||
await f.auth.getUser();
|
||||
const pending = deferred();
|
||||
const received = deferred();
|
||||
f.handle(async () => {
|
||||
received.resolve();
|
||||
return pending.promise;
|
||||
});
|
||||
const request = f.api.apiRequest('/me/stats');
|
||||
await received.promise;
|
||||
f.auth.clearUser(true);
|
||||
pending.resolve({ data: { favoriteCount: 10 } });
|
||||
await assert.rejects(request, /账户状态已变化/);
|
||||
});
|
||||
@@ -173,7 +173,10 @@ test('环境配置:开发版默认本地,正式/体验/未知环境不回退
|
||||
|
||||
test('请求层:HTTP、业务错误、畸形响应、网络错误均拒绝,不当作空成功', async () => {
|
||||
useRows([]);
|
||||
assert.deepEqual(await apiRequest('/categories'), categories);
|
||||
assert.deepEqual(
|
||||
await apiRequest('/categories', { auth: 'none' }),
|
||||
categories,
|
||||
);
|
||||
assert.equal(requests[0].url, 'http://127.0.0.1:8800/api/v1/categories');
|
||||
for (const result of [
|
||||
{ statusCode: 404 },
|
||||
@@ -183,12 +186,18 @@ test('请求层:HTTP、业务错误、畸形响应、网络错误均拒绝,
|
||||
{ raw: '<html>error</html>' },
|
||||
]) {
|
||||
handler = () => result;
|
||||
await assert.rejects(apiRequest('/categories'), ApiRequestError);
|
||||
await assert.rejects(
|
||||
apiRequest('/categories', { auth: 'none' }),
|
||||
ApiRequestError,
|
||||
);
|
||||
}
|
||||
handler = () => {
|
||||
throw new Error('offline');
|
||||
};
|
||||
await assert.rejects(apiRequest('/categories'), /连接失败/);
|
||||
await assert.rejects(
|
||||
apiRequest('/categories', { auth: 'none' }),
|
||||
/连接失败/,
|
||||
);
|
||||
});
|
||||
|
||||
test('完整分页:121 条无遗漏,计数字符串保留精度,空数据正常结束', async () => {
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { test } from 'node:test';
|
||||
import vm from 'node:vm';
|
||||
import ts from 'typescript';
|
||||
import {
|
||||
assetConfig,
|
||||
resolvePreviewUrl,
|
||||
normalizePreviewImages,
|
||||
} from '../miniprogram/config/assets.ts';
|
||||
|
||||
const legacyRoot =
|
||||
'cloud://cloud1-9gifs7a2756e2c87.636c-cloud1-9gifs7a2756e2c87-1351593184/';
|
||||
const preview = 'assets/previews/math/number-find.jpg';
|
||||
const expected = `https://cdn.joeyone.cn/doodle-assets/${preview}`;
|
||||
|
||||
test('预览图:旧 fileID 和云存储临时链接映射到统一七牛配置', () => {
|
||||
assert.equal(resolvePreviewUrl(`${legacyRoot}${preview}`), expected);
|
||||
assert.equal(
|
||||
resolvePreviewUrl(
|
||||
`https://636c-cloud1-9gifs7a2756e2c87-1351593184.tcb.qcloud.la/${preview}?sign=old&t=123`,
|
||||
),
|
||||
expected,
|
||||
);
|
||||
assert.equal(resolvePreviewUrl(expected), expected);
|
||||
const previous = { ...assetConfig };
|
||||
try {
|
||||
assetConfig.publicDomain = 'https://assets.example.test';
|
||||
assetConfig.keyPrefix = 'next/';
|
||||
assert.equal(
|
||||
resolvePreviewUrl(`${legacyRoot}${preview}`),
|
||||
`https://assets.example.test/next/${preview}`,
|
||||
);
|
||||
} finally {
|
||||
Object.assign(assetConfig, previous);
|
||||
}
|
||||
});
|
||||
|
||||
test('预览图:外部地址、本地图片和头像不替换,畸形预览不产生 CDN 请求', () => {
|
||||
for (const raw of [
|
||||
'/assets/local.png',
|
||||
'https://elsewhere.test/assets/previews/a.jpg',
|
||||
'https://636c-cloud1-9gifs7a2756e2c87-1351593184.tcb.qcloud.la.evil.test/assets/previews/a.jpg',
|
||||
`${legacyRoot}user-avatars/u.png`,
|
||||
'cloud://another-env/assets/previews/a.jpg',
|
||||
'',
|
||||
])
|
||||
assert.equal(resolvePreviewUrl(raw), raw);
|
||||
for (const raw of [
|
||||
`${legacyRoot}assets/previews/../private.jpg`,
|
||||
`${legacyRoot}assets/previews/%2e%2e/private.jpg`,
|
||||
`${legacyRoot}assets/previews/%broken.jpg`,
|
||||
])
|
||||
assert.equal(resolvePreviewUrl(raw), '');
|
||||
assert.equal(resolvePreviewUrl(null), '');
|
||||
});
|
||||
|
||||
test('预览图:嵌套首页分区、收藏及下载记录转换且不修改原数据', () => {
|
||||
const raw = {
|
||||
home: {
|
||||
featured: [{ previewImg: `${legacyRoot}${preview}` }],
|
||||
sections: [{ items: [{ previewImg: `${legacyRoot}${preview}` }] }],
|
||||
},
|
||||
records: [{ worksheet: { previewImg: `${legacyRoot}${preview}` } }],
|
||||
avatarUrl: `${legacyRoot}user-avatars/u.png`,
|
||||
};
|
||||
const converted = normalizePreviewImages(raw);
|
||||
assert.equal(converted.home.featured[0].previewImg, expected);
|
||||
assert.equal(converted.home.sections[0].items[0].previewImg, expected);
|
||||
assert.equal(converted.records[0].worksheet.previewImg, expected);
|
||||
assert.equal(raw.home.featured[0].previewImg, `${legacyRoot}${preview}`);
|
||||
assert.equal(converted.avatarUrl, raw.avatarUrl);
|
||||
});
|
||||
|
||||
test('首页配置:预拉取、旧 storage 缓存及新后端兜底均写回七牛预览图', async () => {
|
||||
const source = readFileSync(
|
||||
new URL('../miniprogram/app.ts', import.meta.url),
|
||||
'utf8',
|
||||
);
|
||||
const code = ts.transpileModule(source, {
|
||||
compilerOptions: {
|
||||
module: ts.ModuleKind.CommonJS,
|
||||
target: ts.ScriptTarget.ES2020,
|
||||
},
|
||||
}).outputText;
|
||||
for (const entry of ['prefetch', 'storage', 'backend']) {
|
||||
const old = {
|
||||
home: { featured: [{ previewImg: `${legacyRoot}${preview}` }] },
|
||||
};
|
||||
let app;
|
||||
let stored;
|
||||
const sandbox = {
|
||||
exports: {},
|
||||
getCurrentPages: () => [],
|
||||
require(name) {
|
||||
if (name === './services/site-api')
|
||||
return { fetchPageConfig: async () => old };
|
||||
if (name === './config/assets')
|
||||
return { normalizePreviewImages };
|
||||
return {};
|
||||
},
|
||||
App(definition) {
|
||||
app = definition;
|
||||
},
|
||||
wx: {
|
||||
getBackgroundFetchData({ success, fail }) {
|
||||
if (entry === 'prefetch')
|
||||
success({ fetchedData: JSON.stringify({ data: old }) });
|
||||
else fail(new Error('not available'));
|
||||
},
|
||||
getStorageSync() {
|
||||
return entry === 'storage' ? old : null;
|
||||
},
|
||||
setStorageSync(key, value) {
|
||||
assert.equal(key, 'pageConfig');
|
||||
stored = value;
|
||||
},
|
||||
},
|
||||
};
|
||||
vm.runInNewContext(code, sandbox);
|
||||
const waiting = app.getPageConfig();
|
||||
await app.loadPageConfig();
|
||||
assert.equal((await waiting).home.featured[0].previewImg, expected);
|
||||
assert.equal(
|
||||
app.globalData.pageConfig.home.featured[0].previewImg,
|
||||
expected,
|
||||
);
|
||||
assert.equal(stored.home.featured[0].previewImg, expected);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user