Files
Friendship-and-chat-uniapp/src/common/__tests__/jpush-account-transition.test.mjs
T
2026-11-11 19:38:29 +08:00

157 lines
6.7 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import vm from 'node:vm';
const settle = () => new Promise((resolve) => setImmediate(resolve));
/** 保留用户资料请求,让通知可以在令牌替换与资料返回之间到达。 */
function deferred() {
let resolve;
const promise = new Promise((complete) => { resolve = complete; });
return { promise, resolve };
}
/** 同一 VM 执行实际 user 与 jpush 模块,覆盖公开登录入口的联动行为。 */
async function fixture(authenticated = true) {
const storage = new Map(authenticated ? [['token', 'account-A-token']] : []);
const profile = deferred();
const routes = [];
const unbindings = [];
const bindings = [];
const logins = [];
let route = 'pages/tabbar/home';
const navigate = async ({ url }) => {
routes.push(url);
route = url.replace(/^\//, '').split('?')[0];
};
class NotificationChannel {
static IMPORTANCE_HIGH = 4;
setDescription() {}
enableVibration() {}
}
const context = vm.createContext({
console: { warn() {} }, setTimeout, clearTimeout,
JPUSH_APP_KEY: 'test-app-key', JPUSH_APNS_PRODUCTION: true,
plus: { android: {
importClass: (value) => typeof value === 'string' ? NotificationChannel : value,
runtimeMainActivity: () => ({ getSystemService: () => ({ createNotificationChannel() {} }) }),
} },
nativePush: {
setEventCallBack() {}, setDebug() {}, init() {}, requestRequiredPermission() {},
getRegistrationId: () => 'test-registration', setAlias() {}, deleteAlias() {}, clearNotificationAll() {},
},
PushApi: {
bind: async (data, token) => { bindings.push({ data, token }); return { code: 0 }; },
unbind: async (registrationId, token) => { unbindings.push({ registrationId, token }); return { code: 0 }; },
},
UserApi: { getUserInfo: () => profile.promise },
PayWalletApi: { getPayWallet: async () => ({ code: 0, data: { balance: 0 } }) },
checkPushNotificationPermission: async () => null,
wsApi: { setPushState() {}, destroy() {} },
chatTokenManager: { stop() {} },
appUpdates: { identityChanged() {}, authenticated() {} },
$share: { getShareInfo() {}, bindBrokerageUser() {} },
app: () => ({ platform: { bind_mobile: 0 } }),
cart: () => ({ emptyList() {} }),
clone: (value) => JSON.parse(JSON.stringify(value)),
cloneDeep: (value) => JSON.parse(JSON.stringify(value)),
defineStore: (definition) => {
let store;
return () => {
if (store) return store;
store = definition.state();
for (const [name, action] of Object.entries(definition.actions)) store[name] = action.bind(store);
return store;
};
},
uni: {
getSystemInfoSync: () => ({ platform: 'android' }),
getStorageSync: (key) => storage.get(key),
setStorageSync: (key, value) => storage.set(key, value),
removeStorageSync: (key) => storage.delete(key),
navigateTo: navigate, redirectTo: navigate, switchTab: navigate,
},
getCurrentPages: () => [{ route }],
getApp: () => ({ $vm: { configStore: { appInit: true }, chatStore: { loading: false } } }),
});
const pushSource = (await readFile(new URL('../jpush.js', import.meta.url), 'utf8'))
.replace(/^import .+;\n/gm, '').replace(/^export const /gm, 'const ');
const userSource = (await readFile(new URL('../../sheep/store/user.js', import.meta.url), 'utf8'))
.replace(/^import .+;\n/gm, '').replace('export default user;', '');
vm.runInContext(`${pushSource}\n${userSource}\nglobalThis.service = {
initializePush, bindPushUser, handlePushNotificationClick, getPushAccountId, user,
};`, context);
const service = context.service;
if (authenticated) await service.bindPushUser(42);
else service.initializePush();
const store = service.user();
if (authenticated) store.userInfo = { id: 42, sex: 1 };
const loginAfter = store.loginAfter;
store.loginAfter = () => {
const task = loginAfter();
logins.push(task);
return task;
};
const finishProfile = async (id) => {
profile.resolve({ code: 0, data: { id, sex: 1 } });
await Promise.all(logins);
await settle();
};
return { service, store, storage, routes, bindings, unbindings, finishProfile };
}
test('account transition rejects old-account navigation and unbinds using the old token', async () => {
const { service, store, storage, routes, bindings, unbindings, finishProfile } = await fixture();
store.setToken('account-B-token', 'account-B-refresh');
assert.equal(storage.get('token'), 'account-B-token');
assert.equal(service.getPushAccountId(), '');
service.handlePushNotificationClick({ extras: {
type: 'private_message', taskId: 'late-A', receiverId: 42, targetId: 99,
} });
await settle();
assert.equal(routes.length, 0);
assert.equal(unbindings.length, 1);
assert.equal(unbindings[0].token, 'account-A-token');
assert.equal(unbindings[0].registrationId, 'test-registration');
await finishProfile(43);
assert.equal(service.getPushAccountId(), '43');
assert.equal(storage.has('chat-jpush-pending-route'), false);
assert.equal(bindings.at(-1).token, 'account-B-token');
service.handlePushNotificationClick({ extras: {
type: 'private_message', taskId: 'current-B', receiverId: 43, targetId: 100,
} });
await settle();
assert.equal(routes.length, 1);
assert.match(routes[0], /targetId=100&receiverId=43/);
});
test('refreshing a token keeps the current push account and notification handling', async () => {
const { service, store, unbindings, routes, finishProfile } = await fixture();
store.setToken('account-A-refreshed', 'account-A-refresh', true);
assert.equal(service.getPushAccountId(), '42');
await finishProfile(42);
assert.equal(unbindings.length, 0);
service.handlePushNotificationClick({ extras: {
type: 'private_message', taskId: 'after-refresh', receiverId: 42, targetId: 99,
} });
await settle();
assert.match(routes.at(-1), /targetId=99&receiverId=42/);
});
test('first login preserves an anonymous owned cold click until its user profile is ready', async () => {
const { service, store, storage, routes, unbindings, finishProfile } = await fixture(false);
service.handlePushNotificationClick({ extras: {
type: 'private_message', taskId: 'cold-A', receiverId: 42, targetId: 99,
} });
await settle();
store.setToken('account-A-token', 'account-A-refresh');
assert.equal(storage.get('chat-jpush-pending-route').clickId, 'private_message:cold-A');
assert.equal(unbindings.length, 0);
await finishProfile(42);
assert.equal(service.getPushAccountId(), '42');
assert.match(routes.at(-1), /targetId=99&receiverId=42/);
assert.equal(storage.has('chat-jpush-pending-route'), false);
});