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

Test Specs β€” IV2 Cloud Resources μ œμ–΄ (vNet/SG/SSH Key/Disk/Resource Sync)

생성: 2026-10-14 Β· κΈ°μ€€: v07 μ„€μΉ˜ ν›„ 톡합 검증(IV) Β· 원본: 여정별 κ³„νšμ„œ(Excel) ← Test Scenarios Β· IV ν—ˆλΈŒ Β· κΈ°λŠ₯ TC

Playwright(TypeScript) spec μ†ŒμŠ€μž…λ‹ˆλ‹€. μ‹œλ‚˜λ¦¬μ˜€λ§ˆλ‹€ ## <μ‹œλ‚˜λ¦¬μ˜€λ²ˆν˜Έ> μ„Ήμ…˜μ΄ 있고 μ•΅μ»€λŠ” #<μ‹œλ‚˜λ¦¬μ˜€λ²ˆν˜Έ μ†Œλ¬Έμž> μž…λ‹ˆλ‹€. 곡용 ν—¬νΌλŠ” Test-Specs-Common. λΉ„λ°€λ²ˆν˜Έ 등은 λ§ˆμŠ€ν‚Ήν–ˆμŠ΅λ‹ˆλ‹€.

μ‹œλ‚˜λ¦¬μ˜€ 파일 쀄 ν•΄μ‹œ
IV2_S01 tests/iv2/IV2_S01.spec.ts 15 c4db847c
IV2_S02 tests/iv2/IV2_S02.spec.ts 15 2591f598
IV2_S03 tests/iv2/IV2_S03.spec.ts 15 c6cedb0c
IV2_S04 tests/iv2/IV2_S04.spec.ts 15 deec07d8
IV2_S05 tests/iv2/IV2_S05.spec.ts 53 a954ea92
IV2_S06 tests/iv2/IV2_S06.spec.ts 88 42078aef
IV2_S07 tests/iv2/IV2_S07.spec.ts 31 60b0a2d9
IV2_S08 tests/iv2/IV2_S08.spec.ts 41 425616c0

IV2_S01

tests/iv2/IV2_S01.spec.ts Β· 15쀄 Β· ν•΄μ‹œ c4db847c Β· μ‹œλ‚˜λ¦¬μ˜€ 보기

import { test } from '../../lib/fixtures';
import { activeCsps, tid } from '../../lib/data';
import { createListFilter, resourceBlocker } from '../../lib/resources';

// IV2_S01 : vNet 생성 β†’ λͺ©λ‘ ν‘œμ‹œ β†’ CSP ν•„ν„° [정상, CSP별]
// Cloud Resources > Networks (Create VPC) β†’ λͺ©λ‘ β†’ Provider
// μž¬μ‹€ν–‰ κ°€λŠ₯: 같은 이름이 이미 있으면 생성은 κ±΄λ„ˆλ›°κ³  λͺ©λ‘/ν•„ν„°λ§Œ κ²€μ¦ν•œλ‹€. μ—‘μ…€μ˜ CSP ν–‰κ³Ό 1:1 (TC_VARIANT 둜 CSP 선택).
for (const c of activeCsps()) {
  test(`${tid('IV2_S01', c)} vNet 생성 β†’ λͺ©λ‘ ν‘œμ‹œ β†’ CSP ν•„ν„°`, async ({ page }) => {
    const why = resourceBlocker(c, 'vnet');
    test.skip(!!why, why);
    await createListFilter(page, c, 'vnet');
  });
}

IV2_S02

tests/iv2/IV2_S02.spec.ts Β· 15쀄 Β· ν•΄μ‹œ 2591f598 Β· μ‹œλ‚˜λ¦¬μ˜€ 보기

import { test } from '../../lib/fixtures';
import { activeCsps, tid } from '../../lib/data';
import { createListFilter, resourceBlocker } from '../../lib/resources';

