Files
huike-e2e-moicen/tests/org-data-isolation.spec.ts
T

193 lines
6.5 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 { test, expect, type Page } from '@playwright/test';
const moicenUnionid = process.env.MOICEN_E2E_UNIONID?.trim();
function decodeJwtPayload(token: string): Record<string, unknown> | null {
const raw = token.startsWith('Bearer ') ? token.slice(7).trim() : token;
const parts = raw.split('.');
if (parts.length !== 3) return null;
try {
const base64 = parts[1].replace(/-/g, '+').replace(/_/g, '/');
const padded = base64 + '='.repeat((4 - (base64.length % 4)) % 4);
const json = Buffer.from(padded, 'base64').toString('utf8');
return JSON.parse(json) as Record<string, unknown>;
} catch {
return null;
}
}
async function establishSession(page: 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 });
}
/**
* 停留在 `/` 时 main.ts 不会把 JWT 中的机构写入 CurrentOrgId。
* 深链到 `/course` 会触发守卫;多机构且无上下文时会先进入 `/org/select`,需点选机构后再继续。
*/
async function resolveOrgContextForCoursePage(page: Page) {
for (let attempt = 0; attempt < 6; attempt++) {
await page.goto('/course', {
waitUntil: 'domcontentloaded',
timeout: 90_000,
});
await expect(page.locator('#app')).toBeVisible({ timeout: 90_000 });
if (!page.url().includes('/org/select')) {
return;
}
await expect(page.getByText('请选择机构')).toBeVisible({ timeout: 60_000 });
await Promise.race([
page
.locator('.main .van-cell-group .van-cell')
.first()
.waitFor({ state: 'visible', timeout: 60_000 }),
page.getByText('暂无可用机构').waitFor({ state: 'visible', timeout: 60_000 }),
]);
const emptyOrgs = await page
.getByText('暂无可用机构')
.isVisible()
.catch(() => false);
if (emptyOrgs) {
test.skip(true, '账号无可用机构,跳过机构上下文用例');
}
const pickCells = page.locator('.main .van-cell-group .van-cell');
const n = await pickCells.count();
expect(n, '机构选择页应有可选机构').toBeGreaterThan(0);
await pickCells.first().click();
await expect(page.locator('#app')).toBeVisible({ timeout: 90_000 });
}
throw new Error('resolveOrgContextForCoursePage: 无法在合理步数内离开机构选择页');
}
test.describe('多机构数据隔离(会话与 token', () => {
test.skip(!moicenUnionid, '需要 MOICEN_E2E_UNIONIDSecret 或 .env.e2e');
test('本地 CurrentOrgId 与 JWT current_org_id 一致', async ({ page }) => {
await establishSession(page);
if (
await page.getByText('请返回微信小程序完成登录').isVisible().catch(() => false)
) {
test.skip(true, '当前 unionid 会话未处于可用登录态');
}
await resolveOrgContextForCoursePage(page);
const storage = await page.evaluate(() => ({
currentOrgId: window.localStorage.getItem('CurrentOrgId'),
authorization: window.localStorage.getItem('Authorization'),
}));
expect(storage.authorization, '登录后应有 Authorization').toBeTruthy();
const payload = decodeJwtPayload(storage.authorization!);
expect(payload, 'Authorization 应为可解析的 JWT').not.toBeNull();
const jwtOrgId = payload!.current_org_id;
expect(jwtOrgId, 'JWT 应包含 current_org_id').toBeTruthy();
expect(storage.currentOrgId, '进入业务路由后应写入 CurrentOrgId').toBeTruthy();
expect(String(jwtOrgId)).toBe(storage.currentOrgId);
});
test('机构选择页存在多个机构时,切换后 CurrentOrgId 变化', async ({
page,
}) => {
await establishSession(page);
if (
await page.getByText('请返回微信小程序完成登录').isVisible().catch(() => false)
) {
test.skip(true, '当前 unionid 会话未处于可用登录态');
}
await resolveOrgContextForCoursePage(page);
await page.goto('/org/select', {
waitUntil: 'domcontentloaded',
timeout: 60_000,
});
await expect(page.locator('#app')).toBeVisible({ timeout: 60_000 });
await expect(page.getByText('请选择机构')).toBeVisible({
timeout: 60_000,
});
const orgCells = page.locator('.main .van-cell-group .van-cell');
const count = await orgCells.count();
test.skip(
count < 2,
'账号仅绑定单一机构时跳过切换断言(仍可通过 JWT 对齐用例校验隔离键)'
);
const beforeOrg = await page.evaluate(() =>
window.localStorage.getItem('CurrentOrgId')
);
expect(
beforeOrg,
'切换前应已有当前机构(resolveOrgContextForCoursePage 已落库)'
).toBeTruthy();
await orgCells.nth(1).click();
await page.waitForURL(
(u) => {
try {
return new URL(u).pathname === '/' || new URL(u).pathname === '';
} catch {
return false;
}
},
{ timeout: 90_000 }
);
await expect(page.locator('#app')).toBeVisible({ timeout: 90_000 });
const afterOrg = await page.evaluate(() =>
window.localStorage.getItem('CurrentOrgId')
);
expect(afterOrg).toBeTruthy();
expect(afterOrg).not.toBe(beforeOrg);
const authAfter = await page.evaluate(() =>
window.localStorage.getItem('Authorization')
);
const payloadAfter = decodeJwtPayload(authAfter ?? '');
expect(payloadAfter?.current_org_id).toBeTruthy();
expect(String(payloadAfter!.current_org_id)).toBe(afterOrg);
});
test('课程体系页加载成功且不进入列表错误态(机构维度接口可用)', async ({
page,
}) => {
await establishSession(page);
if (
await page.getByText('请返回微信小程序完成登录').isVisible().catch(() => false)
) {
test.skip(true, '当前 unionid 会话未处于可用登录态');
}
await resolveOrgContextForCoursePage(page);
await expect(
page.getByPlaceholder('请输入课程体系名称搜索')
).toBeVisible({ timeout: 90_000 });
await expect(
page.getByText('请求失败,点击重新加载')
).not.toBeVisible({ timeout: 45_000 });
});
});