1ddeb21ca7
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>
233 lines
8.3 KiB
TypeScript
233 lines
8.3 KiB
TypeScript
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_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<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);
|
||
});
|
||
});
|