// IV2_S02 : Security Group 생성 β†’ λͺ©λ‘ ν‘œμ‹œ β†’ CSP ν•„ν„° [정상, CSP별]
// Cloud Resources > Security Groups (Create SG) β†’ λͺ©λ‘ β†’ Provider. μ„ ν–‰: 같은 CSP 의 vNet(IV2_S01)
// μž¬μ‹€ν–‰ κ°€λŠ₯: 같은 이름이 이미 있으면 생성은 κ±΄λ„ˆλ›°κ³  λͺ©λ‘/ν•„ν„°λ§Œ κ²€μ¦ν•œλ‹€.
for (const c of activeCsps()) {
  test(`${tid('IV2_S02', c)} Security Group 생성 β†’ λͺ©λ‘ ν‘œμ‹œ β†’ CSP ν•„ν„°`, async ({ page }) => {
    const why = resourceBlocker(c, 'sg');
    test.skip(!!why, why);
    await createListFilter(page, c, 'sg');
  });
}

IV2_S03

tests/iv2/IV2_S03.spec.ts Β· 15쀄 Β· ν•΄μ‹œ c6cedb0c Β· μ‹œλ‚˜λ¦¬μ˜€ 보기

import { test } from '../../lib/fixtures';
import { activeCsps, tid } from '../../lib/data';
import { createListFilter, resourceBlocker } from '../../lib/resources';

// IV2_S03 : SSH Key 생성 β†’ λͺ©λ‘ ν‘œμ‹œ β†’ CSP ν•„ν„° [정상, CSP별]
// Cloud Resources > SSH Keys (Create SSH Key) β†’ λͺ©λ‘ β†’ Provider
// μž¬μ‹€ν–‰ κ°€λŠ₯: 같은 이름이 이미 있으면 생성은 κ±΄λ„ˆλ›°κ³  λͺ©λ‘/ν•„ν„°λ§Œ κ²€μ¦ν•œλ‹€.
for (const c of activeCsps()) {
  test(`${tid('IV2_S03', c)} SSH Key 생성 β†’ λͺ©λ‘ ν‘œμ‹œ β†’ CSP ν•„ν„°`, async ({ page }) => {
    const why = resourceBlocker(c, 'key');
    test.skip(!!why, why);
    await createListFilter(page, c, 'key');
  });
}

IV2_S04

tests/iv2/IV2_S04.spec.ts Β· 15쀄 Β· ν•΄μ‹œ deec07d8 Β· μ‹œλ‚˜λ¦¬μ˜€ 보기

import { test } from '../../lib/fixtures';
import { activeCsps, tid } from '../../lib/data';
import { createListFilter, resourceBlocker } from '../../lib/resources';

// IV2_S04 : Data Disk 생성 β†’ λͺ©λ‘ ν‘œμ‹œ β†’ CSP ν•„ν„° [정상, CSP별]
// Cloud Resources > Disks (Create Data Disk) β†’ λͺ©λ‘ β†’ Provider
// μž¬μ‹€ν–‰ κ°€λŠ₯: 같은 이름이 이미 있으면 생성은 κ±΄λ„ˆλ›°κ³  λͺ©λ‘/ν•„ν„°λ§Œ κ²€μ¦ν•œλ‹€.
for (const c of activeCsps()) {
  test(`${tid('IV2_S04', c)} Data Disk 생성 β†’ λͺ©λ‘ ν‘œμ‹œ β†’ CSP ν•„ν„°`, async ({ page }) => {
    const why = resourceBlocker(c, 'disk');
    test.skip(!!why, why);
    await createListFilter(page, c, 'disk');
  });
}

IV2_S05

tests/iv2/IV2_S05.spec.ts Β· 53쀄 Β· ν•΄μ‹œ a954ea92 Β· μ‹œλ‚˜λ¦¬μ˜€ 보기

import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin, menuSnapshot, selectWorkspaceProject } from '../../lib/ui';
import { WORKSPACE, DEFAULT_PROJECT } from '../../lib/data';
import { filterByProvider } from '../../lib/resources';

