Skip to content

Test Specs IV2

MZC-CSC edited this page Oct 14, 2026 · 4 revisions

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

Clone this wiki locally