Files
huike-e2e-moicen/tests/course-package.spec.ts
T
weli 1ddeb21ca7 test(course-package): add API CRUD, pagination, and role permission tests
Verify course_package endpoints are only accessible to TEACHER/SUPERVISOR
roles. Test full CRUD cycle and paginated listing APIs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 08:31:04 +08:00

233 lines
8.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<string, unknown> | 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<string, unknown>;
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_UNIONIDSecret 或 .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<string, string> = {
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<string, string> = {
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);
});
});