// IV2_S05 : Cloud Resources 8μ’… λͺ©λ‘ 톡합 쑰회 (λ©€ν‹°ν΄λΌμš°λ“œ) -> CSP ν•„ν„°
// μ‹€μ œ 메뉴λͺ…(2026-10-01 확인): Specs, Images, Networks, Security Groups, MyImages, SSH Keys, Disks, NLB (+ Resource Sync)
// 메뉴 클릭은 λ“œλ‘­λ‹€μš΄ μ „κ°œ/선택 μ΄ˆκΈ°ν™”λ‘œ timeout 이 λ‚¬μœΌλ―€λ‘œ(2026-10-02) 메뉴 href λ₯Ό 읽어 직접 μ΄λ™ν•˜κ³ , 이동 ν›„ ws01/default λ₯Ό λ‹€μ‹œ μ„ νƒν•œλ‹€.
const CLOUD_RESOURCES = ['Networks', 'Security Groups', 'SSH Keys', 'Disks', 'Specs', 'Images', 'MyImages', 'NLB'];
// IV2_S01~S04 둜 μ—¬λŸ¬ CSP μžμ›μ΄ λ§Œλ“€μ–΄μ§€λŠ” λͺ©λ‘: 2개 이상 CSP κ°€ ν•¨κ»˜ ν‘œμ‹œ + Provider ν•„ν„° 검증 λŒ€μƒ
const MULTI = ['Networks', 'Security Groups', 'SSH Keys', 'Disks'];
const norm = (s: string) => s.toLowerCase().replace(/\s+/g, '');

test('IV2_S05 8μ’… λͺ©λ‘ 톡합 쑰회 (λ©€ν‹°ν΄λΌμš°λ“œ) β†’ CSP ν•„ν„°', async ({ page }) => {
  test.setTimeout(20 * 60_000);
  const errors: string[] = [];
  page.on('pageerror', (e) => errors.push(e.message));
  await loginAsAdmin(page); // admin, ws01 / default

  const links = (await menuSnapshot(page)).map((l) => l.split(' -> ')).filter(([, href]) => href?.includes('/cloudresources/') || /serverspecs|serverimages|nlbs/.test(href ?? ''));
  test.info().annotations.push({ type: 'menu', description: links.map(([t, h]) => `${t}=${h}`).join(', ') });

  for (const name of CLOUD_RESOURCES) {
    const hit = links.find(([t]) => norm(t) === norm(name));
    expect(hit, `Cloud Resources 메뉴에 ${name} 쑴재`).toBeTruthy();
    await page.goto(hit![1]);
    await selectWorkspaceProject(page, WORKSPACE, DEFAULT_PROJECT, true);
    await page.getByText(/Showing \d+-\d+ of \d+ rows|No registered|No .* (found|registered)|Search with connection/i).first().waitFor({ timeout: 30_000 }).catch(() => {});
    await page.waitForTimeout(1500);
    await expect(page.locator('.tabulator, table, [role="grid"]').first(), `${name} λͺ©λ‘ ν‘œμ‹œ`).toBeVisible();
    const status = await page.getByText(/Showing \d+-\d+ of \d+ rows/).first().innerText().catch(() => '(ν–‰ μ—†μŒ/μ•ˆλ‚΄ 문ꡬ)');
    // 행별 [Name, Provider] (헀더 제λͺ©μœΌλ‘œ μ—΄ μ°ΎκΈ°)
    const rows = await page.locator('.tabulator').first().evaluate((t) => {
      const cols = [...t.querySelectorAll('.tabulator-header .tabulator-col')];
      const f = (title: string) => cols.find((c) => (c.textContent ?? '').trim() === title)?.getAttribute('tabulator-field');
      const [fn, fp] = [f('Name'), f('Provider')];
      if (!fp) return [] as string[][];
      const cell = (r: Element, k?: string | null) => (k ? r.querySelector(`.tabulator-cell[tabulator-field="${k}"]`)?.textContent ?? '' : '').trim();
      return [...t.querySelectorAll('.tabulator-row')].map((r) => [cell(r, fn), cell(r, fp).toLowerCase()]);
    }).catch(() => [] as string[][]);
    const uniq = [...new Set(rows.map((r) => r[1]).filter(Boolean))];
    test.info().annotations.push({ type: `list:${name}`, description: `${status} / Provider: ${uniq.join(',') || '-'}` });

    if (MULTI.includes(name)) {
      expect(uniq.length, `${name}: 2개 이상 CSP μžμ›μ΄ ν•¨κ»˜ ν‘œμ‹œ`).toBeGreaterThanOrEqual(2);
      // aws(μ—†μœΌλ©΄ 첫 CSP)둜 ν•„ν„° β†’ κ·Έ CSP μžμ›λ§Œ λ‚¨λŠ”μ§€ 검증
      const prov = uniq.includes('aws') ? 'aws' : uniq[0];
      await filterByProvider(page, prov, rows.find((r) => r[1] === prov)![0]);
    }
  }
  expect(errors, 'JS μ˜ˆμ™Έ μ—†μŒ').toEqual([]);
});

