Test Specs IV10 - MZC-CSC/mc-web-console GitHub Wiki
์์ฑ: 2026-10-14 ยท ๊ธฐ์ค: v07 โ Test Scenarios ยท IV ํ๋ธ ยท ๊ธฐ๋ฅ TC ยท TC-INFRA
IV ํ๋ก์ ํธ(์ด๋ฏธ ์ค์น๋ Playwright)์ tests/iv10/ ์ spec ๊ณผ _register.ts, lib/ ์ iv10.ts ๋ฅผ ์ถ๊ฐํด ์คํํ๋ค. ๊ณต์ฉ ํฌํผ๋ Test-Specs-Common.
BASE_URL=<์น ์ฝ์ ์ฃผ์> npx playwright test tests/iv10/IV10_S01.spec.ts tests/iv10/IV10_S02.spec.ts # ์กฐํ
BASE_URL=<์น ์ฝ์ ์ฃผ์> CRED_VALUES_FILE=<๊ฐ JSON> npx playwright test tests/iv10/IV10_S03.spec.ts # ๋ฑ๋ก(API)
BASE_URL=<์น ์ฝ์ ์ฃผ์> CRED_VALUES_FILE=<๊ฐ JSON> IV10_CSPS=gcp,azure npx playwright test tests/iv10/IV10_S05.spec.ts| ํ๊ฒฝ๋ณ์ | ์ค๋ช |
|---|---|
BASE_URL |
์น ์ฝ์ ์ฃผ์ |
CRED_HOLDER |
๋ฑ๋กํ holder (๊ธฐ๋ณธ test) |
CRED_VALUES_FILE |
์๊ฒฉ์ฆ๋ช
๊ฐ JSON {csp: {key: value}} โ ํ๋ฉด Key(cb-spider ํ์)์ CSP ํ์ Key ๋ฅผ ๋ชจ๋ ๋ด๋๋ค. ๊ถํ 600, ์คํ ํ ์ญ์ |
IV10_CSPS |
S05 ๋์ CSP (๊ธฐ๋ณธ gcp,azure,alibaba,ibm,ncp,nhn,kt,tencent) |
| ํ์ผ | ์ค | ํด์ |
|---|---|---|
lib/iv10.ts |
79 | 70b1e63c |
tests/iv10/_register.ts |
34 | d807c126 |
tests/iv10/IV10_S01.spec.ts |
26 | 53b94608 |
tests/iv10/IV10_S02.spec.ts |
32 | eb633cae |
tests/iv10/IV10_S03.spec.ts |
36 | c458f9f3 |
tests/iv10/IV10_S04.spec.ts |
3 | 708d1dff |
tests/iv10/IV10_S05.spec.ts |
5 | af3e7a15 |
tests/iv10/IV10_S06.spec.ts |
24 | 139b3ee6 |
lib/iv10.ts ยท 79์ค ยท ํด์ 70b1e63c
import { expect, type Page, type Locator } from '@playwright/test';
// IV10 Credential(Settings > Cloud SPs > Credentials) ํ๋ฉด ํฌํผ โ 2026-10-14 ์์ค(credentials.html/js) ๊ธฐ์ค
// - ๋ชฉ๋ก: Tabulator #credential-table (Credential Holder, Providers, Connections, Verified, Default)
// - ํ ํด๋ฆญ โ #credential-detail-panel (#detail-holder-id/provider/connections/verified/default)
// - Register: #credential-create-modal โ #create-holder-name, #create-holder-provider(์ ํ ์ KV ํ ์๋ ์์ฑ), #kv-list-container(.kv-key/.kv-value), Register ๋ฒํผ
export const CREDENTIALS_URL = '/webconsole/settings/environment/cloudsps/credentials';
export const TEST_HOLDER = process.env.CRED_HOLDER ?? 'test';
export async function gotoCredentials(page: Page) {
await page.goto(CREDENTIALS_URL);
await expect(page.locator('#credential-table')).toBeVisible({ timeout: 30_000 });
await expect(page.locator('#credential-table .tabulator-row').first()).toBeVisible({ timeout: 30_000 });
}
export const holderRow = (page: Page, holder: string): Locator =>
page.locator('#credential-table .tabulator-row').filter({ has: page.locator('[tabulator-field="credentialHolder"]', { hasText: new RegExp(`^\\s*${holder}\\s*$`) }) });
export async function rowCells(row: Locator): Promise<Record<string, string>> {
const cells = row.locator('.tabulator-cell');
const out: Record<string, string> = {};
for (let i = 0; i < await cells.count(); i++) {
const c = cells.nth(i);
out[(await c.getAttribute('tabulator-field')) ?? `col${i}`] = (await c.innerText()).replace(/\s+/g, ' ').trim();
}
return out;
}
// ์น ์ฝ์ API(๊ณต์ฉ proxy)๋ก mc-infra-manager ํธ์ถ โ ํ๋ฉด ๊ฐ๊ณผ ๋น๊ตํ ๋๋ง ์ฌ์ฉ
export async function consoleApi(page: Page, operationId: string, data: unknown = {}) {
return page.evaluate(async ([op, body]) => {
const r = await (window as any).webconsolejs['common/api/http'].commonAPIPost(`/api/mc-infra-manager/${op}`, body);
return { status: r?.status, data: r?.data };
}, [operationId, data] as const);
}
// โโ ๋ฑ๋ก โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// ์๊ฒฉ์ฆ๋ช
๊ฐ ํ์ผ(CRED_VALUES_FILE, JSON {csp: {key: value}})์ ์ฝ๋๋ค. ๊ฐ์ ๋ฆฌํฌํธยท๋
นํ์ ๋จ๊ธฐ์ง ์๋๋ค.
import fs from 'node:fs';
export function credValues(csp: string): Record<string, string> {
const f = process.env.CRED_VALUES_FILE;
if (!f || !fs.existsSync(f)) throw new Error('CRED_VALUES_FILE(์๊ฒฉ์ฆ๋ช
๊ฐ JSON) ๋ฏธ์ง์ ');
return JSON.parse(fs.readFileSync(f, 'utf8'))[csp] ?? {};
}
export const scrub = (s: string, values: Record<string, string>) => Object.values(values).reduce((t, v) => (v && v.length > 3 ? t.split(v).join('***') : t), s);
export type HolderInfo = { credentialHolder?: string; providers?: string[]; connectionCount?: number; verifiedConnectionCount?: number };
export async function getHolder(page: Page, holder: string): Promise<{ status: number; info: HolderInfo }> {
const r = await consoleApi(page, 'GetCredentialHolder', { pathParams: { holderId: holder } });
return { status: r.status, info: r.data?.responseData ?? {} };
}
// Register Credential ๋ชจ๋ฌ๋ก ๋ฑ๋ก. ํ๋ฉด์ด ์๋์ผ๋ก ์ฑ์ด Key ๋ฅผ ๊ทธ๋๋ก ์ฐ๊ณ Value ๋ง ์
๋ ฅํ๋ค.
// ๋
นํ์ ๋น๋ฐ๊ฐ์ด ์ฐํ์ง ์๋๋ก Value ์
๋ ฅ๋์ ๊ธ์๋ฅผ ๊ฐ๋ฆฐ๋ค(-webkit-text-security: disc, ํ๋ฉด ๋์์๋ ์ํฅ ์์).
export async function uiRegister(page: Page, holder: string, provider: string, values: Record<string, string>) {
await page.getByRole('button', { name: /Register Credential/ }).first().click();
const modal = page.locator('#credential-create-modal');
await expect(modal).toBeVisible();
await page.addStyleTag({ content: '#credential-create-modal .kv-value{-webkit-text-security:disc;}' });
await page.locator('#create-holder-name').fill(holder);
await page.locator('#create-holder-provider').selectOption(provider);
const keyInputs = page.locator('#kv-list-container .kv-key');
await expect(keyInputs.first()).toBeVisible();
const keys = (await keyInputs.evaluateAll((els) => els.map((e) => (e as HTMLInputElement).value))).filter(Boolean);
const missing: string[] = [];
for (let i = 0; i < keys.length; i++) {
const v = values[keys[i]];
if (v === undefined) { missing.push(keys[i]); continue; }
await page.locator('#kv-list-container .kv-value').nth(i).fill(v);
}
const respP = page.waitForResponse((r) => r.url().includes('/RegisterCredential'), { timeout: 180_000 });
await modal.getByRole('button', { name: /^Register$/ }).click();
const resp = await respP;
let body = '';
try { body = await resp.text(); } catch { /* ignore */ }
const toast = await page.locator('.toast, .toastify, [class*="toast"]').last().innerText({ timeout: 10_000 }).catch(() => '');
if (await modal.isVisible()) await modal.getByRole('button', { name: 'Cancel' }).click().catch(() => {});
return { keys, missing, status: resp.status(), body: scrub(body, values).slice(0, 600), toast: scrub(toast, values) };
}tests/iv10/_register.ts ยท 34์ค ยท ํด์ d807c126
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { gotoCredentials, credValues, getHolder, uiRegister, TEST_HOLDER } from '../../lib/iv10';
// IV10 UI ๋ฑ๋ก ๊ณต์ฉ ํ
์คํธ ๋ณธ๋ฌธ (S04 AWS, S05 CSP๋ณ)
export function uiRegisterTest(id: string, tc: string, csp: string) {
test(`${id} ${tc} Credential ๋ฑ๋ก โ ${csp} UI`, async ({ page }) => {
const v = credValues(csp);
expect(Object.keys(v).length, `${csp} ์๊ฒฉ์ฆ๋ช
๊ฐ ์์`).toBeGreaterThan(0);
let r: Awaited<ReturnType<typeof uiRegister>>;
await test.step('1. admin ๋ก๊ทธ์ธ ํ Settings > Cloud SPs > Credentials ์ด๋', async () => { await loginAsAdmin(page); await gotoCredentials(page); });
await test.step(`2. Register Credential โ holder ${TEST_HOLDER}, Provider ${csp} ์ ํ โ ์๋ ์์ฑ๋ Key ์ Value ์
๋ ฅ โ Register`, async () => {
r = await uiRegister(page, TEST_HOLDER, csp, v);
test.info().annotations.push({ type: 'keys', description: `ํ๋ฉด ์๋ Key: ${r.keys.join(', ')}${r.missing.length ? ` / ๊ฐ ์๋ Key: ${r.missing.join(', ')}` : ''}` });
test.info().annotations.push({ type: 'response', description: `RegisterCredential HTTP ${r.status}: ${r.body}` });
test.info().annotations.push({ type: 'toast', description: r.toast || '(ํ ์คํธ ์์)' });
});
await test.step('3. ๋ฑ๋ก ์ฑ๊ณต: ์๋ฒ ๊ฑฐ๋ถ ์์ ยท ์ฑ๊ณต ํ ์คํธ', async () => {
expect(r.missing, '์๊ฒฉ์ฆ๋ช
๊ฐ์ด ์๋ Key').toEqual([]);
expect(r.body, '์๋ฒ๊ฐ ๋ฑ๋ก์ ๊ฑฐ๋ถํจ').not.toMatch(/invalid credential key|"code":\s*[45]\d\d|Internal Server Error|Bad Request/i);
expect(r.status).toBe(200);
expect(r.toast).toMatch(/registered successfully/i);
});
await test.step(`4. GetCredentialHolder(${TEST_HOLDER}) providers ์ ${csp} ํฌํจ`, async () => {
const h = await getHolder(page, TEST_HOLDER);
test.info().annotations.push({ type: 'holder', description: JSON.stringify(h.info) });
expect(h.info.providers ?? []).toContain(csp);
});
});
}tests/iv10/IV10_S01.spec.ts ยท 26์ค ยท ํด์ 53b94608
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { gotoCredentials, holderRow, rowCells, consoleApi } from '../../lib/iv10';
// IV10_S01 : TC-INFRA-001 Credential ๋ชฉ๋ก ์กฐํ (Settings > Cloud SPs > Credentials)
test('IV10_S01 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/iv10/IV10_S02.spec.ts ยท 32์ค ยท ํด์ eb633cae
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { gotoCredentials, holderRow, consoleApi } from '../../lib/iv10';
// IV10_S02 : TC-INFRA-002 Credential ๋จ๊ฑด ์กฐํ โ ๋ชฉ๋ก์์ holder ํด๋ฆญ โ ์์ธ ํจ๋, GetCredentialHolder ์๋ต๊ณผ ๋น๊ต
const HOLDER = 'admin';
test('IV10_S02 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/iv10/IV10_S03.spec.ts ยท 36์ค ยท ํด์ c458f9f3
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { gotoCredentials, consoleApi, credValues, getHolder, scrub, TEST_HOLDER } from '../../lib/iv10';
// IV10_S03 : TC-INFRA-003 Credential ๋ฑ๋ก โ API (์น ์ฝ์ API POST /api/mc-infra-manager/RegisterCredential, ํ๋ฌธ โ API ์๋ฒ๊ฐ ์ํธํ)
// 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('IV10_S03 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/iv10/IV10_S04.spec.ts ยท 3์ค ยท ํด์ 708d1dff
import { uiRegisterTest } from './_register';
// IV10_S04 : TC-INFRA-004 Credential ๋ฑ๋ก โ AWS UI (ํ๋ฉด ์๋ Key: ClientId, ClientSecret)
uiRegisterTest('IV10_S04', 'TC-INFRA-004', 'aws');tests/iv10/IV10_S05.spec.ts ยท 5์ค ยท ํด์ af3e7a15
import { uiRegisterTest } from './_register';
// IV10_S05 : TC-INFRA-005 Credential ๋ฑ๋ก โ CSP๋ณ UI (Provider ๋ง๋ค ์๋ ์์ฑ Key ๊ฐ ๋ค๋ฆ). ํ
์คํธ 1๊ฑด = CSP 1๊ฐ
// openstack ์ stage ์ ์ฐ๊ฒฐ ์ ๋ณด๊ฐ ์์ด ์ ์ธ. ๋์ CSP ๋ IV10_CSPS ๋ก ๋ฐ๊ฟ ์ ์๋ค.
const CSPS = (process.env.IV10_CSPS ?? 'gcp,azure,alibaba,ibm,ncp,nhn,kt,tencent').split(',');
for (const c of CSPS) uiRegisterTest(`IV10_S05_${c.toUpperCase()}`, 'TC-INFRA-005', c);tests/iv10/IV10_S06.spec.ts ยท 24์ค ยท ํด์ 139b3ee6
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { gotoCredentials, holderRow, TEST_HOLDER } from '../../lib/iv10';
// IV10_S06 : TC-INFRA-006 Credential ์ญ์
// 2026-10-14 ํ์ธ: mc-infra-manager(edge) Swagger ์ credentialยทcredentialHolderยทconnConfig ์ญ์ API ๊ฐ ์๊ณ , Credentials ํ๋ฉด์๋ ์ญ์ ๊ธฐ๋ฅ์ด ์๋ค.
// ํ๋ฉด์์ ์ญ์ ์๋จ์ ์ฐพ๊ณ , ์์ผ๋ฉด ๊ธฐ๋ฅ ๋ฏธ์ ๊ณต์ผ๋ก SKIP ํ๋ค(์ญ์ ๊ธฐ๋ฅ์ด ์๊ธฐ๋ฉด ์ด spec ์ ์ญ์ ์ ์ฐจ๋ก ๋ฐ๊พผ๋ค).
test('IV10_S06 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);
});