diff --git a/tests/course-package.spec.ts b/tests/course-package.spec.ts new file mode 100644 index 0000000..5b2601f --- /dev/null +++ b/tests/course-package.spec.ts @@ -0,0 +1,232 @@ +import { expect, test } from './fixtures'; +import { decodeJwtPayload } from './helpers/music-room-session'; + +const moicenUnionid = process.env.MOICEN_E2E_UNIONID?.trim(); +const kcBase = + process.env.HUIKE_ADMIN_BASE_URL?.trim() || 'https://admin.moicen.com'; + +/** + * 从 JWT payload 中提取 roles 数组(可能嵌在 `sub` JSON 字段内)。 + */ +function extractRoles(payload: Record | null): string[] { + if (!payload) return []; + if (Array.isArray(payload.roles)) return payload.roles as string[]; + const subRaw = payload.sub; + if (typeof subRaw === 'string') { + try { + const sub = JSON.parse(subRaw) as Record; + if (Array.isArray(sub.roles)) return sub.roles as string[]; + } catch { + /* 非 JSON sub,忽略 */ + } + } + return []; +} + +test.describe('课包(course_package)', () => { + test.describe.configure({ timeout: 180_000 }); + + test.skip(!moicenUnionid, '需要 MOICEN_E2E_UNIONID(Secret 或 .env.e2e)'); + + test('权限校验:当前账号 JWT 应包含 TEACHER 或 SUPERVISOR 角色', async ({ + page, + }) => { + const q = new URLSearchParams({ unionid: moicenUnionid!, status: '2' }); + await page.goto(`/?${q.toString()}`, { + waitUntil: 'domcontentloaded', + timeout: 60_000, + }); + await expect(page.locator('#app')).toBeVisible({ timeout: 60_000 }); + + if ( + await page.getByText('请选择您的登录身份').isVisible().catch(() => false) + ) { + await page.locator('.van-grid-item').first().click(); + } + await expect( + page.getByText(/请选择您的登录身份|欢迎回来|进入工作台/) + ).toBeVisible({ timeout: 120_000 }); + + if ( + await page.getByText('请返回微信小程序完成登录').isVisible().catch(() => false) + ) { + test.skip(true, '当前 unionid 会话未处于可用登录态'); + } + + const authToken = await page.evaluate(() => + window.localStorage.getItem('Authorization') + ); + expect(authToken, '登录后应有 JWT').toBeTruthy(); + + const payload = decodeJwtPayload(authToken!); + expect(payload, 'JWT 应可解码').not.toBeNull(); + + const roles = extractRoles(payload); + const hasCoursePkgRole = roles.some((r) => + ['TEACHER', 'SUPERVISOR'].includes(r), + ); + expect( + hasCoursePkgRole, + `课包仅对 TEACHER / SUPERVISOR 开放,当前角色:${JSON.stringify(roles)}`, + ).toBe(true); + }); + + test('API CRUD 全链路:创建 → 按 ID 查询 → 更新 → 逻辑删除', async ({ + page, + request, + }) => { + // ---- 登录获取 JWT ---- + const q = new URLSearchParams({ unionid: moicenUnionid!, status: '2' }); + await page.goto(`/?${q.toString()}`, { + waitUntil: 'domcontentloaded', + timeout: 60_000, + }); + await expect(page.locator('#app')).toBeVisible({ timeout: 60_000 }); + if ( + await page.getByText('请选择您的登录身份').isVisible().catch(() => false) + ) { + await page.locator('.van-grid-item').first().click(); + } + await expect( + page.getByText(/请选择您的登录身份|欢迎回来|进入工作台/) + ).toBeVisible({ timeout: 120_000 }); + + if ( + await page.getByText('请返回微信小程序完成登录').isVisible().catch(() => false) + ) { + test.skip(true, '当前 unionid 会话未处于可用登录态'); + } + + const authToken = await page.evaluate(() => + window.localStorage.getItem('Authorization'), + ); + expect(authToken, '应有 Authorization JWT').toBeTruthy(); + + const headers: Record = { + Authorization: authToken!, + HtySudoerToken: authToken!, + HtyHost: new URL(kcBase).hostname, + }; + + const pkgName = `e2e-test-pkg-${Date.now()}`; + let pkgId: string; + + // ---- CREATE ---- + const createRes = await request.post(`${kcBase}/api/v1/clazz/course-package/create`, { + headers: { ...headers, 'Content-Type': 'application/json' }, + data: { + package_name: pkgName, + description: 'e2e test course package', + total_lessons: 10, + original_price: 200000, + selling_price: 150000, + validity_days: 365, + sort_order: 1, + }, + }); + expect(createRes.ok(), `CREATE HTTP ${createRes.status()}`).toBeTruthy(); + const createBody = await createRes.json(); + expect(createBody.r, `CREATE 业务失败: ${JSON.stringify(createBody)}`).toBe(true); + expect(createBody.d?.id).toBeTruthy(); + expect(createBody.d?.package_name).toBe(pkgName); + pkgId = createBody.d.id; + + // ---- FIND by ID ---- + const findRes = await request.get( + `${kcBase}/api/v1/clazz/course-package/${pkgId}`, + { headers }, + ); + expect(findRes.ok(), `FIND HTTP ${findRes.status()}`).toBeTruthy(); + const findBody = await findRes.json(); + expect(findBody.r, `FIND 业务失败: ${JSON.stringify(findBody)}`).toBe(true); + expect(findBody.d?.package_name).toBe(pkgName); + expect(findBody.d?.total_lessons).toBe(10); + + // ---- UPDATE ---- + const updatedName = `${pkgName}-updated`; + const updateRes = await request.post(`${kcBase}/api/v1/clazz/course-package/update`, { + headers: { ...headers, 'Content-Type': 'application/json' }, + data: { + id: pkgId, + package_name: updatedName, + selling_price: 100000, + }, + }); + expect(updateRes.ok(), `UPDATE HTTP ${updateRes.status()}`).toBeTruthy(); + const updateBody = await updateRes.json(); + expect(updateBody.r, `UPDATE 业务失败: ${JSON.stringify(updateBody)}`).toBe(true); + expect(updateBody.d?.package_name).toBe(updatedName); + expect(updateBody.d?.selling_price).toBe(100000); + + // ---- DELETE (logic delete) ---- + const deleteRes = await request.post( + `${kcBase}/api/v1/clazz/course-package/delete/${pkgId}`, + { headers }, + ); + expect(deleteRes.ok(), `DELETE HTTP ${deleteRes.status()}`).toBeTruthy(); + const deleteBody = await deleteRes.json(); + expect(deleteBody.r, `DELETE 业务失败: ${JSON.stringify(deleteBody)}`).toBe(true); + }); + + test('分页查询:my-packages / org-packages 接口正常', async ({ + page, + request, + }) => { + // ---- 登录获取 JWT ---- + const q = new URLSearchParams({ unionid: moicenUnionid!, status: '2' }); + await page.goto(`/?${q.toString()}`, { + waitUntil: 'domcontentloaded', + timeout: 60_000, + }); + await expect(page.locator('#app')).toBeVisible({ timeout: 60_000 }); + if ( + await page.getByText('请选择您的登录身份').isVisible().catch(() => false) + ) { + await page.locator('.van-grid-item').first().click(); + } + await expect( + page.getByText(/请选择您的登录身份|欢迎回来|进入工作台/) + ).toBeVisible({ timeout: 120_000 }); + + if ( + await page.getByText('请返回微信小程序完成登录').isVisible().catch(() => false) + ) { + test.skip(true, '当前 unionid 会话未处于可用登录态'); + } + + const authToken = await page.evaluate(() => + window.localStorage.getItem('Authorization'), + ); + expect(authToken).toBeTruthy(); + + const headers: Record = { + Authorization: authToken!, + HtySudoerToken: authToken!, + HtyHost: new URL(kcBase).hostname, + }; + + // ---- my-packages(当前教师创建的课包)---- + const myRes = await request.get( + `${kcBase}/api/v1/clazz/course-package/my-packages?page=1&page_size=5`, + { headers }, + ); + expect(myRes.ok(), `my-packages HTTP ${myRes.status()}`).toBeTruthy(); + const myBody = await myRes.json(); + expect(myBody.r, `my-packages 业务失败: ${JSON.stringify(myBody)}`).toBe(true); + expect(Array.isArray(myBody.d?.[0])).toBe(true); + expect(typeof myBody.d?.[1] === 'number').toBe(true); // total pages + expect(typeof myBody.d?.[2] === 'number').toBe(true); // total count + + // ---- org-packages(机构下所有活跃课包)---- + const orgRes = await request.get( + `${kcBase}/api/v1/clazz/course-package/org-packages?page=1&page_size=5`, + { headers }, + ); + expect(orgRes.ok(), `org-packages HTTP ${orgRes.status()}`).toBeTruthy(); + const orgBody = await orgRes.json(); + expect(orgBody.r, `org-packages 业务失败: ${JSON.stringify(orgBody)}`).toBe(true); + expect(Array.isArray(orgBody.d?.[0])).toBe(true); + expect(typeof orgBody.d?.[1] === 'number').toBe(true); + expect(typeof orgBody.d?.[2] === 'number').toBe(true); + }); +});