Test Specs IV9 - MZC-CSC/mc-web-console GitHub Wiki
μμ±: 2026-10-13 Β· κΈ°μ€: v07 β Test Scenarios Β· IV νλΈ Β· κΈ°λ₯ TC Β· TC-IAM
Playwright(TypeScript) spec μμ€μ
λλ€. μ΅μ»€λ #<μλ리μ€λ²νΈ μλ¬Έμ> μ
λλ€. IV νλ‘μ νΈ(μ΄λ―Έ μ€μΉλ Playwright)μ tests/iv9/ μ 볡μ¬νκ³ , μλ IV9 μ μ© ν¬νΌ 2κ°λ₯Ό lib/ μ μΆκ°νλ©΄ μ€νλ©λλ€. λλ¨Έμ§ κ³΅μ© ν¬νΌ(ui.ts, fixtures.ts, data.ts, projects.ts λ±)λ Test-Specs-Common.
npx playwright test tests/iv9/IV9_S13.spec.ts # 1건 (UI: λ
Ήν results/v07/recordings/v07_IV9_S13.webm)
npx playwright test tests/iv9 # μ 체
IAM_URL=https://<iam μ£Όμ> npx playwright test tests/iv9/IV9_S06.spec.ts # API μλ리μ€μ IAM μ£Όμ μ§μ | νκ²½λ³μ | μ€λͺ |
|---|---|
BASE_URL |
μΉ μ½μ μ£Όμ(UI μλ리μ€, playwright.config κΈ°λ³Έκ°) |
IAM_URL |
mc-iam-manager μΈλΆ μ£Όμ(API μλ리μ€, iam nginx 443) |
ADMIN_ID / ADMIN_PW
|
νλ«νΌ κ΄λ¦¬μ κ³μ (lib/data.ts) |
RUN_TAG |
κ²°κ³ΌΒ·λ Ήν ν΄λ ꡬλΆ(κΈ°λ³Έ v07) |
| νμΌ | μ€ | ν΄μ |
|---|---|---|
lib/iv9.ts |
236 | dc57737b |
lib/iam-api.ts |
39 | 03bb64c6 |
IV9_S01 tests/iv9/IV9_S01.spec.ts
|
46 | f240c994 |
IV9_S02 tests/iv9/IV9_S02.spec.ts
|
28 | 6a0b1399 |
IV9_S03 tests/iv9/IV9_S03.spec.ts
|
41 | 2c8b38a0 |
IV9_S04 tests/iv9/IV9_S04.spec.ts
|
32 | ab4f58aa |
IV9_S05 tests/iv9/IV9_S05.spec.ts
|
43 | e20d32ca |
IV9_S06 tests/iv9/IV9_S06.spec.ts
|
28 | 6338c449 |
IV9_S07 tests/iv9/IV9_S07.spec.ts
|
29 | 9eca8861 |
IV9_S08 tests/iv9/IV9_S08.spec.ts
|
26 | 897d2353 |
IV9_S09 tests/iv9/IV9_S09.spec.ts
|
28 | f8f709fe |
IV9_S10 tests/iv9/IV9_S10.spec.ts
|
35 | 9d6b4bf8 |
IV9_S11 tests/iv9/IV9_S11.spec.ts
|
30 | 35c03091 |
IV9_S12 tests/iv9/IV9_S12.spec.ts
|
44 | 84eaec86 |
IV9_S13 tests/iv9/IV9_S13.spec.ts
|
51 | 3679d729 |
IV9_S14 tests/iv9/IV9_S14.spec.ts
|
30 | f986b496 |
IV9_S15 tests/iv9/IV9_S15.spec.ts
|
28 | b70c5605 |
IV9_S16 tests/iv9/IV9_S16.spec.ts
|
75 | d615639a |
IV9_S17 tests/iv9/IV9_S17.spec.ts
|
29 | e0522bef |
IV9_S18 tests/iv9/IV9_S18.spec.ts
|
28 | a2527a2f |
IV9_S19 tests/iv9/IV9_S19.spec.ts
|
36 | 445db2d5 |
IV9_S20 tests/iv9/IV9_S20.spec.ts
|
72 | f5937c38 |
IV9_S21 tests/iv9/IV9_S21.spec.ts
|
76 | 7feaf7e8 |
lib/iv9.ts Β· 236μ€ Β· ν΄μ dc57737b
import type { Locator, Page } from '@playwright/test';
import { test, expect } from './fixtures';
// IV9 (mc-iam-manager TC) κ³΅μ© ν¬νΌ. μ
λ ν°λ 2026-10-13 stage νλ‘λΈλ‘ νμΈ.
// - Workspaces νλ©΄: μ¬μ΄λ λ©λ΄μ λ³λ νλͺ©μ΄ μκ³ 'Workspaces' κ·Έλ£Ήμ href(/webconsole/operations/manage/workspaces)λ‘ μ°λ€.
// λͺ©λ‘ #Workspaceslist-table(Tabulator, 1μ΄ rowSelection 체ν¬λ°μ€), μλ¨ 'Add Workspace' β #workspace-modal-add
// λͺ©λ‘ μΉ΄λ β λ©λ΄: Delete / Edit. Delete λ 체ν¬λ νμ μ§μ°κ³ location.reload()
// ν ν΄λ¦ β νλ¨ #workspace-info-card ν(Details / Projects / Members / Cost Analytics)
// Members ν: + (addUserModalInit) β #user-modal-add (#user-modal-add-userselector TomSelect, #user-modal-add-roleselector)
// β 'Unassign Members' β #commonDefaultModal Confirm
// - Roles νλ©΄: ν ν΄λ¦ β Role Detail, 'Add Role' β Create New Role(#role-name-input, #role-description-input, #save-create-role-btn)
// λͺ©λ‘ μΉ΄λ β λ©λ΄ Delete β #commonDefaultModal('Would you like to delete this role ?')
export const URL_WORKSPACES = '/webconsole/operations/manage/workspaces';
export const URL_CLOUD_OVERVIEW = '/webconsole/settings/environment/cloudsps/cloudoverview';
/** μ΄ μλ리μ€μμ λ§λ€κ³ μ§μλ λλ μ΄λ¦(κ·Έ μΈλ μ λ μμ νμ§ μλλ€) */
export const IV9_WS = 'iv9ws';
export const IV9_WS2 = 'iv9ws2';
export const IV9_ROLE = 'iv9role';
const DELETABLE_WS = [IV9_WS, IV9_WS2];
const DELETABLE_ROLES = [IV9_ROLE];
/**
* μν /λ©€λ² ν λΉμ μ°λ ν
μ€νΈ μ¬μ©μ.
* stage μ λ
Ήνμ© rec####a/b μ¬μ©μκ° μμ΄(2026-10-13 Users λͺ©λ‘ 9λͺ
) μ€μ μ¬λμ΄ μλ ν
μ€νΈ κ³μ pending01(μΉμΈ λκΈ°, μν μμ)μ μ΄λ€.
* κΈμ§ λμ: mcmp, mzc-noh, iam01/workflow01/app01/monitor01/cost01/cost02.
*/
export const IV9_USER = { id: 'pending01', name: 'pending 01', email: '[email protected]' };
const exactName = (s: string) => new RegExp(`^\\s*${s}\\s*$`);
export const tabRow = (page: Page, tableSel: string, field: string, value: string): Locator =>
page.locator(`${tableSel} .tabulator-row`).filter({ has: page.locator(`[tabulator-field="${field}"]`, { hasText: exactName(value) }) });
export const toastTexts = async (page: Page) =>
(await page.locator('#toast-container .toast, [class*="toast"]').allInnerTexts()).map((s) => s.replace(/\s+/g, ' ').trim()).filter(Boolean);
// ---------------- Workspaces ----------------
export const wsRow = (page: Page, name: string) => tabRow(page, '#Workspaceslist-table', 'name', name);
export async function gotoWorkspaces(page: Page) {
await page.goto(URL_WORKSPACES);
for (let attempt = 1; ; attempt++) {
if (await wsRow(page, 'ws01').first().waitFor({ timeout: 30_000 }).then(() => true, () => false)) break;
if (attempt >= 3) throw new Error('Workspaces λͺ©λ‘(ws01)μ΄ 90μ΄ λμ νμλμ§ μμ');
test.info().annotations.push({ type: 'list-retry', description: `μλ ${attempt}: Workspaces λͺ©λ‘ λ―Ένμ β μλ‘κ³ μΉ¨` });
await page.reload();
}
// νμ΄μ§ ν¬κΈ° μ΅λ(νμ΄ 2νμ΄μ§λ‘ λ°λ¦¬μ§ μλλ‘)
const sizeSel = page.locator('#Workspaceslist-table select.tabulator-page-size, #Workspaceslist-table ~ * select.tabulator-page-size').first();
if (await sizeSel.count()) {
const vals = (await sizeSel.locator('option').evaluateAll((os) => os.map((o) => (o as HTMLOptionElement).value))).filter((v) => /^\d+$/.test(v));
const max = vals.sort((a, b) => Number(b) - Number(a))[0];
if (max) await sizeSel.selectOption(max);
}
await page.waitForTimeout(1000);
}
/** Add Workspace νμ
μΌλ‘ μμ±. μ΄λ―Έ μμΌλ©΄ false */
export async function createWorkspace(page: Page, name: string, description: string) {
if (await wsRow(page, name).count()) return false;
await page.getByRole('link', { name: 'Add Workspace' }).click();
const m = page.locator('#workspace-modal-add');
await expect(m).toBeVisible();
await m.locator('#workspace-modal-add-name').fill(name);
await m.locator('#workspace-modal-add-description').fill(description);
await m.getByRole('button', { name: 'Confirm' }).click();
await expect(m, 'Add Workspace νμ
λ«ν(μμ± μ±κ³΅)').toBeHidden({ timeout: 30_000 });
return true;
}
/** μΉ΄λ μμμ ν΄λΉ νλͺ©μ κ°μ§ β λλ‘λ€μ΄μ νΌμΉκ³ νλͺ© ν΄λ¦(μ λ ¬ λλ‘λ€μ΄ λ± λ€λ₯Έ β μ ꡬλΆ) */
async function openDropdownItem(scope: Locator, item: RegExp) {
const dd = scope.locator('.dropdown').filter({ has: scope.page().locator('.dropdown-item', { hasText: item }) }).first();
await dd.locator('[data-bs-toggle="dropdown"]').first().click();
await dd.locator('.dropdown-item', { hasText: item }).first().click();
}
/** λͺ©λ‘ νμ 체ν¬(rowSelection)νλ€ */
async function checkRow(row: Locator) {
const cb = row.locator('input[type="checkbox"]').first();
if (!(await cb.isChecked())) await cb.click();
await expect(cb).toBeChecked();
}
/** μ²΄ν¬ β λͺ©λ‘ μΉ΄λ β β Delete β Confirm. iv9ws/iv9ws2 λ§ νμ© */
export async function deleteWorkspace(page: Page, name: string) {
if (!DELETABLE_WS.includes(name)) throw new Error(`μμ κΈμ§ Workspace: ${name}`);
await gotoWorkspaces(page);
if (!(await wsRow(page, name).count())) return false;
// λ€λ₯Έ νμ΄ μ²΄ν¬λΌ μμ§ μμμ§ νμΈ(λ€λ₯Έ Workspace μμ λ°©μ§)
const checked = page.locator('#Workspaceslist-table .tabulator-row input[type="checkbox"]:checked');
expect(await checked.count(), 'μμ μ 체ν¬λ Workspace μμ').toBe(0);
await checkRow(wsRow(page, name).first());
expect(await checked.count(), `체ν¬λ Workspace λ ${name} νλ`).toBe(1);
const card = page.locator('.card').filter({ has: page.locator('#Workspaceslist-table') }).first();
await openDropdownItem(card, /^\s*Delete\s*$/);
const modal = page.locator('#commonDefaultModal');
await expect(modal).toBeVisible();
test.info().annotations.push({ type: 'confirm-modal', description: (await modal.innerText()).replace(/\s+/g, ' ').trim() });
const apis: string[] = [];
const onResp = async (r: import('@playwright/test').Response) => {
if (/workspace/i.test(r.url()) && r.request().method() !== 'GET' && r.request().method() !== 'OPTIONS')
apis.push(`${r.status()} ${r.request().method()} ${r.url().replace(/^https?:\/\/[^/]+/, '')} ${(r.request().postData() || '').slice(0, 120)} β ${(await r.text().catch(() => '')).slice(0, 200)}`);
};
page.on('response', onResp);
await modal.locator('#commonDefaultModal-confirm-btn').click();
await page.waitForLoadState('load');
await page.waitForTimeout(4000);
page.off('response', onResp);
test.info().annotations.push({ type: 'workspace-delete-api', description: apis.join(' | ') || '(μμ² μμ)' });
await gotoWorkspaces(page);
await expect(wsRow(page, name), `${name} μμ λ¨`).toHaveCount(0, { timeout: 30_000 });
return true;
}
/** Workspace ν ν΄λ¦ β νλ¨ μμΈ μΉ΄λ νμ */
export async function openWorkspace(page: Page, name: string) {
await wsRow(page, name).first().locator('[tabulator-field="name"]').click();
await expect(page.locator('#workspace-details-name')).toHaveText(exactName(name), { timeout: 20_000 });
}
/** Members νμ μ¬μ©μ ν(role μ μ£Όλ©΄ μν κΉμ§ μΌμΉ). μ£Όμ: 2026-10-13 stage μμ Members νλ μν¬μ€νμ΄μ€μ 무κ΄νκ² μ 체 λ§€νμ λ³΄μ¬ μ€λ€ */
export const memberRow = (page: Page, username: string, role?: string) => {
const r = tabRow(page, '#WorkspacesUserInfo-table', 'username', username);
return role ? r.filter({ has: page.locator('[tabulator-field="role"]', { hasText: exactName(role) }) }) : r;
};
export async function openMembersTab(page: Page) {
await page.locator('#workspace-info-card').getByRole('tab', { name: 'Members' }).click();
await page.waitForTimeout(2500);
// νμ΄μ§ ν¬κΈ° μ΅λ(κΈ°λ³Έ 5ν β λ€λ₯Έ μ¬μ©μκ° 2νμ΄μ§λ‘ λ°λ¦Ό)
const sizeSel = page.locator('#workspace-members select.tabulator-page-size').first();
if (await sizeSel.count()) {
const vals = (await sizeSel.locator('option').evaluateAll((os) => os.map((o) => (o as HTMLOptionElement).value))).filter((v) => /^\d+$/.test(v));
const max = vals.sort((a, b) => Number(b) - Number(a))[0];
if (max) await sizeSel.selectOption(max);
await page.waitForTimeout(800);
}
}
/** Members νμ μ¬μ©μ μ΄λ¦ λͺ©λ‘ */
export const memberNames = async (page: Page) =>
(await page.locator('#WorkspacesUserInfo-table .tabulator-row [tabulator-field="username"]').allInnerTexts()).map((s) => s.trim());
/** Members ν + β μ¬μ©μΒ·μν μ ν β Confirm */
export async function addMember(page: Page, username: string, role: string) {
await page.locator('#workspace-members a.btn-action[onclick*="addUserModalInit"]').click();
const m = page.locator('#user-modal-add');
await expect(m).toBeVisible({ timeout: 20_000 });
// TomSelect: μ
λ ₯μ°½ ν΄λ¦ β μ¬μ©μ μ΄λ¦ μ
λ ₯ β μ΅μ
ν΄λ¦
await m.locator('.ts-control').first().click();
await m.locator('.ts-control input').first().fill(username);
await page.locator('.ts-dropdown .option', { hasText: exactName(username) }).first().click();
await page.keyboard.press('Escape').catch(() => {});
// Escape κ° λͺ¨λ¬κΉμ§ λ«μΌλ©΄ μ λλ€ β λͺ¨λ¬ μ μ§ νμΈ
await expect(m).toBeVisible();
await m.locator('#user-modal-add-roleselector').selectOption({ label: role });
await m.getByRole('button', { name: 'Confirm' }).click();
await expect(m, 'Add/Assign user νμ
λ«ν').toBeHidden({ timeout: 30_000 });
}
/** Members νμμ ν΄λΉ μ¬μ©μ νλ§ μ²΄ν¬ β β Unassign Members β Confirm */
export async function unassignMember(page: Page, username: string, role?: string) {
const forbidden = ['mcmp', 'mzc-noh', 'iam01', 'workflow01', 'app01', 'monitor01', 'cost01', 'cost02', 'infra01'];
if (forbidden.includes(username)) throw new Error(`ν΄μ κΈμ§ μ¬μ©μ: ${username}`);
const checked = page.locator('#WorkspacesUserInfo-table .tabulator-row input[type="checkbox"]:checked');
expect(await checked.count(), 'Unassign μ 체ν¬λ λ©€λ² μμ').toBe(0);
await checkRow(memberRow(page, username, role).first());
expect(await checked.count(), `체ν¬λ λ©€λ²λ ${username} νλ`).toBe(1);
const pane = page.locator('#workspace-members');
await openDropdownItem(pane, /Unassign Members/);
const modal = page.locator('#commonDefaultModal');
await expect(modal).toBeVisible();
test.info().annotations.push({ type: 'confirm-modal', description: (await modal.innerText()).replace(/\s+/g, ' ').trim() });
const resp = page.waitForResponse((r) => r.url().includes('/api/mc-iam-manager/removeWorkspaceRole'), { timeout: 30_000 }).catch(() => null);
await modal.locator('#commonDefaultModal-confirm-btn').click();
const r = await resp;
const desc = r ? `${r.status()} ${r.request().postData()} β ${(await r.text().catch(() => '')).slice(0, 200)}` : '(μμ² μμ)';
test.info().annotations.push({ type: 'unassign-api', description: desc });
await page.waitForTimeout(2000);
return r?.ok() ?? false;
}
// ---------------- Roles ----------------
export const roleRow = (page: Page, name: string) => page.locator('#roles-table .tabulator-row').filter({ has: page.getByRole('gridcell', { name, exact: true }) });
export async function gotoRoles(page: Page, url: string) {
await page.goto(url);
for (let attempt = 1; ; attempt++) {
if (await roleRow(page, 'admin').first().waitFor({ timeout: 30_000 }).then(() => true, () => false)) break;
if (attempt >= 3) throw new Error('Roles λͺ©λ‘(admin)μ΄ 90μ΄ λμ νμλμ§ μμ');
test.info().annotations.push({ type: 'list-retry', description: `μλ ${attempt}: Roles λͺ©λ‘ λ―Ένμ β μλ‘κ³ μΉ¨` });
await page.reload();
}
const sizeSel = page.locator('select.tabulator-page-size').first();
if (await sizeSel.count()) {
const vals = (await sizeSel.locator('option').evaluateAll((os) => os.map((o) => (o as HTMLOptionElement).value))).filter((v) => /^\d+$/.test(v));
const max = vals.sort((a, b) => Number(b) - Number(a))[0];
if (max) await sizeSel.selectOption(max);
}
await page.waitForTimeout(1000);
}
/** μν ν ν΄λ¦ β Role Detail νμ */
export async function openRole(page: Page, name: string) {
await roleRow(page, name).first().getByRole('gridcell', { name, exact: true }).click();
await expect(page.locator('#role-detail-rolename-text'), `Role Detail [${name}]`).toHaveText(name, { timeout: 20_000 });
}
/** μ νλ μν Delete (iv9role λ§ νμ©) */
export async function deleteRole(page: Page, url: string, name: string) {
if (!DELETABLE_ROLES.includes(name)) throw new Error(`μμ κΈμ§ Role: ${name}`);
await gotoRoles(page, url);
if (!(await roleRow(page, name).count())) return false;
await openRole(page, name);
const card = page.locator('.card').filter({ has: page.locator('#roles-table') }).first();
await openDropdownItem(card, /^\s*Delete\s*$/);
const modal = page.locator('#commonDefaultModal');
await expect(modal).toBeVisible();
test.info().annotations.push({ type: 'confirm-modal', description: (await modal.innerText()).replace(/\s+/g, ' ').trim() });
// νμΈ μ§μ Role Detail μ΄ μ¬μ ν μμ λμμΈμ§ μ¬νμΈ
await expect(page.locator('#role-detail-rolename-text')).toHaveText(name);
await modal.locator('#commonDefaultModal-confirm-btn').click();
await expect(roleRow(page, name), `${name} μμ λ¨`).toHaveCount(0, { timeout: 30_000 });
return true;
}
// ---------------- Users > User Info > Workspace μΉ΄λ ----------------
/** Users νλ©΄μμ μ¬μ©μ ν ν΄λ¦ β User Info νμ β Workspace μΉ΄λ μ΄λ¦ λͺ©λ‘ λ°ν */
export async function userWorkspaces(page: Page, u: { name: string; email: string }) {
await page.getByRole('gridcell', { name: u.name, exact: true }).first().click();
await expect(page.getByText(`[${u.name}]`).first(), 'User Info νμ').toBeVisible({ timeout: 20_000 });
await page.waitForTimeout(2500); // Workspace μΉ΄λλ λ³λ μμ²μΌλ‘ μ±μμ§λ€
const names = (await page.locator('#workspace-roles-names').innerText()).split('\n').map((s) => s.trim()).filter((s) => s && s !== '-');
return names;
}lib/iam-api.ts Β· 39μ€ Β· ν΄μ 03bb64c6
import { expect, request as pwRequest, type APIRequestContext, type TestInfo } from '@playwright/test';
import { ADMIN } from './data';
// mc-iam-manager REST API ν¬νΌ (IV9 API μλ리μ€μ©)
// IAM_URL: μΈλΆμμ μ κ·Όνλ IAM μ£Όμ(iam nginx 443). κΈ°λ³Έ stage. κ³μ μ lib/data.ts ADMIN(mcmp).
export const IAM_URL = process.env.IAM_URL ?? '<IAM μ£Όμ>';
export async function iamContext(): Promise<APIRequestContext> {
return pwRequest.newContext({ baseURL: IAM_URL, ignoreHTTPSErrors: true });
}
export type Tokens = { access_token: string; refresh_token: string; expires_in?: number };
export async function iamLogin(api: APIRequestContext, id = ADMIN.id, pw = ADMIN.pw): Promise<Tokens> {
const res = await api.post('/api/auth/login', { data: { id, password: pw } });
expect(res.status(), `λ‘κ·ΈμΈ μ€ν¨ HTTP ${res.status()}`).toBe(200);
const t = await res.json();
expect(t.access_token, 'access_token μμ').toBeTruthy();
return t;
}
export const bearer = (t: Tokens | string) => ({ Authorization: `Bearer ${typeof t === 'string' ? t : t.access_token}` });
// μλ΅μ 리ν¬νΈμ 첨λΆ(ν ν°Β·λΉλ°κ°μ κ°λ¦Ό)
export async function attachRes(testInfo: TestInfo, name: string, status: number, body: unknown) {
const text = JSON.stringify(body, null, 1).replace(/("(access_token|refresh_token|id_token|kc_client_secret|password)"\s*:\s*")[^"]*/g, '$1***');
await testInfo.attach(name, { body: `HTTP ${status}\n${text}`, contentType: 'text/plain' });
}
export async function call(api: APIRequestContext, testInfo: TestInfo, name: string, method: 'get' | 'post' | 'put' | 'delete', path: string, opts: { token?: Tokens | string; data?: unknown } = {}) {
const res = await api[method](path, { headers: opts.token ? bearer(opts.token) : {}, data: opts.data });
let body: any = null;
try { body = await res.json(); } catch { body = await res.text().catch(() => ''); }
await attachRes(testInfo, name, res.status(), body);
return { status: res.status(), body };
}
// JWT exp(μ΄)
export const jwtExp = (token: string) => JSON.parse(Buffer.from(token.split('.')[1], 'base64url').toString()).exp as number;tests/iv9/IV9_S01.spec.ts Β· 46μ€ Β· ν΄μ f240c994 Β· μλλ¦¬μ€ λ³΄κΈ°
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin, clickMenu } from '../../lib/ui';
import { URL_CLOUD_OVERVIEW } from '../../lib/iv9';
// IV9_S01 : TC-IAM-001 μλΉμ€ μν νμΈ (Cloud Overview > Readyz μμ > νλ μμν¬λ³ Readyz λ²νΌ)
// TC λμ endpoint λ GET /readyz. νλ©΄μ Readyz νμλ Readyz λ²νΌμ΄ μλ νλ μμν¬λ§ νμλλ©°, κ° κ²°κ³Ό(OK/ERROR)λ₯Ό κΈ°λ‘νλ€.
// νλ©΄ νμ: μ μ='β
Ready'(=OK), μ€ν¨='β ERROR'(+ μ¬μ 문ꡬ).
// νμ : λͺ¨λ Readyz λ²νΌμ΄ κ²°κ³Ό(OK λλ ERROR)λ₯Ό νμνλ©΄ ν΅κ³Ό(κ° νλ μμν¬μ μν μ체λ κΈ°λ‘ λμ).
test('IV9_S01 TC-IAM-001 μλΉμ€ μν νμΈ (Cloud Overview Readyz)', async ({ page }) => {
await test.step('1. admin λ‘κ·ΈμΈ', async () => {
await loginAsAdmin(page);
});
await test.step('2. μ’μΈ‘ λ©λ΄ Cloud SPs > Cloud Overview μ΄λ', async () => {
await clickMenu(page, 'Cloud SPs', 'Cloud Overview');
await expect(page).toHaveURL(new RegExp(URL_CLOUD_OVERVIEW));
await expect(page.getByText('Framework health check and initialization')).toBeVisible({ timeout: 30_000 });
});
const rows = page.locator('tr[id^="readyz-row-"]').filter({ has: page.getByRole('button', { name: 'Readyz' }) });
const results: string[] = [];
await test.step('3. Readyz μμμμ νλ μμν¬λ³ Readyz λ²νΌ ν΄λ¦ β OK/Error νμ νμΈ', async () => {
await expect(rows.first()).toBeVisible({ timeout: 30_000 });
const n = await rows.count();
expect(n, 'Readyz λ²νΌμ΄ μλ νλ μμν¬ μ').toBeGreaterThan(0);
const allNames = (await page.locator('tr[id^="readyz-row-"] td.fw-medium').allInnerTexts()).map((s) => s.trim());
test.info().annotations.push({ type: 'readyz-frameworks', description: `νμλ νλ μμν¬ ${allNames.length}κ°: ${allNames.join(', ')}` });
if (!allNames.includes('mc-iam-manager')) {
test.info().annotations.push({ type: 'note', description: 'Readyz νμ mc-iam-manager νμ΄ μμ(μ½μ μμ²΄κ° iam μΈμ¦μΌλ‘ λμ μ€ = λ‘κ·ΈμΈ μ±κ³΅μΌλ‘ κ°μ νμΈ)' });
}
for (let i = 0; i < n; i++) {
const row = rows.nth(i);
const name = (await row.locator('td.fw-medium').innerText()).trim();
const status = row.locator(`[id="readyz-status-${name}"]`);
await row.getByRole('button', { name: 'Readyz' }).click();
await expect(status, `${name} κ²°κ³Ό νμ`).toContainText(/Ready|ERROR/i, { timeout: 60_000 });
const text = (await row.innerText()).replace(/\s+/g, ' ').replace(name, '').trim();
const st = /Ready/i.test(await status.innerText()) ? 'OK(Ready)' : 'Error';
results.push(`${name}: ${st} (${text.slice(0, 120)})`);
}
});
await test.step('4. νλ μμν¬λ³ OK/Error κ²°κ³Ό κΈ°λ‘', async () => {
for (const r of results) test.info().annotations.push({ type: 'readyz', description: r });
await page.locator('#readyz-row-mc-application-manager, tr[id^="readyz-row-"]').first().scrollIntoViewIfNeeded();
await page.waitForTimeout(2000);
expect(results.length).toBeGreaterThan(0);
});
});tests/iv9/IV9_S02.spec.ts Β· 28μ€ Β· ν΄μ 6a0b1399 Β· μλλ¦¬μ€ λ³΄κΈ°
import { test, expect } from '@playwright/test';
import { iamContext, call, iamLogin } from '../../lib/iam-api';
// IV9_S02 : μ΄κΈ° κ΄λ¦¬μ κ³μ μ€μ (POST /api/initial-admin) β TC-IAM-002
// TC 문ꡬμ POST /api/init μ νμ¬ κ²½λ‘ POST /api/initial-admin (μΈμ¦ μμ΄ νΈμΆ, Keycloak realmΒ·clientΒ·κ΄λ¦¬μ μμ±).
// μ€μΉ ν mc-iam-manager-post-initial μ΄ κΈ°λν λλ§λ€ κ°μ νΈμΆμ νλ€(λ©±λ±) β μ€μΉλ μλ²μμλ μ¬νΈμΆμ΄ μ±κ³΅νκ³ κΈ°μ‘΄ κ΄λ¦¬μκ° μ μ§λλμ§ νμΈνλ€.
test('IV9_S02 μ΄κΈ° κ΄λ¦¬μ κ³μ μ€μ (initial-admin νΈμΆ μ±κ³΅ β νλ«νΌ κ΄λ¦¬μ λ‘κ·ΈμΈΒ·μν μ μ§)', async ({}, testInfo) => {
const api = await iamContext();
await test.step('1. POST /api/initial-admin (λ³Έλ¬Έ μμ) β μ±κ³΅ μλ΅', async () => {
const r = await call(api, testInfo, 'initial-admin', 'post', '/api/initial-admin', { data: {} });
expect(r.status, JSON.stringify(r.body).slice(0, 200)).toBe(200);
expect(r.body?.error ?? false).toBe(false);
});
const t = await test.step('2. νλ«νΌ κ΄λ¦¬μ(.env μ PLATFORMADMIN) λ‘κ·ΈμΈ μ±κ³΅', () => iamLogin(api));
await test.step('3. κ΄λ¦¬μ ν ν°μ platformAdmin μν + νλ«νΌ κ΄λ¦¬μ μ μ© API νΈμΆ μ±κ³΅', async () => {
const claims = JSON.parse(Buffer.from(t.access_token.split('.')[1], 'base64url').toString());
expect(claims.realm_access?.roles ?? [], 'realm μν μ platformAdmin μμ').toContain('platformAdmin');
// /api/setup/* λ PlatformAdminMiddleware λ‘ λ³΄νΈλλ€
const r = await call(api, testInfo, 'check-user-roles', 'get', `/api/setup/check-user-roles?username=${claims.preferred_username}`, { token: t });
expect(r.status).toBe(200);
// μ°Έκ³ : GET /api/users/me/platform-roles λ Keycloak realm μν λ‘ λΆμ¬λ platformAdmin μ λ°μνμ§ μλλ€(null)
const me = await call(api, testInfo, 'my-platform-roles', 'get', '/api/users/me/platform-roles', { token: t });
if (!me.body || (Array.isArray(me.body) && !me.body.length)) testInfo.annotations.push({ type: 'κ°μ λ©λͺ¨', description: 'νλ«νΌ κ΄λ¦¬μ(mcmp)μ /api/users/me/platform-roles κ° null β Keycloak realm μν (platformAdmin)μ΄ IAM μν μ‘°νμ λ°μλμ§ μμ' });
});
});tests/iv9/IV9_S03.spec.ts Β· 41μ€ Β· ν΄μ 2c8b38a0 Β· μλλ¦¬μ€ λ³΄κΈ°
import { test, expect } from '@playwright/test';
import { iamContext, call, iamLogin } from '../../lib/iam-api';
// IV9_S03 : μν Β·λ©λ΄Β·API κΆν μΌκ΄ μ΄κΈ°ν β TC-IAM-003
// TC 문ꡬμ POST /api/init/platform μ νμ¬ μλ€. κ°μ μΌμ νλ API(μ€μΉ μ€ν¬λ¦½νΈ 1_setup_auto.sh μ κ°μ μμ):
// POST /api/setup/initial-menus(λ©λ΄ + μν -λ©λ΄ κΆν μ°μ λ±λ‘) β GET /api/setup/initial-role-menu-permission-yaml β POST /api/setup/sync-mcmp-apis
// initial-menus λ λ©λ΄κ° μ΄λ―Έ μμΌλ©΄ 건λλ΄λ€(force μμ΄ κΈ°μ‘΄ λ°μ΄ν° 보νΈ) β μ€μΉλ μλ²μμλ '건λλ' μλ΅κ³Ό λ°μ΄ν° μ μ§κ° κΈ°λ κ²°κ³Ό.
test('IV9_S03 μν Β·λ©λ΄Β·API κΆν μΌκ΄ μ΄κΈ°ν (initial-menus Β· role-menu κΆν Β· mcmp-api λκΈ°ν β λ°μ΄ν° μ‘΄μ¬)', async ({}, testInfo) => {
const api = await iamContext();
const t = await iamLogin(api);
const count = async (path: string) => {
const r = await call(api, testInfo, path, 'post', path, { token: t, data: {} });
return Array.isArray(r.body) ? r.body.length : (r.body?.length ?? Object.keys(r.body ?? {}).length);
};
const before = { menus: await count('/api/menus/list'), roles: await count('/api/roles/list') };
await test.step('1. POST /api/setup/initial-menus β μ±κ³΅(μ΄λ―Έ μμΌλ©΄ 건λλ)', async () => {
const r = await call(api, testInfo, 'initial-menus', 'post', '/api/setup/initial-menus', { token: t, data: {} });
expect(r.status, JSON.stringify(r.body).slice(0, 200)).toBeLessThan(300);
});
await test.step('2. GET /api/setup/initial-role-menu-permission-yaml β μ±κ³΅', async () => {
const r = await call(api, testInfo, 'role-menu-permission', 'get', '/api/setup/initial-role-menu-permission-yaml', { token: t });
expect(r.status, JSON.stringify(r.body).slice(0, 200)).toBeLessThan(300);
});
await test.step('3. POST /api/setup/sync-mcmp-apis β μ±κ³΅', async () => {
const r = await call(api, testInfo, 'sync-mcmp-apis', 'post', '/api/setup/sync-mcmp-apis', { token: t, data: {} });
expect(r.status, JSON.stringify(r.body).slice(0, 200)).toBe(200);
});
await test.step('4. μν Β·λ©λ΄Β·MCMP API λ°μ΄ν° μ‘΄μ¬, κΈ°μ‘΄ λ©λ΄Β·μν μ μ μ§', async () => {
const after = { menus: await count('/api/menus/list'), roles: await count('/api/roles/list') };
const apis = await call(api, testInfo, 'mcmp-apis', 'post', '/api/mcmp-apis/list', { token: t, data: {} });
testInfo.annotations.push({ type: 'counts', description: `menus ${before.menus}β${after.menus}, roles ${before.roles}β${after.roles}, mcmp-apis HTTP ${apis.status}` });
expect(after.menus).toBeGreaterThan(0);
expect(after.roles).toBeGreaterThanOrEqual(before.roles);
expect(after.menus).toBeGreaterThanOrEqual(before.menus);
expect(apis.status).toBe(200);
});
});tests/iv9/IV9_S04.spec.ts Β· 32μ€ Β· ν΄μ ab4f58aa Β· μλλ¦¬μ€ λ³΄κΈ°
import { test, expect } from '@playwright/test';
import { iamContext, call, iamLogin } from '../../lib/iam-api';
// IV9_S04 : νλ‘μ νΈ λκΈ°ν β TC-IAM-004
// TC 문ꡬμ POST /api/sync/projects λ νμ¬ POST /api/setup/sync-projects (mc-infra-manager namespace β IAM project).
// μ°¨μ΄ μ‘°ν(GET /api/setup/projects/sync-diff) β λκΈ°ν β μ°¨μ΄ μ¬μ‘°νλ‘ νμΈνλ€.
// μλ΅: { missingProjects: [IAM μ μλ namespace], unassignedProjects: [workspace λ―Έν λΉ project] }
const missing = (d: any) => (d?.missingProjects ?? []).map((x: any) => x.nsId);
test('IV9_S04 νλ‘μ νΈ λκΈ°ν (sync-diff β sync-projects β μ°¨μ΄ ν΄μ)', async ({}, testInfo) => {
const api = await iamContext();
const t = await iamLogin(api);
const before = await test.step('1. GET /api/setup/projects/sync-diff β λκΈ°ν μ μ°¨μ΄ μ‘°ν', async () => {
const r = await call(api, testInfo, 'sync-diff-before', 'get', '/api/setup/projects/sync-diff', { token: t });
expect(r.status, JSON.stringify(r.body).slice(0, 200)).toBe(200);
return r.body;
});
await test.step('2. POST /api/setup/sync-projects β "Project synchronization successful"', async () => {
const r = await call(api, testInfo, 'sync-projects', 'post', '/api/setup/sync-projects', { token: t, data: {} });
expect(r.status, JSON.stringify(r.body).slice(0, 200)).toBe(200);
expect(r.body?.message).toContain('Project synchronization successful');
});
await test.step('3. sync-diff μ¬μ‘°ν β infra-manager μλ§ μλ namespace κ° λ¨μ§ μμ', async () => {
const r = await call(api, testInfo, 'sync-diff-after', 'get', '/api/setup/projects/sync-diff', { token: t });
expect(r.status).toBe(200);
testInfo.annotations.push({ type: 'diff', description: `IAM μ μλ namespace: μ [${missing(before).join(', ')}] β ν [${missing(r.body).join(', ')}], workspace λ―Έν λΉ project ν ${(r.body?.unassignedProjects ?? []).length}κ°` });
expect(missing(r.body), 'IAM μ μλ namespace κ° λ¨μ').toEqual([]);
});
});tests/iv9/IV9_S05.spec.ts Β· 43μ€ Β· ν΄μ e20d32ca Β· μλλ¦¬μ€ λ³΄κΈ°
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { URL_ROLES } from '../../lib/forms';
// IV9_S05 : TC-IAM-010 λ‘κ·Έμμ (μλ¨ νλ‘ν λ²νΌ > Logout)
// 2026-10-13 νμΈ: Logout ν΄λ¦ β /auth/logout('Logout' + [Back to Login]) νλ©΄. λ‘κ·Έμμ ν μ½μ μ£Όμλ₯Ό μ΄λ©΄ /auth/unauthorized(#cookieError, [Back to Login]).
// νμ : λ‘κ·Έμμ ν μ½μ νλ©΄μ λ€μ λ€μ΄κ° μ μμ΄μΌ νκ³ (μΈμ¦ νλ©΄μΌλ‘ μ΄λ), λ‘κ·ΈμΈ νλ©΄μΌλ‘ λμκ° μ μμ΄μΌ νλ€.
test('IV9_S05 TC-IAM-010 λ‘κ·Έμμ', async ({ page }) => {
await test.step('1. admin λ‘κ·ΈμΈ', async () => {
await loginAsAdmin(page);
await expect(page).toHaveURL(/\/webconsole\//);
});
await test.step('2. μλ¨ νλ‘ν λ²νΌ ν΄λ¦ β μ¬μ©μ λ©λ΄ νμ', async () => {
await page.getByRole('link', { name: 'Open user menu' }).click();
await expect(page.locator('#logoutbtn')).toBeVisible();
});
await test.step('3. Logout ν΄λ¦ β λ‘κ·Έμμ νλ©΄ νμ', async () => {
await page.locator('#logoutbtn').click();
await page.waitForURL((u) => u.pathname.startsWith('/auth/'), { timeout: 30_000 });
test.info().annotations.push({ type: 'after-logout', description: new URL(page.url()).pathname });
await expect(page.getByRole('link', { name: 'Back to Login' }).or(page.getByPlaceholder('Your ID')).first()).toBeVisible();
});
await test.step('4. λ‘κ·ΈμΈ νλ©΄μΌλ‘ μ΄λ νμΈ', async () => {
const back = page.getByRole('link', { name: 'Back to Login' });
if (await back.isVisible().catch(() => false)) await back.click();
await expect(page).toHaveURL(/\/auth\/login/);
await expect(page.getByPlaceholder('Your ID')).toBeVisible();
await page.waitForTimeout(1500);
});
await test.step('5. μ½μ μ£Όμ(Roles) μ§μ μ΄κΈ° β μ½μ νλ©΄ μ°¨λ¨, λ‘κ·ΈμΈ νμ νλ©΄μΌλ‘ μ΄λ', async () => {
await page.goto(URL_ROLES);
await page.waitForTimeout(3000);
const path = new URL(page.url()).pathname + new URL(page.url()).hash;
test.info().annotations.push({ type: 'console-url-after-logout', description: `${URL_ROLES} β ${path}` });
expect(new URL(page.url()).pathname, 'λ‘κ·Έμμ ν μ½μ νλ©΄ μ κ·Ό λΆκ°').toMatch(/^\/auth\//);
if (!/\/auth\/login/.test(page.url())) {
test.info().annotations.push({ type: 'note', description: `λ‘κ·ΈμΈ νλ©΄μΌλ‘ λ°λ‘ μ΄λνμ§ μκ³ ${path} νλ©΄ νμ β [Back to Login] μΌλ‘ λ‘κ·ΈμΈ νλ©΄ μ΄λ` });
await page.getByRole('link', { name: 'Back to Login' }).click();
}
await expect(page).toHaveURL(/\/auth\/login/);
await expect(page.getByPlaceholder('Your ID')).toBeVisible();
});
});tests/iv9/IV9_S06.spec.ts Β· 28μ€ Β· ν΄μ 6338c449 Β· μλλ¦¬μ€ λ³΄κΈ°
import { test, expect } from '@playwright/test';
import { iamContext, call, iamLogin, jwtExp } from '../../lib/iam-api';
// IV9_S06 : ν ν° κ°±μ (POST /api/auth/refresh) β TC-IAM-011 (TC 문ꡬμ /api/refresh λ νμ¬ /api/auth/refresh)
test('IV9_S06 ν ν° κ°±μ (refresh_token β μ access_token λ°κΈ, μλͺ»λ ν ν°μ 401)', async ({}, testInfo) => {
const api = await iamContext();
const t = await test.step('1. λ‘κ·ΈμΈ β access_tokenΒ·refresh_token λ°κΈ', () => iamLogin(api));
const n = await test.step('2. POST /api/auth/refresh {refresh_token} β 200, μ access_token', async () => {
await new Promise((r) => setTimeout(r, 1500)); // exp κ° λ¬λΌμ§λλ‘ 1μ΄ μ΄μ κ°κ²©
const r = await call(api, testInfo, 'refresh', 'post', '/api/auth/refresh', { data: { refresh_token: t.refresh_token } });
expect(r.status).toBe(200);
expect(r.body?.access_token).toBeTruthy();
expect(r.body.access_token).not.toBe(t.access_token);
expect(jwtExp(r.body.access_token)).toBeGreaterThanOrEqual(jwtExp(t.access_token));
return r.body;
});
await test.step('3. μ access_token μΌλ‘ API νΈμΆ μ±κ³΅ (GET /api/users/me)', async () => {
const r = await call(api, testInfo, 'me', 'get', '/api/users/me', { token: n.access_token });
expect(r.status).toBe(200);
});
await test.step('4. μλͺ»λ refresh_token β 401, λΉ κ° β 400', async () => {
expect((await call(api, testInfo, 'refresh-bad', 'post', '/api/auth/refresh', { data: { refresh_token: 'invalid.token.value' } })).status).toBe(401);
expect((await call(api, testInfo, 'refresh-empty', 'post', '/api/auth/refresh', { data: {} })).status).toBe(400);
});
});tests/iv9/IV9_S07.spec.ts Β· 29μ€ Β· ν΄μ 9eca8861 Β· μλλ¦¬μ€ λ³΄κΈ°
import { test, expect } from '@playwright/test';
import { iamContext, call, iamLogin } from '../../lib/iam-api';
// IV9_S07 : ν ν° κ²μ¦ λ° λ§λ£ μλ° μλ κ°±μ (POST /api/auth/validate) β TC-IAM-012 (TC 문ꡬμ /api/validate λ νμ¬ /api/auth/validate)
// μμ²: Authorization: Bearer <access_token> + λ³Έλ¬Έ {refresh_token}. μ ν¨νλ©΄ valid:true, λ§λ£λμΌλ©΄ refresh λ‘ μ ν ν°κ³Ό valid:false(νΈλ€λ¬ ꡬν).
// 2λ¨κ³λ μλͺ
μ΄ κΉ¨μ§ ν ν°μΌλ‘ 'κ²μ¦ μ€ν¨ β μλ κ°±μ ' κ²½λ‘λ₯Ό νμΈνλ€(μ€μ λ§λ£λ accessΒ·refresh TTL μ΄ λͺ¨λ 1800μ΄λΌ μ¬ν λΆκ°).
test('IV9_S07 ν ν° κ²μ¦ (μ ν¨ ν ν° valid:true Β· λ¬΄ν¨ ν ν°μ refresh λ‘ μ¬λ°κΈ Β· ν€λ μμΌλ©΄ 401)', async ({}, testInfo) => {
const api = await iamContext();
const t = await iamLogin(api);
await test.step('1. μ ν¨ν access_token κ²μ¦ β 200, valid:true', async () => {
const r = await call(api, testInfo, 'validate-ok', 'post', '/api/auth/validate', { token: t, data: { refresh_token: t.refresh_token } });
expect(r.status).toBe(200);
expect(r.body?.valid).toBe(true);
});
await test.step('2. 무ν¨(λ§λ£ κ°μ ) access_token + μ ν¨ refresh_token β 200, valid:false, μ ν ν° λ°κΈ', async () => {
const broken = t.access_token.slice(0, -4) + 'AAAA';
const r = await call(api, testInfo, 'validate-refresh', 'post', '/api/auth/validate', { token: broken, data: { refresh_token: t.refresh_token } });
expect(r.status).toBe(200);
expect(r.body?.valid).toBe(false);
expect(JSON.stringify(r.body)).toContain('access_token');
});
await test.step('3. Authorization ν€λ μμ β 401, refresh_token μμ β 400', async () => {
expect((await call(api, testInfo, 'validate-noauth', 'post', '/api/auth/validate', { data: { refresh_token: t.refresh_token } })).status).toBe(401);
expect((await call(api, testInfo, 'validate-norefresh', 'post', '/api/auth/validate', { token: t, data: {} })).status).toBe(400);
});
});tests/iv9/IV9_S08.spec.ts Β· 26μ€ Β· ν΄μ 897d2353 Β· μλλ¦¬μ€ λ³΄κΈ°
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin, clickMenu } from '../../lib/ui';
import { URL_USERS } from '../../lib/forms';
import { gotoUsersAll } from '../../lib/groups';
// IV9_S08 : TC-IAM-014 μ¬μ©μ λͺ©λ‘ μ‘°ν (Organizations > Users)
test('IV9_S08 TC-IAM-014 μ¬μ©μ λͺ©λ‘ μ‘°ν', async ({ page }) => {
await test.step('1. admin λ‘κ·ΈμΈ', async () => {
await loginAsAdmin(page);
});
await test.step('2. μ’μΈ‘ λ©λ΄ Organizations > Users μ΄λ', async () => {
await clickMenu(page, 'Organizations', 'Users');
await expect(page).toHaveURL(new RegExp(URL_USERS));
await expect(page.getByText('List of Users')).toBeVisible({ timeout: 30_000 });
});
await test.step('3. μ¬μ©μ λͺ©λ‘ νμ νμΈ(μ 체 ν μ, admin mcmp ν¬ν¨)', async () => {
await gotoUsersAll(page); // λͺ©λ‘μ΄ λ¦κ² λ¨λ κ²½μ° μ¬μλ + νμ΄μ§ ν¬κΈ° μ΅λ
const rows = page.locator('.tabulator-row');
const n = await rows.count();
const names = (await rows.allInnerTexts()).map((s) => s.replace(/\s+/g, ' ').trim());
test.info().annotations.push({ type: 'users', description: `${n}λͺ
: ${names.join(' / ').slice(0, 900)}` });
expect(n, 'μ¬μ©μ 1λͺ
μ΄μ').toBeGreaterThan(0);
// admin mcmp: νλ©΄μλ μ΄λ¦ 'mcmp iammanager' / μ΄λ©μΌ [email protected] λ‘ νμ
await expect(rows.filter({ hasText: /mcmp/ }).first(), 'admin mcmp ν').toBeVisible();
});
});tests/iv9/IV9_S09.spec.ts Β· 28μ€ Β· ν΄μ f8f709fe Β· μλλ¦¬μ€ λ³΄κΈ°
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin, clickMenu } from '../../lib/ui';
import { gotoUsersAll } from '../../lib/groups';
import { IV9_USER } from '../../lib/iv9';
// IV9_S09 : TC-IAM-015 μ¬μ©μ μμΈ μ‘°ν (Users > μ¬μ©μ ν ν΄λ¦ > User Info)
// λμ: ν
μ€νΈ κ³μ pending01 (μ‘°νλ§, λ³κ²½ μμ)
test('IV9_S09 TC-IAM-015 μ¬μ©μ μμΈ μ‘°ν', async ({ page }) => {
const u = IV9_USER;
await test.step('1. admin λ‘κ·ΈμΈ ν Organizations > Users μ΄λ', async () => {
await loginAsAdmin(page);
await clickMenu(page, 'Organizations', 'Users');
await gotoUsersAll(page);
});
await test.step(`2. μ¬μ©μ ν(${u.id}) ν΄λ¦`, async () => {
await page.getByRole('gridcell', { name: u.name, exact: true }).first().click();
});
await test.step('3. User Info μμμ μ¬μ©μ μ 보(μ΄λ¦/μ΄λ©μΌ/μν/μν ) νμ νμΈ', async () => {
const info = page.locator('.card').filter({ hasText: 'User Info' }).filter({ hasText: u.email }).last();
await expect(page.getByText(`[${u.name}]`).first(), 'User Info μ λͺ©μ μ¬μ©μ νμ').toBeVisible({ timeout: 20_000 });
await expect(info, 'User Info μ μ΄λ©μΌ νμ').toContainText(u.email);
const text = (await info.innerText()).replace(/\s+/g, ' ');
test.info().annotations.push({ type: 'user-info', description: text.slice(0, 600) });
expect(text).toMatch(/First Name/);
expect(text).toMatch(/Status/);
await page.waitForTimeout(1500);
});
});tests/iv9/IV9_S10.spec.ts Β· 35μ€ Β· ν΄μ 9d6b4bf8 Β· μλλ¦¬μ€ λ³΄κΈ°
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin, clickMenu } from '../../lib/ui';
import { gotoUsersAll } from '../../lib/groups';
// IV9_S10 : TC-IAM-018 μ¬μ©μ λκΈ°ν
// mc-iam-manager μ μ¬μ©μ λκΈ°ν API κ²½λ‘κ° μλ€. Users νλ©΄μ λκΈ°ν κΈ°λ₯(λ²νΌ/λ©λ΄)μ΄ μλμ§ νμΈνκ³ , μμΌλ©΄ κ·Όκ±°(μ€ν¬λ¦°μ·)λ₯Ό λ¨κΈ°κ³ SKIP.
test('IV9_S10 TC-IAM-018 μ¬μ©μ λκΈ°ν', async ({ page }) => {
await test.step('1. admin λ‘κ·ΈμΈ ν Organizations > Users μ΄λ', async () => {
await loginAsAdmin(page);
await clickMenu(page, 'Organizations', 'Users');
await gotoUsersAll(page);
});
let found: string[] = [];
await test.step('2. Users νλ©΄μ λ²νΌ/λ©λ΄μμ μ¬μ©μ λκΈ°ν(Sync) κΈ°λ₯ μ°ΎκΈ°', async () => {
// λͺ©λ‘ μΉ΄λμ β λ©λ΄κ° μμΌλ©΄ νΌμ³μ λ©λ΄ νλͺ©κΉμ§ νμΈ
const toggles = page.locator('.card-header [data-bs-toggle="dropdown"]');
for (let i = 0; i < (await toggles.count()); i++) {
await toggles.nth(i).click().catch(() => {});
await page.waitForTimeout(500);
}
const labels = await page.evaluate(() =>
[...document.querySelectorAll('button, a.btn, .btn-action, .dropdown-item, [role=button]')]
.filter((e) => !(e as HTMLElement).closest('aside, .navbar-vertical'))
.map((e) => `${(e.textContent || '').trim().replace(/\s+/g, ' ')}|${e.getAttribute('title') || ''}|${e.getAttribute('onclick') || ''}`),
);
test.info().annotations.push({ type: 'users-controls', description: [...new Set(labels.map((l) => l.split('|')[0] || l))].filter(Boolean).join(', ').slice(0, 800) });
found = labels.filter((l) => /sync/i.test(l));
await page.keyboard.press('Escape');
await page.screenshot({ path: test.info().outputPath('users-no-sync.png'), fullPage: true });
await test.info().attach('users-no-sync.png', { path: test.info().outputPath('users-no-sync.png'), contentType: 'image/png' });
});
test.info().annotations.push({ type: 'sync-ui', description: found.length ? `λ°κ²¬: ${found.join(' / ')}` : 'Users νλ©΄μ μ¬μ©μ λκΈ°ν(Sync) λ²νΌ/λ©λ΄ μμ' });
test.skip(found.length === 0, 'Users νλ©΄μ μ¬μ©μ λκΈ°ν UI μμ + mc-iam-manager μ μ¬μ©μ λκΈ°ν API κ²½λ‘ μμ(κΈ°λ₯ λ―Έμ 곡)');
expect(found.length).toBeGreaterThan(0); // UI κ° μκΈ°λ©΄ μ΄ν λ¨κ³λ₯Ό μΆκ°ν΄μΌ ν¨
});tests/iv9/IV9_S11.spec.ts Β· 30μ€ Β· ν΄μ 35c03091 Β· μλλ¦¬μ€ λ³΄κΈ°
import { test, expect } from '@playwright/test';
import { iamContext, call, iamLogin } from '../../lib/iam-api';
// IV9_S11 : νμ¬(λ²μΈ) λ±λ‘ (POST /api/company) β TC-IAM-019
// νμ¬λ μμ€ν
μ 1κ°(realm_name μ μΌ). μ€μΉ(post-initial) λ 'Default Company'(realm mciam)κ° λ±λ‘λλ€.
// μ€μΉλ μλ²: λ±λ‘ κ²°κ³Ό μ‘°ν + κ°μ realm μΌλ‘ λ€μ λ±λ‘νλ©΄ 409(μ€λ³΅ κ±°λΆ). νμ¬κ° μλ μλ²μμλ§ μ€μ λ±λ‘μ μννλ€.
test('IV9_S11 νμ¬(λ²μΈ) λ±λ‘ (νμκ° κ²μ¦ 400 Β· λ±λ‘ μ 보 μ‘°ν Β· μ€λ³΅ λ±λ‘ 409)', async ({}, testInfo) => {
const api = await iamContext();
const t = await iamLogin(api);
await test.step('1. νμκ°(realm_name λ±) μμ΄ λ±λ‘ β 400', async () => {
const r = await call(api, testInfo, 'create-missing', 'post', '/api/company', { token: t, data: { name: 'iv9-company' } });
expect(r.status).toBe(400);
});
const company = await test.step('2. GET /api/company β λ±λ‘λ νμ¬ μ 보(μ΄λ¦Β·realmΒ·status active)', async () => {
const r = await call(api, testInfo, 'get-company', 'get', '/api/company', { token: t });
expect(r.status, 'λ±λ‘λ νμ¬ μμ(μ€μΉ μ΄κΈ°ν λ―Έμλ£)').toBe(200);
expect(r.body?.realm_name).toBeTruthy();
expect(r.body?.status).toBe('active');
return r.body;
});
await test.step(`3. κ°μ realm(${company.realm_name})μΌλ‘ λ€μ λ±λ‘ β 409 μ€λ³΅ κ±°λΆ`, async () => {
const r = await call(api, testInfo, 'create-dup', 'post', '/api/company', { token: t, data: { name: 'iv9-dup', realm_name: company.realm_name, kc_client_id: company.kc_client_id, kc_client_secret: 'iv9-dummy' } });
expect(r.status).toBe(409);
const after = await call(api, testInfo, 'get-company-after', 'get', '/api/company', { token: t });
expect(after.body?.name, 'κΈ°μ‘΄ νμ¬ μ λ³΄κ° λ°λ').toBe(company.name);
});
});tests/iv9/IV9_S12.spec.ts Β· 44μ€ Β· ν΄μ 84eaec86 Β· μλλ¦¬μ€ λ³΄κΈ°
import { test, expect } from '@playwright/test';
import { iamContext, call, iamLogin } from '../../lib/iam-api';
// IV9_S12 : νμ¬ μ 보 μμ Β·λΉνμ±νΒ·νμ±ν (PUT / DELETE / POST /activate /api/company) β TC-IAM-020
// μ€λͺ
λ§ λ°κΏ¨λ€κ° μλ κ°μΌλ‘ λλλ¦¬κ³ , λΉνμ±ν μ§ν λ€μ νμ±ννλ€(νμ¬ μνλ λ‘κ·ΈμΈ λ±μ μ°μ΄μ§ μμ β 2026-10-13 μμ€ νμΈ).
test('IV9_S12 νμ¬ μ 보 μμ Β·λΉνμ±νΒ·νμ±ν (μμ β inactive β active, μλ κ° λ³΅μ)', async ({}, testInfo) => {
const api = await iamContext();
const t = await iamLogin(api);
const orig = (await call(api, testInfo, 'get-orig', 'get', '/api/company', { token: t })).body;
expect(orig?.name, 'λ±λ‘λ νμ¬ μμ').toBeTruthy();
const stamp = `iv9 edited ${Date.now()}`;
try {
await test.step('1. PUT /api/company (μ€λͺ
λ³κ²½) β 200, μ‘°ν κ²°κ³Ό λ°μ', async () => {
const r = await call(api, testInfo, 'update', 'put', '/api/company', { token: t, data: { name: orig.name, description: stamp } });
expect(r.status).toBe(200);
expect((await call(api, testInfo, 'get-updated', 'get', '/api/company', { token: t })).body?.description).toBe(stamp);
});
await test.step('2. PUT name μμ΄ β 400', async () => {
expect((await call(api, testInfo, 'update-noname', 'put', '/api/company', { token: t, data: { description: 'x' } })).status).toBe(400);
});
await test.step('3. DELETE /api/company β status inactive', async () => {
const r = await call(api, testInfo, 'deactivate', 'delete', '/api/company', { token: t });
expect(r.status).toBe(200);
expect((await call(api, testInfo, 'get-inactive', 'get', '/api/company', { token: t })).body?.status).toBe('inactive');
});
await test.step('4. POST /api/company/activate β status active', async () => {
const r = await call(api, testInfo, 'activate', 'post', '/api/company/activate', { token: t, data: {} });
expect(r.status).toBe(200);
expect((await call(api, testInfo, 'get-active', 'get', '/api/company', { token: t })).body?.status).toBe('active');
});
} finally {
await test.step('5. μ 리: μλ μ€λͺ
Β·νμ± μνλ‘ λ³΅μ', async () => {
await call(api, testInfo, 'restore', 'put', '/api/company', { token: t, data: { name: orig.name, description: orig.description ?? '' } });
await call(api, testInfo, 'restore-activate', 'post', '/api/company/activate', { token: t, data: {} });
const now = (await call(api, testInfo, 'get-restored', 'get', '/api/company', { token: t })).body;
expect(now?.description ?? '').toBe(orig.description ?? '');
expect(now?.status).toBe('active');
});
}
});tests/iv9/IV9_S13.spec.ts Β· 51μ€ Β· ν΄μ 3679d729 Β· μλλ¦¬μ€ λ³΄κΈ°
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin, clickMenu } from '../../lib/ui';
import { URL_ROLES } from '../../lib/forms';
import { IV9_ROLE, deleteRole, gotoRoles, openRole, roleRow } from '../../lib/iv9';
// IV9_S13 : TC-IAM-033 μν μμ± (Roles > Add Role > Create New Role > Role Name/Description > Save Role)
// ν
μ€νΈ μ μ© μν iv9role μ λ§λ€κ³ , λ§μ§λ§ μ 리 λ¨κ³μμ μμ νλ€(μ€ν¨ν΄λ finally λ‘ μμ μλ).
test('IV9_S13 TC-IAM-033 μν μμ±', async ({ page, dialogs }) => {
const desc = `iv9 test role ${Date.now()}`;
try {
await test.step('1. admin λ‘κ·ΈμΈ ν Workspaces > Roles μ΄λ', async () => {
await loginAsAdmin(page);
await clickMenu(page, 'Workspaces', 'Roles');
await gotoRoles(page, URL_ROLES);
if (await roleRow(page, IV9_ROLE).count()) {
test.info().annotations.push({ type: 'preexisting', description: `${IV9_ROLE} μμ‘΄(μ΄μ μ€ν) β λ¨Όμ μμ ` });
await deleteRole(page, URL_ROLES, IV9_ROLE);
}
});
await test.step('2. Add Role ν΄λ¦ β Create New Role μμ νμ', async () => {
await page.getByRole('link', { name: 'Add Role' }).click();
await expect(page.locator('#create_role')).toBeVisible();
await expect(page.getByText('Create New Role')).toBeVisible();
});
await test.step(`3. Role Name(${IV9_ROLE}) Β· Description μ
λ ₯`, async () => {
await page.locator('#role-name-input').fill(IV9_ROLE);
await page.locator('#role-description-input').fill(desc);
});
await test.step('4. Save Role ν΄λ¦ β μ μ₯ κ²°κ³Ό νμΈ', async () => {
const before = dialogs.length;
await page.locator('#save-create-role-btn').click();
await expect.poll(() => dialogs.slice(before).join(' | '), { timeout: 30_000 }).not.toBe('');
test.info().annotations.push({ type: 'save-alert', description: dialogs.slice(before).join(' | ') });
expect(dialogs.slice(before).join(' | ')).toMatch(/created successfully/i);
});
await test.step(`5. μν λͺ©λ‘μ ${IV9_ROLE} νμ Β· μμΈ νμΈ`, async () => {
await gotoRoles(page, URL_ROLES);
await expect(roleRow(page, IV9_ROLE).first(), `λͺ©λ‘μ ${IV9_ROLE}`).toBeVisible({ timeout: 30_000 });
await openRole(page, IV9_ROLE);
await expect(page.locator('#role-detail-desc-view')).toContainText(desc);
await page.waitForTimeout(1500);
});
} finally {
await test.step(`6. (μ 리) ${IV9_ROLE} μμ `, async () => {
const deleted = await deleteRole(page, URL_ROLES, IV9_ROLE);
test.info().annotations.push({ type: 'cleanup', description: deleted ? `${IV9_ROLE} μμ μλ£` : `${IV9_ROLE} μμ(μμ λΆνμ)` });
await gotoRoles(page, URL_ROLES);
await expect(roleRow(page, IV9_ROLE), `${IV9_ROLE} μ 리λ¨`).toHaveCount(0);
});
}
});tests/iv9/IV9_S14.spec.ts Β· 30μ€ Β· ν΄μ f986b496 Β· μλλ¦¬μ€ λ³΄κΈ°
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin, clickMenu } from '../../lib/ui';
import { URL_ROLES } from '../../lib/forms';
import { gotoRoles, roleRow } from '../../lib/iv9';
// IV9_S14 : TC-IAM-034 μν λͺ©λ‘ μ‘°ν (Workspaces > Roles, Refresh)
// κΈ°λ³Έ μν 5μ’
(admin, operator, viewer, billadmin, billviewer) νμ νμΈ. 2026-10-13: λͺ©λ‘μ νΈλ¦¬(κ³μΈ΅)κ° μλ ν ννλ‘ νμλ¨ β κΈ°λ‘.
const BASE_ROLES = ['admin', 'operator', 'viewer', 'billadmin', 'billviewer'];
test('IV9_S14 TC-IAM-034 μν λͺ©λ‘ μ‘°ν', async ({ page }) => {
await test.step('1. admin λ‘κ·ΈμΈ', async () => {
await loginAsAdmin(page);
});
await test.step('2. μ’μΈ‘ λ©λ΄ Workspaces > Roles μ΄λ', async () => {
await clickMenu(page, 'Workspaces', 'Roles');
await expect(page).toHaveURL(new RegExp(URL_ROLES));
await gotoRoles(page, URL_ROLES);
});
await test.step('3. λͺ©λ‘ Refresh ν΄λ¦', async () => {
await page.locator('a.btn-action[onclick*="refreshRolesList"]').click();
await page.waitForTimeout(3000);
await expect(roleRow(page, 'admin').first()).toBeVisible({ timeout: 30_000 });
});
await test.step('4. μν λͺ©λ‘ νμ νμΈ(admin, operator, viewer, billadmin, billviewer)', async () => {
for (const r of BASE_ROLES) await expect(roleRow(page, r).first(), `μν ${r}`).toBeVisible();
const rows = (await page.locator('#roles-table .tabulator-row').allInnerTexts()).map((s) => s.replace(/\s+/g, ' ').trim());
const tree = await page.locator('#roles-table .tabulator-data-tree-control, #roles-table .tabulator-data-tree-branch').count();
test.info().annotations.push({ type: 'roles', description: `${rows.length}κ°: ${rows.join(' / ')}` });
test.info().annotations.push({ type: 'display', description: tree ? `κ³μΈ΅(νΈλ¦¬) νμ: νΈλ¦¬ 컨νΈλ‘€ ${tree}κ°` : 'κ³μΈ΅(νΈλ¦¬) νμ μλ β νλ©΄ ν(Role Master ID / Name / Platform access / Workspace access / CSP Role)' });
});
});tests/iv9/IV9_S15.spec.ts Β· 28μ€ Β· ν΄μ b70c5605 Β· μλλ¦¬μ€ λ³΄κΈ°
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin, clickMenu } from '../../lib/ui';
import { URL_ROLES } from '../../lib/forms';
import { gotoRoles, openRole } from '../../lib/iv9';
// IV9_S15 : TC-IAM-035 μν μμΈ μ‘°ν (Roles > μν ν΄λ¦ > Role Detail). μ‘°νλ§, λ³κ²½ μμ.
test('IV9_S15 TC-IAM-035 μν μμΈ μ‘°ν', async ({ page }) => {
await test.step('1. admin λ‘κ·ΈμΈ ν Workspaces > Roles μ΄λ', async () => {
await loginAsAdmin(page);
await clickMenu(page, 'Workspaces', 'Roles');
await gotoRoles(page, URL_ROLES);
});
await test.step('2. λͺ©λ‘μμ μν (viewer) ν΄λ¦', async () => {
await openRole(page, 'viewer');
});
await test.step('3. νλ¨ Role Detail μ 보(Name, Description, Platform/Workspace access, CSP Role) νμ νμΈ', async () => {
const card = page.locator('#role-detail-card');
await expect(card).toBeVisible();
await expect(page.locator('#role-detail-name-view')).toHaveText('viewer');
const text = (await card.innerText()).replace(/\s+/g, ' ');
test.info().annotations.push({ type: 'role-detail', description: text.slice(0, 500) });
expect(text).toMatch(/Platform access/);
expect(text).toMatch(/Workspace access/);
expect(text).toMatch(/CSP Role/);
await card.scrollIntoViewIfNeeded();
await page.waitForTimeout(1500);
});
});tests/iv9/IV9_S16.spec.ts Β· 75μ€ Β· ν΄μ d615639a Β· μλλ¦¬μ€ λ³΄κΈ°
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin, clickMenu } from '../../lib/ui';
import { WORKSPACE } from '../../lib/data';
import { gotoUsersAll } from '../../lib/groups';
import { IV9_USER, gotoWorkspaces, memberRow, openMembersTab, openWorkspace, unassignMember, userWorkspaces } from '../../lib/iv9';
// IV9_S16 : TC-IAM-038 μ¬μ©μμκ² μν¬μ€νμ΄μ€ μν ν λΉ (Users > μ¬μ©μ μ ν > User Info > Workspace > Actions + > μν μ ν)
// λμ: ν
μ€νΈ κ³μ pending01 (stage μ rec####a/b μ¬μ©μκ° μμ΄ λ체, 2026-10-13). ws01 μ viewer λ₯Ό ν λΉνκ³ λ§μ§λ§μ ν΄μ νλ€.
// 2026-10-13 νμΈ: Add Role Mapping νμ
μλ Workspace μ νλμ΄ μκ³ , μλ¨ Workspace μ νκ°(ws01)μ ν λΉλλ€.
// Users νλ©΄ Workspace μΉ΄λμ μμ (ν΄μ§ν΅) λ²νΌμ API λ₯Ό νΈμΆνμ§ μλ 미ꡬν κΈ°λ₯(μμ€ TODO) β μ 리λ Workspaces > ws01 > Members > Unassign Members λ‘ νλ€.
const ROLE = 'viewer';
test('IV9_S16 TC-IAM-038 μ¬μ©μμκ² μν¬μ€νμ΄μ€ μν ν λΉ', async ({ page, dialogs }) => {
const u = IV9_USER;
let assignedByTest = false;
try {
await test.step(`1. admin λ‘κ·ΈμΈ(μλ¨ Workspace=${WORKSPACE}) ν Organizations > Users μ΄λ`, async () => {
await loginAsAdmin(page);
await expect(page.locator('#select-current-workspace')).toHaveValue(/.+/);
await clickMenu(page, 'Organizations', 'Users');
await gotoUsersAll(page);
});
let before: string[] = [];
await test.step(`2. μ¬μ©μ(${u.id}) μ ν β User Info μ Workspace μμ νμΈ`, async () => {
before = await userWorkspaces(page, u);
test.info().annotations.push({ type: 'workspaces-before', description: `${u.id}: ${before.join(', ') || '(μμ)'}` });
});
if (before.includes(WORKSPACE)) {
test.info().annotations.push({ type: 'preexisting', description: `${u.id} λ μ΄λ―Έ ${WORKSPACE} μν 보μ β ν λΉ μλ΅(ν΄μ λ νμ§ μμ)` });
} else {
await test.step(`3. Workspace μμ Actions + ν΄λ¦ β Role Type=Workspace Role, Role=${ROLE} μ ν β Add Role`, async () => {
await page.locator('#workspace-roles-actions button[title="Add Workspace role"]').click();
const m = page.locator('#add-role-mapping-modal');
await expect(m).toBeVisible();
await expect(m.locator('#role-mapping-type')).toHaveValue('workspace');
await m.locator('#role-mapping-role').selectOption({ label: ROLE });
const n = dialogs.length;
const apis: string[] = [];
const onResp = async (r: import('@playwright/test').Response) => {
if (r.url().includes('/api/mc-iam-manager/') && r.request().method() === 'POST' && /role/i.test(r.url()) && !/list/i.test(r.url()))
apis.push(`${r.status()} ${r.url().replace(/^https?:\/\/[^/]+/, '')} ${(r.request().postData() || '').slice(0, 150)}`);
};
page.on('response', onResp);
assignedByTest = true; // ν΄λ¦ μ΄νμλ κ²°κ³Όμ κ΄κ³μμ΄ μ 리 λ¨κ³λ₯Ό μν
await m.getByRole('button', { name: 'Add Role' }).click();
await expect(m, 'Add Role Mapping νμ
λ«ν(μ μ₯ μλ£)').toBeHidden({ timeout: 30_000 });
await page.waitForTimeout(2000);
page.off('response', onResp);
test.info().annotations.push({ type: 'add-role-api', description: apis.join(' | ') || '(μμ² μμ)' });
// νλ©΄ μμ€μ μ±κ³΅ μλ¦Ό(Utils.showAlert)μ λΉ ν¨μλΌ λ©μμ§κ° νμλμ§ μλλ€(2026-10-13 νμΈ) β κΈ°λ‘λ§
test.info().annotations.push({ type: 'add-message', description: dialogs.slice(n).join(' | ') || '(μ±κ³΅ λ©μμ§ νμ μμ)' });
});
}
await test.step(`4. Workspace μμμ ${WORKSPACE} νμ νμΈ(λͺ©λ‘ λ€μ μ΄μ΄ μ μ₯ νμΈ)`, async () => {
await gotoUsersAll(page);
const after = await userWorkspaces(page, u);
test.info().annotations.push({ type: 'workspaces-after', description: `${u.id}: ${after.join(', ') || '(μμ)'}` });
expect(after, `${u.id} Workspace μ ${WORKSPACE}`).toContain(WORKSPACE);
await page.locator('#workspace-roles-names').scrollIntoViewIfNeeded();
await page.waitForTimeout(1500);
});
} finally {
if (assignedByTest) {
await test.step(`5. (μ 리) Workspaces > ${WORKSPACE} > Members μμ ${u.id}(${ROLE}) Unassign β Users μμ ν΄μ νμΈ`, async () => {
await gotoWorkspaces(page);
await openWorkspace(page, WORKSPACE);
await openMembersTab(page);
if (await memberRow(page, u.id, ROLE).count()) await unassignMember(page, u.id, ROLE);
await gotoUsersAll(page);
const ws = await userWorkspaces(page, u);
test.info().annotations.push({ type: 'cleanup', description: `${u.id} Workspace(μ 리 ν): ${ws.join(', ') || '(μμ)'}` });
expect(ws, `${u.id} μ ${WORKSPACE} μν ν΄μ λ¨`).not.toContain(WORKSPACE);
});
}
}
});tests/iv9/IV9_S17.spec.ts Β· 29μ€ Β· ν΄μ e0522bef Β· μλλ¦¬μ€ λ³΄κΈ°
import { test, expect } from '@playwright/test';
import { iamContext, call, iamLogin } from '../../lib/iam-api';
// IV9_S17 : MCMP API λκΈ°ν (POST /api/setup/sync-mcmp-apis) β TC-IAM-048
// api.yaml(νλ μμν¬λ³ μλΉμ€Β·μ‘μ
μ μ)μ IAM DB λ‘ λκΈ°ννλ€. μ€μΉ μ€ν¬λ¦½νΈκ° κΈ°λλ§λ€ νΈμΆνλ λ©±λ± λμ.
test('IV9_S17 MCMP API λκΈ°ν (sync-mcmp-apis β μλΉμ€Β·μ‘μ
λͺ©λ‘ μ‘΄μ¬Β·μ μ§)', async ({}, testInfo) => {
const api = await iamContext();
const t = await iamLogin(api);
// μλ΅: { Services: {μλΉμ€λͺ
: {Version, BaseURL, Auth}}, ServiceActions: {μλΉμ€λͺ
: {μ‘μ
λͺ
: {...}}} }
const summary = (b: any) => ({ services: Object.keys(b?.Services ?? {}).length, actions: Object.values(b?.ServiceActions ?? {}).reduce((n: number, a: any) => n + Object.keys(a ?? {}).length, 0) });
const before = summary((await call(api, testInfo, 'list-before', 'post', '/api/mcmp-apis/list', { token: t, data: {} })).body);
await test.step('1. POST /api/setup/sync-mcmp-apis β "Successfully triggered MCMP API sync"', async () => {
const r = await call(api, testInfo, 'sync', 'post', '/api/setup/sync-mcmp-apis', { token: t, data: {} });
expect(r.status, JSON.stringify(r.body).slice(0, 200)).toBe(200);
expect(r.body?.message).toContain('MCMP API sync');
});
await test.step('2. POST /api/mcmp-apis/list β μλΉμ€Β·μ‘μ
λͺ©λ‘ μ‘΄μ¬(λκΈ°ν μ μ΄μ)', async () => {
const r = await call(api, testInfo, 'list-after', 'post', '/api/mcmp-apis/list', { token: t, data: {} });
expect(r.status).toBe(200);
const after = summary(r.body);
testInfo.annotations.push({ type: 'mcmp-apis', description: `μλΉμ€ ${before.services} β ${after.services}, μ‘μ
${before.actions} β ${after.actions}` });
expect(after.services).toBeGreaterThan(0);
expect(after.actions).toBeGreaterThan(0);
expect(after.services).toBeGreaterThanOrEqual(before.services);
expect(after.actions).toBeGreaterThanOrEqual(before.actions);
});
});tests/iv9/IV9_S18.spec.ts Β· 28μ€ Β· ν΄μ a2527a2f Β· μλλ¦¬μ€ λ³΄κΈ°
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { URL_WORKSPACES, gotoWorkspaces, wsRow } from '../../lib/iv9';
// IV9_S18 : TC-IAM-049 μν¬μ€νμ΄μ€ λͺ©λ‘ μ‘°ν
// 2026-10-13: μ’μΈ‘ 'Workspaces' κ·Έλ£Ήμλ Projects/Roles/CSP Roles λ§ μκ³ Workspaces λͺ©λ‘ νλ©΄ λ§ν¬κ° μλ€
// β κ·Έλ£Ή ν΄λ¦μ΄ νλ©΄μ μ΄μ§ μμΌλ©΄ κ·Έλ£Ή μ£Όμ(/webconsole/operations/manage/workspaces)λ‘ μ΄λ(κΈ°λ‘).
test('IV9_S18 TC-IAM-049 μν¬μ€νμ΄μ€ λͺ©λ‘ μ‘°ν', async ({ page }) => {
await test.step('1. admin λ‘κ·ΈμΈ', async () => {
await loginAsAdmin(page);
});
await test.step('2. Workspaces νλ©΄ μ΄λ', async () => {
await page.locator('.nav-link[name="sidebar_workspaces"]').first().click();
await page.waitForTimeout(2000);
if (!new URL(page.url()).pathname.endsWith(URL_WORKSPACES)) {
test.info().annotations.push({ type: 'note', description: `μ’μΈ‘ λ©λ΄ 'Workspaces' ν΄λ¦μΌλ‘ λͺ©λ‘ νλ©΄μ΄ μ΄λ¦¬μ§ μμ(νμ λ©λ΄λ§ νΌμΉ¨) β ${URL_WORKSPACES} λ‘ μ΄λ` });
await gotoWorkspaces(page);
}
await expect(page).toHaveURL(new RegExp(`${URL_WORKSPACES}$`));
});
await test.step('3. μν¬μ€νμ΄μ€ λͺ©λ‘ νμ νμΈ(ws01 ν¬ν¨)', async () => {
await expect(wsRow(page, 'ws01').first(), 'ws01 ν').toBeVisible({ timeout: 30_000 });
const rows = (await page.locator('#Workspaceslist-table .tabulator-row').allInnerTexts()).map((s) => s.replace(/\s+/g, ' ').trim());
const summary = (await page.locator('.card').filter({ hasText: 'Workspaces Summary' }).first().innerText()).replace(/\s+/g, ' ');
test.info().annotations.push({ type: 'workspaces', description: `${rows.length}κ°: ${rows.join(' / ')}` });
test.info().annotations.push({ type: 'summary', description: summary.slice(0, 200) });
});
});tests/iv9/IV9_S19.spec.ts Β· 36μ€ Β· ν΄μ 445db2d5 Β· μλλ¦¬μ€ λ³΄κΈ°
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { IV9_WS, createWorkspace, deleteWorkspace, gotoWorkspaces, openWorkspace, toastTexts, wsRow } from '../../lib/iv9';
// IV9_S19 : TC-IAM-050 μν¬μ€νμ΄μ€ μμ± (Workspaces > Add Workspace > μ΄λ¦Β·μ€λͺ
> Confirm)
// ν
μ€νΈ μ μ© iv9ws λ₯Ό λ§λ€κ³ λ§μ§λ§ μ 리 λ¨κ³μμ μμ νλ€(μ€ν¨ν΄λ finally λ‘ μμ μλ).
test('IV9_S19 TC-IAM-050 μν¬μ€νμ΄μ€ μμ±', async ({ page }) => {
const desc = `iv9 test workspace ${Date.now()}`;
try {
await test.step('1. admin λ‘κ·ΈμΈ ν Workspaces νλ©΄ μ΄λ', async () => {
await loginAsAdmin(page);
await gotoWorkspaces(page);
if (await wsRow(page, IV9_WS).count()) {
test.info().annotations.push({ type: 'preexisting', description: `${IV9_WS} μμ‘΄(μ΄μ μ€ν) β λ¨Όμ μμ ` });
await deleteWorkspace(page, IV9_WS);
}
});
await test.step(`2. Add Workspace ν΄λ¦ β μ΄λ¦(${IV9_WS})Β·μ€λͺ
μ
λ ₯ β Confirm`, async () => {
expect(await createWorkspace(page, IV9_WS, desc)).toBe(true);
await page.waitForTimeout(1000);
test.info().annotations.push({ type: 'toast', description: (await toastTexts(page)).join(' | ') || '(μμ)' });
});
await test.step(`3. λͺ©λ‘μ ${IV9_WS} νμ Β· μμΈ(μ΄λ¦/μ€λͺ
) νμΈ`, async () => {
await expect(wsRow(page, IV9_WS).first(), `λͺ©λ‘μ ${IV9_WS}`).toBeVisible({ timeout: 30_000 });
await openWorkspace(page, IV9_WS);
await expect(page.locator('#workspace-details-description')).toContainText(desc);
await page.waitForTimeout(1500);
});
} finally {
await test.step(`4. (μ 리) ${IV9_WS} μμ `, async () => {
const deleted = await deleteWorkspace(page, IV9_WS);
test.info().annotations.push({ type: 'cleanup', description: deleted ? `${IV9_WS} μμ μλ£` : `${IV9_WS} μμ(μμ λΆνμ)` });
await expect(wsRow(page, 'ws01').first(), 'ws01 μ μ§').toBeVisible();
});
}
});tests/iv9/IV9_S20.spec.ts Β· 72μ€ Β· ν΄μ f5937c38 Β· μλλ¦¬μ€ λ³΄κΈ°
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { gotoUsersAll } from '../../lib/groups';
import {
IV9_USER, IV9_WS2, addMember, createWorkspace, deleteWorkspace, gotoWorkspaces, memberNames, memberRow, openMembersTab, openWorkspace,
unassignMember, userWorkspaces, wsRow,
} from '../../lib/iv9';
// IV9_S20 : TC-IAM-051 μν¬μ€νμ΄μ€μ μ¬μ©μ μΆκ° (Workspaces > μν¬μ€νμ΄μ€ μ ν > Members ν > + > μ¬μ©μΒ·μν > Confirm)
// μμ μν¬μ€νμ΄μ€ iv9ws2 λ₯Ό λ§λ€κ³ ν
μ€νΈ μ¬μ©μ pending01(stage μ rec#### μ¬μ©μκ° μμ΄ λ체)μ viewer λ‘ μΆκ°ν λ€ μ 리νλ€.
// μ 리 μμ: λ©€λ² ν΄μ (Unassign Members) β μν¬μ€νμ΄μ€ μμ . λ©€λ²κ° λ¨μ μμΌλ©΄ μμ API κ° 500 μΌλ‘ μ€ν¨νλ€(2026-10-13 νμΈ).
// 2026-10-13 νμΈ(μ ν): Members νκ° μ νν μν¬μ€νμ΄μ€μ 무κ΄νκ² λͺ¨λ μν¬μ€νμ΄μ€μ μ¬μ©μ-μν λ§€νμ λ³΄μ¬ μ€λ€
// (mc-iam-manager listUsersAndRolesByWorkspaces κ° κ²½λ‘μ workspaceId λ₯Ό μ°μ§ μμ).
// μ΄ TC λ 'μ¬μ©μ μΆκ°' κΈ°λ₯μ νμ νλ―λ‘ μΆκ° ν λμ μ¬μ©μ νμλ‘ νμΈνκ³ , λ€λ₯Έ μν¬μ€νμ΄μ€ λ©€λ²κ° μμ¬ λ³΄μ΄λ νμμ κ²°ν¨ λ©λͺ¨(annotation)λ‘λ§ λ¨κΈ΄λ€(2026-10-13 μ¬μ©μ κ²°μ ).
const ROLE = 'viewer';
test('IV9_S20 TC-IAM-051 μν¬μ€νμ΄μ€μ μ¬μ©μ μΆκ°', async ({ page }) => {
const u = IV9_USER;
let created = false;
try {
await test.step(`1. admin λ‘κ·ΈμΈ ν Workspaces νλ©΄μμ μμ μν¬μ€νμ΄μ€ ${IV9_WS2} μμ±`, async () => {
await loginAsAdmin(page);
await gotoWorkspaces(page);
if (await wsRow(page, IV9_WS2).count()) {
test.info().annotations.push({ type: 'preexisting', description: `${IV9_WS2} μμ‘΄(μ΄μ μ€ν) β μ 리 ν λ€μ μμ±` });
await openWorkspace(page, IV9_WS2);
await openMembersTab(page);
if (await memberRow(page, u.id, ROLE).count()) await unassignMember(page, u.id, ROLE);
await deleteWorkspace(page, IV9_WS2);
}
created = await createWorkspace(page, IV9_WS2, 'iv9 temp workspace for member test');
await expect(wsRow(page, IV9_WS2).first()).toBeVisible({ timeout: 30_000 });
});
await test.step(`2. ${IV9_WS2} ν ν΄λ¦ β Members ν μ΄κΈ°`, async () => {
await openWorkspace(page, IV9_WS2);
await openMembersTab(page);
const before = await memberNames(page);
test.info().annotations.push({ type: 'members-before', description: `${IV9_WS2} Members(μΆκ° μ ): ${before.join(', ') || '(μμ)'}` });
if (before.length) test.info().annotations.push({ type: 'κ²°ν¨ λ©λͺ¨', description: `μ μν¬μ€νμ΄μ€ Members μ λ€λ₯Έ μν¬μ€νμ΄μ€ λ©€λ² ${before.length}λͺ
νμ(${before.join(', ')}) β λͺ©λ‘ API κ° workspaceId λ₯Ό 무μ` });
});
await test.step(`3. + ν΄λ¦ β μ¬μ©μ(${u.id})Β·μν (${ROLE}) μ ν β Confirm`, async () => {
await addMember(page, u.id, ROLE);
});
await test.step(`4. Members λͺ©λ‘μ ${u.id}(${ROLE}) νμ νμΈ`, async () => {
await openMembersTab(page);
const row = memberRow(page, u.id, ROLE).first();
await expect(row, `λ©€λ² ${u.id}(${ROLE})`).toBeVisible({ timeout: 30_000 });
test.info().annotations.push({ type: 'member', description: (await row.innerText()).replace(/\s+/g, ' ').trim() });
});
await test.step(`5. Users > ${u.id} > User Info μ Workspace μ ${IV9_WS2} νμ νμΈ`, async () => {
await gotoUsersAll(page);
const ws = await userWorkspaces(page, u);
test.info().annotations.push({ type: 'user-workspaces', description: `${u.id}: ${ws.join(', ') || '(μμ)'}` });
expect(ws, `${u.id} μ Workspace μ ${IV9_WS2}`).toContain(IV9_WS2);
});
} finally {
await test.step(`6. (μ 리) ${u.id} λ©€λ² ν΄μ β μμ μν¬μ€νμ΄μ€ ${IV9_WS2} μμ `, async () => {
await gotoWorkspaces(page);
if (await wsRow(page, IV9_WS2).count()) {
await openWorkspace(page, IV9_WS2);
await openMembersTab(page);
if (await memberRow(page, u.id, ROLE).count()) await unassignMember(page, u.id, ROLE);
await deleteWorkspace(page, IV9_WS2);
}
test.info().annotations.push({ type: 'cleanup', description: `${IV9_WS2} ${created ? 'μμ±λΆ ' : ''}μμ μλ£` });
await expect(wsRow(page, IV9_WS2)).toHaveCount(0);
await expect(wsRow(page, 'ws01').first(), 'ws01 μ μ§').toBeVisible();
await gotoUsersAll(page);
const ws = await userWorkspaces(page, u);
expect(ws, `${u.id} μ Workspace μμ ${IV9_WS2} μ κ±°λ¨`).not.toContain(IV9_WS2);
});
}
});tests/iv9/IV9_S21.spec.ts Β· 76μ€ Β· ν΄μ 7feaf7e8 Β· μλλ¦¬μ€ λ³΄κΈ°
import { test, expect } from '../../lib/fixtures';
import { PROJECTS_URL, PROTECTED, TEMP_PROJECT, ensureTempProject, openProjects, projectRow, protectedStillThere, selectProject } from '../../lib/projects';
import { toastTexts } from '../../lib/iv9';
// IV9_S21 : TC-IAM-053 νλ‘μ νΈ μμ (Workspaces > Projects > νλ‘μ νΈ μ ν > Edit > μ€λͺ
λ³κ²½ > Save)
// λμ: μμ Project ptmp(lib/projects.ts ensureTempProject). default / p1 / system μ μμ νμ§ μλλ€.
// μ΄ ν
μ€νΈκ° ptmp λ₯Ό μλ‘ λ§λ€μμΌλ©΄ λ§μ§λ§μ μ 리νλ€(ws01 Unassign β Delete). μλ μλ ptmp λ κ·Έλλ‘ λλ€.
test('IV9_S21 TC-IAM-053 νλ‘μ νΈ μμ ', async ({ page }) => {
const newDesc = `iv9 edited ${new Date().toISOString().replace(/[-:]/g, '').slice(0, 15)}`;
let created = false;
try {
await test.step('1. admin λ‘κ·ΈμΈ ν Workspaces > Projects μ΄λ', async () => {
await openProjects(page);
});
await test.step(`2. μμ Project ${TEMP_PROJECT} μ€λΉ(μμΌλ©΄ Add Project λ‘ μμ±)`, async () => {
expect(PROTECTED).not.toContain(TEMP_PROJECT);
created = await ensureTempProject(page);
test.info().annotations.push({ type: 'temp-project', description: created ? `${TEMP_PROJECT} μμ±(ν
μ€νΈ ν μμ )` : `${TEMP_PROJECT} κΈ°μ‘΄ κ² μ¬μ©` });
});
await test.step(`3. ${TEMP_PROJECT} ν ν΄λ¦ β μμΈ νμ β Edit ν΄λ¦`, async () => {
await selectProject(page, TEMP_PROJECT);
await expect(page.locator('#detail-name')).toHaveText(TEMP_PROJECT);
test.info().annotations.push({ type: 'desc-before', description: (await page.locator('#detail-description').innerText()).trim() });
await page.locator('#project-detail-panel').getByRole('button', { name: 'Edit', exact: true }).click();
await expect(page.locator('#modal-project-edit')).toBeVisible();
await expect(page.locator('#edit-project-name')).toHaveValue(TEMP_PROJECT);
});
await test.step(`4. Description μ '${newDesc}' λ‘ λ³κ²½ β Save`, async () => {
const m = page.locator('#modal-project-edit');
await m.locator('#edit-project-description').fill(newDesc);
await m.getByRole('button', { name: 'Save' }).click();
await expect(m, 'Edit νμ
λ«ν').toBeHidden({ timeout: 30_000 });
await page.waitForTimeout(800);
const t = await toastTexts(page);
test.info().annotations.push({ type: 'toast', description: t.join(' | ') || '(μμ)' });
expect(t.join(' | ')).toMatch(/Project updated/i);
});
await test.step('5. μμΈΒ·λͺ©λ‘μ λ³κ²½λ μ€λͺ
νμ νμΈ(λ€μ μ΄μ΄ μ μ₯ νμΈ)', async () => {
await expect(page.locator('#detail-description')).toHaveText(newDesc, { timeout: 20_000 });
await page.goto(PROJECTS_URL);
await expect(projectRow(page, TEMP_PROJECT).first()).toBeVisible({ timeout: 60_000 });
await expect(projectRow(page, TEMP_PROJECT).first().locator('[tabulator-field="description"]')).toHaveText(newDesc);
await selectProject(page, TEMP_PROJECT);
await expect(page.locator('#detail-description')).toHaveText(newDesc);
await protectedStillThere(page);
});
} finally {
if (created) {
await test.step(`6. (μ 리) μ΄ ν
μ€νΈκ° λ§λ ${TEMP_PROJECT}: ws01 Unassign β Delete`, async () => {
await page.goto(PROJECTS_URL);
await expect(projectRow(page, TEMP_PROJECT).first()).toBeVisible({ timeout: 60_000 });
await selectProject(page, TEMP_PROJECT);
await expect(page.locator('#detail-name')).toHaveText(TEMP_PROJECT);
const assigned = page.locator('#assign-list > div').filter({ hasText: 'ws01' });
if (await assigned.count()) {
await assigned.getByRole('button', { name: 'Unassign' }).click();
await expect(page.locator('#assign-list > div').filter({ hasText: 'ws01' })).toHaveCount(0, { timeout: 30_000 });
}
await expect(page.locator('#detail-name')).toHaveText(TEMP_PROJECT);
await page.locator('#project-detail-panel').getByRole('button', { name: 'Delete', exact: true }).click();
const modal = page.locator('#commonDefaultModal');
await expect(modal).toBeVisible();
const msg = (await modal.innerText()).replace(/\s+/g, ' ').trim();
test.info().annotations.push({ type: 'confirm-modal', description: msg });
expect(msg).toContain(`"${TEMP_PROJECT}"`);
await modal.locator('#commonDefaultModal-confirm-btn').click();
await expect(projectRow(page, TEMP_PROJECT)).toHaveCount(0, { timeout: 30_000 });
test.info().annotations.push({ type: 'cleanup', description: `${TEMP_PROJECT} μμ μλ£ / ν μ€νΈ: ${(await toastTexts(page)).join(' | ')}` });
await page.goto(PROJECTS_URL);
await page.getByRole('gridcell', { name: 'default', exact: true }).first().waitFor({ timeout: 60_000 });
await expect(projectRow(page, TEMP_PROJECT), `${TEMP_PROJECT} μ 리λ¨`).toHaveCount(0, { timeout: 30_000 });
await protectedStillThere(page);
});
}
}
});