Test Specs IV1 - MZC-CSC/mc-web-console GitHub Wiki
์์ฑ: 2026-10-14 ยท ๊ธฐ์ค: v07 ์ค์น ํ ํตํฉ ๊ฒ์ฆ(IV) ยท ์๋ณธ: ์ฌ์ ๋ณ ๊ณํ์(Excel) โ Test Scenarios ยท IV ํ๋ธ ยท ๊ธฐ๋ฅ TC
Playwright(TypeScript) spec ์์ค์
๋๋ค. ์๋๋ฆฌ์ค๋ง๋ค ## <์๋๋ฆฌ์ค๋ฒํธ> ์น์
์ด ์๊ณ ์ต์ปค๋ #<์๋๋ฆฌ์ค๋ฒํธ ์๋ฌธ์> ์
๋๋ค. ๊ณต์ฉ ํฌํผ๋ Test-Specs-Common. ๋น๋ฐ๋ฒํธ ๋ฑ์ ๋ง์คํนํ์ต๋๋ค.
| ์๋๋ฆฌ์ค | ํ์ผ | ์ค | ํด์ |
|---|---|---|---|
| IV1_S01 | tests/iv1/IV1_S01.spec.ts |
22 | 4d0616ba |
| IV1_S02 | tests/iv1/IV1_S02.spec.ts |
26 | 66a5d4b2 |
| IV1_S03 | tests/iv1/IV1_S03.spec.ts |
27 | 5260c257 |
| IV1_S04 | tests/iv1/IV1_S04.spec.ts |
25 | 75954919 |
| IV1_S05 | tests/iv1/IV1_S05.spec.ts |
5 | 685c364b |
| IV1_S06 | tests/iv1/IV1_S06.spec.ts |
5 | cbc31f44 |
| IV1_S07 | tests/iv1/IV1_S07.spec.ts |
5 | c5124eb1 |
| IV1_S08 | tests/iv1/IV1_S08.spec.ts |
5 | 0ba3173e |
| IV1_S09 | tests/iv1/IV1_S09.spec.ts |
5 | d7f7e62b |
| IV1_S10 | tests/iv1/IV1_S10.spec.ts |
14 | 15c788bf |
| IV1_S11 | tests/iv1/IV1_S11.spec.ts |
24 | 25497423 |
| IV1_S12 | tests/iv1/IV1_S12.spec.ts |
18 | 5f92dbd9 |
| IV1_S13 | tests/iv1/IV1_S13.spec.ts |
5 | 9e6522a3 |
| IV1_S14 | tests/iv1/IV1_S14.spec.ts |
5 | a43ff7be |
| IV1_S15 | tests/iv1/IV1_S15.spec.ts |
5 | 44a66632 |
| IV1_S16 | tests/iv1/IV1_S16.spec.ts |
5 | 6f3a9ef6 |
| IV1_S17 | tests/iv1/IV1_S17.spec.ts |
35 | f2d56ae7 |
| IV1_S18 | tests/iv1/IV1_S18.spec.ts |
36 | fbab4993 |
| IV1_S19 | tests/iv1/IV1_S19.spec.ts |
19 | 3727775a |
| IV1_S20 | tests/iv1/IV1_S20.spec.ts |
25 | 04bf21ce |
| IV1_S21 | tests/iv1/IV1_S21.spec.ts |
30 | f2287741 |
| IV1_S22 | tests/iv1/IV1_S22.spec.ts |
30 | 1d8286f1 |
| IV1_S23 | tests/iv1/IV1_S23.spec.ts |
32 | e20e3862 |
| IV1_S24 | tests/iv1/IV1_S24.spec.ts |
27 | 782d7f99 |
| IV1_S25 | tests/iv1/IV1_S25.spec.ts |
21 | 411ddd2d |
| IV1_S26 | tests/iv1/IV1_S26.spec.ts |
26 | 7766748c |
| IV1_S27 | tests/iv1/IV1_S27.spec.ts |
33 | f1f360e2 |
| IV1_S28 | tests/iv1/IV1_S28.spec.ts |
28 | b09ce7c5 |
| IV1_S29 | tests/iv1/IV1_S29.spec.ts |
19 | 1f0b6d13 |
| IV1_S30 | tests/iv1/IV1_S30.spec.ts |
23 | 4eb9c563 |
| IV1_S31 | tests/iv1/IV1_S31.spec.ts |
18 | f3191ed5 |
| IV1_S32 | tests/iv1/IV1_S32.spec.ts |
28 | fa00655a |
| IV1_S33 | tests/iv1/IV1_S33.spec.ts |
34 | ae180d2e |
| IV1_S34 | tests/iv1/IV1_S34.spec.ts |
38 | 36df68db |
| IV1_S35 | tests/iv1/IV1_S35.spec.ts |
6 | c20df840 |
| IV1_S36 | tests/iv1/IV1_S36.spec.ts |
7 | fbed94ad |
| IV1_S37 | tests/iv1/IV1_S37.spec.ts |
26 | f486d866 |
tests/iv1/IV1_S01.spec.ts ยท 22์ค ยท ํด์ 4d0616ba ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { REC_ORG, createGroup, gotoGroups, openGroup, treeItem } from '../../lib/groups';
// IV1_S01 : admin ์ต์์ ์กฐ์ง ์์ฑ (Groups > Create Org, Parent=None)
// ํ ํ
์คํธ = ํ ๋์(2026-10-05 ์ฌ์ฉ์ ์ง์): ํ์ ์กฐ์ง ์์ฑ์ IV1_S25, ์ค์ ์กฐ์ง(mcmp + 6๊ฐ ํ) ํ์ธ์ IV1_S26.
// ์ค์ ์กฐ์ง์ ์ฌ์ฉ์๊ฐ ๋ฐฐ์ ๋ผ ์์ด ๋ค์ ๋ง๋ค ์ ์์ผ๋ฏ๋ก(2026-10-04 ์ฌ์ฉ์ ๊ฒฐ์ ) ๋
นํ์ฉ ์กฐ์ง recorg ๋ก ์์ฑ ์ฅ๋ฉด์ ๋ณด์ฌ ์ค๋ค.
// recorg ๋ IV1_S21(์ญ์ )์์ ์ ๋ฆฌ๋๋ค. ์ด๋ฏธ ์์ผ๋ฉด ์์ฑ์ ๊ฑด๋๋ฐ๊ณ (preexisting) ์์ธ๋ง ํ์ธ.
test('IV1_S01 admin: ์ต์์ ์กฐ์ง ์์ฑ (recorg)', async ({ page, dialogs }) => {
await loginAsAdmin(page, 'ws01', null);
await gotoGroups(page);
if (await treeItem(page, REC_ORG).count()) {
test.info().annotations.push({ type: 'preexisting', description: `${REC_ORG} ์ด๋ฏธ ์กด์ฌ(์ด์ ์คํ ์์กด) โ ์์ฑ ๊ฑด๋๋. IV1_S21 ๋ก ์ ๋ฆฌ ํ์` });
} else {
await createGroup(page, REC_ORG, 'IV1 ๋
นํ์ฉ ์ต์์ ์กฐ์ง');
await expect.poll(() => dialogs.join('|')).toContain('Group created successfully');
}
const detail = await openGroup(page, REC_ORG, `/${REC_ORG}`);
expect(detail, '์ต์์ ์กฐ์ง Level 1').toMatch(/Level 1/);
expect(detail, '์ค๋ช
ํ์').toContain('IV1 ๋
นํ์ฉ ์ต์์ ์กฐ์ง');
});tests/iv1/IV1_S02.spec.ts ยท 26์ค ยท ํด์ 66a5d4b2 ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { USERS } from '../../lib/data';
import { adminCreateUser } from '../../lib/forms';
import { REC_ADMIN_USER, gotoUsersAll, userExistsAll } from '../../lib/groups';
// IV1_S02 : admin ์ฌ์ฉ์ ์ถ๊ฐ (Users > Add User) โ ํ ํ
์คํธ = ํ ๋์(2026-10-05 ์ฌ์ฉ์ ์ง์)
// ์กฐ์งยท์ญํ ํ ๋น์ IV1_S23, ๋น๋ฐ๋ฒํธ ์ค์ ์ IV1_S24 ๋ก ๋ถ๋ฆฌ.
// ๋
นํ์ฉ ์ฌ์ฉ์ rec{MMDD}a ๋ก ์์ฑ ์ฅ๋ฉด์ ๋ณด์ฌ ์ค๋ค(IV1_S22 ์์ ์ญ์ ๋๋ฏ๋ก ์ฌ๋
นํ ์ ๋ค์ ์์ฑ๋จ).
// ์ค์ ์ฌ์ฉ์ iam01ยทinfra01 ์ ์์ ๋๋ง ์์ฑ(๋ฉฑ๋ฑ).
test('IV1_S02 admin: ์ฌ์ฉ์ ์ถ๊ฐ (Add User)', async ({ page, dialogs }) => {
await loginAsAdmin(page, 'ws01', null);
for (const u of [REC_ADMIN_USER, ...USERS.filter((x) => x.via === 'admin')]) {
if (await userExistsAll(page, u)) {
test.info().annotations.push({ type: 'preexisting', description: `${u.id}: ์ด๋ฏธ ์กด์ฌ โ ์์ฑ ๊ฑด๋๋` });
continue;
}
const before = dialogs.length;
await adminCreateUser(page, u);
await expect.poll(() => dialogs.slice(before).join('|')).toContain('User created successfully');
await gotoUsersAll(page);
await expect(page.getByRole('row').filter({ hasText: u.email }).first(), `๋ชฉ๋ก์ ${u.id}`).toBeVisible({ timeout: 20_000 });
test.info().annotations.push({ type: 'created', description: `${u.id} (${u.email})` });
}
});tests/iv1/IV1_S03.spec.ts ยท 27์ค ยท ํด์ 5260c257 ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test } from '../../lib/fixtures';
import { loginAsAdmin, logout } from '../../lib/ui';
import { USERS } from '../../lib/data';
import { signUp, pendingApproval } from '../../lib/forms';
import { REC_SIGNUP_USER, userExistsAll } from '../../lib/groups';
// IV1_S03 : ์ฌ์ฉ์ ์ง์ ๊ฐ์
(๋ก๊ทธ์ธ ํ๋ฉด -> Sign Up). ๊ด๋ฆฌ์๊ฐ ๋ง๋๋ ํ๋ฆ(IV1_S02)๊ณผ ๋ณ๊ฐ.
// ์ค์ ๋์(workflow01 โฆ)์ ์ด๋ฏธ ๊ฐ์
๋ผ ์์ด ๊ฐ์
์ฅ๋ฉด์ด ๋
นํ๋์ง ์์ผ๋ฏ๋ก(2026-10-04 ์ฌ์ฉ์ ์ง์ ),
// ๋
นํ์ฉ ์ฌ์ฉ์ rec{MMDD}b ๋ก Sign Up โ 'Registration request submitted' ์๋ด๊น์ง ๋ณด์ฌ ์ค๋ค(์น์ธ์ IV1_S04).
// ์ฌ์คํ ๊ฐ๋ฅ: ์ด๋ฏธ ๊ฐ์
/์น์ธ๋ ์ฌ์ฉ์๋ ๊ฑด๋๋ด๋ค(์ด๋
ธํ
์ด์
'preexisting').
// ์กด์ฌ ํ์ธ์ Users ๋ชฉ๋ก ์ ์ฒด ํ์ด์ง ๊ธฐ์ค(userExistsAll) โ ์ฌ์ฉ์๊ฐ ๋์ด 2ํ์ด์ง๋ก ๋ฐ๋ฆฌ๋ฉด '์์'์ผ๋ก ์คํํ๋ ๋ฌธ์ (2026-10-05) ๋ณด์.
test('IV1_S03 ์ ๊ท ๊ฐ์
(workflow01, app01, monitor01, cost01, cost02)', async ({ page }) => {
const targets = [REC_SIGNUP_USER, ...USERS.filter((x) => x.via === 'signup')];
const todo: typeof targets = [];
await loginAsAdmin(page, 'ws01', null);
for (const u of targets) {
if ((await userExistsAll(page, u)) || (await pendingApproval(page, u))) {
test.info().annotations.push({ type: 'preexisting', description: `${u.id}: ์ด๋ฏธ ๊ฐ์
/์น์ธ๋จ โ ๊ฐ์
๊ฑด๋๋` });
} else todo.push(u);
}
await logout(page);
for (const u of todo) {
await signUp(page, u); // ์๋ด 'Registration request submittedโฆ' ํ์ธ ํฌํจ
test.info().annotations.push({ type: 'signup', description: `${u.id} (${u.email}) ๊ฐ์
์์ฒญ ์๋ฃ` });
}
});tests/iv1/IV1_S04.spec.ts ยท 25์ค ยท ํด์ 75954919 ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { USERS } from '../../lib/data';
import { pendingApproval } from '../../lib/forms';
import { REC_SIGNUP_USER, pendingApprovalWait } from '../../lib/groups';
// IV1_S04 : admin ๊ฐ์
์น์ธ (Organizations > Approvals > Approve) โ ํ ํ
์คํธ = ํ ๋์(2026-10-05 ์ฌ์ฉ์ ์ง์)
// ์กฐ์ง ํ ๋น์ IV1_S27, ์ญํ ๋ถ์ฌ๋ IV1_S28 ๋ก ๋ถ๋ฆฌ.
// ์ ํ: IV1_S03(๊ฐ์
). ๋
นํ์ฉ rec{MMDD}b ๋ ์น์ธ ๋๊ธฐ์ฌ์ผ ํ๋ฏ๋ก ๋ชฉ๋ก์ด ๋ฐ ๋๊น์ง ๊ธฐ๋ค๋ฆฐ๋ค. ์ค์ ๋์์ ๋๊ธฐ ์ค์ผ ๋๋ง ์น์ธ(๋ฉฑ๋ฑ).
test('IV1_S04 admin: ๊ฐ์
์น์ธ (Approvals)', async ({ page, dialogs }) => {
await loginAsAdmin(page, 'ws01', null);
for (const u of [REC_SIGNUP_USER, ...USERS.filter((x) => x.via === 'signup')]) {
const pending = u === REC_SIGNUP_USER ? await pendingApprovalWait(page, u) : await pendingApproval(page, u);
if (!pending) {
test.info().annotations.push({ type: 'preexisting', description: `${u.id}: ์น์ธ ๋๊ธฐ ์์(์ด๋ฏธ ์น์ธ๋จ ๋๋ ๋ฏธ๊ฐ์
)` });
continue;
}
const before = dialogs.length;
const row = page.getByRole('row', { name: new RegExp(u.email) });
await row.getByRole('button', { name: 'Approve' }).click(); // confirm("Approve this user?") ๋ fixture ๊ฐ ์๋ฝ
await expect(row, `${u.id} ์น์ธ ํ ๋๊ธฐ ๋ชฉ๋ก์์ ์ฌ๋ผ์ง`).toHaveCount(0, { timeout: 20_000 });
test.info().annotations.push({ type: 'approved', description: `${u.id} / ${dialogs.slice(before).join(' | ') || '(์๋ฟ ์์)'}` });
}
});tests/iv1/IV1_S05.spec.ts ยท 5์ค ยท ํด์ 685c364b ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { menuTest } from '../../lib/roles';
// IV1_S05 : ์ญํ ๋ณ ๋ก๊ทธ์ธ -> ๋
ธ์ถ ๋ฉ๋ด ํ์ธ (๋ํ ์ฌ์ฉ์๋ lib/roles.ts ์ MENU_USERS)
menuTest('IV1_S05');tests/iv1/IV1_S06.spec.ts ยท 5์ค ยท ํด์ cbc31f44 ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { menuTest } from '../../lib/roles';
// IV1_S06 : ์ญํ ๋ณ ๋ก๊ทธ์ธ -> ๋
ธ์ถ ๋ฉ๋ด ํ์ธ (๋ํ ์ฌ์ฉ์๋ lib/roles.ts ์ MENU_USERS)
menuTest('IV1_S06');tests/iv1/IV1_S07.spec.ts ยท 5์ค ยท ํด์ c5124eb1 ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { menuTest } from '../../lib/roles';
// IV1_S07 : ์ญํ ๋ณ ๋ก๊ทธ์ธ -> ๋
ธ์ถ ๋ฉ๋ด ํ์ธ (๋ํ ์ฌ์ฉ์๋ lib/roles.ts ์ MENU_USERS)
menuTest('IV1_S07');tests/iv1/IV1_S08.spec.ts ยท 5์ค ยท ํด์ 0ba3173e ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { menuTest } from '../../lib/roles';
// IV1_S08 : ์ญํ ๋ณ ๋ก๊ทธ์ธ -> ๋
ธ์ถ ๋ฉ๋ด ํ์ธ (๋ํ ์ฌ์ฉ์๋ lib/roles.ts ์ MENU_USERS)
menuTest('IV1_S08');tests/iv1/IV1_S09.spec.ts ยท 5์ค ยท ํด์ d7f7e62b ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { menuTest } from '../../lib/roles';
// IV1_S09 : ์ญํ ๋ณ ๋ก๊ทธ์ธ -> ๋
ธ์ถ ๋ฉ๋ด ํ์ธ (๋ํ ์ฌ์ฉ์๋ lib/roles.ts ์ MENU_USERS)
menuTest('IV1_S09');tests/iv1/IV1_S10.spec.ts ยท 14์ค ยท ํด์ 15c788bf ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '../../lib/fixtures';
import { USERS } from '../../lib/data';
// IV1_S10 : ์๋ชป๋ ๋น๋ฐ๋ฒํธ๋ก ๋ก๊ทธ์ธ (์์ ์คํจ)
test('IV1_S10 ์๋ชป๋ ๋น๋ฐ๋ฒํธ๋ก ๋ก๊ทธ์ธ (์์์คํจ)', async ({ page }) => {
const u = USERS[0];
await page.goto('/auth/login');
await page.getByPlaceholder('Your ID').fill(u.id);
await page.getByPlaceholder('Your password').fill('wrong_password');
await page.getByRole('button', { name: 'Login' }).click();
// ๊ธฐ๋: ๋ก๊ทธ์ธ ๊ฑฐ๋ถ + ์๋ฌ ๋ฉ์์ง(4xx). ๋ก๊ทธ์ธ ํ๋ฉด์ ๋จธ๋ฌธ๋ค. (์๋ฌ ๋ฌธ๊ตฌ๋ ์ฒซ ์คํ์์ ํ์ธ ํ ๋ณด์)
await expect(page).toHaveURL(/\/auth\/login/);
});tests/iv1/IV1_S11.spec.ts ยท 24์ค ยท ํด์ 25497423 ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '../../lib/fixtures';
import { PENDING_USER, pwOf } from '../../lib/data';
import { signUp } from '../../lib/forms';
// IV1_S11 : ๋ฏธ์น์ธ ์ฌ์ฉ์ ๊ฐ์
ํ ๋ก๊ทธ์ธ (์์ ์คํจ)
// ์ฌ์คํ ๊ฐ๋ฅ: ์ด๋ฏธ ๊ฐ์
๋ ์ด๋ฉ์ผ์ด๋ฉด ๊ฐ์
์ ๊ฑด๋๋ฐ๊ณ (์ด๋
ธํ
์ด์
์ ํ๋ฉด ๋ฌธ๊ตฌ ๊ธฐ๋ก) ๋ก๊ทธ์ธ ์๋๋ง ํ๋ค.
test('IV1_S11 ๋ฏธ์น์ธ ์ ์ ๊ฐ์
ํ ๋ก๊ทธ์ธ (์์์คํจ)', async ({ page }) => {
try {
await signUp(page, PENDING_USER); // ๊ฐ์
ํ /auth/signup ์ ์๋ด ๋ฌธ๊ตฌ๊ฐ ํ์๋จ(๋ก๊ทธ์ธ ํ๋ฉด ์๋)
} catch {
const text = (await page.locator('body').innerText().catch(() => '')).replace(/\s+/g, ' ').slice(0, 200);
test.info().annotations.push({ type: 'preexisting', description: `${PENDING_USER.id}: ์ด๋ฏธ ๊ฐ์
๋จ. ์ฌ๊ฐ์
์ ํ๋ฉด: "${text}"` });
}
await page.goto('/auth/login');
await page.getByPlaceholder('Your ID').fill(PENDING_USER.id);
await page.getByPlaceholder('Your password').fill(pwOf(PENDING_USER.id));
await page.getByRole('button', { name: 'Login' }).click();
await page.waitForTimeout(3000);
const after = (await page.locator('body').innerText().catch(() => '')).replace(/\s+/g, ' ').slice(0, 200);
test.info().annotations.push({ type: 'unapproved-login-result', description: `url=${page.url()} text="${after}"` });
// ๊ธฐ๋: ๋ก๊ทธ์ธ ๊ฑฐ๋ถ ๋๋ ์น์ธ ๋๊ธฐ ์๋ด โ ์ฝ์ ํ๋ฉด์ผ๋ก ๋ค์ด๊ฐ์ง ๋ชปํ๋ค.
await expect(page).not.toHaveURL(/\/webconsole\//);
});tests/iv1/IV1_S12.spec.ts ยท 18์ค ยท ํด์ 5f92dbd9 ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin, toasts } from '../../lib/ui';
import { ROOT_ORG } from '../../lib/data';
import { createOrg } from '../../lib/forms';
import { gotoGroups, treeItem } from '../../lib/groups';
// IV1_S12 : ์กฐ์ง ์ด๋ฆ ์ค๋ณต ์์ฑ (์์ ์คํจ) โ ํ์๊ฐ ๋๋ฝ์ IV1_S29 ๋ก ๋ถ๋ฆฌ(2026-10-05)
// ๊ธฐ๋: 409 ์ค๋ฅ ์๋ฟ, ํธ๋ฆฌ์ mcmp ๊ฐ ํ๋๋ฟ(์ค๋ณต ์์ฑ ์ ๋จ)
test('IV1_S12 ์กฐ์ง ์ด๋ฆ ์ค๋ณต ์์ฑ (์์์คํจ)', async ({ page, dialogs }) => {
await loginAsAdmin(page, 'ws01', null);
await gotoGroups(page);
await createOrg(page, ROOT_ORG);
await expect.poll(() => dialogs.join(' | ')).toMatch(/Failed to create group|409/);
await page.waitForTimeout(1500);
await expect(treeItem(page, ROOT_ORG), `${ROOT_ORG} ๋ ํ๋๋ฟ`).toHaveCount(1);
test.info().annotations.push({ type: 'duplicate-result', description: [...dialogs, ...(await toasts(page))].join(' | ') || '(๋ฉ์์ง ์์)' });
});tests/iv1/IV1_S13.spec.ts ยท 5์ค ยท ํด์ 9e6522a3 ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
๊ณต์ฉ ๋ก์ง: lib/menu-access.ts (2026-10-15 WEB-BUG-126 ๊ธฐ์ค์ผ๋ก ๊ต์ฒด โ ์ด์ lib/roles.ts ์ accessTest ๋ GroupsยทUsersยทRoles 3๊ฐ ํ๋ฉด์ ๋ชฉ๋ก API ์๋ต ์ฝ๋๋ก ํ์ ํ์)
import { menuAccessTest } from '../../lib/menu-access';
// IV1_S13 : TC-IAM-044 operator(workflow01) ๋ฉ๋ด URL ์ง์ ์ ๊ทผ โ ์ฌ์ฉ์ ๋ฉ๋ด์ ์๋ ํ๋ฉด์ ์ด๋ฆฌ๊ณ ์๋ ํ๋ฉด์ /auth/unauthorized (lib/menu-access.ts)
menuAccessTest('IV1_S13');tests/iv1/IV1_S14.spec.ts ยท 5์ค ยท ํด์ a43ff7be ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
๊ณต์ฉ ๋ก์ง: lib/menu-access.ts (2026-10-15 WEB-BUG-126 ๊ธฐ์ค์ผ๋ก ๊ต์ฒด โ ์ด์ lib/roles.ts ์ accessTest ๋ GroupsยทUsersยทRoles 3๊ฐ ํ๋ฉด์ ๋ชฉ๋ก API ์๋ต ์ฝ๋๋ก ํ์ ํ์)
import { menuAccessTest } from '../../lib/menu-access';
// IV1_S14 : TC-IAM-045 viewer(monitor01) ๋ฉ๋ด URL ์ง์ ์ ๊ทผ โ ์ฌ์ฉ์ ๋ฉ๋ด์ ์๋ ํ๋ฉด์ ์ด๋ฆฌ๊ณ ์๋ ํ๋ฉด์ /auth/unauthorized (lib/menu-access.ts)
menuAccessTest('IV1_S14');tests/iv1/IV1_S15.spec.ts ยท 5์ค ยท ํด์ 44a66632 ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
๊ณต์ฉ ๋ก์ง: lib/menu-access.ts (2026-10-15 WEB-BUG-126 ๊ธฐ์ค์ผ๋ก ๊ต์ฒด โ ์ด์ lib/roles.ts ์ accessTest ๋ GroupsยทUsersยทRoles 3๊ฐ ํ๋ฉด์ ๋ชฉ๋ก API ์๋ต ์ฝ๋๋ก ํ์ ํ์)
import { menuAccessTest } from '../../lib/menu-access';
// IV1_S15 : TC-IAM-046 billviewer(cost01) ๋ฉ๋ด URL ์ง์ ์ ๊ทผ โ ์ฌ์ฉ์ ๋ฉ๋ด์ ์๋ ํ๋ฉด์ ์ด๋ฆฌ๊ณ ์๋ ํ๋ฉด์ /auth/unauthorized (lib/menu-access.ts)
menuAccessTest('IV1_S15');tests/iv1/IV1_S16.spec.ts ยท 5์ค ยท ํด์ 6f3a9ef6 ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
๊ณต์ฉ ๋ก์ง: lib/menu-access.ts (2026-10-15 WEB-BUG-126 ๊ธฐ์ค์ผ๋ก ๊ต์ฒด โ ์ด์ lib/roles.ts ์ accessTest ๋ GroupsยทUsersยทRoles 3๊ฐ ํ๋ฉด์ ๋ชฉ๋ก API ์๋ต ์ฝ๋๋ก ํ์ ํ์)
import { menuAccessTest } from '../../lib/menu-access';
// IV1_S16 : TC-IAM-047 billadmin(cost02) ๋ฉ๋ด URL ์ง์ ์ ๊ทผ โ ์ฌ์ฉ์ ๋ฉ๋ด์ ์๋ ํ๋ฉด์ ์ด๋ฆฌ๊ณ ์๋ ํ๋ฉด์ /auth/unauthorized (lib/menu-access.ts)
menuAccessTest('IV1_S16');tests/iv1/IV1_S17.spec.ts ยท 35์ค ยท ํด์ f2d56ae7 ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import fs from 'node:fs';
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { ROOT_ORG, TEAMS } from '../../lib/data';
import { URL_GROUPS, exportGroupsJson, showDownloadOverlay } from '../../lib/forms';
// IV1_S17 : admin Groups ์กฐ์ง๋ Export JSON (์ ํ: IV1_S01). ๋ฐ์ดํฐ๋ฅผ ๋ฐ๊พธ์ง ์์ผ๋ฏ๋ก ๋ฐ๋ณต ์คํ ๊ฐ๋ฅ.
// 2026-10-08: Export JSON ํด๋ฆญ โ ๋ค์ด๋ก๋ ํ์ผ๋ช
ํ์ โ ํ์ผ ๋ด์ฉ(JSON) ํ์๊น์ง ๋
นํ(์ฌ์ฉ์ ์ง์). ๋ค์ด๋ก๋ ํ์์ค์ ํค๋๋ฆฌ์ค์์ ์์์ ์ ๋ณด์ฌ ํ๋ฉด ์ค๋ฒ๋ ์ด๋ก ๋์ฒด.
test('IV1_S17 admin: Groups ์กฐ์ง๋ Export JSON', async ({ page }) => {
await loginAsAdmin(page, 'ws01', null);
await page.goto(URL_GROUPS);
await expect(page.getByRole('tree').getByText(ROOT_ORG, { exact: true })).toBeVisible({ timeout: 60_000 });
const file = test.info().outputPath('groups-export.json');
const name = await exportGroupsJson(page, file);
expect(name).toMatch(/^.+-OrganizationTree-\d{8}\.json$/);
// ๋ด๋ ค๋ฐ์ ํ์ผ๋ช
๊ณผ ๋ด์ฉ์ ํ๋ฉด์ ํ์ํด ๋
นํ์ ๋จ๊ธด๋ค(ํค๋๋ฆฌ์ค ๋ธ๋ผ์ฐ์ ๋ ๋ค์ด๋ก๋ ํ์์ค์ด ์์์ ์์)
await showDownloadOverlay(page, name, fs.readFileSync(file, 'utf-8'));
const data = JSON.parse(fs.readFileSync(file, 'utf-8'));
expect(data.version).toBe('1.0');
const root = (data.groups ?? []).find((g: any) => g.name === ROOT_ORG);
expect(root, `${ROOT_ORG} ๊ฐ export ํ์ผ์ ์์`).toBeTruthy();
const children: any[] = root.children ?? [];
for (const t of TEAMS) expect(children.map((c) => c.name)).toContain(t);
for (const c of children) expect(c.organization_code, `${c.name} ์ฝ๋ ์์`).toBeTruthy();
test.info().annotations.push({
type: 'export-file',
description: `${name} (์ต์์ ${data.groups.length}๊ฐ) ${ROOT_ORG}(${root.organization_code ?? '-'}) > ${children.map((c) => `${c.name}(${c.organization_code})`).join(', ')}`,
});
await test.info().attach('groups-export.json', { path: file, contentType: 'application/json' });
});tests/iv1/IV1_S18.spec.ts ยท 36์ค ยท ํด์ fbab4993 ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { ROOT_ORG, TEAMS } from '../../lib/data';
import { URL_GROUPS, exportGroupsJson, importGroupsJson } from '../../lib/forms';
// IV1_S18 : admin Groups ์กฐ์ง๋ Import JSON (์ ํ: IV1_S01).
// ๋ฐฉ๊ธ Export ํ ํ์ผ์ ๊ทธ๋๋ก Import ํ๋ค โ ๋ชจ๋ ๊ทธ๋ฃน์ด organization_codeยท์ด๋ฆ์ด ๊ฐ์ผ๋ฏ๋ก ์์ฑ/์์ 0๊ฑด(๋ณ๊ฒฝ ์์)์ด์ด์ผ ํ๋ค.
// stage ๋ ๋ฐ์ดํฐ๋ฅผ ๋๋๋ฆฌ๊ธฐ ์ด๋ ค์ ๊ธฐ์กด ์กฐ์ง์ ๋ฐ๊พธ์ง ์๋ ์๋ณต(round-trip) ๋ฐฉ์์ผ๋ก ๊ฒ์ฆํ๋ค. ๋ฐ๋ณต ์คํ ๊ฐ๋ฅ.
test('IV1_S18 admin: Groups ์กฐ์ง๋ Import JSON (Export ํ์ผ ์ฌ์ ์ฉ)', async ({ page, dialogs }) => {
await loginAsAdmin(page, 'ws01', null);
await page.goto(URL_GROUPS);
const tree = page.getByRole('tree');
await expect(tree.getByText(ROOT_ORG, { exact: true })).toBeVisible({ timeout: 60_000 });
const file = test.info().outputPath('groups-export.json');
await exportGroupsJson(page, file);
await importGroupsJson(page, file);
const result = page.locator('#import-result-body');
await expect(result).toContainText('Import complete', { timeout: 60_000 });
const text = (await result.innerText()).replace(/\s+/g, ' ').trim();
test.info().annotations.push({ type: 'import-result', description: text });
expect(text, '๊ฐ์ ํ์ผ์ ๋ค์ ๊ฐ์ ธ์ค๋ฉด ์์ฑ/์์ /์คํจ๊ฐ ์์ด์ผ ํจ').not.toMatch(/created|updated|fail/i);
// ํธ๋ฆฌ๊ฐ ๊ทธ๋๋ก์ธ์ง(์ค๋ณต ์์ฑ ์์) ํ๋ฉด์ Refresh ๋ก ๋ค์ ๋ถ๋ฌ์ ํ์ธ.
// (์ฃผ์๋ก ๋ค์ ์ด๋ํ๋ฉด ํธ๋ฆฌ๊ฐ 60์ด ๋๊ฒ ๋น์ด ์๋ ๊ฒฝ์ฐ๊ฐ ์์์, 2026-10-02)
// ๊ฒฐ๊ณผ ์ฐฝ(#import-result-modal)์ Escape ๋ก ๋ซํ์ง ์์ Refresh ํด๋ฆญ์ ๊ฐ๋ฆฐ๋ค(2026-10-05) โ ์ฐฝ์ ๋ซ๊ธฐ ๋ฒํผ ์ฌ์ฉ
const modal = page.locator('#import-result-modal');
await modal.locator('[data-bs-dismiss="modal"]').or(modal.getByRole('button', { name: /^(Close|OK|Confirm)$/i })).first().click();
await expect(modal).toBeHidden({ timeout: 10_000 });
await page.getByRole('button', { name: 'Refresh' }).click();
await expect(tree.getByText(ROOT_ORG, { exact: true })).toHaveCount(1, { timeout: 60_000 });
for (const t of TEAMS) await expect(tree.getByText(t, { exact: true })).toHaveCount(1);
expect(dialogs.join(' | ')).not.toMatch(/Invalid JSON/);
});tests/iv1/IV1_S19.spec.ts ยท 19์ค ยท ํด์ 3727775a ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { ROOT_ORG } from '../../lib/data';
import { URL_GROUPS, importGroupsJson } from '../../lib/forms';
// IV1_S19 : admin Groups Import JSON ์ ์๋ชป๋(ํ์ฑ ๋ถ๊ฐ) JSON ํ์ผ โ ์๋ฟ "Invalid JSON file.", ์กฐ์ง ๋ณ๊ฒฝ ์์ (์์์คํจ)
test('IV1_S19 admin: Groups Import JSON ์๋ชป๋ ํ์ผ (์์์คํจ)', async ({ page, dialogs }) => {
await loginAsAdmin(page, 'ws01', null);
await page.goto(URL_GROUPS);
const tree = page.getByRole('tree');
await expect(tree.getByText(ROOT_ORG, { exact: true })).toBeVisible({ timeout: 60_000 });
await importGroupsJson(page, { name: 'invalid-groups.json', mimeType: 'application/json', buffer: Buffer.from('{ "version": "1.0", "groups": [ { "name": ') });
await expect.poll(() => dialogs.join(' | ')).toContain('Invalid JSON file');
await page.waitForTimeout(1500);
await expect(page.locator('#import-result-body')).not.toContainText('Import complete');
await expect(tree.getByText(ROOT_ORG, { exact: true })).toHaveCount(1);
});tests/iv1/IV1_S20.spec.ts ยท 25์ค ยท ํด์ 04bf21ce ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { REC_ORG, REC_TEAM, REC_TEAM_EDITED, editGroup, gotoGroups, groupToasts, openGroup, treeItem } from '../../lib/groups';
// IV1_S20 : admin ์กฐ์ง ์์ (Groups > ์กฐ์ง ์ ํ > Edit) [์ ์]
// ๋์: IV1_S01 ์์ ๋ง๋ ๋
นํ์ฉ ํ์ ์กฐ์ง recteam โ ์ด๋ฆ recteam2, ์ค๋ช
๋ณ๊ฒฝ โ ํธ๋ฆฌยท์์ธ(Path/Description) ๋ฐ์ ํ์ธ
// ๋ฉฑ๋ฑ: ์ด๋ฏธ recteam2 ๋ก ๋ฐ๋์ด ์์ผ๋ฉด ์์ ์ ๊ฑด๋๋ฐ๊ณ ์ํ๋ง ํ์ธ.
test('IV1_S20 admin: ์กฐ์ง ์์ (Edit)', async ({ page, dialogs }) => {
await loginAsAdmin(page, 'ws01', null);
await gotoGroups(page);
const desc = 'IV1_S20 ์์ ๋จ';
if (await treeItem(page, REC_TEAM).count()) {
await openGroup(page, REC_TEAM, `/${REC_ORG}/${REC_TEAM}`);
await editGroup(page, REC_TEAM_EDITED, desc);
await expect(treeItem(page, REC_TEAM_EDITED).first(), `ํธ๋ฆฌ์ ${REC_TEAM_EDITED} ํ์`).toBeVisible({ timeout: 20_000 });
await expect(treeItem(page, REC_TEAM), `์ด์ ์ด๋ฆ ${REC_TEAM} ์์`).toHaveCount(0);
} else {
test.skip(!(await treeItem(page, REC_TEAM_EDITED).count()), `์ ํ IV1_S01 ์ ${REC_TEAM} ์์`);
test.info().annotations.push({ type: 'preexisting', description: `${REC_TEAM_EDITED} ์ด๋ฏธ ์์ ๋จ โ ํ์ธ๋ง` });
}
const detail = await openGroup(page, REC_TEAM_EDITED, `/${REC_ORG}/${REC_TEAM_EDITED}`);
expect(detail, '์์ ํ ์ค๋ช
ํ์').toContain(desc);
test.info().annotations.push({ type: 'messages', description: [...dialogs, ...(await groupToasts(page))].join(' | ') || '(์์)' });
});tests/iv1/IV1_S21.spec.ts ยท 30์ค ยท ํด์ f2287741 ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { REC_ORG, REC_TEAM, REC_TEAM_EDITED, deleteGroup, gotoGroups, groupToasts, openGroup, treeItem } from '../../lib/groups';
// IV1_S21 : admin ์กฐ์ง ์ญ์ (Groups > ์กฐ์ง ์ ํ > Delete) โ ํ์ ์กฐ์ง โ ์์ ์กฐ์ง ์์๋ก ์ญ์
// ํ์ ์กฐ์ง์ด ์๋ ์กฐ์ง์ ์ญ์ ๊ฑฐ๋ถ๋ IV1_S30 ์ผ๋ก ๋ถ๋ฆฌ(2026-10-05).
// ๋์: ๋
นํ์ฉ recteam2(IV1_S20 ์์ ํ, ์์ผ๋ฉด recteam) โ recorg. ์ค์ ์กฐ์ง์ ๋์ ์๋.
test('IV1_S21 admin: ์กฐ์ง ์ญ์ (Delete)', async ({ page, dialogs }) => {
await loginAsAdmin(page, 'ws01', null);
await gotoGroups(page);
test.skip(!(await treeItem(page, REC_ORG).count()), `์ ํ IV1_S01 ์ ${REC_ORG} ์์`);
for (const child of [REC_TEAM_EDITED, REC_TEAM]) {
if (!(await treeItem(page, child).count())) continue;
await openGroup(page, child, `/${REC_ORG}/${child}`);
const b = dialogs.length;
await deleteGroup(page);
test.info().annotations.push({ type: `delete:${child}`, description: [...dialogs.slice(b), ...(await groupToasts(page))].join(' | ') || '(๋ฉ์์ง ์์)' });
await gotoGroups(page);
await expect(treeItem(page, child), `${child} ์ญ์ ๋จ`).toHaveCount(0);
}
await openGroup(page, REC_ORG, `/${REC_ORG}`);
const b = dialogs.length;
await deleteGroup(page);
test.info().annotations.push({ type: `delete:${REC_ORG}`, description: [...dialogs.slice(b), ...(await groupToasts(page))].join(' | ') || '(๋ฉ์์ง ์์)' });
await gotoGroups(page);
await expect(treeItem(page, REC_ORG), `${REC_ORG} ์ญ์ ๋จ`).toHaveCount(0);
});tests/iv1/IV1_S22.spec.ts ยท 30์ค ยท ํด์ 1d8286f1 ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { REC_USER_ID, deleteUserByRow, gotoUsersAll, recUserFromId } from '../../lib/groups';
// IV1_S22 : admin ์ฌ์ฉ์ ์ญ์ (Users > ์ฌ์ฉ์ ํ ํด๋ฆญ > โ Delete) [์ ์]
// ๋์: IV1_S02~S04 ๋
นํ์ฉ ์ฌ์ฉ์ rec{MMDD}{a|bโฆ}(์ญ์ ๊ฐ๋ฅ, 2026-10-05 ์ฌ์ฉ์ ํ์ธ). ์ค์ ์ฌ์ฉ์(iam01 ๋ฑ)๋ ๋์ ์๋.
// ์ฌ์ฉ์๋ง๋ค ํ์ ํด๋ฆญํด ์ ํํ ๋ค Delete(์ฒดํฌ๋ฐ์ค ์ ํ์ ์ฌ์ฉํ์ง ์์, 2026-10-05 ์ฌ์ฉ์ ์ง์) โ ๋ชฉ๋ก์์ ์ฌ๋ผ์ง ํ์ธ
test('IV1_S22 admin: ์ฌ์ฉ์ ์ญ์ (๋
นํ์ฉ ์ฌ์ฉ์)', async ({ page, dialogs }) => {
await loginAsAdmin(page, 'ws01', null);
await gotoUsersAll(page);
const emails = await page.getByRole('row').filter({ hasText: /rec\d{4}[a-z]{1,2}@mcmp\.test/ }).allInnerTexts();
const ids = [...new Set(emails.map((t) => t.match(/(rec\d{4}[a-z]{1,2})@mcmp\.test/)?.[1]).filter((x): x is string => !!x && REC_USER_ID.test(x)))].sort();
test.info().annotations.push({ type: 'targets', description: ids.join(', ') || '(์์)' });
test.skip(!ids.length, '์ญ์ ํ ๋
นํ์ฉ ์ฌ์ฉ์ ์์');
const users = ids.map(recUserFromId);
const gone = async (list: typeof users) => {
await gotoUsersAll(page);
for (const u of list) await expect(page.getByRole('row').filter({ hasText: u.email }), `${u.id} ๋ชฉ๋ก์์ ์ญ์ ๋จ`).toHaveCount(0, { timeout: 20_000 });
};
// ์ฌ์ฉ์๋ณ: ํ ํด๋ฆญ โ โ Delete โ Confirm โ ๋ชฉ๋ก์์ ์ฌ๋ผ์ง
for (const u of users) {
const b = dialogs.length;
const m = await deleteUserByRow(page, u);
test.info().annotations.push({ type: `delete:${u.id}`, description: `๋ชจ๋ฌ '${m}' โ ${dialogs.slice(b).join(' | ') || '(์๋ฟ ์์)'}` });
await gone([u]);
}
});tests/iv1/IV1_S23.spec.ts ยท 32์ค ยท ํด์ e20e3862 ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { USERS } from '../../lib/data';
import { selectUserRow, addGroupFromUsers, addBothRoles } from '../../lib/forms';
import { REC_ADMIN_USER, gotoUsersAll, roleAssignedAll, userDetail, userExistsAll } from '../../lib/groups';
// IV1_S23 : admin ์ฌ์ฉ์ ์กฐ์งยท์ญํ ํ ๋น (Users > ์ฌ์ฉ์ ์ ํ > Add Group, Add platform role / Add Workspace role) โ IV1_S02 ์์ ๋ถ๋ฆฌ(2026-10-05)
// ์ ํ: IV1_S02. ๋
นํ์ฉ rec{MMDD}a = ์กฐ์ง monitor / ์ญํ viewer, ์ค์ iam01(iam/admin)ยทinfra01(infra/operator).
// ์ด๋ฏธ ์ญํ ์ด ํ ๋น๋ ์ฌ์ฉ์(Platform Roles=Y)๋ ๊ฑด๋๋ด๋ค(๋ฉฑ๋ฑ).
test('IV1_S23 admin: ์ฌ์ฉ์ ์กฐ์งยท์ญํ ํ ๋น', async ({ page, dialogs }) => {
await loginAsAdmin(page, 'ws01', null);
const r = REC_ADMIN_USER;
test.skip(!(await userExistsAll(page, r)), `์ ํ IV1_S02 ์ ${r.id} ์์`);
for (const u of [r, ...USERS.filter((x) => x.via === 'admin')]) {
if (await roleAssignedAll(page, u)) {
test.info().annotations.push({ type: 'preexisting', description: `${u.id}: ์ญํ ์ด๋ฏธ ํ ๋น๋จ โ ๊ฑด๋๋` });
continue;
}
await gotoUsersAll(page);
await selectUserRow(page, u);
const before = dialogs.length;
await addGroupFromUsers(page, u);
await expect.poll(() => dialogs.slice(before).join('|')).toContain('Group assigned successfully');
await addBothRoles(page, u.role);
test.info().annotations.push({ type: 'assigned', description: `${u.id}: ์กฐ์ง ${u.team}, ์ญํ ${u.role}` });
}
await gotoUsersAll(page);
const d = await userDetail(page, r);
expect(d, `${r.id} ์์ธ์ ์กฐ์ง ${r.team}`).toContain(r.team);
expect(await roleAssignedAll(page, r), `${r.id} Platform Roles=Y`).toBe(true);
});tests/iv1/IV1_S24.spec.ts ยท 27์ค ยท ํด์ 782d7f99 ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin, loginAndSettle, logout } from '../../lib/ui';
import { USERS, pwOf } from '../../lib/data';
import { selectUserRow, resetPassword } from '../../lib/forms';
import { REC_ADMIN_USER, gotoUsersAll, userExistsAll } from '../../lib/groups';
// IV1_S24 : admin ์ฌ์ฉ์ ๋น๋ฐ๋ฒํธ ์ค์ (Users > ์ฌ์ฉ์ ์ ํ > Reset Password) โ IV1_S02 ์์ ๋ถ๋ฆฌ(2026-10-05)
// Add User ํผ์๋ ๋น๋ฐ๋ฒํธ ์
๋ ฅ๋์ด ์์ด Reset Password ๋ก {id}_password ๋ฅผ ์ค์ ํ๋ค. ์ค์ ํ ๋
นํ์ฉ ์ฌ์ฉ์๋ก ๋ก๊ทธ์ธํด ํ์ธ.
// ์ ํ: IV1_S02(์ฌ์ฉ์), IV1_S23(์ญํ โ ์ญํ ์ด ์์ผ๋ฉด ๋ก๊ทธ์ธ ํ ํ๋ฉด์ด ๋น์ด ์์ ์ ์์).
test('IV1_S24 admin: ์ฌ์ฉ์ ๋น๋ฐ๋ฒํธ ์ค์ (Reset Password)', async ({ page, dialogs }) => {
await loginAsAdmin(page, 'ws01', null);
const r = REC_ADMIN_USER;
test.skip(!(await userExistsAll(page, r)), `์ ํ IV1_S02 ์ ${r.id} ์์`);
for (const u of [r, ...USERS.filter((x) => x.via === 'admin')]) {
await gotoUsersAll(page);
await selectUserRow(page, u);
const before = dialogs.length;
await resetPassword(page, u);
await expect.poll(() => dialogs.slice(before).join('|')).toContain('Password reset successfully');
}
// ์ค์ ํ ๋น๋ฐ๋ฒํธ๋ก ๋ก๊ทธ์ธ๋๋์ง ํ์ธ
await logout(page);
await loginAndSettle(page, r.id, pwOf(r.id));
await expect(page, `${r.id} ๋ก๊ทธ์ธ ์ฑ๊ณต`).not.toHaveURL(/\/auth\//);
test.info().annotations.push({ type: 'login', description: `${r.id} ๋ก๊ทธ์ธ ์ฑ๊ณต โ ${new URL(page.url()).pathname}` });
});tests/iv1/IV1_S25.spec.ts ยท 21์ค ยท ํด์ 411ddd2d ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { REC_ORG, REC_TEAM, createGroup, gotoGroups, openGroup, treeItem } from '../../lib/groups';
// IV1_S25 : admin ํ์ ์กฐ์ง ์์ฑ (Groups > Create Org, Parent=recorg) โ IV1_S01 ์์ ๋ถ๋ฆฌ(2026-10-05)
// ์ ํ: IV1_S01(recorg). recteam ์ IV1_S20(์์ )ยทIV1_S21(์ญ์ )์์ ์ด์ด์ ์ฌ์ฉ. ์ด๋ฏธ ์์ผ๋ฉด ์์ฑ์ ๊ฑด๋๋ฐ๊ณ ์์ธ๋ง ํ์ธ.
test('IV1_S25 admin: ํ์ ์กฐ์ง ์์ฑ (recteam)', async ({ page, dialogs }) => {
await loginAsAdmin(page, 'ws01', null);
await gotoGroups(page);
test.skip(!(await treeItem(page, REC_ORG).count()), `์ ํ IV1_S01 ์ ${REC_ORG} ์์`);
if (await treeItem(page, REC_TEAM).count()) {
test.info().annotations.push({ type: 'preexisting', description: `${REC_TEAM} ์ด๋ฏธ ์กด์ฌ โ ์์ฑ ๊ฑด๋๋` });
} else {
await createGroup(page, REC_TEAM, 'IV1 ๋
นํ์ฉ ํ์ ์กฐ์ง', REC_ORG);
await expect.poll(() => dialogs.join('|')).toContain('Group created successfully');
}
const detail = await openGroup(page, REC_TEAM, `/${REC_ORG}/${REC_TEAM}`);
expect(detail, 'ํ์ ์กฐ์ง Level 2').toMatch(/Level 2/);
expect(detail, '์ค๋ช
ํ์').toContain('IV1 ๋
นํ์ฉ ํ์ ์กฐ์ง');
});tests/iv1/IV1_S26.spec.ts ยท 26์ค ยท ํด์ 7766748c ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { ROOT_ORG, TEAMS } from '../../lib/data';
import { createOrg } from '../../lib/forms';
import { gotoGroups, openGroup, treeItem } from '../../lib/groups';
// IV1_S26 : admin ์ค์ ์กฐ์ง ๊ตฌ์ฑ ํ์ธ (mcmp + ํ์ 6๊ฐ ํ) โ IV1_S01 ์์ ๋ถ๋ฆฌ(2026-10-05)
// ๋ค๋ฅธ ์๋๋ฆฌ์ค(์ฌ์ฉ์ ์กฐ์ง ํ ๋น ๋ฑ)์ ์ ์ . ์์ ๋๋ง ์์ฑํ๊ณ (๋ฉฑ๋ฑ) ๊ฐ ์กฐ์ง ์์ธ Path /mcmp/{team} ์ ํ์ธํ๋ค.
test('IV1_S26 admin: ์ค์ ์กฐ์ง ๊ตฌ์ฑ ํ์ธ (mcmp + 6๊ฐ ํ)', async ({ page, dialogs }) => {
await loginAsAdmin(page, 'ws01', null);
await gotoGroups(page);
if (!(await treeItem(page, ROOT_ORG).count())) {
await createOrg(page, ROOT_ORG);
await expect.poll(() => dialogs.join('|')).toContain('Group created successfully');
test.info().annotations.push({ type: 'created', description: ROOT_ORG });
}
for (const team of TEAMS) {
if (await treeItem(page, team).count()) continue;
await createOrg(page, team, ROOT_ORG);
await expect(treeItem(page, team).first()).toBeVisible({ timeout: 20_000 });
test.info().annotations.push({ type: 'created', description: team });
}
await openGroup(page, ROOT_ORG, `/${ROOT_ORG}`);
for (const team of TEAMS) await openGroup(page, team, `/${ROOT_ORG}/${team}`);
});tests/iv1/IV1_S27.spec.ts ยท 33์ค ยท ํด์ f1f360e2 ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { USERS } from '../../lib/data';
import { selectUserRow, addGroupFromUsers } from '../../lib/forms';
import { REC_SIGNUP_USER, gotoUsersAll, inviteToGroup, roleAssignedAll, userDetail, userExistsAll } from '../../lib/groups';
// IV1_S27 : admin ๊ฐ์
์ฌ์ฉ์ ์กฐ์ง ํ ๋น โ IV1_S04 ์์ ๋ถ๋ฆฌ(2026-10-05)
// ํ ๋น ๋ฐฉ๋ฒ 2์ข
: ๋
นํ์ฉ rec{MMDD}bยทworkflow01ยทapp01 = Groups > ์กฐ์ง ์ ํ > Invite Member, ๋๋จธ์ง(monitor01ยทcost01ยทcost02) = Users > Add Group.
// ์ ํ: IV1_S04(์น์ธ). Invite Member ๋ ์ด๋ฏธ ๋ฉค๋ฒ๋ฉด ์ฒดํฌ๋ฐ์ค๊ฐ ๋นํ์ฑ โ ๊ฑด๋๋. ์ค์ ์ฌ์ฉ์๋ ์ญํ ๊น์ง ๋๋(Platform Roles=Y) ๊ฒฝ์ฐ ๊ฑด๋๋.
test('IV1_S27 admin: ๊ฐ์
์ฌ์ฉ์ ์กฐ์ง ํ ๋น (Invite Member / Add Group)', async ({ page, dialogs }) => {
await loginAsAdmin(page, 'ws01', null);
const r = REC_SIGNUP_USER;
test.skip(!(await userExistsAll(page, r)), `์ ํ IV1_S03ยทS04 ์ ${r.id} ์์(๋ฏธ์น์ธ ์ฌ์ฉ์๋ Users ๋ชฉ๋ก์ ์์)`);
for (const u of [r, ...USERS.filter((x) => x.via === 'signup')]) {
if (u !== r && (await roleAssignedAll(page, u))) {
test.info().annotations.push({ type: 'preexisting', description: `${u.id}: ์กฐ์งยท์ญํ ์ด๋ฏธ ํ ๋น๋จ โ ๊ฑด๋๋` });
continue;
}
if (u === r || u.team === 'workflow' || u.team === 'app') {
await inviteToGroup(page, u);
} else {
await gotoUsersAll(page);
await selectUserRow(page, u);
const before = dialogs.length;
await addGroupFromUsers(page, u);
await expect.poll(() => dialogs.slice(before).join('|')).toContain('Group assigned successfully');
}
test.info().annotations.push({ type: 'assigned', description: `${u.id} โ ${u.team}` });
}
await gotoUsersAll(page);
expect(await userDetail(page, r), `${r.id} ์์ธ์ ์กฐ์ง ${r.team}`).toContain(r.team);
});tests/iv1/IV1_S28.spec.ts ยท 28์ค ยท ํด์ b09ce7c5 ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { USERS } from '../../lib/data';
import { selectUserRow, addBothRoles } from '../../lib/forms';
import { REC_SIGNUP_USER, gotoUsersAll, roleAssignedAll, userDetail, userExistsAll } from '../../lib/groups';
// IV1_S28 : admin ๊ฐ์
์ฌ์ฉ์ ์ญํ ๋ถ์ฌ (Users > ์ฌ์ฉ์ ์ ํ > Add platform role / Add Workspace role) โ IV1_S04 ์์ ๋ถ๋ฆฌ(2026-10-05)
// ์ ํ: IV1_S27(์กฐ์ง ํ ๋น). ๋
นํ์ฉ rec{MMDD}b = viewer, ์ค์ workflow01ยทapp01=operator, monitor01=viewer, cost01=billviewer, cost02=billadmin.
// Platform Roles=Y ์ธ ์ฌ์ฉ์๋ ๊ฑด๋๋ด๋ค(๋ฉฑ๋ฑ).
test('IV1_S28 admin: ๊ฐ์
์ฌ์ฉ์ ์ญํ ๋ถ์ฌ', async ({ page }) => {
await loginAsAdmin(page, 'ws01', null);
const r = REC_SIGNUP_USER;
test.skip(!(await userExistsAll(page, r)), `์ ํ IV1_S03ยทS04 ์ ${r.id} ์์`);
for (const u of [r, ...USERS.filter((x) => x.via === 'signup')]) {
if (await roleAssignedAll(page, u)) {
test.info().annotations.push({ type: 'preexisting', description: `${u.id}: ์ญํ ์ด๋ฏธ ํ ๋น๋จ โ ๊ฑด๋๋` });
continue;
}
await gotoUsersAll(page);
await selectUserRow(page, u);
await addBothRoles(page, u.role);
test.info().annotations.push({ type: 'assigned', description: `${u.id} โ ${u.role} (platformยทworkspace)` });
}
expect(await roleAssignedAll(page, r), `${r.id} Platform Roles=Y`).toBe(true);
await gotoUsersAll(page);
test.info().annotations.push({ type: 'detail', description: (await userDetail(page, r)).slice(0, 300) });
});tests/iv1/IV1_S29.spec.ts ยท 19์ค ยท ํด์ 1f0b6d13 ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { gotoGroups } from '../../lib/groups';
// IV1_S29 : ์กฐ์ง ์์ฑ ํ์๊ฐ(Name) ๋๋ฝ (์์ ์คํจ) โ IV1_S12 ์์ ๋ถ๋ฆฌ(2026-10-05)
// ๊ธฐ๋: Name ์ ๋น์ฐ๊ณ Save โ ์๋ฟ 'Name is required.', ํ์
์ด ๋ซํ์ง ์๊ณ ์์ฑ๋์ง ์์
test('IV1_S29 ์กฐ์ง ์์ฑ ํ์๊ฐ ๋๋ฝ (์์์คํจ)', async ({ page, dialogs }) => {
await loginAsAdmin(page, 'ws01', null);
await gotoGroups(page);
await page.getByRole('button', { name: 'Create Org' }).click();
const dlg = page.getByRole('dialog');
await expect(dlg.locator('#group-name')).toHaveValue('');
await dlg.getByRole('button', { name: 'Save' }).click();
await expect.poll(() => dialogs.join(' | ')).toContain('Name is required');
await expect(dlg, 'ํ์
์ด ๋ซํ์ง ์์').toBeVisible();
test.info().annotations.push({ type: 'required-result', description: dialogs.join(' | ') });
await dlg.getByRole('button', { name: 'Cancel' }).click();
});tests/iv1/IV1_S30.spec.ts ยท 23์ค ยท ํด์ 4eb9c563 ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { REC_ORG, REC_TEAM, REC_TEAM_EDITED, deleteGroup, gotoGroups, groupToasts, openGroup, treeItem } from '../../lib/groups';
// IV1_S30 : ํ์ ์กฐ์ง์ด ์๋ ์กฐ์ง ์ญ์ (์์ ์คํจ) โ IV1_S21 ์์ ๋ถ๋ฆฌ(2026-10-05), IV1_S21(์ญ์ ) ์ ์ ์คํ
// ๊ธฐ๋: ํ์ธ ํ์
์๋ด("Groups with child organizations or assigned users cannot be deleted.") ํ Delete ํด๋ ์ญ์ ๋์ง ์๊ณ ํธ๋ฆฌ์ ๋จ์
// ์ ํ: IV1_S01(recorg) + IV1_S25(recteam, IV1_S20 ์ดํ recteam2)
test('IV1_S30 ํ์ ์กฐ์ง์ด ์๋ ์กฐ์ง ์ญ์ (์์์คํจ)', async ({ page, dialogs }) => {
await loginAsAdmin(page, 'ws01', null);
await gotoGroups(page);
const child = (await treeItem(page, REC_TEAM_EDITED).count()) ? REC_TEAM_EDITED : REC_TEAM;
test.skip(!(await treeItem(page, REC_ORG).count()) || !(await treeItem(page, child).count()), `์ ํ ${REC_ORG}/${child} ์์(IV1_S01, IV1_S25)`);
await openGroup(page, REC_ORG, `/${REC_ORG}`);
const before = dialogs.length;
const confirmText = await deleteGroup(page);
const result = [...dialogs.slice(before), ...(await groupToasts(page))].join(' | ') || '(๋ฉ์์ง ์์)';
test.info().annotations.push({ type: 'delete-parent', description: `ํ์
: ${confirmText} / ๊ฒฐ๊ณผ: ${result}` });
await gotoGroups(page);
await expect(treeItem(page, REC_ORG).first(), 'ํ์ ์กฐ์ง์ด ์์ผ๋ฉด ์์ ์กฐ์ง์ ์ญ์ ๋์ง ์์').toBeVisible();
await expect(treeItem(page, child).first()).toBeVisible();
});tests/iv1/IV1_S31.spec.ts ยท 18์ค ยท ํด์ f3191ed5 ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '../../lib/fixtures';
import { loginAndSettle, menuSnapshot } from '../../lib/ui';
import { pwOf } from '../../lib/data';
import { REC_SIGNUP_USER } from '../../lib/groups';
// IV1_S31 : ์น์ธ๋ ์ฌ์ฉ์ ๋ก๊ทธ์ธ โ ๊ฐ์
(IV1_S03) โ ๊ด๋ฆฌ์ ์น์ธ(IV1_S04) โ ๋ก๊ทธ์ธ ํ๋ฆ์ ๋ง์ง๋ง ๋จ๊ณ(2026-10-05 ์ฌ์ฉ์ ์์ฒญ)
// ๋
นํ์ฉ rec{MMDD}b ๊ฐ ๊ฐ์
๋ ์
๋ ฅํ ๋น๋ฐ๋ฒํธ({id}_password)๋ก ๋ก๊ทธ์ธํ๋ค. ์ด ์์ ์๋ ์กฐ์งยท์ญํ ์ด ์์ง ์๋ค(IV1_S27ยทS28 ์์ ํ ๋น).
// ๊ธฐ๋: ๋ก๊ทธ์ธ ์ฑ๊ณต(์ฝ์ ํ๋ฉด ์ง์
). ์ญํ ์ด ์์ ๋ ๋ณด์ด๋ ๋ฉ๋ดยทํ๋ฉด์ ์ด๋
ธํ
์ด์
์ผ๋ก ๊ธฐ๋ก.
test('IV1_S31 ์น์ธ๋ ์ฌ์ฉ์ ๋ก๊ทธ์ธ', async ({ page, dialogs }) => {
const u = REC_SIGNUP_USER;
await loginAndSettle(page, u.id, pwOf(u.id));
await expect(page, `${u.id} ๋ก๊ทธ์ธ ์ฑ๊ณต(๋ก๊ทธ์ธ ํ๋ฉด์ ๋ฒ์ด๋จ)`).not.toHaveURL(/\/auth\//);
await page.waitForTimeout(2000);
const menus = await menuSnapshot(page);
test.info().annotations.push({ type: 'login', description: `${u.id} โ ${new URL(page.url()).pathname}, ๋ฉ๋ด ${menus.length}๊ฐ${menus.length ? `: ${menus.map((m) => m.split(' -> ')[0]).join(', ')}` : ''}` });
test.info().annotations.push({ type: 'messages', description: dialogs.join(' | ') || '(์๋ฟ ์์)' });
});tests/iv1/IV1_S32.spec.ts ยท 28์ค ยท ํด์ fa00655a ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { gotoCredentials, holderRow, rowCells, consoleApi } from '../../lib/credentials';
// IV1_S32 : TC-INFRA-001 Credential ๋ชฉ๋ก ์กฐํ (Settings > Cloud SPs > Credentials)
// ์ถ์ฒ: ์ํค Test-Specs-IV10 ์ IV10_S01(๋ค๋ฅธ ์ธ์
์์ฑ, 2026-10-14). ๊ฐ์ ๋ IV1(ํ๋ซํผ ์จ๋ณด๋ฉ)๋ก ํธ์
ํ๋ฉฐ ๋ฒํธ๋ง ๋ฐ๊ฟ
test('IV1_S32 TC-INFRA-001 Credential ๋ชฉ๋ก ์กฐํ', async ({ page }) => {
await test.step('1. admin ๋ก๊ทธ์ธ', () => loginAsAdmin(page));
await test.step('2. Settings > Cloud SPs > Credentials ์ด๋ โ ๋ชฉ๋ก ํ์', () => gotoCredentials(page));
await test.step('3. ๊ธฐ๋ณธ holder(admin) ํ์ ProvidersยทConnectionsยทVerifiedยทDefault ํ์ ํ์ธ', async () => {
const row = holderRow(page, 'admin');
await expect(row).toHaveCount(1);
const cells = await rowCells(row);
test.info().annotations.push({ type: 'admin-row', description: JSON.stringify(cells) });
expect(Object.values(cells).join(' ')).toMatch(/\d+/);
});
await test.step('4. ํ๋ฉด ๋ชฉ๋ก = GetCredentialHolderList ์๋ต (holder ์ด๋ฆยท์)', async () => {
const api = await consoleApi(page, 'GetCredentialHolderList');
const list: any[] = api.data?.responseData?.credentialHolderList ?? [];
const shown = await page.locator('#credential-table [tabulator-field="credentialHolder"]').filter({ hasNotText: 'Credential Holder' }).allInnerTexts();
test.info().annotations.push({ type: 'holders', description: `API ${list.map((h) => `${h.credentialHolder}(${h.connectionCount}/${h.verifiedConnectionCount})`).join(', ')} / ํ๋ฉด ${shown.map((s) => s.trim()).join(', ')}` });
expect(shown.map((s) => s.trim()).sort()).toEqual(list.map((h) => h.credentialHolder).sort());
});
});tests/iv1/IV1_S33.spec.ts ยท 34์ค ยท ํด์ ae180d2e ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { gotoCredentials, holderRow, consoleApi } from '../../lib/credentials';
// IV1_S33 : TC-INFRA-002 Credential ๋จ๊ฑด ์กฐํ โ ๋ชฉ๋ก์์ holder ํด๋ฆญ โ ์์ธ ํจ๋, GetCredentialHolder ์๋ต๊ณผ ๋น๊ต
// ์ถ์ฒ: ์ํค Test-Specs-IV10 ์ IV10_S02(๋ค๋ฅธ ์ธ์
์์ฑ, 2026-10-14). ๊ฐ์ ๋ IV1(ํ๋ซํผ ์จ๋ณด๋ฉ)๋ก ํธ์
ํ๋ฉฐ ๋ฒํธ๋ง ๋ฐ๊ฟ
const HOLDER = 'admin';
test('IV1_S33 TC-INFRA-002 Credential ๋จ๊ฑด ์กฐํ', async ({ page }) => {
await test.step('1. admin ๋ก๊ทธ์ธ ํ Credentials ์ด๋', async () => { await loginAsAdmin(page); await gotoCredentials(page); });
await test.step(`2. ๋ชฉ๋ก์์ ${HOLDER} ํ ํด๋ฆญ โ ์์ธ ํจ๋ ํ์`, async () => {
await holderRow(page, HOLDER).click();
await expect(page.locator('#credential-detail-panel')).toBeVisible();
await expect(page.locator('#detail-holder-id')).toHaveText(HOLDER);
});
await test.step('3. ์์ธ(ProvidersยทConnectionsยทVerifiedยทDefault) = GetCredentialHolder ์๋ต', async () => {
const ui = {
providers: (await page.locator('#detail-holder-provider').innerText()).replace(/\s+/g, ' ').trim(),
connections: (await page.locator('#detail-holder-connections').innerText()).trim(),
verified: (await page.locator('#detail-holder-verified').innerText()).trim(),
isDefault: (await page.locator('#detail-holder-default').innerText()).trim(),
};
const api = await consoleApi(page, 'GetCredentialHolder', { pathParams: { holderId: HOLDER } });
const h = api.data?.responseData ?? {};
test.info().annotations.push({ type: 'detail', description: `ํ๋ฉด ${JSON.stringify(ui)} / API(${api.status}) ${JSON.stringify(h).slice(0, 300)}` });
expect(api.status, 'GetCredentialHolder ํธ์ถ ์คํจ').toBe(200);
expect(h.credentialHolder).toBe(HOLDER);
expect(Number(ui.connections)).toBe(h.connectionCount);
expect(Number(ui.verified)).toBe(h.verifiedConnectionCount);
for (const p of h.providers ?? []) expect(ui.providers.toLowerCase(), `Providers ์ ${p}`).toContain(String(p).toLowerCase());
});
});tests/iv1/IV1_S34.spec.ts ยท 38์ค ยท ํด์ 36df68db ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { gotoCredentials, consoleApi, credValues, getHolder, scrub, TEST_HOLDER } from '../../lib/credentials';
// IV1_S34 : TC-INFRA-003 Credential ๋ฑ๋ก โ API (์น ์ฝ์ API POST /api/mc-infra-manager/RegisterCredential, ํ๋ฌธ โ API ์๋ฒ๊ฐ ์ํธํ)
// ์ถ์ฒ: ์ํค Test-Specs-IV10 ์ IV10_S03(๋ค๋ฅธ ์ธ์
์์ฑ, 2026-10-14). ๊ฐ์ ๋ IV1(ํ๋ซํผ ์จ๋ณด๋ฉ)๋ก ํธ์
ํ๋ฉฐ ๋ฒํธ๋ง ๋ฐ๊ฟ
// mc-infra-manager ๊ฐ ํ์ฉํ๋ CSP ํ์ ํค(metainfo CredentialCSP: aws_access_key_id / aws_secret_access_key)๋ก holder 'test' ์ AWS ๋ฅผ ๋ฑ๋กํ๋ค.
// ์ฃผ์: mc-infra-manager ์ credential ์ญ์ API ๊ฐ ์์ด ๋ฑ๋ก ๊ฒฐ๊ณผ๋ ๋จ๋๋ค(2026-10-14 ์ฌ์ฉ์ ์น์ธ).
const CSP = 'aws';
test('IV1_S34 TC-INFRA-003 Credential ๋ฑ๋ก (API)', async ({ page }) => {
const v = credValues(CSP);
const kv = ['aws_access_key_id', 'aws_secret_access_key'].map((key) => ({ key, value: v[key] }));
expect(kv.every((x) => x.value), 'aws ์๊ฒฉ์ฆ๋ช
๊ฐ ์์').toBe(true);
await test.step('1. admin ๋ก๊ทธ์ธ ํ Credentials ์ด๋', async () => { await loginAsAdmin(page); await gotoCredentials(page); });
await test.step(`2. RegisterCredential API ํธ์ถ โ holder ${TEST_HOLDER}, provider ${CSP}, CSP ํ์ ํค`, async () => {
const r = await consoleApi(page, 'RegisterCredential', { request: { credentialHolder: TEST_HOLDER, providerName: CSP, credentialKeyValueList: kv } });
const text = scrub(JSON.stringify(r.data ?? {}), v);
test.info().annotations.push({ type: 'register', description: `HTTP ${r.status}: ${text.slice(0, 300)}` });
expect(r.status, text.slice(0, 200)).toBe(200);
});
await test.step(`3. GetCredentialHolder(${TEST_HOLDER}) โ ${CSP} ํฌํจ, ์ฐ๊ฒฐยทverified ์์ฑ`, async () => {
const h = await getHolder(page, TEST_HOLDER);
test.info().annotations.push({ type: 'holder', description: JSON.stringify(h.info) });
expect(h.status).toBe(200);
expect(h.info.providers ?? []).toContain(CSP);
expect(h.info.connectionCount ?? 0).toBeGreaterThan(0);
expect(h.info.verifiedConnectionCount ?? 0).toBeGreaterThan(0);
});
await test.step(`4. ๋ชฉ๋ก ์๋ก๊ณ ์นจ โ ${TEST_HOLDER} ํ ํ์`, async () => {
await gotoCredentials(page);
await expect(page.locator('#credential-table [tabulator-field="credentialHolder"]', { hasText: TEST_HOLDER })).toBeVisible();
});
});tests/iv1/IV1_S35.spec.ts ยท 6์ค ยท ํด์ c20df840 ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { activeCsps, tid } from '../../lib/data';
import { uiRegisterTest, PROVIDER } from '../../lib/credentials';
// IV1_S35 : TC-INFRA-004 Credential ๋ฑ๋ก โ AWS UI (ํ๋ฉด ์๋ Key โ WEB-TECH-108 ์์ ํ aws_access_key_id, aws_secret_access_key)
// ์ถ์ฒ: ์ํค Test-Specs-IV10 ์ IV10_S04(๋ค๋ฅธ ์ธ์
์์ฑ, 2026-10-14). ๊ฐ์ ๋ IV1(ํ๋ซํผ ์จ๋ณด๋ฉ)๋ก ํธ์
ํ๋ฉฐ ๋ฒํธ๋ง ๋ฐ๊ฟ
for (const c of activeCsps(['AWS'])) uiRegisterTest(tid('IV1_S35', c), 'TC-INFRA-004', PROVIDER[c.code]);tests/iv1/IV1_S36.spec.ts ยท 7์ค ยท ํด์ fbed94ad ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { activeCsps, tid } from '../../lib/data';
import { uiRegisterTest, PROVIDER } from '../../lib/credentials';
// IV1_S36 : TC-INFRA-005 Credential ๋ฑ๋ก โ CSP๋ณ UI (Provider ๋ง๋ค ์๋ ์์ฑ Key ๊ฐ ๋ค๋ฆ). ํ
์คํธ 1๊ฑด = CSP 1๊ฐ
// ์ถ์ฒ: ์ํค Test-Specs-IV10 ์ IV10_S05(๋ค๋ฅธ ์ธ์
์์ฑ, 2026-10-14). ๊ฐ์ ๋ IV1(ํ๋ซํผ ์จ๋ณด๋ฉ)๋ก ํธ์
ํ๋ฉฐ ๋ฒํธ๋ง ๋ฐ๊ฟ
// AWS ๋ IV1_S35. openstack ์ stage ์ ์ฐ๊ฒฐ ์ ๋ณด๊ฐ ์์ด ์ ์ธ. ๋์ CSP ๋ TC_VARIANT ๋ก ๊ณ ๋ฅธ๋ค.
for (const c of activeCsps(['Azure', 'Alibaba', 'GCP', 'Tencent', 'NHN Cloud', 'NCP', 'IBM', 'KT'])) uiRegisterTest(tid('IV1_S36', c), 'TC-INFRA-005', PROVIDER[c.code]);tests/iv1/IV1_S37.spec.ts ยท 26์ค ยท ํด์ f486d866 ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { gotoCredentials, holderRow, TEST_HOLDER } from '../../lib/credentials';
// IV1_S37 : TC-INFRA-006 Credential ์ญ์
// ์ถ์ฒ: ์ํค Test-Specs-IV10 ์ IV10_S06(๋ค๋ฅธ ์ธ์
์์ฑ, 2026-10-14). ๊ฐ์ ๋ IV1(ํ๋ซํผ ์จ๋ณด๋ฉ)๋ก ํธ์
ํ๋ฉฐ ๋ฒํธ๋ง ๋ฐ๊ฟ
// 2026-10-14 ํ์ธ: mc-infra-manager(edge) Swagger ์ credentialยทcredentialHolderยทconnConfig ์ญ์ API ๊ฐ ์๊ณ , Credentials ํ๋ฉด์๋ ์ญ์ ๊ธฐ๋ฅ์ด ์๋ค.
// ํ๋ฉด์์ ์ญ์ ์๋จ์ ์ฐพ๊ณ , ์์ผ๋ฉด ๊ธฐ๋ฅ ๋ฏธ์ ๊ณต์ผ๋ก SKIP ํ๋ค(์ญ์ ๊ธฐ๋ฅ์ด ์๊ธฐ๋ฉด ์ด spec ์ ์ญ์ ์ ์ฐจ๋ก ๋ฐ๊พผ๋ค).
test('IV1_S37 TC-INFRA-006 Credential ์ญ์ ', async ({ page }) => {
await test.step('1. admin ๋ก๊ทธ์ธ ํ Credentials ์ด๋', async () => { await loginAsAdmin(page); await gotoCredentials(page); });
const found = await test.step(`2. ${TEST_HOLDER} ํ ์ ํ โ ์์ธยท๋ชฉ๋กยทActions ์์ Delete ๊ธฐ๋ฅ ์ฐพ๊ธฐ`, async () => {
const row = holderRow(page, TEST_HOLDER);
if (await row.count()) await row.click();
const del = page.locator('#credential-detail-panel, .page-header, #page-header-btn-list, .card').getByRole('button', { name: /delete|remove/i });
const n = await del.count();
await page.screenshot({ path: test.info().outputPath('credentials-no-delete.png'), fullPage: true });
await test.info().attach('credentials-no-delete.png', { path: test.info().outputPath('credentials-no-delete.png'), contentType: 'image/png' });
test.info().annotations.push({ type: 'delete-ui', description: `Delete ๋ฒํผ ${n}๊ฐ` });
return n > 0;
});
test.skip(!found, 'Credential ์ญ์ ๊ธฐ๋ฅ ๋ฏธ์ ๊ณต โ ํ๋ฉด์ ์ญ์ ๋ฒํผ ์์, mc-infra-manager ์ ์ญ์ API ์์');
expect(found).toBe(true);
});