157 lines
6.7 KiB
JavaScript
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);
|
|
});
|