IV2_S06

tests/iv2/IV2_S06.spec.ts Β· 88쀄 Β· ν•΄μ‹œ 42078aef Β· μ‹œλ‚˜λ¦¬μ˜€ 보기

// IV2_S06: Resource Sync: NHN vNet 동기화 β†’ Networks λͺ©λ‘μ— NHN vNet ν‘œμ‹œ [정상, NHN Cloud]  (μ‚¬μš©μž μ§€μ‹œ 2026-10-08)
// ν™”λ©΄: Cloud Resources > Resource Sync (Sync Status Resources νƒ­) β†’ Cloud Resources > Networks
// 절차: 1.Resource Sync μ§„μž… 2.Search Conditions: Provider=Partial β†’ NHN 체크, Connection=NHN 체크 β†’ nhn-kr1, nhn-kr2 체크, Resource Type=Partial β†’ vNet 체크 β†’ Query
//   3.CSP Resource Sync Status μ—μ„œ Connection nhn-kr1, nhn-kr2 선택 4.Run Sync: Namespace=default, Target Provider=Partial β†’ NHN 체크, Resource Type=Partial β†’ vNet 체크 β†’ Run Sync
//   5.Cloud Resources > Networks μ—μ„œ NHN vNet 이 λͺ©λ‘μ— ν‘œμ‹œλ˜λŠ”μ§€ 확인(μ‚¬μš©μž μ§€μ‹œ: 확인 방법)
// κΈ°λŒ€κ²°κ³Ό: Run Sync μˆ˜ν–‰ ν›„ Networks λͺ©λ‘μ— Provider nhn 의 vNet 이 μžˆλ‹€
// ν™”λ©΄ μš”μ†Œ(2026-10-08 ν”„λ‘œλΈŒ): 쑰회 쑰건 radio name=query-provider-radio / query-restype-radio, μ²΄ν¬λ°•μŠ€ value=nhn, nhn-kr1, nhn-kr2, vNet, Run Sync μ˜μ—­ #sync-ns, radio name=provider-radio / restype-radio
import { test, expect } from '../../lib/fixtures';
import { activeCsps, tid } from '../../lib/data';
import { loginAsAdmin, selectWorkspaceProject, toasts } from '../../lib/ui';

const SYNC_URL = '/webconsole/settings/environment/cloudresources/resourcesync';
const NETWORKS_URL = '/webconsole/settings/environment/cloudresources/networks';
const CONNS = ['nhn-kr1', 'nhn-kr2'];

