| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251 |
- 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';
- const flowSource = await readFile(new URL('../src/utils/login-flow.ts', import.meta.url), 'utf8');
- const pageSource = await readFile(
- new URL('../src/pages/account/login.vue', import.meta.url),
- 'utf8',
- );
- const { descriptor } = parse(pageSource, { filename: 'login.vue' });
- const script = compileScript(descriptor, { id: 'login-test' });
- function evaluate(source, dependencies, runtime = {}) {
- const { outputText } = ts.transpileModule(source, {
- compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS },
- });
- const exports = {};
- new Function('require', 'exports', 'setInterval', 'clearInterval', 'uni', outputText)(
- (name) => {
- if (name in dependencies) return dependencies[name];
- throw new Error('Unexpected dependency: ' + name);
- },
- exports,
- runtime.setInterval,
- runtime.clearInterval,
- runtime.uni,
- );
- return exports;
- }
- function fixture(capabilities = { sms_login: true, test_phone_login: true }) {
- const posts = [],
- messages = [],
- timers = new Map(),
- accepted = [],
- navigations = [];
- let timerId = 0;
- let unmount;
- const client = {
- post: (path, payload, authenticated) =>
- new Promise((resolve, reject) =>
- posts.push({ path, payload, authenticated, resolve, reject }),
- ),
- };
- const format = { toast: (message) => messages.push(message), go() {} };
- const flow = evaluate(flowSource, { '../api/client': client, './format': format });
- const state = evaluate(
- script.content,
- {
- vue: {
- ...vue,
- onUnmounted: (callback) => {
- unmount = callback;
- },
- },
- '@dcloudio/uni-app': { onShow() {} },
- '../../stores/session': {
- session: vue.reactive({ configuration: { capabilities } }),
- loadConfiguration() {},
- acceptLogin: (login) => accepted.push(login),
- accountPage: () => '/pages/home/index',
- },
- '../../api/client': client,
- '../../utils/format': format,
- '../../utils/login-flow': flow,
- },
- {
- setInterval: (callback) => {
- timers.set(++timerId, callback);
- return timerId;
- },
- clearInterval: (id) => timers.delete(id),
- uni: { reLaunch: (options) => navigations.push(options.url) },
- },
- ).default.setup({}, { expose() {} });
- return { ...flow, state, posts, messages, timers, accepted, navigations, unmount };
- }
- test('real phone numbers request SMS and use SMS login', async () => {
- for (const phone of ['13800138000', '13900139000']) {
- const app = fixture();
- const send = app.requestPhoneCode(phone);
- assert.equal(app.posts.length, 1);
- assert.equal(app.posts[0].path, '/auth/send-sms');
- assert.deepEqual(app.posts[0].payload, { phone });
- assert.equal(app.posts[0].authenticated, false);
- app.posts[0].resolve({ sent: true, expires_in: 120 });
- assert.equal(await send, true);
- const login = app.loginByPhoneCode(phone, '728415');
- assert.equal(app.posts[1].path, '/auth/phone-login');
- assert.deepEqual(app.posts[1].payload, { phone, code: '728415' });
- assert.equal(app.posts[1].authenticated, false);
- app.posts[1].resolve({ user_id: '42' });
- assert.equal((await login).user_id, '42');
- }
- });
- test('all three test accounts log in without SMS or a verification code', async () => {
- for (const phone of ['13012345678', '13012345679', '13012345680']) {
- const app = fixture();
- assert.equal(await app.requestPhoneCode(phone), false);
- assert.equal(app.posts.length, 0);
- assert.match(app.messages.at(-1), /无需获取短信/);
- app.state.phone.value = phone;
- app.state.agreed.value = true;
- const login = app.state.loginByPhone();
- assert.equal(app.posts.length, 1);
- assert.equal(app.posts[0].path, '/auth/test-phone');
- assert.deepEqual(app.posts[0].payload, { phone });
- assert.equal(app.posts[0].authenticated, false);
- app.posts[0].resolve({ user_id: '42' });
- await login;
- assert.equal(app.accepted.length, 1);
- }
- });
- test('get code works before agreement, serializes taps, and starts countdown only after success', async () => {
- const app = fixture();
- app.state.phone.value = '13800138000';
- app.state.phoneCode.value = '123456';
- assert.equal(app.state.agreed.value, false);
- const send = app.state.sendCode();
- await app.state.sendCode();
- assert.equal(app.posts.length, 1);
- assert.equal(app.state.sendingCode.value, true);
- assert.equal(app.state.codeCountdown.value, 0);
- app.posts[0].resolve({ sent: true, expires_in: 120 });
- await send;
- assert.equal(app.state.sendingCode.value, false);
- assert.equal(app.state.phoneCode.value, '');
- assert.equal(app.state.codeCountdown.value, 60);
- assert.match(app.messages.at(-1), /查看手机短信/);
- await app.state.sendCode();
- assert.equal(app.posts.length, 1, 'countdown prevents resending');
- for (let i = 0; i < 60; i++) [...app.timers.values()].forEach((tick) => tick());
- assert.equal(app.state.codeCountdown.value, 0);
- assert.equal(app.timers.size, 0);
- const resend = app.state.sendCode();
- assert.equal(app.posts.length, 2);
- app.posts[1].resolve({ sent: true });
- await resend;
- app.unmount();
- assert.equal(app.timers.size, 0);
- });
- test('failed SMS shows the server error, releases the button and permits retry without countdown', async () => {
- const app = fixture();
- app.state.phone.value = '13800138000';
- const send = app.state.sendCode();
- app.posts[0].reject(new Error('短信发送失败,请稍后再试'));
- await send;
- assert.match(app.messages.at(-1), /短信发送失败/);
- assert.equal(app.state.sendingCode.value, false);
- assert.equal(app.state.codeCountdown.value, 0);
- assert.equal(app.timers.size, 0);
- const retry = app.state.sendCode();
- assert.equal(app.posts.length, 2);
- app.posts[1].resolve({ sent: true });
- await retry;
- app.unmount();
- });
- test('test capability enables test login without enabling real SMS', async () => {
- const app = fixture({ test_phone_login: true });
- app.state.phone.value = '13012345678';
- assert.equal(app.state.phoneLoginAvailable.value, true);
- await app.state.sendCode();
- assert.equal(app.posts.length, 0);
- assert.equal(app.state.codeCountdown.value, 0);
- assert.match(app.messages.at(-1), /无需获取短信/);
- app.state.phone.value = '13800138000';
- await app.state.sendCode();
- assert.equal(app.posts.length, 0);
- assert.match(app.messages.at(-1), /短信登录暂未开放/);
- });
- test('disabled test login cannot fall through to SMS even when SMS is enabled', async () => {
- const app = fixture({ sms_login: true, test_phone_login: false });
- app.state.phone.value = '13012345678';
- app.state.phoneCode.value = '66666';
- app.state.agreed.value = true;
- await app.state.loginByPhone();
- await app.state.sendCode();
- assert.equal(app.posts.length, 0);
- assert.match(app.messages.at(-1), /未开启测试账号登录/);
- });
- test('phone login is unavailable only when both capabilities are disabled', () => {
- assert.equal(
- fixture({ sms_login: false, test_phone_login: false }).state.phoneLoginAvailable.value,
- false,
- );
- });
- test('invalid numbers are explained without sending requests', async () => {
- const app = fixture();
- app.state.phone.value = '12345';
- await app.state.sendCode();
- assert.equal(app.posts.length, 0);
- assert.match(app.messages.at(-1), /正确的手机号码/);
- });
- test('real login requires agreement, forwards the code and serializes taps', async () => {
- const app = fixture();
- app.state.phone.value = '13800138000';
- app.state.phoneCode.value = '728415';
- await app.state.loginByPhone();
- assert.equal(app.posts.length, 0);
- assert.match(app.messages.at(-1), /同意服务说明/);
- app.state.agreed.value = true;
- const login = app.state.loginByPhone();
- await app.state.loginByPhone();
- assert.equal(app.posts.length, 1);
- assert.equal(app.posts[0].path, '/auth/phone-login');
- assert.deepEqual(app.posts[0].payload, { phone: '13800138000', code: '728415' });
- const session = { user_id: '42', access_token: 'token', identities: [] };
- app.posts[0].resolve(session);
- await login;
- assert.deepEqual(app.accepted, [session]);
- assert.deepEqual(app.navigations, ['/pages/home/index']);
- assert.equal(app.state.busy.value, false);
- });
- test('real phone numbers still require a six-digit SMS code', async () => {
- const app = fixture();
- app.state.agreed.value = true;
- app.state.phone.value = '13800138000';
- app.state.phoneCode.value = '66666';
- await app.state.loginByPhone();
- app.state.phoneCode.value = '';
- await app.state.loginByPhone();
- assert.equal(app.posts.length, 0);
- assert.match(app.messages.at(-1), /6位短信验证码/);
- });
- test('wrong code is reported without accepting a session or navigating away', async () => {
- const app = fixture();
- app.state.phone.value = '13800138000';
- app.state.phoneCode.value = '123456';
- app.state.agreed.value = true;
- const login = app.state.loginByPhone();
- app.posts[0].reject(new Error('验证码错误'));
- await login;
- assert.match(app.messages.at(-1), /验证码错误/);
- assert.equal(app.accepted.length, 0);
- assert.equal(app.navigations.length, 0);
- assert.equal(app.state.busy.value, false);
- });
|