轻松聘
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

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();
});
});