You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
260 lines
8.6 KiB
260 lines
8.6 KiB
import {
|
|
changePassword,
|
|
getPasswordRuleMessage,
|
|
resetPassword,
|
|
validatePassword,
|
|
validatePasswordConfirm,
|
|
} from '../api/api';
|
|
import { loginByPassword, sendSmsCode, setMockMode } from '../api';
|
|
import { PASSWORD_RULE_LABEL } from '../constants';
|
|
import type { PasswordRuleError } from '../types';
|
|
import { isValidPhone, isValidSmsCode } from '../utils';
|
|
|
|
// api.ts 依赖 storage,Jest 环境无原生 AsyncStorage,需内存版 mock(与 salary.test.ts 同)
|
|
jest.mock('../lib/storage', () => ({
|
|
storageGet: jest.fn(async () => null),
|
|
storageSet: jest.fn(async () => undefined),
|
|
storageRemove: jest.fn(async () => undefined),
|
|
}));
|
|
|
|
/** 联合类型的全部成员:新增成员而漏配文案时,下面的完整性用例会失败 */
|
|
const ALL_RULE_ERRORS: PasswordRuleError[] = [
|
|
'empty',
|
|
'too-short',
|
|
'too-long',
|
|
'need-letter-digit',
|
|
'mismatch',
|
|
'same-as-old',
|
|
];
|
|
|
|
/** 构造 fetch 的响应桩:支持 ok / status / headers.get / text() */
|
|
function makeResponse(status: number, body: unknown) {
|
|
return {
|
|
ok: status >= 200 && status < 300,
|
|
status,
|
|
headers: { get: (k: string) => (k === 'X-Request-Id' ? 'rid' : null) },
|
|
text: async () => JSON.stringify(body),
|
|
};
|
|
}
|
|
|
|
describe('validatePassword(密码强度业务规则)', () => {
|
|
it('空密码返回 empty', () => {
|
|
expect(validatePassword('')).toBe('empty');
|
|
});
|
|
|
|
it('少于 8 位返回 too-short', () => {
|
|
expect(validatePassword('ab12')).toBe('too-short');
|
|
expect(validatePassword('abc1234')).toBe('too-short');
|
|
});
|
|
|
|
it('超过 20 位返回 too-long', () => {
|
|
expect(validatePassword('abc12345678901234567890')).toBe('too-long');
|
|
});
|
|
|
|
it('纯数字返回 need-letter-digit', () => {
|
|
expect(validatePassword('12345678')).toBe('need-letter-digit');
|
|
});
|
|
|
|
it('纯字母返回 need-letter-digit', () => {
|
|
expect(validatePassword('abcdefgh')).toBe('need-letter-digit');
|
|
});
|
|
|
|
it('8–20 位且含字母和数字时通过', () => {
|
|
expect(validatePassword('abc12345')).toBeNull();
|
|
expect(validatePassword('a1b2c3d4e5f6g7h8i9j0')).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('validatePasswordConfirm(两次输入一致性)', () => {
|
|
it('一致时返回 null', () => {
|
|
expect(validatePasswordConfirm('abc12345', 'abc12345')).toBeNull();
|
|
});
|
|
|
|
it('不一致时返回 mismatch', () => {
|
|
expect(validatePasswordConfirm('abc12345', 'abc12346')).toBe('mismatch');
|
|
});
|
|
});
|
|
|
|
describe('PASSWORD_RULE_LABEL(错误码 → 文案映射完整性)', () => {
|
|
it('每个错误码都有非空中文文案', () => {
|
|
ALL_RULE_ERRORS.forEach((code) => {
|
|
const label = PASSWORD_RULE_LABEL[code];
|
|
expect(typeof label).toBe('string');
|
|
expect(label.length).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
it('getPasswordRuleMessage 与映射表一致', () => {
|
|
ALL_RULE_ERRORS.forEach((code) => {
|
|
expect(getPasswordRuleMessage(code)).toBe(PASSWORD_RULE_LABEL[code]);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('validators(手机号 / 验证码格式)', () => {
|
|
it('手机号必须是 1 开头共 11 位', () => {
|
|
expect(isValidPhone('13800138000')).toBe(true);
|
|
expect(isValidPhone('1380013800')).toBe(false);
|
|
expect(isValidPhone('23800138000')).toBe(false);
|
|
expect(isValidPhone('138001380001')).toBe(false);
|
|
expect(isValidPhone('')).toBe(false);
|
|
});
|
|
|
|
it('验证码为 4–6 位数字', () => {
|
|
expect(isValidSmsCode('1234')).toBe(true);
|
|
expect(isValidSmsCode('123456')).toBe(true);
|
|
expect(isValidSmsCode('123')).toBe(false);
|
|
expect(isValidSmsCode('1234567')).toBe(false);
|
|
expect(isValidSmsCode('12a4')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('loginByPassword(手机号 + 密码登录 · 真实后端)', () => {
|
|
beforeEach(() => {
|
|
setMockMode(false);
|
|
(global as unknown as { fetch: jest.Mock }).fetch = jest.fn();
|
|
});
|
|
|
|
it('手机号非法时本地拒绝,不发请求', async () => {
|
|
await expect(
|
|
loginByPassword({ phone: '138', password: 'abc12345' }),
|
|
).rejects.toThrow('手机号格式不正确');
|
|
expect((global as unknown as { fetch: jest.Mock }).fetch).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('参数合法时 POST /auth/password/login 且返回双 Token(匿名接口不带 Bearer)', async () => {
|
|
(global as unknown as { fetch: jest.Mock }).fetch = jest.fn(
|
|
async () =>
|
|
makeResponse(200, {
|
|
code: 200,
|
|
message: 'ok',
|
|
data: {
|
|
userId: 123,
|
|
phone: '13800138000',
|
|
nickname: '测试用户',
|
|
role: 'STUDENT',
|
|
accessToken: 'A',
|
|
refreshToken: 'R',
|
|
expiresIn: 86400,
|
|
},
|
|
requestId: 'r1',
|
|
timestamp: 't',
|
|
}),
|
|
);
|
|
const result = await loginByPassword({ phone: '13800138000', password: 'abc12345' });
|
|
expect(result.accessToken).toBe('A');
|
|
expect(result.refreshToken).toBe('R');
|
|
const [url, init] = (
|
|
(global as unknown as { fetch: jest.Mock }).fetch as jest.Mock
|
|
).mock.calls[0];
|
|
expect(url).toContain('/auth/password/login');
|
|
expect(init.method).toBe('POST');
|
|
expect(init.headers['Authorization']).toBeUndefined();
|
|
expect(JSON.parse(init.body)).toEqual({ phone: '13800138000', password: 'abc12345' });
|
|
});
|
|
|
|
it('HTTP 401 时抛出错误(无 refreshToken 不自动刷新)', async () => {
|
|
(global as unknown as { fetch: jest.Mock }).fetch = jest.fn(
|
|
async () =>
|
|
makeResponse(401, {
|
|
code: 401,
|
|
message: '凭证无效',
|
|
data: null,
|
|
requestId: 'r2',
|
|
timestamp: 't',
|
|
}),
|
|
);
|
|
await expect(
|
|
loginByPassword({ phone: '13800138000', password: 'abc12345' }),
|
|
).rejects.toThrow();
|
|
});
|
|
});
|
|
|
|
describe('sendSmsCode(发送验证码 · 真实后端)', () => {
|
|
beforeEach(() => {
|
|
setMockMode(false);
|
|
(global as unknown as { fetch: jest.Mock }).fetch = jest.fn();
|
|
});
|
|
|
|
it('手机号非法时本地拒绝,不发请求', async () => {
|
|
await expect(sendSmsCode({ phone: '138', purpose: 'login' })).rejects.toThrow(
|
|
'手机号格式不正确',
|
|
);
|
|
expect((global as unknown as { fetch: jest.Mock }).fetch).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('手机号合法时 POST /auth/sms/send(带 purpose)', async () => {
|
|
(global as unknown as { fetch: jest.Mock }).fetch = jest.fn(
|
|
async () =>
|
|
makeResponse(200, {
|
|
code: 200,
|
|
message: 'ok',
|
|
data: null,
|
|
requestId: 'r3',
|
|
timestamp: 't',
|
|
}),
|
|
);
|
|
await sendSmsCode({ phone: '13800138000', purpose: 'login' });
|
|
const [url, init] = (
|
|
(global as unknown as { fetch: jest.Mock }).fetch as jest.Mock
|
|
).mock.calls[0];
|
|
expect(url).toContain('/auth/sms/send');
|
|
// sms/send 后端只接受 Query 参数(body 为空)
|
|
expect(url).toContain('phone=13800138000');
|
|
expect(url).toContain('purpose=login');
|
|
expect(init.method).toBe('POST');
|
|
expect(init.body).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('resetPassword(重置 / 首次设置密码)', () => {
|
|
it('验证码不合法时拒绝', async () => {
|
|
await expect(
|
|
resetPassword({ phone: '13800138000', code: '12', password: 'abc12345' }),
|
|
).rejects.toThrow('验证码不正确');
|
|
});
|
|
|
|
it('新密码不合规时返回对应文案', async () => {
|
|
await expect(
|
|
resetPassword({ phone: '13800138000', code: '1234', password: '12345678' }),
|
|
).rejects.toThrow(PASSWORD_RULE_LABEL['need-letter-digit']);
|
|
});
|
|
|
|
it('参数合法时直接下发登录态,用户无需再登录一次', async () => {
|
|
const result = await resetPassword({
|
|
phone: '13800138000',
|
|
code: '1234',
|
|
password: 'abc12345',
|
|
});
|
|
expect(result.phone).toBe('13800138000');
|
|
expect(result.token).toContain('13800138000');
|
|
});
|
|
});
|
|
|
|
describe('changePassword(已登录态改密)', () => {
|
|
beforeAll(() => setMockMode(true));
|
|
|
|
it('未填原密码时拒绝', async () => {
|
|
await expect(
|
|
changePassword({ oldPassword: '', newPassword: 'abc12345', confirmPassword: 'abc12345' }),
|
|
).rejects.toThrow('请输入原密码');
|
|
});
|
|
|
|
it('两次新密码不一致时拒绝', async () => {
|
|
await expect(
|
|
changePassword({ oldPassword: 'old12345', newPassword: 'abc12345', confirmPassword: 'abc12346' }),
|
|
).rejects.toThrow(PASSWORD_RULE_LABEL.mismatch);
|
|
});
|
|
|
|
it('新密码与原密码相同时拒绝', async () => {
|
|
await expect(
|
|
changePassword({ oldPassword: 'abc12345', newPassword: 'abc12345', confirmPassword: 'abc12345' }),
|
|
).rejects.toThrow(PASSWORD_RULE_LABEL['same-as-old']);
|
|
});
|
|
|
|
it('参数合法时成功', async () => {
|
|
await expect(
|
|
changePassword({ oldPassword: 'old12345', newPassword: 'abc12345', confirmPassword: 'abc12345' }),
|
|
).resolves.toBeUndefined();
|
|
});
|
|
});
|
|
|