Test Specs IV10 - MZC-CSC/mc-web-console GitHub Wiki

Test Specs โ€” IV10 CSP Credential

์ƒ์„ฑ: 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

iv10.ts

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) };
}

_register.ts

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);
    });
  });
}

IV10_S01

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());
  });
});

IV10_S02

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());
  });
});

IV10_S03

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();
  });
});

IV10_S04

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');

IV10_S05

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);

IV10_S06

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);
});
โš ๏ธ **GitHub.com Fallback** โš ๏ธ