for (const c of activeCsps(['NHN Cloud'])) {
  test(`${tid('IV2_S06', c)} Resource Sync: NHN vNet 동기화 β†’ Networks λͺ©λ‘μ— NHN vNet ν‘œμ‹œ`, async ({ page }) => {
    test.setTimeout(15 * 60_000);
    await loginAsAdmin(page);
    await page.goto(SYNC_URL);
    await selectWorkspaceProject(page, 'ws01', 'default', true);
    await expect(page.locator('input[name="query-provider-radio"][value="partial"]')).toBeVisible({ timeout: 60_000 });
    await page.waitForTimeout(3000);

    // 2. Search Conditions
    await page.locator('input[name="query-provider-radio"][value="partial"]').check();
    await page.locator('input[value="nhn"]').first().check(); // Provider NHN
    await expect(page.locator('input[value="nhn-kr1"]').first(), 'Provider NHN 체크 ν›„ Connection λͺ©λ‘ ν‘œμ‹œ').toBeVisible({ timeout: 15_000 });
    await page.locator('input[value="nhn"]').nth(1).check(); // Connection κ·Έλ£Ή NHN
    for (const cn of CONNS) {
      const box = page.locator(`input[value="${cn}"]`).first();
      if (!(await box.isChecked())) await box.check();
      await expect(box, `${cn} 체크`).toBeChecked();
    }
    await page.locator('input[name="query-restype-radio"][value="partial"]').check();
    await page.locator('input[value="vNet"]').first().check(); // 쑰회 쑰건의 vNet
    await page.getByRole('button', { name: 'Query', exact: true }).click();
    // 3. CSP Resource Sync Status: 'Fetching resource status...' κ°€ λλ‚˜λ©΄(μ•½ 15초) 연결별 ν–‰(Connection / vNet λ“±λ‘μˆ˜/μ „μ²΄μˆ˜)이 λ‚˜μ˜¨λ‹€ β†’ 행을 눌러 μ—°κ²° 선택
    await expect(page.getByText('Fetching resource status')).toBeHidden({ timeout: 180_000 });
    await page.waitForTimeout(1500);
    const bodyAfterQuery = (await page.locator('body').innerText()).replace(/\s+/g, ' ');
    const si = bodyAfterQuery.indexOf('CSP Resource Sync Status');
    test.info().annotations.push({ type: 'sync-status', description: bodyAfterQuery.slice(si, si + 400) });
    for (const cn of CONNS) {
      const row = page.getByRole('row').filter({ has: page.getByRole('gridcell', { name: cn, exact: true }) }).first();
      await expect(row, `Sync Status 에 ${cn} ν–‰`).toBeVisible({ timeout: 30_000 });
      await row.getByRole('checkbox').first().check(); // ν–‰ 클릭은 Connection Detail 만 μ—΄κ³ , Target Connection 은 ν–‰ μ²΄ν¬λ°•μŠ€λ‘œ μ„ νƒλœλ‹€
    }
    const target = (await page.locator('body').innerText()).replace(/\s+/g, ' ');
    test.info().annotations.push({ type: 'target-connection', description: target.slice(target.indexOf('Target Connection'), target.indexOf('Target Connection') + 160) });

    // 4. Run Sync (Namespace default, Target Provider partial+NHN, Resource Type partial+vNet)
    const run = page.locator('form, div').filter({ has: page.locator('#sync-ns') }).last();
    await page.locator('#sync-ns').selectOption({ label: 'default' });
    await page.locator('input[name="provider-radio"][value="partial"]').check();
    await page.waitForTimeout(800);
    // Run Sync μ˜μ—­μ˜ NHN / vNet μ²΄ν¬λ°•μŠ€(쑰회 쑰건의 같은 이름 μ²΄ν¬λ°•μŠ€μ™€ κ΅¬λΆ„ν•˜λ €κ³  ν•΄λ‹Ή λΌλ””μ˜€ λ°”λ‘œ λ’€μ˜ 첫 μ²΄ν¬λ°•μŠ€λ₯Ό μ“΄λ‹€)
    const runNhn = page.locator('input[name="provider-radio"][value="partial"]').locator('xpath=following::input[@value="nhn"][1]');
    if (!(await runNhn.isChecked())) await runNhn.check(); // 연결을 κ³ λ₯΄λ©΄ NHN 이 μžλ™ μ²΄ν¬λ˜μ–΄ 있음
    await page.locator('input[name="restype-radio"][value="partial"]').check();
    await page.waitForTimeout(800);
    await page.locator('input[name="restype-radio"][value="partial"]').locator('xpath=following::input[@value="vNet"][1]').check();
    test.info().annotations.push({ type: 'run-sync-form', description: (await run.innerText().catch(() => '')).replace(/\s+/g, ' ').slice(0, 300) });
    await page.getByRole('button', { name: 'Run Sync', exact: true }).click();
    // κ²°κ³Ό: 확인 νŒμ—…/ν† μŠ€νŠΈ/κ²°κ³Ό 문ꡬ μˆ˜μ§‘(μ΅œλŒ€ ~2λΆ„)
    const msgs = new Set<string>();
    for (let i = 0; i < 24; i++) {
      for (const m of await page.locator('.modal.show').all()) { const t = (await m.innerText().catch(() => '')).replace(/\s+/g, ' ').trim(); if (t) msgs.add(`modal: ${t.slice(0, 300)}`); }
      for (const t of await toasts(page)) { const s = t.replace(/\s+/g, ' ').trim(); if (s) msgs.add(`toast: ${s.slice(0, 300)}`); }
      const confirm = page.locator('.modal.show').getByRole('button', { name: /Confirm|OK|Yes|Run/i }).last();
      if (await confirm.isVisible().catch(() => false)) await confirm.click().catch(() => {});
      await page.waitForTimeout(5000);
    }
    test.info().annotations.push({ type: 'run-sync-messages', description: [...msgs].join(' || ').slice(0, 800) || '(λ©”μ‹œμ§€ μ—†μŒ)' });
    const after = (await page.locator('body').innerText()).replace(/\s+/g, ' ');
    test.info().annotations.push({ type: 'sync-after', description: after.slice(after.indexOf('Run Sync'), after.indexOf('Run Sync') + 500) });

    // 5. Networks λͺ©λ‘μ— NHN vNet
    await page.goto(NETWORKS_URL);
    await selectWorkspaceProject(page, 'ws01', 'default', true);
    await page.getByText(/Showing \d+-\d+ of \d+ rows|No registered/).first().waitFor({ timeout: 30_000 }).catch(() => {});
    await page.waitForTimeout(2000);
    const rows = (await page.locator('.tabulator-row').allInnerTexts()).map((s) => s.replace(/\s+/g, ' ')).filter((s) => /nhn/i.test(s));
    test.info().annotations.push({ type: 'networks-nhn', description: rows.join(' || ') || '(NHN vNet μ—†μŒ)' });
    expect(rows.length, 'Networks λͺ©λ‘μ— NHN vNet').toBeGreaterThan(0);
  });
}

