| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169 |
- import test from 'node:test';
- import assert from 'node:assert/strict';
- import { readFile } from 'node:fs/promises';
- import { parse, compileScript } from '@vue/compiler-sfc';
- import ts from 'typescript';
- import * as vue from 'vue';
- import { prepareCommand, mayHaveCommitted } from '../src/utils/command-core.mjs';
- const source = await readFile(new URL('../src/pages/account/rewards.vue', import.meta.url), 'utf8');
- const { descriptor } = parse(source, { filename: 'rewards.vue' });
- const script = compileScript(descriptor, { id: 'promotion-test' });
- const resourceSource = await readFile(
- new URL('../src/composables/useResource.ts', import.meta.url),
- 'utf8',
- );
- const commandSource = await readFile(
- new URL('../src/composables/useCommand.ts', import.meta.url),
- 'utf8',
- );
- function evaluate(source, dependencies) {
- const { outputText } = ts.transpileModule(source, {
- compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS },
- });
- const exports = {};
- new Function('require', 'exports', outputText)((name) => {
- if (name in dependencies) return dependencies[name];
- if (name.endsWith('.vue')) return {};
- throw new Error('Unexpected dependency: ' + name);
- }, exports);
- return exports;
- }
- const flush = () => new Promise((resolve) => setImmediate(resolve));
- class ApiFailure extends Error {
- constructor(message, status = 0) {
- super(message);
- this.status = status;
- }
- }
- function fixture() {
- const hooks = {},
- storage = new Map(),
- posts = [];
- const session = vue.reactive({ token: '', userId: '', epoch: 0, active: null });
- const code = 'AABBCCDDEEFF00112233';
- globalThis.uni = {
- getStorageSync: (key) => storage.get(key) || '',
- setStorageSync: (key, value) => storage.set(key, value),
- removeStorageSync: (key) => storage.delete(key),
- stopPullDownRefresh() {},
- showToast() {},
- };
- const response = {
- enabled: true,
- invite_code: code,
- total_buckets: 0,
- total: 0,
- items: [],
- bound: null,
- policy: {},
- };
- const client = {
- ApiFailure,
- get: async () => response,
- post: (path, payload) =>
- new Promise((resolve, reject) => posts.push({ path, payload, resolve, reject })),
- };
- const format = { toast() {} };
- const resource = evaluate(resourceSource, { vue, '../stores/session': { session } });
- const command = evaluate(commandSource, {
- vue,
- '../stores/session': { session },
- '../api/client': client,
- '../utils/command-core.mjs': { prepareCommand, mayHaveCommitted },
- '../utils/format': format,
- });
- const state = evaluate(script.content, {
- vue,
- '@dcloudio/uni-app': {
- onLoad: (fn) => (hooks.load = fn),
- onShow: (fn) => (hooks.show = fn),
- onShareAppMessage: (fn) => (hooks.share = fn),
- },
- '../../api/client': client,
- '../../stores/session': {
- session,
- resourcePath: () => `/enterprises/${session.active.resource_id}`,
- canManageEnterprise: () => session.active?.role === 'enterprise_admin',
- },
- '../../composables/useResource': resource,
- '../../composables/useCommand': command,
- '../../utils/format': format,
- }).default.setup({}, { expose() {} });
- const login = () =>
- Object.assign(session, {
- token: 'a'.repeat(64),
- userId: 'promotion-user',
- active: { role: 'enterprise_admin', resource_id: 42 },
- });
- return { state, hooks, storage, posts, session, login, code };
- }
- test('shared invitation survives login and does not bind until explicitly confirmed', async () => {
- const app = fixture();
- app.hooks.load({ invite_code: app.code.toLowerCase() });
- app.hooks.show();
- assert.equal(app.state.inviteInput.value, app.code);
- assert.equal(app.posts.length, 0);
- app.login();
- app.hooks.show();
- await flush();
- assert.equal(app.posts.length, 0, 'opening a shared page must never write attribution');
- app.state.bind();
- app.state.bind();
- assert.equal(app.posts.length, 1, 'double confirmation is serialized');
- assert.equal(app.posts[0].path, '/enterprises/42/promotion/bind');
- assert.equal(app.posts[0].payload.invite_code, app.code);
- assert.match(app.posts[0].payload.request_key, /^[A-Za-z0-9_-]{8,80}$/);
- app.posts[0].resolve({ id: 1 });
- await flush();
- assert.equal(
- app.storage.has('water.promotion-invite'),
- false,
- 'clear pending invitation only after success',
- );
- });
- test('invalid share parameters cannot create or bind an invitation', async () => {
- const app = fixture();
- app.hooks.load({ invite_code: 'https://untrusted.example/?token=anything' });
- app.login();
- app.hooks.show();
- await flush();
- assert.equal(app.state.inviteInput.value, '');
- app.state.inviteInput.value = 'invalid';
- app.state.bind();
- assert.equal(app.posts.length, 0);
- assert.equal(app.storage.has('water.promotion-invite'), false);
- });
- test('an unknown binding result retains its payload and cannot be replaced by another inviter', async () => {
- const app = fixture();
- app.login();
- app.state.inviteInput.value = app.code;
- app.state.bind();
- const original = app.posts[0].payload;
- app.posts[0].reject(new ApiFailure('网络超时'));
- await flush();
- assert.equal(app.state.binding.pending, true);
- app.state.inviteInput.value = '00112233445566778899';
- app.state.bind();
- await flush();
- assert.equal(app.posts.length, 1, 'changed inviter must not replace unresolved binding');
- const retry = app.state.binding.retry();
- assert.deepEqual(app.posts[1].payload, original);
- app.posts[1].resolve({ id: 1, replayed: true });
- await retry;
- });
- test('shared links contain only the public enterprise code and clear it when identity changes', async () => {
- const app = fixture();
- app.login();
- app.hooks.show();
- await flush();
- assert.equal(app.hooks.share().path, '/pages/account/rewards?invite_code=' + app.code);
- assert.equal(app.hooks.share().path.includes(app.session.token), false);
- app.session.epoch++;
- assert.equal(app.state.data.value, null);
- assert.equal(app.hooks.share().path, '/pages/account/rewards');
- });
|