test(e2e): 多机构隔离 — JWT/CurrentOrgId 对齐、机构切换、课程体系列表
Made-with: Cursor
This commit is contained in:
@@ -21,9 +21,11 @@ npx playwright test
|
||||
| `tests/guest-onboarding.spec.ts` | 未登录:仅见「请返回微信小程序完成登录」,不出现已登录工作台 |
|
||||
| `tests/home-shell.spec.ts` | `#app`、伪造 unionid 不白屏、`page_path` 净化 |
|
||||
| `tests/logged-in-and-isolation.spec.ts` | 需 Secret:登录后非访客态;异主 unionid 剥离;多角色选身份后见「欢迎回来」(不校验姓名) |
|
||||
| `tests/org-multi-tenant.spec.ts` | 需 Secret:机构选择页可达且呈现机构列表 UI |
|
||||
| `tests/org-data-isolation.spec.ts` | 需 Secret:`CurrentOrgId` 与 JWT `current_org_id` 一致;多机构时可切换机构并更新上下文;`/course` 课程体系列表不出现「请求失败」错误态 |
|
||||
| `tests/admin-tasks.spec.ts` | 需 `MOICEN_ADMIN_USER` / `MOICEN_ADMIN_PASSWORD`:管理端登录后打开 `/tasks` |
|
||||
|
||||
未配置 `MOICEN_E2E_UNIONID` 时,`logged-in-and-isolation` 内用例全部 skip。未配置管理端账号时,`admin-tasks` 内用例 skip。
|
||||
未配置 `MOICEN_E2E_UNIONID` 时,`logged-in-and-isolation`、`org-multi-tenant`、`org-data-isolation` 内用例全部 skip。未配置管理端账号时,`admin-tasks` 内用例 skip。
|
||||
|
||||
## GitHub Actions
|
||||
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
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 });
|
||||
}
|
||||
|
||||
test.describe('多机构数据隔离(会话与 token)', () => {
|
||||
test.skip(!moicenUnionid, '需要 MOICEN_E2E_UNIONID(Secret 或 .env.e2e)');
|
||||
|
||||
test('本地 CurrentOrgId 与 JWT current_org_id 一致', async ({ page }) => {
|
||||
await establishSession(page);
|
||||
if (
|
||||
await page.getByText('请返回微信小程序完成登录').isVisible().catch(() => false)
|
||||
) {
|
||||
test.skip(true, '当前 unionid 会话未处于可用登录态');
|
||||
}
|
||||
|
||||
const storage = await page.evaluate(() => ({
|
||||
currentOrgId: window.localStorage.getItem('CurrentOrgId'),
|
||||
authorization: window.localStorage.getItem('Authorization'),
|
||||
}));
|
||||
expect(storage.currentOrgId, '登录后应写入 CurrentOrgId').toBeTruthy();
|
||||
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(String(jwtOrgId)).toBe(storage.currentOrgId);
|
||||
});
|
||||
|
||||
test('机构选择页存在多个机构时,切换后 CurrentOrgId 变化', async ({
|
||||
page,
|
||||
}) => {
|
||||
await establishSession(page);
|
||||
if (
|
||||
await page.getByText('请返回微信小程序完成登录').isVisible().catch(() => false)
|
||||
) {
|
||||
test.skip(true, '当前 unionid 会话未处于可用登录态');
|
||||
}
|
||||
|
||||
await page.goto('/org/select', {
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: 60_000,
|
||||
});
|
||||
await expect(page.locator('#app')).toBeVisible({ timeout: 60_000 });
|
||||
await expect(page.getByRole('heading', { name: '请选择机构' })).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).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 page.goto('/course', {
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: 90_000,
|
||||
});
|
||||
await expect(page.locator('#app')).toBeVisible({ timeout: 90_000 });
|
||||
await expect(
|
||||
page.getByPlaceholder('请输入课程体系名称搜索')
|
||||
).toBeVisible({ timeout: 90_000 });
|
||||
|
||||
await expect(
|
||||
page.getByText('请求失败,点击重新加载')
|
||||
).not.toBeVisible({ timeout: 45_000 });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user