IV2_S07

tests/iv2/IV2_S07.spec.ts Β· 31쀄 Β· ν•΄μ‹œ 60b0a2d9 Β· μ‹œλ‚˜λ¦¬μ˜€ 보기

// IV2_S07: Resource Sync: NCP vNetΒ·Security Group 동기화 β†’ Networks / Security Groups λͺ©λ‘μ— NCP ν‘œμ‹œ [정상, NCP]  (μ‚¬μš©μž μ§€μ‹œ 2026-10-08)
// λ°°κ²½: NCP 계정은 VPC μ‹ κ·œ 생성이 λ§‰ν˜€(returnCode 1015009 'You can no longer create a product') IV2_S01/S02 λ₯Ό μ‹ κ·œ μƒμ„±μœΌλ‘œ ν•  수 μ—†λ‹€ β†’ κΈ°μ‘΄ NCP VPC/SG λ₯Ό Resource Sync 둜 가져와 μ‚¬μš©
// ν™”λ©΄: Cloud Resources > Resource Sync β†’ Cloud Resources > Networks, Security Groups
// 절차: 1.Resource Sync μ§„μž… 2.Search Conditions: Provider=Partial β†’ NCP, Connection=NCP β†’ ncp-kr, Resource Type=Partial β†’ vNet, Security Group β†’ Query
//   3.CSP Resource Sync Status μ—μ„œ ncp-kr 선택 4.Run Sync: Namespace=default, Target Provider=Partial β†’ NCP, Resource Type=Partial β†’ vNet, Security Group β†’ Run Sync
//   5.Networks λͺ©λ‘μ— NCP vNet, Security Groups λͺ©λ‘μ— NCP SG ν‘œμ‹œ 확인
// κΈ°λŒ€κ²°κ³Ό: 동기화 ν›„ 두 λͺ©λ‘μ— Provider ncp μžμ›μ΄ μžˆλ‹€
// 동기화 λŒ€μƒ μ—°κ²°: SYNC_CONNS ν™˜κ²½λ³€μˆ˜(μ‰Όν‘œ, κΈ°λ³Έ ncp-kr β€” IV2 κ°€ μ“°λŠ” μ—°κ²°)
import { test, expect } from '../../lib/fixtures';
import { activeCsps, tid } from '../../lib/data';
import { loginAsAdmin } from '../../lib/ui';
import { runResourceSync, listProviderRows } from '../../lib/sync';

