auth.service.property.test.ts 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  1. import * as fc from 'fast-check';
  2. import jwt from 'jsonwebtoken';
  3. /**
  4. * **Feature: backend-frontend-integration, Property 8: Authentication Token Contains Role**
  5. * **Validates: Requirements 10.1**
  6. *
  7. * *For any* successful login, the returned JWT token SHALL contain a role claim
  8. * that matches the user's role in the database.
  9. */
  10. // 定义有效的用户角色
  11. type UserRole = 'buyer' | 'seller' | 'admin' | 'customer_service';
  12. /**
  13. * 模拟用户数据
  14. */
  15. interface MockUser {
  16. id: string;
  17. username: string;
  18. email: string;
  19. role: UserRole;
  20. permissions?: string[];
  21. }
  22. /**
  23. * JWT载荷接口
  24. */
  25. interface JwtPayload {
  26. userId: string;
  27. username: string;
  28. email: string;
  29. role: UserRole;
  30. permissions?: string[];
  31. iat?: number;
  32. exp?: number;
  33. }
  34. /**
  35. * 模拟Token生成逻辑
  36. * 这是一个纯函数,用于属性测试验证业务逻辑的正确性
  37. */
  38. function generateAccessToken(user: MockUser, secret: string, expiresIn: string = '24h'): string {
  39. const payload: Omit<JwtPayload, 'iat' | 'exp'> = {
  40. userId: user.id,
  41. username: user.username,
  42. email: user.email,
  43. role: user.role,
  44. permissions: user.permissions || [],
  45. };
  46. return jwt.sign(payload, secret, { expiresIn } as jwt.SignOptions);
  47. }
  48. /**
  49. * 验证Token中的角色信息
  50. */
  51. function verifyTokenRole(token: string, secret: string): JwtPayload | null {
  52. try {
  53. const decoded = jwt.verify(token, secret) as JwtPayload;
  54. return decoded;
  55. } catch {
  56. return null;
  57. }
  58. }
  59. /**
  60. * 模拟登录响应
  61. */
  62. interface LoginResponse {
  63. success: boolean;
  64. token?: string;
  65. user?: {
  66. id: string;
  67. username: string;
  68. email: string;
  69. role: UserRole;
  70. };
  71. error?: string;
  72. }
  73. /**
  74. * 模拟登录逻辑
  75. */
  76. function simulateLogin(user: MockUser | null, secret: string): LoginResponse {
  77. if (!user) {
  78. return { success: false, error: '用户不存在' };
  79. }
  80. const token = generateAccessToken(user, secret);
  81. return {
  82. success: true,
  83. token,
  84. user: {
  85. id: user.id,
  86. username: user.username,
  87. email: user.email,
  88. role: user.role,
  89. },
  90. };
  91. }
  92. describe('AuthService Property Tests', () => {
  93. const TEST_SECRET = 'test-jwt-secret-key-for-property-testing';
  94. describe('Property 8: Authentication Token Contains Role', () => {
  95. // 生成有效的用户角色
  96. const roleArbitrary = fc.constantFrom<UserRole>('buyer', 'seller', 'admin', 'customer_service');
  97. // 生成有效的用户名(字母数字,3-20字符)
  98. const usernameArbitrary = fc.stringMatching(/^[a-zA-Z][a-zA-Z0-9_]{2,19}$/);
  99. // 生成有效的邮箱
  100. const emailArbitrary = fc.emailAddress();
  101. // 生成有效的用户ID
  102. const userIdArbitrary = fc.uuid();
  103. // 生成完整的用户对象
  104. const userArbitrary = fc.record({
  105. id: userIdArbitrary,
  106. username: usernameArbitrary,
  107. email: emailArbitrary,
  108. role: roleArbitrary,
  109. permissions: fc.array(fc.string({ minLength: 1, maxLength: 50 }), { maxLength: 10 }),
  110. });
  111. it('生成的Token应该包含与用户相同的角色', () => {
  112. fc.assert(
  113. fc.property(userArbitrary, (user) => {
  114. // 生成Token
  115. const token = generateAccessToken(user, TEST_SECRET);
  116. // 解码Token
  117. const decoded = verifyTokenRole(token, TEST_SECRET);
  118. // 验证:Token中的角色应该与用户角色匹配
  119. return decoded !== null && decoded.role === user.role;
  120. }),
  121. { numRuns: 100 }
  122. );
  123. });
  124. it('登录成功后返回的Token应该包含正确的角色信息', () => {
  125. fc.assert(
  126. fc.property(userArbitrary, (user) => {
  127. // 模拟登录
  128. const loginResult = simulateLogin(user, TEST_SECRET);
  129. if (!loginResult.success || !loginResult.token) {
  130. return false;
  131. }
  132. // 解码Token
  133. const decoded = verifyTokenRole(loginResult.token, TEST_SECRET);
  134. // 验证:Token中的角色应该与登录响应中的用户角色匹配
  135. return (
  136. decoded !== null &&
  137. decoded.role === user.role &&
  138. loginResult.user?.role === user.role
  139. );
  140. }),
  141. { numRuns: 100 }
  142. );
  143. });
  144. it('Token中的角色应该是有效的角色类型', () => {
  145. const validRoles: UserRole[] = ['buyer', 'seller', 'admin', 'customer_service'];
  146. fc.assert(
  147. fc.property(userArbitrary, (user) => {
  148. // 生成Token
  149. const token = generateAccessToken(user, TEST_SECRET);
  150. // 解码Token
  151. const decoded = verifyTokenRole(token, TEST_SECRET);
  152. // 验证:Token中的角色应该是有效的角色类型
  153. return decoded !== null && validRoles.includes(decoded.role);
  154. }),
  155. { numRuns: 100 }
  156. );
  157. });
  158. it('seller角色的用户登录后Token应该包含seller角色', () => {
  159. fc.assert(
  160. fc.property(
  161. fc.record({
  162. id: userIdArbitrary,
  163. username: usernameArbitrary,
  164. email: emailArbitrary,
  165. role: fc.constant<UserRole>('seller'),
  166. permissions: fc.array(fc.string({ minLength: 1, maxLength: 50 }), { maxLength: 10 }),
  167. }),
  168. (sellerUser) => {
  169. // 模拟登录
  170. const loginResult = simulateLogin(sellerUser, TEST_SECRET);
  171. if (!loginResult.success || !loginResult.token) {
  172. return false;
  173. }
  174. // 解码Token
  175. const decoded = verifyTokenRole(loginResult.token, TEST_SECRET);
  176. // 验证:Token中的角色应该是seller
  177. return decoded !== null && decoded.role === 'seller';
  178. }
  179. ),
  180. { numRuns: 100 }
  181. );
  182. });
  183. it('Token中应该包含完整的用户信息', () => {
  184. fc.assert(
  185. fc.property(userArbitrary, (user) => {
  186. // 生成Token
  187. const token = generateAccessToken(user, TEST_SECRET);
  188. // 解码Token
  189. const decoded = verifyTokenRole(token, TEST_SECRET);
  190. // 验证:Token应该包含所有必要的用户信息
  191. return (
  192. decoded !== null &&
  193. decoded.userId === user.id &&
  194. decoded.username === user.username &&
  195. decoded.email === user.email &&
  196. decoded.role === user.role
  197. );
  198. }),
  199. { numRuns: 100 }
  200. );
  201. });
  202. it('不同角色的用户生成的Token应该有不同的角色声明', () => {
  203. fc.assert(
  204. fc.property(
  205. fc.tuple(
  206. fc.record({
  207. id: userIdArbitrary,
  208. username: usernameArbitrary,
  209. email: emailArbitrary,
  210. role: fc.constant<UserRole>('buyer'),
  211. }),
  212. fc.record({
  213. id: userIdArbitrary,
  214. username: usernameArbitrary,
  215. email: emailArbitrary,
  216. role: fc.constant<UserRole>('seller'),
  217. })
  218. ),
  219. ([buyerUser, sellerUser]) => {
  220. // 生成两个Token
  221. const buyerToken = generateAccessToken(buyerUser, TEST_SECRET);
  222. const sellerToken = generateAccessToken(sellerUser, TEST_SECRET);
  223. // 解码Token
  224. const buyerDecoded = verifyTokenRole(buyerToken, TEST_SECRET);
  225. const sellerDecoded = verifyTokenRole(sellerToken, TEST_SECRET);
  226. // 验证:两个Token的角色应该不同
  227. return (
  228. buyerDecoded !== null &&
  229. sellerDecoded !== null &&
  230. buyerDecoded.role === 'buyer' &&
  231. sellerDecoded.role === 'seller' &&
  232. (buyerDecoded.role as string) !== (sellerDecoded.role as string)
  233. );
  234. }
  235. ),
  236. { numRuns: 100 }
  237. );
  238. });
  239. it('Token应该可以被正确验证', () => {
  240. fc.assert(
  241. fc.property(userArbitrary, (user) => {
  242. // 生成Token
  243. const token = generateAccessToken(user, TEST_SECRET);
  244. // 使用正确的密钥验证
  245. const decoded = verifyTokenRole(token, TEST_SECRET);
  246. // 使用错误的密钥验证
  247. const invalidDecoded = verifyTokenRole(token, 'wrong-secret');
  248. // 验证:正确密钥应该成功,错误密钥应该失败
  249. return decoded !== null && invalidDecoded === null;
  250. }),
  251. { numRuns: 100 }
  252. );
  253. });
  254. it('admin角色应该能够访问所有资源', () => {
  255. fc.assert(
  256. fc.property(
  257. fc.record({
  258. id: userIdArbitrary,
  259. username: usernameArbitrary,
  260. email: emailArbitrary,
  261. role: fc.constant<UserRole>('admin'),
  262. permissions: fc.array(fc.string({ minLength: 1, maxLength: 50 }), { maxLength: 10 }),
  263. }),
  264. (adminUser) => {
  265. // 生成Token
  266. const token = generateAccessToken(adminUser, TEST_SECRET);
  267. // 解码Token
  268. const decoded = verifyTokenRole(token, TEST_SECRET);
  269. // 验证:admin角色应该被正确设置
  270. return decoded !== null && decoded.role === 'admin';
  271. }
  272. ),
  273. { numRuns: 100 }
  274. );
  275. });
  276. });
  277. });