verification-code.service.test.ts 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. import { VerificationCodeService } from '../../../src/services/verification/verification-code.service';
  2. // Mock cache
  3. jest.mock('../../../src/config/redis', () => ({
  4. cache: {
  5. get: jest.fn(),
  6. set: jest.fn(),
  7. del: jest.fn(),
  8. exists: jest.fn(),
  9. },
  10. }));
  11. // Mock logger
  12. jest.mock('../../../src/utils/logger', () => ({
  13. logger: {
  14. info: jest.fn(),
  15. error: jest.fn(),
  16. warn: jest.fn(),
  17. },
  18. }));
  19. describe('VerificationCodeService 单元测试', () => {
  20. let verificationService: VerificationCodeService;
  21. const mockCache = require('../../../src/config/redis').cache;
  22. beforeEach(() => {
  23. verificationService = new VerificationCodeService();
  24. jest.clearAllMocks();
  25. });
  26. describe('generateAndSendCode', () => {
  27. it('应该成功生成并发送验证码', async () => {
  28. const phone = '13800138000';
  29. const type = 'register';
  30. // Mock频率检查通过
  31. mockCache.exists.mockResolvedValue(false);
  32. // Mock存储成功
  33. mockCache.set.mockResolvedValue(true);
  34. const result = await verificationService.generateAndSendCode(phone, type);
  35. expect(mockCache.exists).toHaveBeenCalledWith(`rate_limit:${type}:${phone}`);
  36. expect(mockCache.set).toHaveBeenCalledWith(
  37. `verification:${type}:${phone}`,
  38. expect.objectContaining({
  39. code: expect.stringMatching(/^\d{6}$/),
  40. createdAt: expect.any(Number),
  41. attempts: 0,
  42. type,
  43. }),
  44. 300 // 5分钟
  45. );
  46. expect(mockCache.set).toHaveBeenCalledWith(
  47. `rate_limit:${type}:${phone}`,
  48. '1',
  49. 60 // 60秒
  50. );
  51. expect(result).toMatch(/^\d{6}$/);
  52. });
  53. it('应该拒绝频繁发送验证码', async () => {
  54. const phone = '13800138000';
  55. const type = 'register';
  56. // Mock频率检查不通过
  57. mockCache.exists.mockResolvedValue(true);
  58. await expect(verificationService.generateAndSendCode(phone, type))
  59. .rejects.toThrow('验证码发送过于频繁,请稍后重试');
  60. expect(mockCache.exists).toHaveBeenCalledWith(`rate_limit:${type}:${phone}`);
  61. expect(mockCache.set).not.toHaveBeenCalledWith(
  62. `verification:${type}:${phone}`,
  63. expect.any(Object),
  64. expect.any(Number)
  65. );
  66. });
  67. });
  68. describe('verifyCode', () => {
  69. it('应该成功验证正确的验证码', async () => {
  70. const phone = '13800138000';
  71. const code = '123456';
  72. const type = 'register';
  73. // Mock存储的验证码数据
  74. const codeData = {
  75. code: '123456',
  76. createdAt: Date.now() - 10000, // 10秒前创建
  77. attempts: 0,
  78. type,
  79. };
  80. mockCache.get.mockResolvedValue(codeData);
  81. mockCache.del.mockResolvedValue(true);
  82. const result = await verificationService.verifyCode(phone, code, type);
  83. expect(mockCache.get).toHaveBeenCalledWith(`verification:${type}:${phone}`);
  84. expect(mockCache.del).toHaveBeenCalledWith(`verification:${type}:${phone}`);
  85. expect(result).toBe(true);
  86. });
  87. it('应该拒绝错误的验证码', async () => {
  88. const phone = '13800138000';
  89. const code = '123456';
  90. const type = 'register';
  91. // Mock存储的验证码数据(不同的验证码)
  92. const codeData = {
  93. code: '654321',
  94. createdAt: Date.now() - 10000, // 10秒前创建
  95. attempts: 0,
  96. type,
  97. };
  98. mockCache.get.mockResolvedValue(codeData);
  99. mockCache.set.mockResolvedValue(true);
  100. const result = await verificationService.verifyCode(phone, code, type);
  101. expect(mockCache.get).toHaveBeenCalledWith(`verification:${type}:${phone}`);
  102. expect(mockCache.set).toHaveBeenCalledWith(
  103. `verification:${type}:${phone}`,
  104. expect.objectContaining({
  105. code: '654321',
  106. attempts: 1, // 尝试次数增加
  107. }),
  108. 300
  109. );
  110. expect(result).toBe(false);
  111. });
  112. it('应该拒绝过期的验证码', async () => {
  113. const phone = '13800138000';
  114. const code = '123456';
  115. const type = 'register';
  116. // Mock没有找到验证码数据(已过期)
  117. mockCache.get.mockResolvedValue(null);
  118. await expect(verificationService.verifyCode(phone, code, type))
  119. .rejects.toThrow('验证码已过期,请重新获取');
  120. expect(mockCache.get).toHaveBeenCalledWith(`verification:${type}:${phone}`);
  121. });
  122. it('应该拒绝尝试次数过多的验证码', async () => {
  123. const phone = '13800138000';
  124. const code = '123456';
  125. const type = 'register';
  126. // Mock存储的验证码数据(尝试次数已达上限)
  127. const codeData = {
  128. code: '654321',
  129. createdAt: Date.now() - 10000, // 10秒前创建
  130. attempts: 5, // 已达到最大尝试次数
  131. type,
  132. };
  133. mockCache.get.mockResolvedValue(codeData);
  134. mockCache.del.mockResolvedValue(true);
  135. await expect(verificationService.verifyCode(phone, code, type))
  136. .rejects.toThrow('验证码尝试次数过多,请重新获取');
  137. expect(mockCache.get).toHaveBeenCalledWith(`verification:${type}:${phone}`);
  138. expect(mockCache.del).toHaveBeenCalledWith(`verification:${type}:${phone}`);
  139. });
  140. });
  141. describe('checkRateLimit', () => {
  142. it('应该允许在限制时间外发送验证码', async () => {
  143. const phone = '13800138000';
  144. const type = 'register';
  145. // Mock频率限制键不存在
  146. mockCache.exists.mockResolvedValue(false);
  147. const result = await verificationService.checkRateLimit(phone, type);
  148. expect(mockCache.exists).toHaveBeenCalledWith(`rate_limit:${type}:${phone}`);
  149. expect(result).toBe(true);
  150. });
  151. it('应该拒绝在限制时间内发送验证码', async () => {
  152. const phone = '13800138000';
  153. const type = 'register';
  154. // Mock频率限制键存在
  155. mockCache.exists.mockResolvedValue(true);
  156. const result = await verificationService.checkRateLimit(phone, type);
  157. expect(mockCache.exists).toHaveBeenCalledWith(`rate_limit:${type}:${phone}`);
  158. expect(result).toBe(false);
  159. });
  160. });
  161. describe('getStoredCode', () => {
  162. it('应该返回存储的验证码数据', async () => {
  163. const phone = '13800138000';
  164. const type = 'register';
  165. const codeData = {
  166. code: '123456',
  167. createdAt: Date.now() - 10000,
  168. attempts: 0,
  169. type,
  170. };
  171. mockCache.get.mockResolvedValue(codeData);
  172. const result = await verificationService.getStoredCode(phone, type);
  173. expect(mockCache.get).toHaveBeenCalledWith(`verification:${type}:${phone}`);
  174. expect(result).toEqual(codeData);
  175. });
  176. it('应该返回null当验证码不存在', async () => {
  177. const phone = '13800138000';
  178. const type = 'register';
  179. // Mock没有找到验证码数据
  180. mockCache.get.mockResolvedValue(null);
  181. const result = await verificationService.getStoredCode(phone, type);
  182. expect(mockCache.get).toHaveBeenCalledWith(`verification:${type}:${phone}`);
  183. expect(result).toBeNull();
  184. });
  185. });
  186. describe('deleteCode', () => {
  187. it('应该成功删除验证码', async () => {
  188. const phone = '13800138000';
  189. const type = 'register';
  190. mockCache.del.mockResolvedValue(true);
  191. await verificationService.deleteCode(phone, type);
  192. expect(mockCache.del).toHaveBeenCalledWith(`verification:${type}:${phone}`);
  193. });
  194. });
  195. });