const NETWORKS_URL = '/webconsole/settings/environment/cloudresources/networks';
const SG_URL = '/webconsole/settings/environment/cloudresources/securitygroups';

for (const c of activeCsps(['NCP'])) {
  test(`${tid('IV2_S07', c)} Resource Sync: NCP vNetΒ·Security Group 동기화 β†’ λͺ©λ‘μ— NCP ν‘œμ‹œ`, async ({ page }) => {
    test.setTimeout(15 * 60_000);
    const conns = (process.env.SYNC_CONNS || 'ncp-kr').split(',').map((s) => s.trim()).filter(Boolean);
    await loginAsAdmin(page);
    await runResourceSync(page, { provider: 'ncp', conns, types: ['vNet', 'securityGroup'] });
    const nets = await listProviderRows(page, NETWORKS_URL, 'ncp');
    test.info().annotations.push({ type: 'networks-ncp', description: nets.map((s) => s.slice(0, 90)).join(' || ') || '(NCP vNet μ—†μŒ)' });
    const sgs = await listProviderRows(page, SG_URL, 'ncp');
    test.info().annotations.push({ type: 'securitygroups-ncp', description: `${sgs.length}건: ${sgs.slice(0, 5).map((s) => s.slice(0, 70)).join(' || ') || '(NCP SG μ—†μŒ)'}` });
    expect.soft(nets.length, 'Networks λͺ©λ‘μ— NCP vNet').toBeGreaterThan(0);
    expect.soft(sgs.length, 'Security Groups λͺ©λ‘μ— NCP SG').toBeGreaterThan(0);
  });
}

IV2_S08

tests/iv2/IV2_S08.spec.ts Β· 41쀄 Β· ν•΄μ‹œ 425616c0 Β· μ‹œλ‚˜λ¦¬μ˜€ 보기

