unified-advertisement-api.spec.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
  1. import { test, expect } from '@playwright/test';
  2. /**
  3. * E2E测试:统一广告API兼容性验证
  4. *
  5. * 目的:验证统一广告模块替换后,用户端API路径和响应结构保持100%兼容
  6. * 确保:小程序端无需任何修改即可正常工作
  7. *
  8. * ## 测试前置条件
  9. *
  10. * 本测试需要数据库中有测试数据:
  11. * 1. 至少一个租户记录在 `tenant_mt` 表中
  12. * 2. 至少一个用户记录在 `users_mt` 表中
  13. * 3. 用户的密码为 `admin123` (测试默认密码)
  14. *
  15. * ## 创建测试数据
  16. *
  17. * 如果测试失败提示401错误,请先创建测试数据:
  18. *
  19. * ```sql
  20. * -- 创建测试租户
  21. * INSERT INTO tenant_mt (id, name, code, status, created_at, updated_at)
  22. * VALUES (1, '测试租户', 'test-tenant', 1, NOW(), NOW());
  23. *
  24. * -- 创建测试用户 (密码: admin123)
  25. * -- 注意:密码需要使用bcrypt加密
  26. * INSERT INTO users_mt (id, tenant_id, username, password, registration_source, is_disabled, is_deleted, created_at, updated_at)
  27. * VALUES (1, 1, 'admin', '$2b$10$x3t2kofPmACnk6y6lfL6ouU836LBEuZE9BinQ3ZzA4Xd04izyY42K', 'web', 0, 0, NOW(), NOW());
  28. * ```
  29. *
  30. * ## 认证说明
  31. *
  32. * - **用户端API**: 使用 `authMiddleware` (多租户认证),需要提供有效的JWT token
  33. * - **小程序端**: 应该使用用户登录后的token访问这些API
  34. * - **管理员API**: 使用 `tenantAuthMiddleware` (超级管理员专用,ID=1)
  35. * - **租户ID**: 通过查询参数 `?tenantId=1` 或请求头 `X-Tenant-Id: 1` 指定
  36. *
  37. * ## API响应格式
  38. *
  39. * API返回包装的响应格式:
  40. * ```json
  41. * {
  42. * "code": 200,
  43. * "message": "success",
  44. * "data": {
  45. * "list": [],
  46. * "total": 0,
  47. * "page": 1,
  48. * "pageSize": 10
  49. * }
  50. * }
  51. * ```
  52. */
  53. test.describe('统一广告API兼容性测试', () => {
  54. const baseUrl = process.env.API_BASE_URL || 'http://localhost:8080';
  55. const testUsername = process.env.TEST_USERNAME || 'admin';
  56. const testPassword = process.env.TEST_PASSWORD || 'admin123';
  57. const testTenantId = process.env.TEST_TENANT_ID || '1';
  58. test.describe('用户端广告API (小程序使用)', () => {
  59. let userToken: string;
  60. test.beforeAll(async ({ request }) => {
  61. // 使用测试用户账号登录获取token
  62. // 注意:多租户系统中,用户端API需要认证来确定租户上下文
  63. const loginResponse = await request.post(`${baseUrl}/api/v1/auth/login?tenantId=${testTenantId}`, {
  64. data: {
  65. username: testUsername,
  66. password: testPassword
  67. }
  68. });
  69. if (loginResponse.status() === 200) {
  70. const loginData = await loginResponse.json();
  71. userToken = loginData.token || loginData.access_token;
  72. console.log('✅ 登录成功,获取到token');
  73. } else {
  74. const error = await loginResponse.json();
  75. console.error('❌ 登录失败:', error);
  76. console.error('💡 提示: 请确保数据库中有测试用户和租户');
  77. }
  78. });
  79. test('GET /api/v1/advertisements - 获取广告列表', async ({ request }) => {
  80. if (!userToken) {
  81. test.skip(true, '缺少认证token,请先创建测试用户');
  82. }
  83. const response = await request.get(`${baseUrl}/api/v1/advertisements`, {
  84. headers: {
  85. 'Authorization': `Bearer ${userToken}`
  86. }
  87. });
  88. // 验证响应状态
  89. expect(response.status()).toBe(200);
  90. const result = await response.json();
  91. expect(result).toHaveProperty('code', 200);
  92. expect(result).toHaveProperty('data');
  93. expect(result.data).toHaveProperty('list');
  94. expect(result.data).toHaveProperty('total');
  95. const data = result.data.list;
  96. // 验证响应结构包含广告列表或空数组
  97. expect(Array.isArray(data)).toBeTruthy();
  98. // 如果有数据,验证字段结构
  99. if (data.length > 0) {
  100. const ad = data[0];
  101. expect(ad).toHaveProperty('id');
  102. expect(ad).toHaveProperty('title');
  103. expect(ad).toHaveProperty('imageUrl');
  104. expect(ad).toHaveProperty('linkUrl');
  105. expect(ad).toHaveProperty('position');
  106. expect(ad).toHaveProperty('status');
  107. }
  108. });
  109. test('GET /api/v1/advertisements?position=home - 获取指定位置广告', async ({ request }) => {
  110. if (!userToken) {
  111. test.skip(true, '缺少认证token,请先创建测试用户');
  112. }
  113. const response = await request.get(`${baseUrl}/api/v1/advertisements?position=home`, {
  114. headers: {
  115. 'Authorization': `Bearer ${userToken}`
  116. }
  117. });
  118. expect(response.status()).toBe(200);
  119. const result = await response.json();
  120. const data = result.data.list;
  121. expect(Array.isArray(data)).toBeTruthy();
  122. // 验证返回的ads都是home位置
  123. if (data.length > 0) {
  124. data.forEach((ad: any) => {
  125. expect(ad.position).toBe('home');
  126. });
  127. }
  128. });
  129. test('GET /api/v1/advertisements/:id - 获取广告详情', async ({ request }) => {
  130. if (!userToken) {
  131. test.skip(true, '缺少认证token,请先创建测试用户');
  132. }
  133. // 先获取列表,找一个有效的ID
  134. const listResponse = await request.get(`${baseUrl}/api/v1/advertisements`, {
  135. headers: {
  136. 'Authorization': `Bearer ${userToken}`
  137. }
  138. });
  139. const listResult = await listResponse.json();
  140. const listData = listResult.data.list;
  141. if (listData.length > 0) {
  142. const adId = listData[0].id;
  143. const response = await request.get(`${baseUrl}/api/v1/advertisements/${adId}`, {
  144. headers: {
  145. 'Authorization': `Bearer ${userToken}`
  146. }
  147. });
  148. expect(response.status()).toBe(200);
  149. const result = await response.json();
  150. const data = result.data;
  151. expect(data).toHaveProperty('id', adId);
  152. expect(data).toHaveProperty('title');
  153. expect(data).toHaveProperty('imageUrl');
  154. expect(data).toHaveProperty('linkUrl');
  155. expect(data).toHaveProperty('position');
  156. expect(data).toHaveProperty('status');
  157. } else {
  158. test.skip(true, '没有广告数据,请先创建测试广告');
  159. }
  160. });
  161. test('GET /api/v1/advertisement-types - 获取广告类型列表', async ({ request }) => {
  162. if (!userToken) {
  163. test.skip(true, '缺少认证token,请先创建测试用户');
  164. }
  165. const response = await request.get(`${baseUrl}/api/v1/advertisement-types`, {
  166. headers: {
  167. 'Authorization': `Bearer ${userToken}`
  168. }
  169. });
  170. expect(response.status()).toBe(200);
  171. const result = await response.json();
  172. const data = result.data.list;
  173. expect(Array.isArray(data)).toBeTruthy();
  174. // 验证响应结构
  175. if (data.length > 0) {
  176. const adType = data[0];
  177. expect(adType).toHaveProperty('id');
  178. expect(adType).toHaveProperty('typeName');
  179. expect(adType).toHaveProperty('description');
  180. expect(adType).toHaveProperty('status');
  181. }
  182. });
  183. test('验证响应字段类型正确性', async ({ request }) => {
  184. if (!userToken) {
  185. test.skip(true, '缺少认证token,请先创建测试用户');
  186. }
  187. const response = await request.get(`${baseUrl}/api/v1/advertisements`, {
  188. headers: {
  189. 'Authorization': `Bearer ${userToken}`
  190. }
  191. });
  192. const result = await response.json();
  193. const data = result.data.list;
  194. if (data.length > 0) {
  195. const ad = data[0];
  196. expect(typeof ad.id).toBe('number');
  197. expect(typeof ad.title).toBe('string');
  198. expect(typeof ad.imageUrl).toBe('string');
  199. expect(typeof ad.linkUrl).toBe('string');
  200. expect(typeof ad.position).toBe('string');
  201. expect(typeof ad.status).toBe('number');
  202. }
  203. });
  204. });
  205. test.describe('管理员广告API (租户后台使用)', () => {
  206. let authToken: string;
  207. test.beforeAll(async ({ request }) => {
  208. // 使用超级管理员账号登录获取token
  209. const loginResponse = await request.post(`${baseUrl}/api/v1/auth/login?tenantId=${testTenantId}`, {
  210. data: {
  211. username: testUsername,
  212. password: testPassword
  213. }
  214. });
  215. if (loginResponse.status() === 200) {
  216. const loginData = await loginResponse.json();
  217. authToken = loginData.token || loginData.access_token;
  218. }
  219. });
  220. test('GET /api/v1/admin/unified-advertisements - 管理员获取广告列表', async ({ request }) => {
  221. if (!authToken) {
  222. test.skip(true, '缺少认证token,请先创建测试用户');
  223. }
  224. const response = await request.get(`${baseUrl}/api/v1/admin/unified-advertisements`, {
  225. headers: {
  226. 'Authorization': `Bearer ${authToken}`
  227. }
  228. });
  229. // 管理员API应该返回200或401/403(取决于权限配置)
  230. expect([200, 401, 403]).toContain(response.status());
  231. if (response.status() === 200) {
  232. const result = await response.json();
  233. const data = result.data.list;
  234. expect(Array.isArray(data)).toBeTruthy();
  235. }
  236. });
  237. test('GET /api/v1/admin/unified-advertisement-types - 管理员获取广告类型列表', async ({ request }) => {
  238. if (!authToken) {
  239. test.skip(true, '缺少认证token,请先创建测试用户');
  240. }
  241. const response = await request.get(`${baseUrl}/api/v1/admin/unified-advertisement-types`, {
  242. headers: {
  243. 'Authorization': `Bearer ${authToken}`
  244. }
  245. });
  246. expect([200, 401, 403]).toContain(response.status());
  247. if (response.status() === 200) {
  248. const result = await response.json();
  249. const data = result.data.list;
  250. expect(Array.isArray(data)).toBeTruthy();
  251. }
  252. });
  253. });
  254. test.describe('API路径兼容性验证', () => {
  255. let userToken: string;
  256. test.beforeAll(async ({ request }) => {
  257. // 登录获取token
  258. const loginResponse = await request.post(`${baseUrl}/api/v1/auth/login?tenantId=${testTenantId}`, {
  259. data: {
  260. username: testUsername,
  261. password: testPassword
  262. }
  263. });
  264. if (loginResponse.status() === 200) {
  265. const loginData = await loginResponse.json();
  266. userToken = loginData.token || loginData.access_token;
  267. }
  268. });
  269. test('验证所有用户端广告API端点可访问', async ({ request }) => {
  270. if (!userToken) {
  271. test.skip(true, '缺少认证token,请先创建测试用户');
  272. }
  273. const endpoints = [
  274. '/api/v1/advertisements',
  275. '/api/v1/advertisement-types'
  276. ];
  277. for (const endpoint of endpoints) {
  278. const response = await request.get(`${baseUrl}${endpoint}`, {
  279. headers: {
  280. 'Authorization': `Bearer ${userToken}`
  281. }
  282. });
  283. expect(response.status(), `端点 ${endpoint} 应该返回200`).toBe(200);
  284. }
  285. });
  286. test('验证管理员API端点存在', async ({ request }) => {
  287. const endpoints = [
  288. '/api/v1/admin/unified-advertisements',
  289. '/api/v1/admin/unified-advertisement-types'
  290. ];
  291. for (const endpoint of endpoints) {
  292. const response = await request.get(`${baseUrl}${endpoint}`);
  293. // 管理员API需要认证,所以期望401或403,而不是404
  294. expect([401, 403], `端点 ${endpoint} 应该存在但需要认证`).toContain(response.status());
  295. }
  296. });
  297. });
  298. test.describe('响应数据结构兼容性', () => {
  299. let userToken: string;
  300. test.beforeAll(async ({ request }) => {
  301. // 登录获取token
  302. const loginResponse = await request.post(`${baseUrl}/api/v1/auth/login?tenantId=${testTenantId}`, {
  303. data: {
  304. username: testUsername,
  305. password: testPassword
  306. }
  307. });
  308. if (loginResponse.status() === 200) {
  309. const loginData = await loginResponse.json();
  310. userToken = loginData.token || loginData.access_token;
  311. }
  312. });
  313. test('广告列表响应结构与原模块一致', async ({ request }) => {
  314. if (!userToken) {
  315. test.skip(true, '缺少认证token,请先创建测试用户');
  316. }
  317. const response = await request.get(`${baseUrl}/api/v1/advertisements`, {
  318. headers: {
  319. 'Authorization': `Bearer ${userToken}`
  320. }
  321. });
  322. const result = await response.json();
  323. const data = result.data.list;
  324. if (data.length > 0) {
  325. const ad = data[0];
  326. // 验证必填字段存在
  327. expect(ad).toHaveProperty('id');
  328. expect(ad).toHaveProperty('title');
  329. expect(ad).toHaveProperty('imageUrl');
  330. expect(ad).toHaveProperty('linkUrl');
  331. expect(ad).toHaveProperty('position');
  332. // 验证可选字段存在
  333. expect(ad).toHaveProperty('description');
  334. expect(ad).toHaveProperty('status');
  335. expect(ad).toHaveProperty('sortOrder');
  336. }
  337. });
  338. test('广告类型响应结构与原模块一致', async ({ request }) => {
  339. if (!userToken) {
  340. test.skip(true, '缺少认证token,请先创建测试用户');
  341. }
  342. const response = await request.get(`${baseUrl}/api/v1/advertisement-types`, {
  343. headers: {
  344. 'Authorization': `Bearer ${userToken}`
  345. }
  346. });
  347. const result = await response.json();
  348. const data = result.data.list;
  349. if (data.length > 0) {
  350. const adType = data[0];
  351. // 验证必填字段存在
  352. expect(adType).toHaveProperty('id');
  353. expect(adType).toHaveProperty('typeName');
  354. expect(adType).toHaveProperty('description');
  355. expect(adType).toHaveProperty('status');
  356. }
  357. });
  358. });
  359. });