// IV2_S08: Resource Sync ν›„ CSP Resource Sync Status λ“±λ‘μˆ˜ 확인 (λͺ©λ‘κ³Ό 일치) [정상, NHN CloudΒ·NCP]  (μ‚¬μš©μž μ§€μ‹œ 2026-10-08)
// ν™”λ©΄: Cloud Resources > Resource Sync (Search Conditions β†’ Query β†’ CSP Resource Sync Status 의 연결별 λ“±λ‘μˆ˜/μ „μ²΄μˆ˜) β†’ Cloud Resources > Networks, Security Groups
// 절차: 1.Resource Sync μ§„μž… 2.Provider/Connection/Resource Type 쑰건 β†’ Query(Run Sync λŠ” ν•˜μ§€ μ•ŠμŒ) 3.연결별 λ“±λ‘μˆ˜/μ „μ²΄μˆ˜ 읽기 4.λͺ©λ‘(Networks / Security Groups)μ—μ„œ 같은 μ—°κ²°(이름이 '<μ—°κ²°>-' 둜 μ‹œμž‘)의 개수 μ„ΈκΈ° 5.비ꡐ
// κΈ°λŒ€κ²°κ³Ό: λ™κΈ°ν™”ν•œ μ—°κ²°μ˜ λ“±λ‘μˆ˜ = μ „μ²΄μˆ˜ 이고, λͺ©λ‘ κ°œμˆ˜μ™€ κ°™λ‹€. λ‹€λ₯΄λ©΄ FAIL(값은 비고에 기둝) β€” NHN nhn-kr2 λŠ” λͺ©λ‘ 3개인데 Sync Status 0/3 으둜 λ‚¨λŠ” λΆˆμΌμΉ˜κ°€ 관찰됨(2026-10-08)
// μ„ ν–‰: NHN β†’ IV2_S06, NCP β†’ IV2_S07 (동기화 μ™„λ£Œ)
import { test, expect } from '../../lib/fixtures';
import { activeCsps, tid } from '../../lib/data';
import { loginAsAdmin } from '../../lib/ui';
import { queryResourceStatus, listProviderRows } from '../../lib/sync';

const NET = '/webconsole/settings/environment/cloudresources/networks';
const SG = '/webconsole/settings/environment/cloudresources/securitygroups';
const CFG: Record<string, { provider: string; conns: string[]; types: Array<{ value: 'vNet' | 'securityGroup'; label: string }> }> = {
  NHNCloud: { provider: 'nhn', conns: ['nhn-kr1', 'nhn-kr2'], types: [{ value: 'vNet', label: 'vNet' }] },
  NCP: { provider: 'ncp', conns: ['ncp-kr'], types: [{ value: 'vNet', label: 'vNet' }, { value: 'securityGroup', label: 'Security Group' }] },
};

for (const c of activeCsps(['NHN Cloud', 'NCP'])) {
  test(`${tid('IV2_S08', c)} Resource Sync ν›„ CSP Resource Sync Status λ“±λ‘μˆ˜ 확인 (λͺ©λ‘κ³Ό 일치)`, async ({ page }) => {
    test.setTimeout(10 * 60_000);
    const cfg = CFG[c.code];
    await loginAsAdmin(page);
    const status = await queryResourceStatus(page, cfg);
    test.info().annotations.push({ type: 'sync-status', description: JSON.stringify(status) });
    const lists: Record<string, string[]> = {};
    for (const t of cfg.types) lists[t.label] = await listProviderRows(page, t.label === 'vNet' ? NET : SG, cfg.provider);
    const mismatches: string[] = [];
    for (const cn of cfg.conns) {
      for (const t of cfg.types) {
        const m = (status[cn][t.label] ?? '').match(/^(\d+)\/(\d+)$/);
        const listed = lists[t.label].filter((r) => r.startsWith(`${cn}-`)).length;
        const reg = m ? Number(m[1]) : -1, total = m ? Number(m[2]) : -1;
        test.info().annotations.push({ type: 'compare', description: `${cn} ${t.label}: Sync Status ${status[cn][t.label]} / λͺ©λ‘ ${listed}개` });
        if (reg !== total) mismatches.push(`${cn} ${t.label}: λ“±λ‘μˆ˜ ${reg} β‰  μ „μ²΄μˆ˜ ${total}`);
        if (reg !== listed) mismatches.push(`${cn} ${t.label}: Sync Status λ“±λ‘μˆ˜ ${reg} β‰  λͺ©λ‘ ${listed}개`);
      }
    }
    expect(mismatches, `Sync Status 와 λͺ©λ‘ 뢈일치: ${mismatches.join(' | ')}`).toEqual([]);
  });
}
⚠️ **GitHub.com Fallback** ⚠️