Test Specs IV5 - MZC-CSC/mc-web-console GitHub Wiki
์์ฑ: 2026-10-16 ยท ๊ธฐ์ค: v07 ์ค์น ํ ํตํฉ ๊ฒ์ฆ(IV) ยท ์๋ณธ: ์ฌ์ ๋ณ ๊ณํ์(Excel) โ Test Scenarios ยท IV ํ๋ธ ยท ๊ธฐ๋ฅ TC
Playwright(TypeScript) spec ์์ค์
๋๋ค. ์๋๋ฆฌ์ค๋ง๋ค ## <์๋๋ฆฌ์ค๋ฒํธ> ์น์
์ด ์๊ณ ์ต์ปค๋ #<์๋๋ฆฌ์ค๋ฒํธ ์๋ฌธ์> ์
๋๋ค. ๊ณต์ฉ ํฌํผ๋ Test-Specs-Common. ๋น๋ฐ๋ฒํธ ๋ฑ์ ๋ง์คํนํ์ต๋๋ค.
| ์๋๋ฆฌ์ค | ํ์ผ | ์ค | ํด์ |
|---|---|---|---|
| IV5_S01 | tests/iv5/IV5_S01.spec.ts |
32 | 4c203285 |
| IV5_S02 | tests/iv5/IV5_S02.spec.ts |
42 | eb84b3c2 |
| IV5_S03 | tests/iv5/IV5_S03.spec.ts |
28 | 93f828e5 |
| IV5_S04 | tests/iv5/IV5_S04.spec.ts |
37 | 86135518 |
| IV5_S05 | tests/iv5/IV5_S05.spec.ts |
69 | 87f17b3d |
| IV5_S06 | tests/iv5/IV5_S06.spec.ts |
47 | 987d3a9b |
| IV5_S07 | tests/iv5/IV5_S07.spec.ts |
67 | d1ef508b |
| IV5_S08 | tests/iv5/IV5_S08.spec.ts |
27 | b95cdfad |
| IV5_S09 | tests/iv5/IV5_S09.spec.ts |
28 | a5bdb721 |
| IV5_S10 | tests/iv5/IV5_S10.spec.ts |
32 | 856b6932 |
| IV5_S11 | tests/iv5/IV5_S11.spec.ts |
28 | 519e2611 |
| IV5_S12 | tests/iv5/IV5_S12.spec.ts |
38 | c7838d9c |
| IV5_S13 | tests/iv5/IV5_S13.spec.ts |
37 | 2a4b2ab8 |
| IV5_S14 | tests/iv5/IV5_S14.spec.ts |
34 | 2693d893 |
| IV5_S15 | tests/iv5/IV5_S15.spec.ts |
69 | 3fa4aae3 |
| IV5_S16 | tests/iv5/IV5_S16.spec.ts |
56 | e6af5efe |
| IV5_S17 | tests/iv5/IV5_S17.spec.ts |
57 | d7291202 |
| IV5_S18 | tests/iv5/IV5_S18.spec.ts |
39 | 6d1a0d83 |
| IV5_S19 | tests/iv5/IV5_S19.spec.ts |
36 | 561dd9dd |
| IV5_S20 | tests/iv5/IV5_S20.spec.ts |
29 | a0bc955e |
| IV5_S21 | tests/iv5/IV5_S21.spec.ts |
42 | 27d1329c |
| IV5_S22 | tests/iv5/IV5_S22.spec.ts |
53 | 619a390d |
| IV5_S23 | tests/iv5/IV5_S23.spec.ts |
79 | 62189d52 |
| IV5_S24 | tests/iv5/IV5_S24.spec.ts |
35 | 57f7eb94 |
| IV5_S25 | tests/iv5/IV5_S25.spec.ts |
57 | ce7516d4 |
| IV5_S26 | tests/iv5/IV5_S26.spec.ts |
60 | af800887 |
| IV5_S27 | tests/iv5/IV5_S27.spec.ts |
68 | 461f5265 |
| IV5_S28 | tests/iv5/IV5_S28.spec.ts |
44 | 8d072c8a |
| IV5_S29 | tests/iv5/IV5_S29.spec.ts |
77 | 3f81d492 |
tests/iv5/IV5_S01.spec.ts ยท 32์ค ยท ํด์ 4c203285 ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '../../lib/fixtures';
import { activeCsps, tid } from '../../lib/data';
import { loginAsAdmin, selectWorkspaceProject } from '../../lib/ui';
import { CATALOG_URL, openNode, catalogFrame, deployToVm, appStatus, vmTarget } from '../../lib/iv5';
// IV5_S01 : SW Catalogs: Infra์ SW ์ค์น [์ ์, CSP๋ณ]
// Applications > SW Catalogs(iframe) โ Deploy โ Application Installation ๋ชจ๋ฌ(VM / mc{n} / g{n}-1 / SW) โ Deploy
// ์ ํ: IV3 Infra mc{n} ์ g{n}-1 Running. ๊ฐ์ VM ์ IV5_S03(Nginx)๋ ์ค์นํ๋ฏ๋ก ์ฌ๊ธฐ์๋ Apache HTTP Server(ํฌํธ 8081)๋ฅผ ์ด๋ค.
const APP = 'Apache HTTP Server';
const PORT = '8081';
for (const c of activeCsps()) {
test(`${tid('IV5_S01', c)} SW Catalogs: Infra์ SW ์ค์น`, async ({ page, dialogs }) => {
await loginAsAdmin(page);
// ์ ํ ํ์ธ: ๋์ node Running
const st = await openNode(page, c);
expect(st, '์ ํ์กฐ๊ฑด: ๋์ node Running').toMatch(/Running/i);
await page.goto(CATALOG_URL);
await selectWorkspaceProject(page, 'ws01', 'default', true);
const f = await catalogFrame(page);
const r = await deployToVm(page, f, c, { app: APP, port: PORT });
// ๊ธฐ๋๊ฒฐ๊ณผ: ์ค์น ์์ฒญ ์ ์(์ค๋ฅ ์๋ฟ/๋ฌธ๊ตฌ ์์)
const errs = [...dialogs, ...r.msgs, r.after].filter((s) => /error|fail|invalid|์ค๋ฅ|์คํจ/i.test(s));
expect(errs, '์ค์น ์์ฒญ ์ค๋ฅ ์์').toEqual([]);
// 2026-10-05 ์ฒซ ์คํ: ์์ฒญ์ ์ ์๋์ผ๋ App Status=Failed(SSH Docker ์ค๋น ์คํจ, g1-1 Public IP ์์)์ธ๋ฐ ํต๊ณผ ์ฒ๋ฆฌ๋จ โ ์ํ๊น์ง ํ์
const s = await appStatus(page, vmTarget(c), APP);
expect(s, 'App Status ์ค์น ์๋ฃ(Failed ๋ฉด deployment-logs ์ด๋
ธํ
์ด์
์ฐธ์กฐ)').toMatch(/^Running$/i);
});
}tests/iv5/IV5_S02.spec.ts ยท 42์ค ยท ํด์ eb84b3c2 ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '../../lib/fixtures';
import { activeCsps, tid, k8sName, k8sProject } from '../../lib/data';
import { loginAsAdmin, selectWorkspaceProject } from '../../lib/ui';
import { gotoK8s, clusterCells } from '../../lib/iv3-k8s';
import { CATALOG_URL, catalogFrame, deployToK8s, appStatus } from '../../lib/iv5';
// IV5_S02 : SW Catalogs: K8s์ SW/workload ๋ฐฐํฌ [์ ์, CSP๋ณ]
// Applications > SW Catalogs(iframe) โ Deploy โ Application Installation ๋ชจ๋ฌ Target Infra=K8S(namespace=Project ๊ณ ์ , Cluster, Helm chart)
// โ Nginx Helm chart, Port 80 โ Spec Check โ Deploy โ Applications > App Status ์์ ์ํ ํ์ธ
// ๋์: ๊ธฐ๋ณธ k{n} / p{n}. Azure ๋ ์ฌ์ฉ์ ์ง์(2026-10-06)๋ก IV3_S18 ํด๋ฌ์คํฐ k2e(NodeGroup g2e, p2). ํ๊ฒฝ๋ณ์ K8S_CLUSTER ๋ก ๋ฎ์ด์ธ ์ ์์.
const APP = 'Nginx';
const PORT = '80';
const CLUSTER: Record<string, string> = { Azure: 'k2e' };
for (const c of activeCsps()) {
test(`${tid('IV5_S02', c)} SW Catalogs: K8s์ SW/workload ๋ฐฐํฌ`, async ({ page, dialogs }) => {
const prj = process.env.K8S_PROJECT || k8sProject(c); // K8S_PROJECT ๋ก ํ๋ก์ ํธ ์ง์ ๊ฐ๋ฅ(2026-10-13 NHN: default ์ ๊ธฐ์กด vNet ์ฌ์ฉ)
const cluster = process.env.K8S_CLUSTER || CLUSTER[c.code] || k8sName(c);
await loginAsAdmin(page, 'ws01', prj);
// ์ ํ ํ์ธ: ํด๋ฌ์คํฐ Active
await gotoK8s(page, prj);
// ๋ชฉ๋ก์ด ๋ฆ๊ฒ ์ฑ์์ง(2026-10-06: ์ง์
์งํ ์ฝ์ผ๋ฉด ํ ์์) โ ํ์ด ๋์ฌ ๋๊น์ง ๋๊ธฐ
let cells: Record<string, string> = {};
await expect.poll(async () => (cells = await clusterCells(page, cluster)).status ?? '', { timeout: 60_000, intervals: [3000] }).not.toBe('');
test.info().annotations.push({ type: 'cluster', description: `${prj}/${cluster}: status=${cells.status ?? '(๋ชฉ๋ก์ ์์)'} version=${cells.version ?? ''} nodegroup=${cells.nodegroup ?? ''}` });
expect(cells.status, `์ ํ์กฐ๊ฑด: ํด๋ฌ์คํฐ ${cluster} Active`).toMatch(/Active/i);
await page.goto(CATALOG_URL);
await selectWorkspaceProject(page, 'ws01', prj, true);
const f = await catalogFrame(page, prj);
const r = await deployToK8s(page, f, cluster, { app: APP, port: PORT });
const errs = [...dialogs, ...r.msgs, r.after].filter((s) => /error|fail|invalid|์ค๋ฅ|์คํจ/i.test(s));
expect(errs, '๋ฐฐํฌ ์์ฒญ ์ค๋ฅ ์์').toEqual([]);
// ๊ธฐ๋๊ฒฐ๊ณผ: ๋ฐฐํฌ ์์ฒญ ์ ์ + App Status Running
const s = await appStatus(page, { key: cluster, project: prj }, APP);
expect(s, 'App Status ์ ๋ฐฐํฌ SW ํ ํ์').not.toBe('');
expect(s, 'App Status ๋ฐฐํฌ ์๋ฃ(Failed ๋ฉด deployment-logs ์ด๋
ธํ
์ด์
์ฐธ์กฐ)').toMatch(/^Running$/i);
});
}tests/iv5/IV5_S03.spec.ts ยท 28์ค ยท ํด์ 93f828e5 ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '../../lib/fixtures';
import { activeCsps, tid } from '../../lib/data';
import { loginAsAdmin } from '../../lib/ui';
import { openNode, installFromNode, appStatus, vmTarget } from '../../lib/iv5';
// IV5_S03 : Infra ์์ธ Install SW ๋ฒํผ์ผ๋ก SW ์ค์น [์ ์, CSP๋ณ]
// Infra Workloads > mc{n} > node g{n}-1 > Node Info 'Install SW' โ "Install SW โ Node [g{n} / g{n}-1]" ๋ชจ๋ฌ(๋์ ๊ณ ์ )
// โ Nginx ์ ํ โ Spec Check โ Deploy โ Applications > App Status ์์ ์ํ ํ์ธ
const APP = 'Nginx';
const PORT = '8082';
for (const c of activeCsps()) {
test(`${tid('IV5_S03', c)} Infra ์์ธ Install SW ๋ฒํผ์ผ๋ก SW ์ค์น`, async ({ page, dialogs }) => {
await loginAsAdmin(page);
const st = await openNode(page, c);
expect(st, '์ ํ์กฐ๊ฑด: ๋์ node Running').toMatch(/Running/i);
const r = await installFromNode(page, c, { app: APP, port: PORT });
const errs = [...dialogs, ...r.msgs, r.after].filter((s) => /error|fail|invalid|์ค๋ฅ|์คํจ/i.test(s));
expect(errs, '์ค์น ์์ฒญ ์ค๋ฅ ์์').toEqual([]);
// ๊ธฐ๋๊ฒฐ๊ณผ: ์งํ/์๋ฃ ์ํ ํ์
const s = await appStatus(page, vmTarget(c), APP);
expect(s, 'App Status ์ ์ค์น SW ํ ํ์').not.toBe('');
expect(s, 'App Status ์ค์น ์๋ฃ(Failed ๋ฉด deployment-logs ์ด๋
ธํ
์ด์
์ฐธ์กฐ)').toMatch(/^Running$/i);
});
}tests/iv5/IV5_S04.spec.ts ยท 37์ค ยท ํด์ 86135518 ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '../../lib/fixtures';
import { activeCsps, tid } from '../../lib/data';
import { loginAsAdmin } from '../../lib/ui';
import { openNode, termRunLong } from '../../lib/iv5';
import { openNodeTerminal, termRun } from '../../lib/iv4-infra';
// IV5_S04 : Terminal ๋ก VM ์ ์ยทDocker ํ์ธ (SW ์ค์น ํ) [์ ์, CSP๋ณ]
// Infra Workloads > mc{n} > node g{n}-1 > Node Info Public IP ์ 'Terminal' โ Confirm โ Remote Terminal(xterm)
// 1) ์ ์ ํ์ธ: echo hello / whoami / hostname 2) Docker ํ์ธ: which docker / docker version 2>&1 / sudo docker version 2>&1
// ์ ํ: IV5_S03 ์ผ๋ก SW ์ค์น ์๋ฃ(App Status Running) โ Docker ๊ฐ ์ค๋น๋์ด ์์ด์ผ ํจ.
// 2026-10-06 ๋ฐฐ๊ฒฝ: Public IP ๊ฐ Tumblebug ์ ์์ ๋ Terminal ๋ช
๋ น ์ถ๋ ฅ์ด ๋น์ด ์์๊ณ SW ์ค์น๋ ์คํจํ๋ค(์ค์น ์ docker: command not found ๋ ์ ์).
for (const c of activeCsps()) {
test(`${tid('IV5_S04', c)} Terminal ๋ก VM ์ ์ยทDocker ํ์ธ`, async ({ page }) => {
await loginAsAdmin(page);
const st = await openNode(page, c);
expect(st, '์ ํ์กฐ๊ฑด: ๋์ node Running').toMatch(/Running/i);
await openNodeTerminal(page);
// 1) ์ ์ ํ์ธ โ ์ถ๋ ฅ์ด ๋น์ด ์์ผ๋ฉด ์๊ฒฉ ๋ช
๋ น ๊ฒฝ๋ก(SSH/Public IP) ๋ฌธ์
expect(await termRun(page, 'echo hello'), 'echo ์ถ๋ ฅ').toContain('hello');
const who = await termRun(page, 'whoami');
expect(who, 'whoami ์ถ๋ ฅ').not.toBe('');
expect(await termRun(page, 'hostname'), 'hostname ์ถ๋ ฅ').not.toBe('');
// 2) Docker ํ์ธ โ ์ค๋ฅ ์ถ๋ ฅ๋ ๋ณด์ด๋๋ก 2>&1
const which = await termRun(page, 'which docker');
expect(which, 'docker ์ค์น ๊ฒฝ๋ก(IV5_S03 ์ค์น๋ก ์ค๋น๋จ)').toMatch(/\/docker\b/);
// ์ถ๋ ฅ์ด ๊ธธ์ด termRunLong ์ฌ์ฉ(์๋ถ๋ถ์ ํ๋ฉด ๋ฐ์ผ๋ก ๋ฐ๋ฆด ์ ์์ด ๋๋ถ๋ถ Server ์น์
์ผ๋ก ํ์ )
const plain = await termRunLong(page, 'docker version 2>&1');
// sudo ์์ด: docker ๊ทธ๋ฃน์ด ์๋๋ฉด permission denied ๊ฐ ๋ ์ ์์ โ ํ์ ์ ์ฐ์ง ์๊ณ ๊ธฐ๋ก๋ง
test.info().annotations.push({ type: 'docker-no-sudo', description: /permission denied/i.test(plain) ? `๊ถํ ์ค๋ฅ(${who} ๊ฐ docker ๊ทธ๋ฃน ์๋)` : /Server/.test(plain) ? 'Server ์๋ต' : '๊ธฐํ(terminal ์ด๋
ธํ
์ด์
์ฐธ์กฐ)' });
const sudo = await termRunLong(page, 'sudo docker version 2>&1');
expect(sudo, 'sudo docker version: Server(๋ฐ๋ชฌ ์๋ต)').toMatch(/Server/);
expect(sudo, 'sudo docker version: Engine ๋ฒ์ ').toMatch(/Version:\s*\d+\.\d+/);
});
}tests/iv5/IV5_S05.spec.ts ยท 69์ค ยท ํด์ 87f17b3d ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '../../lib/fixtures';
import { activeCsps, tid } from '../../lib/data';
import { loginAsAdmin } from '../../lib/ui';
import { openNode, appStatus, vmTarget, termRunLong, appAction } from '../../lib/iv5';
import { openNodeTerminal } from '../../lib/iv4-infra';
// IV5_S05 : [Infra] ์ค์น๋ SW ํธ์ถ ํ์ธ โ Apache(8081)ยทNginx(8082) HTTP ์๋ต [์ ์, CSP๋ณ] (์ฌ์ฉ์ ์ง์ 2026-10-07)
// ๋์: Infra node (K8s ์๋). ์ ํ: IV5_S01(Apache 8081)ยทIV5_S03(Nginx 8082) ์ค์น, ๋์ node Running (Suspend ์ํ๋ฉด Resume ํ)
// ํ๋ฉด: Infra Workloads > mc{n} > node g{n}-1 > Node Info Terminal(Remote Terminal) + Applications > App Status
// ์ ์ฐจ: 1.node Running ํ์ธ 2.App Status ์์ ๋ SW ์ํ ํ์ธ(๊ธฐ๋ก) 3.Terminal ์์ curl http://localhost:{port}/ โ HTTP 200 + ๊ธฐ๋ณธ ํ์ด์ง ๋ณธ๋ฌธ 4.๊ณต์ธ IP:{port} ์ธ๋ถ ํธ์ถ ๊ฒฐ๊ณผ ๊ธฐ๋ก
// ๊ธฐ๋๊ฒฐ๊ณผ: Apache(8081)ยทNginx(8082) ๊ฐ HTTP 200 ๊ณผ ๊ธฐ๋ณธ ํ์ด์ง ๋ณธ๋ฌธ์ผ๋ก ์๋ต
// ํ์ : HTTP 200 + ์๋ต ํค๋ Server ๊ฐ. ๋ณธ๋ฌธ์ Nginx ๋ง ๊ธฐ๋ณธ ํ์ด์ง ๋ฌธ๊ตฌ๋ก ํ์ธ(Apache ์ปจํ
์ด๋์ ๊ธฐ๋ณธ ์๋ต์ 'It works!' ๊ฐ ์๋๋ผ HTML 4.01 ํ์ ํ์ด์ง์์, 2026-10-07 โ ๋ณธ๋ฌธ ์๋ถ๋ถ์ ๊ธฐ๋ก๋ง)
const APPS = [
{ name: 'Apache HTTP Server', port: '8081', server: /Server:\s*Apache/i, sign: null as RegExp | null },
{ name: 'Nginx', port: '8082', server: /Server:\s*nginx/i, sign: /Welcome to nginx/i },
];
for (const c of activeCsps()) {
test(`${tid('IV5_S05', c)} [Infra] ์ค์น๋ SW ํธ์ถ ํ์ธ: Apache(8081)ยทNginx(8082) HTTP ์๋ต`, async ({ page, request }) => {
test.setTimeout(30 * 60_000);
test.info().annotations.push({ type: 'target', description: 'Infra node (K8s ์๋): mc/g-1 ์ Docker ์ปจํ
์ด๋ SW' });
await loginAsAdmin(page);
const st = await openNode(page, c);
expect(st, '์ ํ์กฐ๊ฑด: ๋์ node Running').toMatch(/Running/i);
const pip = (await page.getByText('Public IP(IPv4)', { exact: true }).locator('xpath=following-sibling::*[1]').first().innerText().catch(() => '')).trim();
// 2. App Status ์ํ ๊ธฐ๋ก. VM ์ SuspendโResume ํ๋ฉด Docker ์ปจํ
์ด๋๊ฐ ๋ค์ ์ฌ๋ผ์ค์ง ์์ Stopped(Resume ์งํ์๋ Error)๋ก ๋ณด์ธ๋ค(2026-10-07 AWS ๊ด์ฐฐ).
// Stopped ์ด๋ฉด App Status ์ 'Start' ๋ก ๊ธฐ๋ํ ๋ค ํธ์ถํ๋ค.
const statuses: Record<string, string> = {};
for (const a of APPS) {
statuses[a.name] = await appStatus(page, vmTarget(c), a.name, 3 * 60_000);
if (/stopped/i.test(statuses[a.name])) {
await appAction(page, vmTarget(c), a.name, 'Start');
statuses[a.name] += ` โ Start โ ${await appStatus(page, vmTarget(c), a.name, 5 * 60_000)}`;
}
}
test.info().annotations.push({ type: 'app-status-summary', description: JSON.stringify(statuses) });
// 3. Terminal ์์ ํธ์ถ(localhost)
await openNode(page, c);
await openNodeTerminal(page);
const dock = await termRunLong(page, 'sudo docker ps --format "{{.Names}} {{.Status}} {{.Ports}}"');
test.info().annotations.push({ type: 'docker-ps', description: dock.replace(/\s+/g, ' ').slice(0, 300) });
const results: string[] = [];
for (const a of APPS) {
const code = await termRunLong(page, `curl -s -o /dev/null -w "HTTP_%{http_code}" -m 5 http://localhost:${a.port}/`);
const hdr = await termRunLong(page, `curl -sI -m 5 http://localhost:${a.port}/ | grep -i "^server"`);
const body = await termRunLong(page, `curl -s -m 5 http://localhost:${a.port}/ | head -c 100`);
results.push(`${a.name}(:${a.port}) code=${code.replace(/\s+/g, ' ').slice(-12)} ${hdr.replace(/\s+/g, ' ').slice(-30)} body=${body.replace(/\s+/g, ' ').slice(0, 70)}`);
expect.soft(code, `${a.name} localhost:${a.port} HTTP 200`).toContain('HTTP_200');
expect.soft(hdr, `${a.name} ์๋ต ํค๋ Server`).toMatch(a.server);
if (a.sign) expect.soft(body, `${a.name} ๋ณธ๋ฌธ`).toMatch(a.sign);
}
test.info().annotations.push({ type: 'localhost-call', description: results.join(' | ') });
// 4. ์ธ๋ถ ํธ์ถ(๊ณต์ธ IP) โ ๋ณด์๊ทธ๋ฃน ๋ฑ์ผ๋ก ์ฐจ๋จ๋ ์ ์์ด ๊ธฐ๋ก๋ง
const ext: string[] = [];
for (const a of APPS) {
try {
const r = await request.get(`http://${pip}:${a.port}/`, { timeout: 10_000, failOnStatusCode: false });
ext.push(`${a.name}(:${a.port}) ${r.status()} ${(await r.text()).replace(/\s+/g, ' ').slice(0, 50)}`);
} catch (e) {
ext.push(`${a.name}(:${a.port}) ์ธ๋ถ ํธ์ถ ๋ถ๊ฐ(${String(e).split('\n')[0].slice(0, 60)})`);
}
}
test.info().annotations.push({ type: 'external-call', description: `${pip}: ${ext.join(' | ')}` });
});
}tests/iv5/IV5_S06.spec.ts ยท 47์ค ยท ํด์ 987d3a9b ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '../../lib/fixtures';
import { activeCsps, tid, k8sName, k8sProject } from '../../lib/data';
import { loginAsAdmin } from '../../lib/ui';
import { appStatus } from '../../lib/iv5';
// IV5_S06 : [K8s] ์ค์น๋ Nginx(Helm) ๋์ ํ์ธ โ ์ํยทHealth CheckยทPod [์ ์, CSP๋ณ] (์ฌ์ฉ์ ์ง์ 2026-10-07)
// ๋์: K8s cluster (Infra ์๋). ์ ํ: IV5_S02(Nginx Helm ๋ฐฐํฌ) ์๋ฃ, K8s Active (Project p{n})
// ํ๋ฉด: Applications > App Status(Project p{n}) > K8S ํ ์ ํ > Application Detail(Status, Health Check, Network & Service Information)
// ์ ์ฐจ: 1.App Status ์์ K8S (p{n} / {cluster}) Nginx for Kubernetes Running ํ์ธ 2.ํ ์ ํ โ Application Detail: Status Running, Health Check Healthy, Pod n/n running, Service Port 80
// ๊ธฐ๋๊ฒฐ๊ณผ: Nginx(Helm) ๊ฐ Running / Healthy ์ด๊ณ Pod ๊ฐ ๋ชจ๋ running. PUBLIC IP ๊ฐ ์์ผ๋ฉด ํธ์ถ ํ์ธ(AWS ๋ ๋น์ด ์์ โ ์ํ/Health ๋ก ํ์ธ)
// ๋์ ํด๋ฌ์คํฐ: ๊ธฐ๋ณธ k{n}, K8S_CLUSTER ๋ก ๋ณ๊ฒฝ(v07 AWS: k1d)
const APP = 'Nginx for Kubernetes';
for (const c of activeCsps()) {
test(`${tid('IV5_S06', c)} [K8s] ์ค์น๋ Nginx(Helm) ๋์ ํ์ธ: ์ํยทHealth CheckยทPod`, async ({ page, request }) => {
const prj = process.env.K8S_PROJECT || k8sProject(c); // K8S_PROJECT ๋ก ํ๋ก์ ํธ ์ง์ ๊ฐ๋ฅ(2026-10-13 NHN: default ์ ๊ธฐ์กด vNet ์ฌ์ฉ)
const cluster = process.env.K8S_CLUSTER || k8sName(c);
test.info().annotations.push({ type: 'target', description: `K8s cluster (Infra ์๋): ${prj} / ${cluster}` });
await loginAsAdmin(page, 'ws01', prj);
const s = await appStatus(page, { key: cluster, project: prj }, APP, 3 * 60_000);
expect(s, `App Status ${APP} Running`).toMatch(/^Running$/i);
// ์์ธ: ํ(์ฑ ์ด๋ฆ) ํด๋ฆญ โ Application Detail
const f = page.frames().find((x) => x.url().includes('/applications/status'))!;
await f.getByText(APP, { exact: true }).first().click();
await expect(f.getByText('Application Detail').first()).toBeVisible({ timeout: 30_000 });
await page.waitForTimeout(3000);
const body = (await f.locator('body').innerText()).replace(/\s+/g, ' ');
const detail = body.slice(body.indexOf('Application Detail'), body.indexOf('Application Detail') + 1600);
test.info().annotations.push({ type: 'app-detail', description: detail.slice(0, 700) });
expect(detail, 'Status Running').toMatch(/Status:\s*Running/i);
expect(detail, 'Health Check Healthy').toMatch(/Health Check:\s*Healthy/i);
const pod = detail.match(/(\d+)\/(\d+) running/);
test.info().annotations.push({ type: 'pods', description: pod ? pod[0] : '(Pod ์ ๋ณด ์์)' });
expect(pod, 'Pod running ํ์').not.toBeNull();
expect(pod![1], 'Pod ๋ชจ๋ running').toBe(pod![2]);
// PUBLIC IP ๊ฐ ์์ผ๋ฉด ํธ์ถ(์์ผ๋ฉด ๊ธฐ๋ก๋ง)
const pubIp = detail.match(/PUBLIC IP[^0-9]{0,40}(\d+\.\d+\.\d+\.\d+)/)?.[1];
if (pubIp) {
const r = await request.get(`http://${pubIp}/`, { timeout: 10_000, failOnStatusCode: false }).catch((e) => e);
test.info().annotations.push({ type: 'external-call', description: `${pubIp}: ${typeof r.status === 'function' ? r.status() : String(r).split('\n')[0].slice(0, 80)}` });
} else {
test.info().annotations.push({ type: 'external-call', description: '์๋น์ค PUBLIC IP ์์ โ ์ธ๋ถ ํธ์ถ ๋ถ๊ฐ(์ํ/Health Check ๋ก ํ์ธ)' });
}
});
}tests/iv5/IV5_S07.spec.ts ยท 67์ค ยท ํด์ d1ef508b ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '../../lib/fixtures';
import { activeCsps, tid } from '../../lib/data';
import { loginAsAdmin } from '../../lib/ui';
import { openNode, appStatus, vmTarget, termRunLong, appAction, waitUninstalled } from '../../lib/iv5';
import { openNodeTerminal } from '../../lib/iv4-infra';
// IV5_S07 : [Infra] ์ค์น SW Uninstall โ ์ญ์ ํ์ธ: Apache(8081)ยทNginx(8082) [์ ์, CSP๋ณ] (์ฌ์ฉ์ ์ง์ 2026-10-07)
// ๋์: Infra node (K8s ์๋). ์ ํ: IV5_S05 ์๋ฃ, ๋์ node Running (Suspend ์ํ๋ฉด Resume ํ)
// ์ ์ฐจ: 1.App Status ์์ ๋ SW ๊ฐ Running ์ธ์ง ํ์ธ(Stopped ๋ฉด Start) 2.๊ฐ SW ํ Uninstall โ ํ์ธ 3.ํ ์ ๊ฑฐ(๋๋ Uninstalled) ํ์ธ
// 4.Terminal: docker ps ์ ํด๋น ์ปจํ
์ด๋ ์์, curl localhost:{port} ์ฐ๊ฒฐ ์คํจ(HTTP_000)
// ๊ธฐ๋๊ฒฐ๊ณผ: Uninstall ํ App Status ์์ ์ ๊ฑฐ๋๊ณ ์ปจํ
์ด๋/ํฌํธ๊ฐ ์ ๋ฆฌ๋๋ค
const APPS = [
{ name: 'Apache HTTP Server', port: '8081', ctr: /apache/i },
{ name: 'Nginx', port: '8082', ctr: /nginx/i },
];
for (const c of activeCsps()) {
test(`${tid('IV5_S07', c)} [Infra] ์ค์น SW Uninstall โ ์ญ์ ํ์ธ: ApacheยทNginx`, async ({ page }) => {
test.setTimeout(60 * 60_000);
test.info().annotations.push({ type: 'target', description: 'Infra node (K8s ์๋): mc/g-1 ์ Docker ์ปจํ
์ด๋ SW' });
await loginAsAdmin(page);
const st = await openNode(page, c);
expect(st, '์ ํ์กฐ๊ฑด: ๋์ node Running').toMatch(/Running/i);
// 1. ์ฌ์ ์ํ: Running ์ด ์๋๋ฉด(Resume ํ Stopped) Start
for (const a of APPS) {
let s = await appStatus(page, vmTarget(c), a.name, 3 * 60_000);
if (/stopped/i.test(s)) { await appAction(page, vmTarget(c), a.name, 'Start'); s += ` โ Start โ ${await appStatus(page, vmTarget(c), a.name, 5 * 60_000)}`; }
test.info().annotations.push({ type: 'pre-status', description: `${a.name}: ${s}` });
}
// 2~3. Uninstall โ ์ ๊ฑฐ ํ์ธ
const progress: string[] = [];
for (const a of APPS) {
await appAction(page, vmTarget(c), a.name, 'Uninstall');
const r = await waitUninstalled(page, vmTarget(c), a.name, 5 * 60_000);
progress.push(`${a.name}: ${r.seen.join(' โ ')}${r.done ? '' : ' (์๊ฐ ๋ด ์ ๊ฑฐ ์ ๋จ)'}`);
expect.soft(r.done, `${a.name} App Status ์์ ์ ๊ฑฐ/Uninstalled`).toBe(true);
}
test.info().annotations.push({ type: 'uninstall-progress', description: progress.join(' | ') });
// 4. Terminal ํ์ธ (Resume ์งํ์๋ bastion ๊ณต์ธ IP ๊ฐฑ์ ์ง์ฐ์ผ๋ก ์ฐ๊ฒฐ ์คํจํ ์ ์์ด ์ฌ์๋)
let first = '';
for (let i = 0; i < 8; i++) {
await openNode(page, c);
try { first = await openNodeTerminal(page); } catch (e) { first = 'ERR ' + String(e).split('\n')[0].slice(0, 80); }
if (!/Error|ERR/i.test(first)) break;
test.info().annotations.push({ type: 'terminal-retry', description: `${i + 1}ํ์ฐจ ์คํจ: ${first.replace(/\s+/g, ' ').slice(0, 100)}` });
await page.waitForTimeout(150_000);
}
expect(first, 'Terminal ์ ์').not.toMatch(/Error|ERR/i);
const dock = (await termRunLong(page, 'sudo docker ps -a --format "{{.Names}} {{.Status}}"')).replace(/\s+/g, ' ');
test.info().annotations.push({ type: 'docker-ps-a', description: dock.slice(-300) });
const res: string[] = [];
for (const a of APPS) {
const code = (await termRunLong(page, `curl -s -o /dev/null -w "HTTP_%{http_code}" -m 5 http://localhost:${a.port}/`)).replace(/\s+/g, ' ');
const tail = code.slice(-14);
res.push(`${a.name}(:${a.port}) ${tail}`);
expect.soft(code, `${a.name} ํธ์ถ ์คํจ(HTTP_000)`).toMatch(/HTTP_000\s*@@L\d+@@\s*$|HTTP_000/);
expect.soft(code.endsWith('HTTP_200') || /HTTP_200@@L\d+@@\s*$/.test(code), `${a.name} 200 ์๋ต ์๋`).toBe(false);
}
// docker ps -a ์ถ๋ ฅ ๋๋ถ๋ถ๋ง(๋ช
๋ น ์์ฝ ์ ์ธ) ๋ณด๊ณ ์ปจํ
์ด๋ ์์กด ์ฌ๋ถ ํ์ธ
const after = dock.split('@@')[1] ?? dock;
for (const a of APPS) expect.soft(after, `${a.name} ์ปจํ
์ด๋ ์์กด ์์`).not.toMatch(a.ctr);
test.info().annotations.push({ type: 'curl-after', description: res.join(' | ') });
});
}tests/iv5/IV5_S08.spec.ts ยท 27์ค ยท ํด์ b95cdfad ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '../../lib/fixtures';
import { activeCsps, tid, k8sName, k8sProject } from '../../lib/data';
import { loginAsAdmin } from '../../lib/ui';
import { appStatus, appAction, waitUninstalled } from '../../lib/iv5';
// IV5_S08 : [K8s] Nginx(Helm) Uninstall โ ์ญ์ ํ์ธ [์ ์, CSP๋ณ] (์ฌ์ฉ์ ์ง์ 2026-10-07)
// ๋์: K8s cluster (Infra ์๋). ์ ํ: IV5_S06 ์๋ฃ, K8s Active (Project p{n}). ๋์ ํด๋ฌ์คํฐ๋ K8S_CLUSTER(v07 AWS: k1d)
// ์ ์ฐจ: 1.App Status(Project p{n}) ์์ K8S ํ Nginx for Kubernetes Running ํ์ธ 2.Uninstall โ ํ์ธ 3.ํ ์ ๊ฑฐ(๋๋ Uninstalled) ํ์ธ
// ๊ธฐ๋๊ฒฐ๊ณผ: Uninstall ํ App Status ์์ ์ ๊ฑฐ๋๋ค(Helm release/Pod ์ ๋ฆฌ๋ ํ๋ฉด์ผ๋ก ์ง์ ํ์ธํ ์ ์์ด ํ ์ ๊ฑฐ๋ก ํ๋จ โ ํ์ ์ ๋ณ๋ ํ์ธ)
const APP = 'Nginx for Kubernetes';
for (const c of activeCsps()) {
test(`${tid('IV5_S08', c)} [K8s] Nginx(Helm) Uninstall โ ์ญ์ ํ์ธ`, async ({ page }) => {
test.setTimeout(30 * 60_000);
const prj = process.env.K8S_PROJECT || k8sProject(c); // K8S_PROJECT ๋ก ํ๋ก์ ํธ ์ง์ ๊ฐ๋ฅ(2026-10-13 NHN: default ์ ๊ธฐ์กด vNet ์ฌ์ฉ)
const cluster = process.env.K8S_CLUSTER || k8sName(c);
const t = { key: cluster, project: prj };
test.info().annotations.push({ type: 'target', description: `K8s cluster (Infra ์๋): ${prj} / ${cluster}` });
await loginAsAdmin(page, 'ws01', prj);
const s = await appStatus(page, t, APP, 3 * 60_000);
expect(s, `์ ํ์กฐ๊ฑด: ${APP} Running`).toMatch(/^Running$/i);
await appAction(page, t, APP, 'Uninstall');
const r = await waitUninstalled(page, t, APP, 10 * 60_000);
test.info().annotations.push({ type: 'uninstall-progress', description: `${APP}: ${r.seen.join(' โ ')}${r.done ? '' : ' (์๊ฐ ๋ด ์ ๊ฑฐ ์ ๋จ)'}` });
expect(r.done, 'App Status ์์ ์ ๊ฑฐ/Uninstalled').toBe(true);
});
}tests/iv5/IV5_S09.spec.ts ยท 28์ค ยท ํด์ a5bdb721 ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
// IV5_S09: TC-APP-001 Repository ๋ชฉ๋ก ์กฐํ [์ ์, ๊ณตํต]
// v0.6 TC-ID: TC-APP-REP-01 โ ๊ธฐ์กด spec(mcmp-e2e mc-web-console/specs/sw/TC-APP-REP-01-repository-๋ชฉ๋ก-์กฐํ.spec.ts)์ ์ ์ฐจ๋ง ์ฐธ๊ณ (์ฌ์ฉ์ ๊ฒฐ์ 2026-10-14: ๋์ ์ฝ๋๋ ์ฌ์ฌ์ฉํ์ง ์์)
// ํ๋ฉด: Applications > Repository (iframe :18084/web/applications/repository)
// ์์ ์ํ: admin(mcmp) ๋ก๊ทธ์ธ, Workspace=ws01 / Project=default
// ์ ์ฐจ: 1.Repository ๋ฉ๋ด ์ง์
2.๋ชฉ๋ก ์ปฌ๋ผ(Name/Format/URL/Type(hosted)/Action) ํ์ธ 3.๋ฑ๋ก๋ Nexus repository ํ์ = GET /oss/v1/repositories/nexus/list 4.๋ชฉ๋ก ์์์ด๋ฉด ์ค๋ฅ ์์ด ๋น ๋ชฉ๋ก
// ๊ธฐ๋๊ฒฐ๊ณผ: Repository ๋ชฉ๋ก ํ๋ฉด ํ์, ๋ฑ๋ก๋ Nexus repository ์ ๋ณด ํ์, ๋ชฉ๋ก ์์ ์ํ๋ ์ค๋ฅ ์์ด ํ์
// ๋น๊ณ : ์ํธ ์ ์ฐจ๋ 'SW Catalogs > Repository ํญ'์ด๋ v0.7 ํ๋ฉด์ ์ฌ์ด๋๋ฐ Applications > Repository ๋ณ๋ ๋ฉ๋ด(2026-10-14)
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { gotoApp, REPO_URL, repoList, showAllRows, clean } from '../../lib/iv5-app';
test('IV5_S09 TC-APP-001 Repository ๋ชฉ๋ก ์กฐํ', async ({ page }) => {
await loginAsAdmin(page);
const f = await test.step('1. Applications > Repository ์ง์
', () => gotoApp(page, REPO_URL, /repository/));
await test.step('2. ์ปฌ๋ผ Name / Format / URL / Type(hosted) / Action', async () => {
for (const h of ['Name', 'Format', 'URL', 'Type(hosted)', 'Action']) await expect(f.getByText(h, { exact: true }).first()).toBeVisible();
});
await test.step('3~4. ํ๋ฉด ๋ชฉ๋ก = Nexus repository ๋ชฉ๋ก API (์์ผ๋ฉด ๋น ๋ชฉ๋ก, ์ค๋ฅ ์์)', async () => {
await showAllRows(f);
const api = await repoList(page);
const names = (await f.locator('.tabulator-row .tabulator-cell:first-child').allInnerTexts()).map(clean);
test.info().annotations.push({ type: 'rows', description: `ํ๋ฉด ${names.length}๊ฑด: ${names.join(', ') || '(์์)'} / API ${api.length}๊ฑด: ${api.map((r) => `${r.name}(${r.format})`).join(', ')}` });
expect(names.sort()).toEqual(api.map((r) => r.name).sort());
await expect(f.getByText(/Unable to fetch data/)).toHaveCount(0);
});
});tests/iv5/IV5_S10.spec.ts ยท 32์ค ยท ํด์ 856b6932 ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
// IV5_S10: TC-APP-002 Repository ์ ๊ท ์์ฑ [์ ์, ๊ณตํต]
// v0.6 TC-ID: TC-APP-REP-02 โ ๊ธฐ์กด spec(mcmp-e2e mc-web-console/specs/sw/TC-APP-REP-02-repository-์ ๊ท-์์ฑ-*.spec.ts)์ ์ ์ฐจ๋ง ์ฐธ๊ณ (์ฌ์ฉ์ ๊ฒฐ์ 2026-10-14)
// ํ๋ฉด: Applications > Repository โ New Repository ๋ชจ๋ฌ(#repositoryForm "Repository Create")
// ์์ ์ํ: admin(mcmp) ๋ก๊ทธ์ธ, ws01/default. IV5_S09 ์๋ฃ, Nexus ์์ฑ ๊ถํ
// ์ ์ฐจ: 1.New Repository ํด๋ฆญ 2.์์ฑ ํ๋ฉด(๋ชจ๋ฌ) ํ์ 3.Name am-test-docker-hosted, Format docker (Type ์ hosted ๊ณ ์ ) 4.Create 5.๋ชฉ๋ก์ ์ ๊ท ํ์
// ๊ธฐ๋๊ฒฐ๊ณผ: Repository ์์ฑ ์ฑ๊ณต ๋ฉ์์ง, ๋ชฉ๋ก์ ์ ๊ท ํญ๋ชฉ, Nexus ์ ๋์ผ repository ์์ฑ(๋ชฉ๋ก API ๋ก ํ์ธ)
// ๋น๊ณ : ์ด๋ฏธ ์์ผ๋ฉด(์ฌ์คํ) ๋จผ์ ์ญ์
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { createRepository, deleteRepository, repoList, gotoApp, REPO_URL, repoRow, showAllRows } from '../../lib/iv5-app';
export const REPO = 'am-test-docker-hosted';
test('IV5_S10 TC-APP-002 Repository ์ ๊ท ์์ฑ', async ({ page }) => {
await loginAsAdmin(page);
if ((await repoList(page)).some((r) => r.name === REPO)) test.info().annotations.push({ type: 'pre-clean', description: (await deleteRepository(page, REPO)).toasts.join(' | ') });
await test.step(`1~4. New Repository โ ${REPO} / docker โ Create`, async () => {
const { toasts } = await createRepository(page, REPO, 'docker');
test.info().annotations.push({ type: 'create', description: toasts.join(' | ') || '(ํ ์คํธ ์์)' });
expect(toasts.join(' ')).toMatch(/Repository created successfully/);
});
await test.step('5. ๋ชฉ๋ก ํ์ ยท Nexus ๋ชฉ๋ก API ์ ์์ฑ', async () => {
const f = await gotoApp(page, REPO_URL, /repository/);
await showAllRows(f);
await expect(repoRow(f, REPO)).toHaveCount(1);
const r = (await repoList(page)).find((x) => x.name === REPO);
test.info().annotations.push({ type: 'nexus', description: JSON.stringify(r) });
expect(r?.format).toBe('docker');
});
});tests/iv5/IV5_S11.spec.ts ยท 28์ค ยท ํด์ 519e2611 ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
// IV5_S11: TC-APP-003 Repository ์์ธ ์ง์
[์ ์, ๊ณตํต]
// v0.6 TC-ID: TC-APP-REP-03 โ ๊ธฐ์กด spec(mcmp-e2e mc-web-console/specs/sw/TC-APP-REP-03-repository-์์ธ-์ง์
.spec.ts)์ ์ ์ฐจ๋ง ์ฐธ๊ณ (์ฌ์ฉ์ ๊ฒฐ์ 2026-10-14)
// ํ๋ฉด: Applications > Repository โ ํ์ ์ด๋ฆ ์
ํด๋ฆญ โ Repository Detail
// ์์ ์ํ: admin(mcmp) ๋ก๊ทธ์ธ, ws01/default. IV5_S10 ์๋ฃ
// ์ ์ฐจ: 1.๋ชฉ๋ก์์ am-test-docker-hosted ์ ํ(์ด๋ฆ ์
ํด๋ฆญ) 2.์์ธ ํ๋ฉด ํ์ 3.Name / Format / URL(Path) / Type(hosted) ํ์ธ
// ๊ธฐ๋๊ฒฐ๊ณผ: Repository ์์ธ ํ๋ฉด ํ์(Name, Format, Type, URL)
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { openRepositoryDetail, repoList } from '../../lib/iv5-app';
const REPO = 'am-test-docker-hosted';
test('IV5_S11 TC-APP-003 Repository ์์ธ ์ง์
', async ({ page }) => {
await loginAsAdmin(page);
const r = (await repoList(page)).find((x) => x.name === REPO);
test.skip(!r, `์ ํ IV5_S10 ์ ${REPO} ์์`);
const f = await test.step(`1~2. ${REPO} ์ด๋ฆ ํด๋ฆญ โ Repository Detail`, () => openRepositoryDetail(page, REPO));
await test.step('3. Name / Format / URL(Path) / Type(hosted)', async () => {
const vals = await f.locator('input[disabled]').evaluateAll((els) => els.map((e) => (e as HTMLInputElement).value));
test.info().annotations.push({ type: 'detail', description: vals.join(' | ') });
for (const l of ['Name', 'Format', 'URL(Path)', 'Type(hosted)']) await expect(f.getByText(l, { exact: true }).first()).toBeVisible();
expect(vals).toContain(REPO);
expect(vals.join(' ')).toMatch(/docker/i);
for (const h of ['Name', 'URL(Path)', 'Size', 'Action']) await expect(f.getByText(h, { exact: true }).first()).toBeVisible();
});
});tests/iv5/IV5_S12.spec.ts ยท 38์ค ยท ํด์ c7838d9c ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
// IV5_S12: TC-APP-006 ๋นํธ์ธ Catalog ๋ชฉ๋ก ํ์ [์ ์, ๊ณตํต]
// v0.6 TC-ID: TC-APP-CAT-01 โ ๊ธฐ์กด spec(mcmp-e2e mc-web-console/specs/sw/TC-APP-CAT-01-๋นํธ์ธ-catalog-๋ชฉ๋ก-ํ์.spec.ts)์ ์ ์ฐจ๋ง ์ฐธ๊ณ (์ฌ์ฉ์ ๊ฒฐ์ 2026-10-14)
// ํ๋ฉด: Applications > SW Catalogs (iframe :18084/web/applications/swcatalog)
// ์์ ์ํ: admin(mcmp) ๋ก๊ทธ์ธ, ws01/default. Built-in Catalog seed ๋ฑ๋ก
// ์ ์ฐจ: 1.SW Catalogs ์ง์
2.Built-in Catalog ๋ชฉ๋ก ํ์ 3.ํ
์คํธ ๋์(Elasticsearch, Grafana ๋ฑ) ํ์ 4.ํญ๋ชฉ๋ง๋ค ์ด๋ฆยท์์ฝยทCategoryยทRating
// ๊ธฐ๋๊ฒฐ๊ณผ: Built-in Catalog ๋ชฉ๋ก ํ์, ์ด๋ฆยท์์ฝยทCategoryยทRating ์์ญ ํ์
// ๋น๊ณ : VM/K8s ๋ฉค๋ฒ๋ ํ ์นด๋๋ก ๋ฌถ์ฌ API 13๊ฑด โ ํ๋ฉด 12๊ฑด(Nginx for Kubernetes ๋ Nginx ์นด๋). ์ํธ ์์์ Loki ๋ 2026-10-14 stage ๋นํธ์ธ ๋ชฉ๋ก์ ์์(12์ข
: Tomcat, Redis, Nginx, Apache HTTP Server, Nexus, MariaDB, Grafana, Prometheus, Elasticsearch, Rclone GUI, JupyterLab for Object Storage, PostgreSQL)
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { gotoApp, CATALOG_URL, catalogList, clean } from '../../lib/iv5-app';
const MUST = ['Elasticsearch', 'Grafana', 'Nginx', 'MariaDB'];
test('IV5_S12 TC-APP-006 ๋นํธ์ธ Catalog ๋ชฉ๋ก ํ์', async ({ page }) => {
await loginAsAdmin(page);
const f = await test.step('1. SW Catalogs ์ง์
', () => gotoApp(page, CATALOG_URL, /swcatalog/));
await test.step('2~4. ๋ชฉ๋ก = GET /catalog/software ยท ๋์ Catalog ํ์ ยท ์ด๋ฆ/์์ฝ/Category/Rating', async () => {
const items = f.locator('#sc-list-group .list-group-item');
await expect(items.first()).toBeVisible();
const api = await catalogList(page);
const n = await items.count();
const shown: string[] = [];
for (let i = 0; i < n; i++) shown.push(clean(await items.nth(i).innerText()).slice(0, 90));
test.info().annotations.push({ type: 'items', description: `${n}๊ฑด: ${shown.join(' || ')}` });
test.info().annotations.push({ type: 'api', description: `${api.length}๊ฑด: ${api.map((c) => c.title ?? c.name).join(', ')}` });
// VM/K8s ๋ฉค๋ฒ๋ ํ ์นด๋๋ก ๋ฌถ์ธ๋ค(์: Nginx + Nginx for Kubernetes โ Nginx ์นด๋, 2026-10-14) โ API ํญ๋ชฉ์ ๊ทธ๋๋ก ๋ณด์ด๊ฑฐ๋ base ์ด๋ฆ ์นด๋์ ๋ฌถ์ฌ์ผ ํ๋ค
const titles = api.map((c) => String(c.title ?? c.name));
const hidden = titles.filter((t) => !shown.some((s) => s.startsWith(t)));
test.info().annotations.push({ type: 'grouped', description: hidden.join(', ') || '์์' });
for (const t of hidden) expect(shown.some((s) => s.startsWith(t.replace(/ for Kubernetes$/i, ''))), `${t} โ ๋ฌถ์ธ ์นด๋ ํ์`).toBe(true);
for (const t of MUST) expect(shown.some((s) => s.startsWith(t)), `${t} ํ์`).toBe(true);
const first = clean(await items.first().innerText());
expect(first, 'Rating ํ์ "ํ๊ท (๊ฑด์)"').toMatch(/\d+(\.\d+)? \(\d+\)/);
test.info().annotations.push({ type: 'loki', description: `Loki ํ์ ${shown.some((s) => /Loki/i.test(s))} (์ํธ ์์)` });
});
});tests/iv5/IV5_S13.spec.ts ยท 37์ค ยท ํด์ 2a4b2ab8 ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
// IV5_S13: TC-APP-007 Catalog ์์ธ ํผ์นจ/์ ํ [์ ์, ๊ณตํต]
// v0.6 TC-ID: TC-APP-CAT-02 โ ๊ธฐ์กด spec(mcmp-e2e mc-web-console/specs/sw/TC-APP-CAT-02-catalog-์์ธ-ํผ์นจ-์ ํ.spec.ts)์ ์ ์ฐจ๋ง ์ฐธ๊ณ (์ฌ์ฉ์ ๊ฒฐ์ 2026-10-14)
// ํ๋ฉด: Applications > SW Catalogs โ ๋ชฉ๋ก ํญ๋ชฉ ์ด๋ฆ ํด๋ฆญ(ํผ์นจ #accordion_{id}) โ ๋ค์ ํด๋ฆญ(์ ํ)
// ์์ ์ํ: admin(mcmp) ๋ก๊ทธ์ธ, ws01/default. IV5_S12 ์๋ฃ
// ์ ์ฐจ: 1.๋์ Catalog(Elasticsearch) ์ ํ 2.์ด๋ฆ ํด๋ฆญ โ ์์ธ ํผ์นจ 3.DescriptionยทRef InformationยทTAGSยทRecommended SpecยทDeployment Status ํ์ 4.๋ค์ ํด๋ฆญ โ ์ ํ, ๋ชฉ๋ก ๋ ์ด์์ ์ ์ง
// ๊ธฐ๋๊ฒฐ๊ณผ: ์์ธ ์ ๋ณด ํผ์ณ์ง, Ref InformationยทRecommended SpecยทDeployment Status ํ์, ์ ํ ํ ๋ชฉ๋ก ์ ์ง
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { gotoApp, CATALOG_URL, catalogItem, clean } from '../../lib/iv5-app';
const TARGET = 'Elasticsearch';
test('IV5_S13 TC-APP-007 Catalog ์์ธ ํผ์นจ/์ ํ', async ({ page }) => {
await loginAsAdmin(page);
const f = await gotoApp(page, CATALOG_URL, /swcatalog/);
const item = catalogItem(f, TARGET);
const count = await f.locator('#sc-list-group .list-group-item').count();
const acc = item.locator('[id^="accordion_"]').first();
await test.step(`1~2. ${TARGET} ์ด๋ฆ ํด๋ฆญ โ ํผ์นจ`, async () => {
await item.locator('.col-5').first().click();
await expect(acc).toBeVisible();
});
await test.step('3. Ref Information ยท TAGS ยท Recommended Spec ยท Deployment Status', async () => {
const t = clean(await acc.innerText());
test.info().annotations.push({ type: 'detail', description: t.slice(0, 600) });
for (const s of ['Ref Information', 'Recommended Spec', 'Deployment Status']) expect(t, s).toContain(s);
expect(t).toMatch(/CPU : [\d.]+ Core/);
});
await test.step('4. ๋ค์ ํด๋ฆญ โ ์ ํ ยท ๋ชฉ๋ก ํญ๋ชฉ ์ ์ ์ง', async () => {
await item.locator('.col-5').first().click();
await expect(acc).toBeHidden();
await expect(f.locator('#sc-list-group .list-group-item')).toHaveCount(count);
});
});tests/iv5/IV5_S14.spec.ts ยท 34์ค ยท ํด์ 2693d893 ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
// IV5_S14: TC-APP-008 ์ธ๋ถ ๊ฒ์ ๊ฒฐ๊ณผ ํ์ (DockerHub/ArtifactHub) [์ ์, ๊ณตํต]
// v0.6 TC-ID: TC-APP-CAT-03 โ ๊ธฐ์กด spec(mcmp-e2e mc-web-console/specs/sw/TC-APP-CAT-03-์ธ๋ถ-๊ฒ์-๊ฒฐ๊ณผ-ํ์.spec.ts)์ ์ ์ฐจ๋ง ์ฐธ๊ณ (์ฌ์ฉ์ ๊ฒฐ์ 2026-10-14)
// ํ๋ฉด: Applications > SW Catalogs โ Register โ Application Registration 1. Source & Package โ Public Registry
// ์์ ์ํ: admin(mcmp) ๋ก๊ทธ์ธ, ws01/default. ์ธ๋ถ๋ง(Docker Hub / Artifact Hub API) ์ ๊ทผ ๊ฐ๋ฅ
// ์ ์ฐจ: 1.Register โ Public Registry 2.Target VM(Docker Hub)์์ ghost ๊ฒ์ โ ๊ฒฐ๊ณผ ๋ชฉ๋ก 3.Target K8S(Artifact Hub)์์ ghost ๊ฒ์ โ ๊ฒฐ๊ณผ ๋ชฉ๋ก 4.Cancel(๋ฑ๋ก ์ ํจ)
// ์
๋ ฅ๊ฐ(์ํธ): ghost
// ๊ธฐ๋๊ฒฐ๊ณผ: ์ธ๋ถ ๊ฒ์ ๊ฒฐ๊ณผ ๋ชฉ๋ก ํ์
// ๋น๊ณ : v0.7 ํ๋ฉด์๋ ๋ชฉ๋ก์ ์ธ๋ถ ๊ฒ์์ฐฝ์ด ์๊ณ Register ๋ง๋ฒ์ฌ์ Public Registry ๊ฐ ์ธ๋ถ ๊ฒ์(2026-10-14 ํ์ธ). VM=Docker Hub, K8S=Artifact Hub
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { openRegister, publicSearch } from '../../lib/iv5-app';
test('IV5_S14 TC-APP-008 ์ธ๋ถ ๊ฒ์ ๊ฒฐ๊ณผ ํ์ (DockerHub/ArtifactHub)', async ({ page }) => {
await loginAsAdmin(page);
const { m } = await openRegister(page);
await test.step('1~2. Public Registry ยท VM(Docker Hub) โ ghost ๊ฒ์', async () => {
const r = await publicSearch(page, m, 'ghost');
test.info().annotations.push({ type: 'dockerhub', description: r.join(', ') || '(๊ฒฐ๊ณผ ์์)' });
expect(r.length, 'Docker Hub ๊ฒฐ๊ณผ').toBeGreaterThan(0);
expect(r.join(' ')).toMatch(/ghost/i);
});
await test.step('3. K8S(Artifact Hub) โ ghost ๊ฒ์', async () => {
await m.locator('#targetK8S').check();
await m.locator('#registrySearchInput').fill('ghost');
await m.getByRole('button', { name: 'Search', exact: true }).click();
await expect(m.locator('.registry-results button.list-group-item').first().or(m.getByText('No matching packages found.'))).toBeVisible({ timeout: 60_000 });
const r = (await m.locator('.registry-results button.list-group-item .fw-semibold').allInnerTexts()).map((s) => s.trim());
test.info().annotations.push({ type: 'artifacthub', description: r.join(', ') || '(๊ฒฐ๊ณผ ์์)' });
expect(r.length, 'Artifact Hub ๊ฒฐ๊ณผ').toBeGreaterThan(0);
});
await test.step('4. Cancel', async () => { await m.getByRole('button', { name: 'Cancel' }).first().click(); });
});tests/iv5/IV5_S15.spec.ts ยท 69์ค ยท ํด์ 3fa4aae3 ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
// IV5_S15: TC-APP-009 ์ธ๋ถ ๊ฒ์ ๊ฒฐ๊ณผ ์
๋ก๋ (Public Registry โ Import & Create) [์ ์, ๊ณตํต]
// v0.6 TC-ID: TC-APP-CAT-04 โ ๊ธฐ์กด spec(mcmp-e2e mc-web-console/specs/sw/TC-APP-CAT-04-์ธ๋ถ-๊ฒ์-๊ฒฐ๊ณผ-์
๋ก๋.spec.ts)์ ์ ์ฐจ๋ง ์ฐธ๊ณ (์ฌ์ฉ์ ๊ฒฐ์ 2026-10-14)
// ํ๋ฉด: Applications > SW Catalogs โ Register โ 1. Source & Package(Public Registry) โ 2~4๋จ๊ณ โ Import & Create
// ์์ ์ํ: admin(mcmp) ๋ก๊ทธ์ธ, ws01/default. **IV5_S10(Nexus docker repository, :5000) ์๋ฃ ํ์**, Docker Hub ์ ๊ทผ ๊ฐ๋ฅ
// ์ ์ฐจ: 1.Public Registry ยท Target VM ์์ ghost ๊ฒ์ โ ๊ฒฐ๊ณผ ghost ์ ํ 2.Version(Tag) 6.44.0-bookworm ์ ํ ยท Category ์ ํ 3.General/Resource/Network ์
๋ ฅ 4.Import & Create
// 5.๊ฒฐ๊ณผ ๋ฉ์์ง ํ์ธ ยท ์นดํ๋ก๊ทธ ๋ชฉ๋ก์ ๋ฐ์
ํญ๋ชฉ ํ์
// ์
๋ ฅ๊ฐ(์ํธ): Source DockerHub / Package ghost / Tag 6.44.0-bookworm (ํ๋ฉด Version ๋ชฉ๋ก์ ์์ผ๋ฉด bookworm) / Category CONTENT_MANAGEMENT_SYSTEM
// ๊ธฐ๋๊ฒฐ๊ณผ: ์
๋ก๋(๋ฐ์
) ์์ฒญ ์ ์, ์ฑ๊ณต ๋๋ ์คํจ ๋ฉ์์ง ํ์
// ๋น๊ณ : ์ธ๋ถ ๊ฒ์ ๊ฒฐ๊ณผ๋ฅผ ์ ์ฅ์๋ก ์ฌ๋ฆฌ๋ ๋ณ๋ ์
๋ก๋ ํ๋ฉด(uploadForm)์ v0.7 FE ์์ ์ฐ์ด์ง ์๊ณ Import & Create ๊ฐ ๋ฐ์
(POST /catalog/docker/register)์ ๊ฒธํจ(2026-10-14 ์์ค ํ์ธ)
// ๋ฐ์
์ผ๋ก ์๊ธด ์นดํ๋ก๊ทธ(ghost-import-test)๋ IV5_S19(์ญ์ ) ๋จ๊ณ์์ ํจ๊ป ์ ๋ฆฌ
// 2026-10-14 stage: Nexus docker ๋ ์ง์คํธ๋ฆฌ(:5000)๊ฐ 'You must accept the End User License Agreement' 403 โ ๋ฐ์
์คํจ(๋ฉ์์ง ํ์). EULA ์๋ฝ์ ์ฝ์ ๋ฐ ์ฌ์ ์ค์
// ์ ํ ์กฐ๊ฑด(2026-10-16 ํ์ธ): Nexus ์ 5000 ์ปค๋ฅํฐ๋ฅผ ๊ฐ์ง docker hosted ์ ์ฅ์๊ฐ ์์ด์ผ ํ๋ค โ IV5_S10(Repository ์ ๊ท ์์ฑ, docker)์ด ์ฝ์์์ ๋ง๋ค๋ฉด httpPort 5000 ์ผ๋ก ์์ฑ.
// Docker Hub ๋ฐ์
๊ฒฝ๋ก(DockerHubService.registerDockerHubImage โ pushImageToNexus)๋ ์ ์ฅ์ ์กด์ฌ๋ฅผ ํ์ธํ์ง ์๋๋ค(์๋ ์์ฑ ensureDockerRepositoryExists ๋ registerToInternalNexus ๊ฒฝ๋ก์์๋ง ํธ์ถ).
// ์ ์ฅ์๊ฐ ์์ผ๋ฉด(์: stage ์ฌ์์์ผ๋ก Nexus ์ด๊ธฐํ) :5000 ๋ฏธ๊ธฐ๋ โ 'Failed to copy image to Nexus โฆ connection refused' ๋ก ์คํจ. S15 ๋จ๋
์คํ ์ S10 ์ ํ ํ์
// 2026-10-16: EULA ๋ ์ด๋ฏธ ์๋ฝ ์ํ(accepted:true). Nexus ์ด๊ธฐํ๋ก docker ์ ์ฅ์๊ฐ ์์ด ์คํจ โ IV5_S10 ์ฌ์คํ ํ ๋ฐ์
ยท๋ฑ๋ก ์ฑ๊ณต(PASS)
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { openRegister, publicSearch, selectByText, fillWizardRest, catalogList, collectToasts, clean } from '../../lib/iv5-app';
const NAME = 'ghost-import-test';
const TAG = '6.44.0-bookworm';
test('IV5_S15 TC-APP-009 ์ธ๋ถ ๊ฒ์ ๊ฒฐ๊ณผ ์
๋ก๋ (Public Registry โ Import & Create)', async ({ page }) => {
test.setTimeout(15 * 60_000);
await loginAsAdmin(page);
test.skip((await catalogList(page)).some((c) => (c.title ?? c.name) === NAME), `${NAME} ์ด๋ฏธ ์์(์ด์ ์คํ ์์ฌ) โ IV5_S19 ๋ก ์ ๋ฆฌ ํ ์ฌ์คํ`);
const imports: string[] = [];
page.on('response', async (r) => { if (/\/catalog\/(docker|helm)\/register|\/catalog\/software$/.test(r.url()) && r.request().method() === 'POST') { const b = clean(await r.text().catch(() => '')); imports.push(`${r.status()} ${r.url().replace(/^https?:\/\/[^/]+/, '')} :: ${b.slice(0, 200)} โฆ ${b.slice(-700)}`); } });
const { f, m } = await openRegister(page);
await test.step('1. Public Registry ยท VM(Docker Hub) โ ghost ๊ฒ์ โ ghost ์ ํ', async () => {
const r = await publicSearch(page, m, 'ghost');
test.info().annotations.push({ type: 'results', description: r.join(', ') });
await m.locator('.registry-results button.list-group-item').filter({ has: f.locator('.fw-semibold', { hasText: /^ghost$/ }) }).first().click();
});
await test.step(`2. Version ${TAG} ยท Category ์ ํ`, async () => {
const selects = m.locator('select');
// Version ๋ชฉ๋ก์ Docker Hub ์ต๊ทผ ํ๊ทธ 10๊ฐ๋ง ๋์จ๋ค(2026-10-14: latest, next-*, bookworm, alpine, 6.70.0-next-*) โ ์ํธ ๊ฐ์ด ์์ผ๋ฉด ๊ฐ์ ๊ณ์ด 'bookworm' ์ผ๋ก(์ง์นจ: ํ๋ฉด ๊ฐ์ผ๋ก ์คํยท๋น๊ณ ๊ธฐ๋ก)
const verSel = selects.filter({ has: f.locator('option', { hasText: 'Select Version' }) }).first();
await expect.poll(async () => (await verSel.locator('option').count()) > 1, { timeout: 60_000 }).toBe(true);
const opts = (await verSel.locator('option').allInnerTexts()).map((o) => o.trim());
const ver = await selectByText(verSel, opts.includes(TAG) ? new RegExp(`^${TAG}$`) : /^bookworm$/);
test.info().annotations.push({ type: 'versions', description: `${opts.join(', ')} โ ${ver}${ver !== TAG ? ` (์ํธ ${TAG} ๋ชฉ๋ก์ ์์)` : ''}` });
const catSel = selects.filter({ has: f.locator('option', { hasText: /Select Category/ }) }).first();
const cat = await selectByText(catSel, /CONTENT_MANAGEMENT/i);
test.info().annotations.push({ type: 'selected', description: `version ${ver} / category ${cat}` });
});
let toasts: string[] = [];
await test.step('3~4. General ยท Resource ยท Network ์
๋ ฅ โ Import & Create', async () => {
// ๋ฐ์
(์ด๋ฏธ์ง push)์ด ์ฑ๊ณตํ๋ฉด 30์ด๋ฅผ ๋๊ธธ ์ ์์ด(2026-10-16: 6๊ฐ ์ด๋ฏธ์ง ๋ณต์ฌ) ์นดํ๋ก๊ทธ ์์ฑ ์๋ต(POST /catalog/software)๊น์ง ๊ธฐ๋ค๋ฆฐ ๋ค ํ ์คํธ๋ฅผ ๋ค์ ๋ชจ์๋ค
const created = page.waitForResponse((r) => /\/catalog\/software$/.test(r.url()) && r.request().method() === 'POST', { timeout: 10 * 60_000 }).catch(() => null);
toasts = await fillWizardRest(page, f, m, { name: NAME, summary: 'ghost import test', desc: 'Imported from Docker Hub by IV5_S15', cpu: [1, 2], mem: [1, 2], disk: [1, 5], port: '2368' }, /Import & Create/);
if (!toasts.length && (await created)) toasts = await collectToasts(page, f, 15_000);
test.info().annotations.push({ type: 'toasts', description: toasts.join(' | ') || '(ํ ์คํธ ์์)' });
test.info().annotations.push({ type: 'api', description: imports.join(' || ') || '(๊ธฐ๋ก ์์)' });
});
await test.step('5. ๋ฐ์
์์ฒญ ์ ์ ยท ๊ฒฐ๊ณผ ๋ฉ์์ง ยท ๋ชฉ๋ก', async () => {
expect(imports.some((x) => /\/catalog\/docker\/register/.test(x)), '๋ฐ์
์์ฒญ(POST /catalog/docker/register) ์ ์').toBe(true);
expect(toasts.length, '์ฑ๊ณต ๋๋ ์คํจ ๋ฉ์์ง ํ์').toBeGreaterThan(0);
const inList = (await catalogList(page)).some((c) => (c.title ?? c.name) === NAME);
test.info().annotations.push({ type: 'catalog', description: `๋ชฉ๋ก์ ${NAME}: ${inList}` });
// ์ํธ ๊ธฐ๋๊ฒฐ๊ณผ๋ '์ฑ๊ณต ๋๋ ์คํจ ๋ฉ์์ง ํ์' โ ๋ฉ์์ง ํ์๋ก ํ์ ํ๊ณ ๋ฐ์
์ฑ๊ณต ์ฌ๋ถ๋ ๊ธฐ๋ก(2026-10-14: Nexus :5000 ์ด EULA ๋ฏธ์๋ฝ 403 ์ผ๋ก push ๊ฑฐ๋ถ)
const push = imports.find((x) => /docker\/register/.test(x)) ?? '';
test.info().annotations.push({ type: 'import-result', description: /"success":\s*false/.test(push) ? `๋ฐ์
์คํจ: ${(push.match(/StatusCode: \d+[^"\\]*/) ?? [''])[0]}` : (/Registration Success/.test(toasts.join(' ')) ? '๋ฐ์
ยท๋ฑ๋ก ์ฑ๊ณต' : '(ํ๋จ ๋ถ๊ฐ)') });
expect(toasts.join(' '), '์ฑ๊ณต ๋๋ ์คํจ ๋ฉ์์ง').toMatch(/Registration Success|Failed to import|Registration Failed|Failed/);
});
});tests/iv5/IV5_S16.spec.ts ยท 56์ค ยท ํด์ e6af5efe ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
// IV5_S16: TC-APP-010 Catalog ์ ๊ท ๋ฑ๋ก (Register, Existing Package) [์ ์, ๊ณตํต]
// v0.6 TC-ID: TC-APP-CAT-05 โ ๊ธฐ์กด spec(mcmp-e2e mc-web-console/specs/sw/TC-APP-CAT-05-catalog-์ ๊ท-๋ฑ๋ก.spec.ts)์ ์ ์ฐจ๋ง ์ฐธ๊ณ (์ฌ์ฉ์ ๊ฒฐ์ 2026-10-14)
// ํ๋ฉด: Applications > SW Catalogs โ Register โ Application Registration(1. Source & Package / 2. General / 3. Resource Requirements / 4. Network)
// ์์ ์ํ: admin(mcmp) ๋ก๊ทธ์ธ, ws01/default. IV5_S15(ghost ๋ฐ์
) ์๋ฃ
// ์ ์ฐจ: 1.Register 2.Existing Package ยท Target VM 3.Category โ Package ghost โ Version 6.44.0-bookworm 4.Application Name Ghost CMS, Summary, Description, Reference
// 5.Resource(CPU 0.5/1, Memory 1/2, Storage 1/5) ยท Network Port 2368 6.Create โ ๋ชฉ๋ก์ Ghost CMS, ์์ธ(ํผ์นจ)์ ์ค๋ช
ยทspec ํ์
// ์
๋ ฅ๊ฐ(์ํธ): Packages ghost / Version 6.44.0-bookworm / Application Name Ghost CMS / CPU min 0.5 rec 1 / Memory 1ยท2 GB / Storage 1ยท5 GB / Port 2368 TCP
// ๊ธฐ๋๊ฒฐ๊ณผ: Catalog ๋ฑ๋ก ์ฑ๊ณต ๋ฉ์์ง, Ghost CMS ๊ฐ ๋ชฉ๋ก์ ํ์, ์์ธ ํ๋ฉด์์ ์
๋ ฅํ ์ค๋ช
ยทspecยทport ํ์
// ๋น๊ณ : ๋ฒํผ ์ด๋ฆ์ ์ํธ์ 'Regist' ๊ฐ ์๋๋ผ 'Register'(2026-10-14). Existing Package ๋ชฉ๋ก์ ๋ฐ์
(IV5_S15)ํ ํจํค์ง๋ง ๋์จ๋ค(๋ฐ์
์ Category ๋ชฉ๋ก ๋น์ด ์์).
// Version ์ S15 ์์ ๋ฐ์
ํ ํ๊ทธ(์ํธ 6.44.0-bookworm ์ด Docker Hub ์ต๊ทผ ๋ชฉ๋ก์ ์์ด bookworm). ์ด๋ฏธ ์์ผ๋ฉด(์ฌ์คํ) skip
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { openRegister, selectByText, fillWizardRest, catalogList, gotoApp, CATALOG_URL, catalogItem, clean } from '../../lib/iv5-app';
const NAME = 'Ghost CMS';
const DESC = 'Ghost CMS registered by IV5_S16 (VM, ghost 6.44.0-bookworm)';
test('IV5_S16 TC-APP-010 Catalog ์ ๊ท ๋ฑ๋ก (Register)', async ({ page }) => {
test.setTimeout(10 * 60_000);
await loginAsAdmin(page);
test.skip((await catalogList(page)).some((c) => (c.title ?? c.name) === NAME), `${NAME} ์ด๋ฏธ ์์ โ IV5_S19 ๋ก ์ ๋ฆฌ ํ ์ฌ์คํ`);
const { f, m } = await openRegister(page);
await test.step('1~3. Existing Package ยท VM ยท Category โ ghost โ 6.44.0-bookworm', async () => {
await m.locator('#sourceExisting').check();
await m.locator('#targetVM').check();
const selects = m.locator('select');
const cat = await selectByText(selects.filter({ has: f.locator('option', { hasText: 'Select Category' }) }).first(), /content.?management/i);
const pkg = await selectByText(selects.filter({ has: f.locator('option', { hasText: 'Select Package' }) }).first(), /^\s*ghost\s*$/i);
const verSel = selects.filter({ has: f.locator('option', { hasText: 'Select Version' }) }).first();
await expect.poll(async () => (await verSel.locator('option').count()) > 1, { timeout: 60_000 }).toBe(true);
const ver = await selectByText(verSel, (await verSel.locator('option').allInnerTexts()).some((o) => /6\.44\.0-bookworm/.test(o)) ? /6\.44\.0-bookworm/ : /bookworm/); // IV5_S15 ์์ ๋ฐ์
ํ ํ๊ทธ
test.info().annotations.push({ type: 'package', description: `${cat} / ${pkg} / ${ver}` });
});
let toasts: string[] = [];
await test.step('4~6. General ยท Resource ยท Network โ Create', async () => {
toasts = await fillWizardRest(page, f, m, { name: NAME, summary: 'Ghost publishing platform', desc: DESC, ref: ['HOMEPAGE', 'https://ghost.org/'], cpu: [0.5, 1], mem: [1, 2], disk: [1, 5], port: '2368' }, /^Create$/);
test.info().annotations.push({ type: 'toasts', description: toasts.join(' | ') || '(ํ ์คํธ ์์)' });
expect(toasts.join(' ')).toMatch(/Registration Success/);
});
await test.step('7. ๋ชฉ๋ก ยท ์์ธ(ํผ์นจ): ์ค๋ช
ยท spec ยท port', async () => {
const c = (await catalogList(page)).find((x) => (x.title ?? x.name) === NAME);
test.info().annotations.push({ type: 'saved', description: JSON.stringify(c).slice(0, 800) });
expect(c, '๋ชฉ๋ก API ์ ๋ฑ๋ก').toBeTruthy();
const g = await gotoApp(page, CATALOG_URL, /swcatalog/);
const item = catalogItem(g, NAME);
await expect(item).toBeVisible();
await item.locator('.col-5').first().click();
const t = clean(await item.innerText());
test.info().annotations.push({ type: 'detail', description: t.slice(0, 600) });
expect(t).toContain(DESC);
expect(t).toMatch(/CPU : 1 Core/);
expect(JSON.stringify(c)).toContain('2368');
});
});tests/iv5/IV5_S17.spec.ts ยท 57์ค ยท ํด์ d7291202 ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
// IV5_S17: TC-APP-011 Catalog ์์ (Edit) [์ ์, ๊ณตํต]
// v0.6 TC-ID: TC-APP-CAT-06 โ ๊ธฐ์กด spec(mcmp-e2e mc-web-console/specs/sw/TC-APP-CAT-06-catalog-์์ .spec.ts)์ ์ ์ฐจ๋ง ์ฐธ๊ณ (์ฌ์ฉ์ ๊ฒฐ์ 2026-10-14)
// ํ๋ฉด: Applications > SW Catalogs โ Ghost CMS ํ ํธ์ง ์์ด์ฝ โ Application Update(#modal-wizard) โ Update
// ์์ ์ํ: admin(mcmp) ๋ก๊ทธ์ธ, ws01/default. IV5_S16 ์๋ฃ
// ์ ์ฐจ: 1.Ghost CMS ํ์ ํธ์ง ์์ด์ฝ 2.Application Name ยท Summary ยท Description ยท Recommended Spec ์์ 3.Update 4.์์ธ ์ฌ์กฐํ ยท ๋ชฉ๋ก ๊ฐฑ์
// ๊ธฐ๋๊ฒฐ๊ณผ: Catalog ์์ ์ฑ๊ณต ๋ฉ์์ง, ์์ธ ์ฌ์กฐํ ์ ์์ ๊ฐ ํ์, ๋ชฉ๋ก ํ์ ์ ๋ณด ๊ฐฑ์
// ๋น๊ณ : ํธ์ง/์ญ์ ๋ ํ hover ์ ๋์ค๋ ์์ด์ฝ(ํ
์คํธ ์์, ์ฒซ ๋ฒ์งธ=ํธ์ง). ์ด๋ฆ์ Ghost CMS ๋ก ๋๋๋ฆฌ์ง ์๊ณ 'Ghost CMS (edited)' ๋ก ๋จ๊น โ IV5_S19 ๊ฐ ์ด ์ด๋ฆ์ ์ญ์
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { gotoApp, CATALOG_URL, catalogAction, catalogList, catalogItem, collectToasts, toastGone, clean } from '../../lib/iv5-app';
const FROM = 'Ghost CMS';
const TO = 'Ghost CMS (edited)';
const DESC = 'Ghost CMS updated by IV5_S17';
test('IV5_S17 TC-APP-011 Catalog ์์ (Edit)', async ({ page }) => {
test.setTimeout(10 * 60_000);
await loginAsAdmin(page);
test.skip(!(await catalogList(page)).some((c) => (c.title ?? c.name) === FROM), `์ ํ IV5_S16 ์ ${FROM} ์์`);
const f = await gotoApp(page, CATALOG_URL, /swcatalog/);
const m = f.locator('#modal-wizard');
await test.step(`1. ${FROM} ํธ์ง ์์ด์ฝ โ Application Update`, async () => {
await catalogAction(f, FROM, 'edit');
await expect(m.getByText('Application Update')).toBeVisible();
});
let toasts: string[] = [];
await test.step('2~3. Name ยท Summary ยท Description ยท Recommended Spec ์์ โ Update', async () => {
await m.getByRole('button', { name: 'Next', exact: true }).click();
await m.getByPlaceholder('Application name').fill(TO);
await m.getByPlaceholder('Application summary').fill('Ghost publishing platform (edited)');
await m.getByPlaceholder('Application description').fill(DESC);
await m.getByRole('button', { name: 'Next', exact: true }).click();
await m.getByPlaceholder('2', { exact: true }).first().fill('2'); // CPU recommended 1 โ 2
await m.getByPlaceholder('8', { exact: true }).first().fill('4'); // Memory recommended 2 โ 4
await m.getByRole('button', { name: 'Next', exact: true }).click();
await toastGone(f);
await m.getByRole('button', { name: /^Update$/ }).click();
toasts = await collectToasts(page, f, 15_000);
test.info().annotations.push({ type: 'toasts', description: toasts.join(' | ') || '(ํ ์คํธ ์์)' });
expect(toasts.join(' ')).toMatch(/Update Success/);
});
await test.step('4. ์ฌ์กฐํ: ์์ ๊ฐ ยท ๋ชฉ๋ก ๊ฐฑ์ ', async () => {
const c = (await catalogList(page)).find((x) => (x.title ?? x.name) === TO);
test.info().annotations.push({ type: 'saved', description: JSON.stringify(c).slice(0, 800) });
expect(c, `๋ชฉ๋ก API ์ ${TO}`).toBeTruthy();
const g = await gotoApp(page, CATALOG_URL, /swcatalog/);
const item = catalogItem(g, TO);
await expect(item).toBeVisible();
await item.locator('.col-5').first().click();
const t = clean(await item.innerText());
test.info().annotations.push({ type: 'detail', description: t.slice(0, 500) });
expect(t).toContain(DESC);
expect(t).toMatch(/CPU : 2 Core/);
});
});tests/iv5/IV5_S18.spec.ts ยท 39์ค ยท ํด์ 6d1a0d83 ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
// IV5_S18: TC-APP-004 Repository ์ปดํฌ๋ํธ ์ญ์ [์ ์, ๊ณตํต]
// v0.6 TC-ID: TC-APP-REP-04 โ ๊ธฐ์กด spec(mcmp-e2e mc-web-console/specs/sw/TC-APP-REP-04-repository-์ปดํฌ๋ํธ-์ญ์ .spec.ts)์ ์ ์ฐจ๋ง ์ฐธ๊ณ (์ฌ์ฉ์ ๊ฒฐ์ 2026-10-14)
// ํ๋ฉด: Applications > Repository โ am-test-docker-hosted ์์ธ โ ์ปดํฌ๋ํธ ํ Delete โ Delete Component ๋ชจ๋ฌ
// ์์ ์ํ: admin(mcmp) ๋ก๊ทธ์ธ, ws01/default. IV5_S11 ์๋ฃ, ์ญ์ ๊ฒ์ฆ์ฉ Component ์กด์ฌ(IV5_S15 ๋ฐ์
์ผ๋ก ์๊ธด ์ด๋ฏธ์ง)
// ์ ์ฐจ: 1.Repository ์์ธ ์ด๋ 2.์ญ์ ๋์ Component ํ์ธ 3.Delete 4.ํ์ธ ๋ชจ๋ฌ Delete 5.Component ๋ชฉ๋ก ๊ฐฑ์
// ๊ธฐ๋๊ฒฐ๊ณผ: ์ญ์ ํ์ธ ๋ชจ๋ฌ, Component ์ญ์ ์ฑ๊ณต ๋ฉ์์ง, ๋์ Component ๋ชฉ๋ก์์ ์ ๊ฑฐ
// ๋น๊ณ : ์์ธ ํ๋ฉด์ File Upload ๋ฒํผ์ FE ์์ ์ฃผ์ ์ฒ๋ฆฌ(2026-10-14) โ ์ปดํฌ๋ํธ๋ ๋ฐ์
(IV5_S15)์ผ๋ก๋ง ์๊ธด๋ค. ์ปดํฌ๋ํธ๊ฐ ์์ผ๋ฉด SKIP(์ ํ ๋ฏธ์ถฉ์กฑ)
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { openRepositoryDetail, repoList, collectToasts, toastGone, appApi, clean } from '../../lib/iv5-app';
const REPO = 'am-test-docker-hosted';
test('IV5_S18 TC-APP-004 Repository ์ปดํฌ๋ํธ ์ญ์ ', async ({ page }) => {
await loginAsAdmin(page);
test.skip(!(await repoList(page)).some((r) => r.name === REPO), `์ ํ IV5_S10 ์ ${REPO} ์์`);
const comps = ((await appApi(page, 'GET', `/oss/v1/components/nexus/list/${REPO}`)).body?.data ?? []) as any[];
test.info().annotations.push({ type: 'components', description: `${comps.length}๊ฑด: ${comps.map((c) => c.name).join(', ')}` });
test.skip(!comps.length, `${REPO} ์ ์ญ์ ํ Component ์์(๋ฐ์
์คํจ ๋ฑ ์ ํ ๋ฏธ์ถฉ์กฑ)`);
const f = await test.step('1~2. ์์ธ ์ด๋ ยท Component ํ์ธ', () => openRepositoryDetail(page, REPO));
const target = clean(await f.locator('.tabulator-row').first().locator('.tabulator-cell').first().innerText());
await test.step(`3~4. ${target} Delete โ ํ์ธ ๋ชจ๋ฌ Delete`, async () => {
await f.locator('.tabulator-row').first().locator('#delete-btn').click();
const m = f.locator('#deleteComponent');
await expect(m.getByText('Delete Component')).toBeVisible();
await toastGone(f);
await m.getByText('Delete', { exact: true }).last().click();
const t = await collectToasts(page, f, 5000);
test.info().annotations.push({ type: 'delete', description: t.join(' | ') || '(ํ ์คํธ ์์)' });
expect(t.join(' ')).toMatch(/Deleted successfully/);
});
await test.step('5. Component ๋ชฉ๋ก ๊ฐฑ์ ', async () => {
const after = ((await appApi(page, 'GET', `/oss/v1/components/nexus/list/${REPO}`)).body?.data ?? []) as any[];
test.info().annotations.push({ type: 'after', description: `${after.length}๊ฑด` });
expect(after.length).toBe(comps.length - 1);
});
});tests/iv5/IV5_S19.spec.ts ยท 36์ค ยท ํด์ 561dd9dd ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
// IV5_S19: TC-APP-012 Catalog ์ญ์ (Trash) [์ ์, ๊ณตํต]
// v0.6 TC-ID: TC-APP-CAT-07 โ ๊ธฐ์กด spec(mcmp-e2e mc-web-console/specs/sw/TC-APP-CAT-07-catalog-์ญ์ .spec.ts)์ ์ ์ฐจ๋ง ์ฐธ๊ณ (์ฌ์ฉ์ ๊ฒฐ์ 2026-10-14)
// ํ๋ฉด: Applications > SW Catalogs โ Ghost CMS (edited) ํ ์ญ์ ์์ด์ฝ โ Confirm Catalog Deletion ๋ชจ๋ฌ
// ์์ ์ํ: admin(mcmp) ๋ก๊ทธ์ธ, ws01/default. IV5_S17 ์๋ฃ(๋ฐฐํฌ ํ
์คํธ์ ํ์ํ Catalog ๋ ์ฌ์ฉ ์๋ฃ)
// ์ ์ฐจ: 1.๋์ ํ ์ญ์ (ํด์งํต) ์์ด์ฝ 2.์ญ์ ํ์ธ ๋ชจ๋ฌ ํ์ 3.Delete 4.๋ชฉ๋ก์์ ์ ๊ฑฐ 5.(์ ๋ฆฌ) IV5_S15 ๋ฐ์
์นดํ๋ก๊ทธ ghost-import-test ๋ ๊ฐ์ ๋ฐฉ๋ฒ์ผ๋ก ์ญ์ (์์ผ๋ฉด)
// ๊ธฐ๋๊ฒฐ๊ณผ: ์ญ์ ํ์ธ ๋ชจ๋ฌ, Catalog ์ญ์ ์ฑ๊ณต ๋ฉ์์ง, ์ญ์ ๋์ Catalog ๋ชฉ๋ก์์ ์ ๊ฑฐ
// ๋น๊ณ : S17 ์์ ์ด๋ฆ์ 'Ghost CMS (edited)' ๋ก ๋ฐ๊ฟ. ๋ฐฐํฌ๋ ์ฑ์ด ์์ผ๋ฉด Delete ๋นํ์ฑ(๋ชจ๋ฌ ์๋ด)
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { gotoApp, CATALOG_URL, catalogAction, catalogList, collectToasts, toastGone } from '../../lib/iv5-app';
const TARGETS = ['Ghost CMS (edited)', 'Ghost CMS', 'ghost-import-test'];
test('IV5_S19 TC-APP-012 Catalog ์ญ์ (Trash)', async ({ page }) => {
await loginAsAdmin(page);
const names = (await catalogList(page)).map((c) => String(c.title ?? c.name));
const todo = TARGETS.filter((t) => names.includes(t));
test.skip(!todo.length, '์ญ์ ํ Ghost Catalog ์์(์ ํ IV5_S16/S17 ๋ฏธ์ถฉ์กฑ)');
for (const name of todo) {
await test.step(`${name}: ์ญ์ ์์ด์ฝ โ Confirm Catalog Deletion โ Delete`, async () => {
const f = await gotoApp(page, CATALOG_URL, /swcatalog/);
await catalogAction(f, name, 'delete');
const m = f.locator('#deleteConfirmModal');
await expect(m.getByText('Confirm Catalog Deletion')).toBeVisible();
await expect(m.getByRole('button', { name: 'Delete', exact: true })).toBeEnabled({ timeout: 30_000 });
await toastGone(f);
await m.getByRole('button', { name: 'Delete', exact: true }).click();
const t = await collectToasts(page, f, 5000);
test.info().annotations.push({ type: `delete-${name}`, description: t.join(' | ') || '(ํ ์คํธ ์์)' });
expect(t.join(' ')).toMatch(/successfully deleted/);
expect((await catalogList(page)).map((c) => String(c.title ?? c.name))).not.toContain(name);
});
}
});tests/iv5/IV5_S20.spec.ts ยท 29์ค ยท ํด์ a0bc955e ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
// IV5_S20: TC-APP-005 Repository ์ญ์ [์ ์, ๊ณตํต]
// v0.6 TC-ID: TC-APP-REP-05 โ ๊ธฐ์กด spec(mcmp-e2e mc-web-console/specs/sw/TC-APP-REP-05-repository-์ญ์ .spec.ts, describe.skip ์ํ)์ ์ ์ฐจ๋ง ์ฐธ๊ณ (์ฌ์ฉ์ ๊ฒฐ์ 2026-10-14)
// ํ๋ฉด: Applications > Repository โ ๋์ ํ Delete โ Delete Repository ๋ชจ๋ฌ
// ์์ ์ํ: admin(mcmp) ๋ก๊ทธ์ธ, ws01/default. IV5_S18 ์๋ฃ(์ญ์ ๋์ Repository ๋ด ํ์ Component ์ ๋ฆฌ)
// ์ ์ฐจ: 1.Repository ๋ชฉ๋ก 2.am-test-docker-hosted ํ Delete 3.ํ์ธ ๋ชจ๋ฌ Delete 4.๋ชฉ๋ก์์ ์ ๊ฑฐ ยท Nexus ๋ชฉ๋ก API ์์ ์ ๊ฑฐ
// ๊ธฐ๋๊ฒฐ๊ณผ: Repository ์ญ์ ์ฑ๊ณต ๋ฉ์์ง, ๋ชฉ๋ก์์ ์ ๊ฑฐ, Nexus ์์๋ ์ ๊ฑฐ
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { deleteRepository, repoList, gotoApp, REPO_URL, repoRow, showAllRows } from '../../lib/iv5-app';
const REPO = 'am-test-docker-hosted';
test('IV5_S20 TC-APP-005 Repository ์ญ์ ', async ({ page }) => {
await loginAsAdmin(page);
test.skip(!(await repoList(page)).some((r) => r.name === REPO), `์ ํ IV5_S10 ์ ${REPO} ์์`);
await test.step(`1~3. ${REPO} Delete โ ํ์ธ ๋ชจ๋ฌ Delete`, async () => {
const { toasts } = await deleteRepository(page, REPO);
test.info().annotations.push({ type: 'delete', description: toasts.join(' | ') || '(ํ ์คํธ ์์)' });
expect(toasts.join(' ')).toMatch(/Deleted successfully/);
});
await test.step('4. ๋ชฉ๋ก ยท Nexus ๋ชฉ๋ก API ์์ ์ ๊ฑฐ', async () => {
const f = await gotoApp(page, REPO_URL, /repository/);
await showAllRows(f);
await expect(repoRow(f, REPO)).toHaveCount(0);
expect((await repoList(page)).some((r) => r.name === REPO)).toBe(false);
});
});tests/iv5/IV5_S21.spec.ts ยท 42์ค ยท ํด์ 27d1329c ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
// IV5_S21: TC-APP-017 ์ ํ๋ฆฌ์ผ์ด์
๋ฐฐํฌ ์ํ ๋ชฉ๋ก ๊ฐฑ์ (Refresh) [์ ์, ๊ณตํต]
// v0.6 TC-ID: TC-APP-APPS-01 โ ๊ธฐ์กด spec(mcmp-e2e mc-web-console/specs/sw/TC-APP-APPS-01-๋ฐฐํฌ-์ํ-๋ชฉ๋ก-๊ฐฑ์ -refresh.spec.ts)์ ์ ์ฐจ๋ง ์ฐธ๊ณ (์ฌ์ฉ์ ๊ฒฐ์ 2026-10-14)
// ํ๋ฉด: Applications > App Status (iframe :18084/web/applications/status) โ Refresh
// ์์ ์ํ: admin(mcmp) ๋ก๊ทธ์ธ, ws01/default. VM/K8S ๋ฐฐํฌ ์ด๋ ฅ ์กด์ฌ(IV5_S01~S08)
// ์ ์ฐจ: 1.App Status ์ง์
2.Refresh ํด๋ฆญ โ ๋ชฉ๋ก ์ฌ์กฐํ(GET /api/applications/status/groups) ยท ๊ฐฑ์ ์๊ฐ ๋ณ๊ฒฝ 3.์ต๊ทผ VM/K8S ๋ฐฐํฌ ํญ๋ชฉ์ด ๋ชฉ๋ก์ ํ์
// ๊ธฐ๋๊ฒฐ๊ณผ: ๋ฐฐํฌ ์ํ ๋ชฉ๋ก์ด ๊ฐฑ์ ๋จ
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { gotoApp, STATUS_URL, showAllRows, clean } from '../../lib/iv5-app';
test('IV5_S21 TC-APP-017 ์ ํ๋ฆฌ์ผ์ด์
๋ฐฐํฌ ์ํ ๋ชฉ๋ก ๊ฐฑ์ (Refresh)', async ({ page }) => {
await loginAsAdmin(page);
const f = await gotoApp(page, STATUS_URL, /status/);
const stamp = async () => clean((await f.locator('body').innerText()).match(/\d{4}\. ?\d{1,2}\. ?\d{1,2}\. ?[^\n]*?\d{1,2}:\d{2}:\d{2}/)?.[0] ?? '');
const before = await stamp();
let refreshed: any[] = [];
await test.step('1~2. Refresh โ ๋ชฉ๋ก ์ฌ์กฐํ ยท ๊ฐฑ์ ์๊ฐ', async () => {
await page.waitForTimeout(1500);
const [res] = await Promise.all([
page.waitForResponse((r) => /\/api\/applications\/status\/groups/.test(r.url()), { timeout: 30_000 }),
f.getByRole('button', { name: 'Refresh', exact: true }).click(),
]);
await page.waitForTimeout(1500);
const after = await stamp();
refreshed = ((await res.json().catch(() => ({})))?.data ?? []) as any[];
test.info().annotations.push({ type: 'refresh', description: `GET ${res.status()} ${res.url().replace(/^https?:\/\/[^/]+/, '')} / ๊ฐฑ์ ์๊ฐ ${before} โ ${after}` });
expect(res.status()).toBe(200);
expect(after, '๊ฐฑ์ ์๊ฐ ๋ณ๊ฒฝ').not.toBe(before);
});
await test.step('3. ๋ชฉ๋ก = API(VM/K8S ๋ฐฐํฌ ํญ๋ชฉ)', async () => {
await showAllRows(f);
const api = refreshed; // ํ๋ฉด์ด ๋ฐ์ ์๋ต(ํ
์คํธ ์ปจํ
์คํธ ์ง์ ํธ์ถ์ ์ธ์ฆ ํค๋๊ฐ ์์ด 0๊ฑด, 2026-10-14)
const rows = (await f.locator('.tabulator-row').allInnerTexts()).map(clean);
test.info().annotations.push({ type: 'rows', description: `ํ๋ฉด ${rows.length} / API ${api.length}: ${rows.slice(0, 6).join(' || ')}` });
const total = Number((await f.locator('body').innerText()).match(/of (\d+) rows/)?.[1] ?? rows.length); // ํ์ด์ง ํฌ๊ธฐ ์ต๋ 10
test.info().annotations.push({ type: 'total', description: `ํ๋ฉด ์ ์ฒด ${total}๊ฑด / API ${api.length}๊ฑด` });
expect(total).toBe(api.length);
expect(rows.some((r) => /^VM /.test(r)) || rows.some((r) => /^K8S /.test(r)), 'VM/K8S ๋ฐฐํฌ ํญ๋ชฉ').toBe(true);
});
});tests/iv5/IV5_S22.spec.ts ยท 53์ค ยท ํด์ 619a390d ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
// IV5_S22: TC-APP-018 ์ ํ๋ฆฌ์ผ์ด์
Rating ์ ์ถ [์ ์, ๊ณตํต]
// v0.6 TC-ID: TC-APP-APPS-04 โ ๊ธฐ์กด spec(mcmp-e2e mc-web-console/specs/sw/TC-APP-APPS-04-rating-์ ์ถ.spec.ts)์ ์ ์ฐจ๋ง ์ฐธ๊ณ (์ฌ์ฉ์ ๊ฒฐ์ 2026-10-14)
// ํ๋ฉด: Applications > App Status โ ๋ฐฐํฌ ํญ๋ชฉ ํ์ Rating โ Application Rating ๋ชจ๋ฌ(#rating-modal) โ Submit
// ์์ ์ํ: admin(mcmp) ๋ก๊ทธ์ธ, ws01/default. Rating ๋์ ๋ฐฐํฌ ํญ๋ชฉ ์กด์ฌ
// ์ ์ฐจ: 1.App Status ์ฒซ ๋ฐฐํฌ ํญ๋ชฉ์ Rating 2.๋ณ์ 4 ์ ํ 3.Category Performance ยท Comment ยท ์ด๋ฆ ยท ์ด๋ฉ์ผ ์
๋ ฅ 4.Submit 5.SW Catalogs ๋ชฉ๋ก์ ํด๋น Catalog Rating ์ ์ยท๊ฑด์ ๊ฐฑ์
// ๊ธฐ๋๊ฒฐ๊ณผ: Rating ์ ์ถ ์ฑ๊ณต ๋ฉ์์ง, Rating ์ ์ ๋๋ ๊ฑด์ ๊ฐฑ์
// ๋น๊ณ : Rating ๋ฒํผ์ ์ค์น ์ํ ํญ๋ชฉ์์๋ง ํ์ฑ. ๋์์ด ์์ผ๋ฉด SKIP(2026-10-14 stage: ์ ํญ๋ชฉ Uninstalled). Submit ์ ๋ณ์ + Categoryยท์๊ฒฌยท์ด๋ฆยท์ด๋ฉ์ผ์ด ๋ชจ๋ ์์ด์ผ ํ์ฑํ(2026-10-14 FE ํ์ธ)
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { gotoApp, STATUS_URL, showAllRows, catalogList, collectToasts, toastGone } from '../../lib/iv5-app';
test('IV5_S22 TC-APP-018 ์ ํ๋ฆฌ์ผ์ด์
Rating ์ ์ถ', async ({ page }) => {
await loginAsAdmin(page);
// ๋ชฉ๋ก ๋ฐ์ดํฐ๋ ํ๋ฉด์ด ๋ฐ์ ์๋ต์ผ๋ก(ํ
์คํธ ์ปจํ
์คํธ ์ง์ ํธ์ถ์ ์ธ์ฆ ํค๋๊ฐ ์์ด 0๊ฑด, 2026-10-14)
const resP = page.waitForResponse((r) => /\/api\/applications\/status\/groups/.test(r.url()), { timeout: 90_000 });
const f = await gotoApp(page, STATUS_URL, /status/);
const groups = ((await (await resP).json().catch(() => ({})))?.data ?? []) as any[];
// Rating ๋ฒํผ์ ์ค์น๋(Uninstalled ์๋) ํญ๋ชฉ์์๋ง ํ์ฑ(2026-10-14: ์ ํญ๋ชฉ Uninstalled โ ๋นํ์ฑ)
const target = groups.find((g) => !/UNINSTALL|FAIL/i.test(String(g.status))); // UNINSTALLEDยทFAILED ๋ Rating ๋นํ์ฑ(2026-10-14)
test.info().annotations.push({ type: 'statuses', description: groups.map((g) => `${g.applicationName}:${g.status}`).join(', ') });
test.skip(!target, `Rating ๋์(์ค์น ์ํ) ๋ฐฐํฌ ํญ๋ชฉ ์์ โ ์ ${groups.length}๊ฑด Uninstalled/Failed, Rating ๋ฒํผ ๋นํ์ฑ`);
const before = (await catalogList(page)).find((c) => c.id === target!.catalogId);
test.info().annotations.push({ type: 'target', description: `${target!.applicationName} (catalogId ${target!.catalogId}) / ์ด์ Rating ${before?.averageRating ?? 0} (${before?.ratingCount ?? 0})` });
const m = f.locator('#rating-modal');
await test.step(`1. ${target!.applicationName} ํ Rating โ Application Rating`, async () => {
// ๋์ ํ์ด 2ํ์ด์ง ์ดํ์ผ ์ ์๋ค(ํ์ด์ง ํฌ๊ธฐ ์ต๋ 10) โ ํ์ฑ Rating ๋ฒํผ์ด ๋ณด์ผ ๋๊น์ง Next
await showAllRows(f);
const btn = f.locator('.tabulator-row #rating-btn:not([disabled])').first();
for (let i = 0; i < 6 && !(await btn.count()); i++) { await f.getByRole('button', { name: 'Next', exact: true }).click(); await page.waitForTimeout(800); }
test.info().annotations.push({ type: 'row', description: (await btn.locator('xpath=ancestor::div[contains(@class,"tabulator-row")]').innerText()).replace(/\s+/g, ' ') });
await btn.click();
await expect(m.getByText('Application Rating')).toBeVisible();
});
await test.step('2~4. ๋ณ์ 4 ยท Category ยท Comment ยท ์ด๋ฆ ยท ์ด๋ฉ์ผ โ Submit', async () => {
await m.locator('.rating-stars span.star').nth(3).click();
await m.locator('select').first().selectOption({ label: 'Performance' });
await m.getByPlaceholder('Enter detailed comments').fill('IV5_S22 rating test');
await m.getByPlaceholder('Enter your name').fill('mcmp');
await m.getByPlaceholder('Enter your email').fill('[email protected]');
await toastGone(f);
await m.getByRole('button', { name: 'Submit', exact: true }).click();
const t = await collectToasts(page, f, 5000);
test.info().annotations.push({ type: 'submit', description: t.join(' | ') || '(ํ ์คํธ ์์)' });
expect(t.join(' ')).toMatch(/Rating submitted successfully/);
});
await test.step('5. Catalog Rating ์ ์ยท๊ฑด์ ๊ฐฑ์ ', async () => {
const after = (await catalogList(page)).find((c) => c.id === target!.catalogId);
test.info().annotations.push({ type: 'after', description: `${after?.averageRating ?? 0} (${after?.ratingCount ?? 0})` });
expect(Number(after?.ratingCount ?? 0)).toBe(Number(before?.ratingCount ?? 0) + 1);
});
});tests/iv5/IV5_S23.spec.ts ยท 79์ค ยท ํด์ 62189d52 ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
// IV5_S23: TC-APP-015 VM ๋ค์ค ๋ฐฐํฌ (Clustering) [์ ์, AWS]
// v0.6 TC-ID: TC-APP-DEP-02 โ ๊ธฐ์กด spec(mcmp-e2e integration/specs/05-scenarios/c4-app-deploy-infra.spec.ts Step4, mc-web-console/specs/sw/TC-APP-DEP-02-vm-clustering.spec.ts)์ ์ ์ฐจ๋ง ์ฐธ๊ณ (์ฌ์ฉ์ ๊ฒฐ์ 2026-10-14)
// ํ๋ฉด: Applications > SW Catalogs โ Deploy โ Application Installation(#install-form) โ Spec Check โ Deploy โ App Status
// ์์ ์ํ: admin(mcmp) ๋ก๊ทธ์ธ, ws01/default. Workflow ๋ก ๋ง๋ MCI(VM 2๋ ์ด์, Running), Built-in Elasticsearch Catalog
// ์ ์ฐจ: 1.Deploy 2.Target Infra VM ยท Namespace ยท Infra ID(WF_CLUSTER_INFRA) ์ ํ 3.Deployment Target VM ยท Clustering 4.VM 2๋ ์ด์ ์ ํ 5.Application Elasticsearch ยท Port 9200
// 6.Spec Check(์ฌ์ ๋ฏธ๋ฌ confirm ์ ์๋ฝ) โ Deploy 7.App Status ์์ ์ ํ VM ๋ค์ Elasticsearch ๊ฐ ์งํ ํ Running(์ฑ๊ณต)
// ์
๋ ฅ๊ฐ(์ํธ): Catalog Elasticsearch / Target VM Clustering / MCI VM 2๋ ์ด์ / Cluster node count 2 ์ด์
// ๊ธฐ๋๊ฒฐ๊ณผ: Elasticsearch clustering ๋ฐฐํฌ ์์ฒญ ์์ฑ, ์ ํํ VM ๋ค์ node ๊ตฌ์ฑ, Apps Status ์์ ๋ฐฐํฌ ์ฑ๊ณต
// ๋น๊ณ : ๐ฐ VM 2๋ ์ด์ MCI ๊ฐ ํ์(WF ๋ก ์์ฑ) โ ์คํ ๋ณด๋ฅ(์ฌ์ฉ์ ๊ฒฐ์ 2026-10-14). ๋์ Infra ๋ ํ๊ฒฝ๋ณ์ WF_CLUSTER_INFRA(๊ธฐ๋ณธ ์์ โ SKIP)
// Clustering ๋ผ๋์ค๋ Redis/Elasticsearch ์นดํ๋ก๊ทธยทVM ๋์์์๋ง ํ์(FE ํ์ธ)
// 2026-10-16 ์คํ: WF multi-csp-vm-deploy ๋ก mwc1(AWS t3.medium ร2, gc1-aws-1ยท2, Image img1) ์์ฑ ํ WF_CLUSTER_INFRA=mwc1. 1์ฐจ ์คํจ โ #vm-namespace ๊ฐ disabled(๊ณ ์ ) โ ๊ฐ ํ์ธ์ผ๋ก ์์
// 2์ฐจ ์คํ FAIL: ๋ฐฐํฌ ๋ก๊ทธ 'All VM deployments successful' ์ดํ ๋ VM ๋ชจ๋ Stopped/Unhealthy. Terminal ํ์ธ: ์ปจํ
์ด๋ Exited(78),
// 'bootstrap checks failed [1]', VM vm.max_map_count=65530(ES ์๊ตฌ 262144) โ ์ค์น๊ฐ ์ปค๋ ์ค์ ์ ๋ง์ถ์ง ์์(๊ฒฐํจ ํ๋ณด). 7๋จ๊ณ๋ ์ํ ์ฌ์กฐํ๋ก ๋ณด๊ฐ
// Clustering ์์ #vm-name ์ ์ ํํ VM ์ด ์นฉ(X)์ผ๋ก ์์ด๊ณ ๋ชฉ๋ก์์ ๋น ์ง(2026-10-16 ํ๋ก๋ธ). ์ ๋ฆฌ: multi-csp-vm-cleanup(INFRA_ID_LIST=mwc1)
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { gotoApp, CATALOG_URL, STATUS_URL, collectToasts } from '../../lib/iv5-app';
import { WORKSPACE, DEFAULT_PROJECT } from '../../lib/data';
const INFRA = process.env.WF_CLUSTER_INFRA ?? '';
const NS = process.env.WF_CLUSTER_NS ?? DEFAULT_PROJECT;
test('IV5_S23_AWS TC-APP-015 VM ๋ค์ค ๋ฐฐํฌ (Clustering)', async ({ page }) => {
test.setTimeout(40 * 60_000);
test.skip(!INFRA, 'WF_CLUSTER_INFRA ๋ฏธ์ง์ โ VM 2๋ ์ด์ MCI ์ค๋น ์ ์คํ ๋ณด๋ฅ(์ฌ์ฉ์ ๊ฒฐ์ 2026-10-14)');
await loginAsAdmin(page);
const f = await gotoApp(page, CATALOG_URL, /swcatalog/);
const m = f.locator('#install-form');
await test.step('1~2. Deploy โ VM ยท Namespace ยท Infra ID', async () => {
await f.getByRole('button', { name: 'Deploy', exact: true }).click();
await expect(m.getByText('Application Installation')).toBeVisible();
await m.locator('#infra').selectOption({ label: 'VM' });
// 2026-10-16: Namespace ๋ ํค๋ Project ๋ก ๊ณ ์ (disabled, 'Deployment targets are scoped to ws01 / default') โ ๊ฐ๋ง ํ์ธ
if (await m.locator('#vm-namespace').isDisabled()) await expect(m.locator('#vm-namespace')).toHaveValue(NS);
else await m.locator('#vm-namespace').selectOption({ label: NS });
await m.locator('#vm-mci').selectOption({ label: INFRA });
});
const vms: string[] = [];
await test.step('3~5. VM ยท Clustering ยท VM 2๋ ์ด์ ยท Elasticsearch ยท Port 9200', async () => {
await m.locator('#vm-target-single').check();
await m.locator('select').filter({ has: f.locator('option', { hasText: /Elasticsearch/ }) }).first().selectOption({ label: (await m.locator('option', { hasText: /Elasticsearch/ }).first().innerText()).trim() });
await m.locator('#Clustering').check();
const vmSel = m.locator('#vm-name');
for (let i = 0; i < 2; i++) {
const opt = (await vmSel.locator('option').allInnerTexts()).map((o) => o.trim()).find((o) => o && o !== 'Select VM')!;
await vmSel.selectOption({ label: opt }); vms.push(opt);
}
await m.getByPlaceholder('8080').fill('9200');
test.info().annotations.push({ type: 'vms', description: vms.join(', ') });
expect(vms.length).toBeGreaterThanOrEqual(2);
});
await test.step('6. Spec Check โ Deploy', async () => {
await m.getByRole('button', { name: 'Spec Check' }).click();
test.info().annotations.push({ type: 'spec-check', description: (await collectToasts(page, f, 15_000)).join(' | ') });
const [sub] = await Promise.all([
page.waitForResponse((r) => /deployment-submissions\/VM/.test(r.url()) && r.request().method() === 'POST', { timeout: 60_000 }),
m.getByRole('button', { name: 'Deploy', exact: true }).click(),
]);
test.info().annotations.push({ type: 'submit', description: `${sub.status()} ${(await sub.text()).slice(0, 300)}` });
expect(sub.status()).toBeLessThan(300);
await expect(m.getByText('Deployment completed.')).toBeVisible({ timeout: 30 * 60_000 });
});
await test.step('7. App Status: ์ ํ VM ๋ค์ Elasticsearch Running', async () => {
// 2026-10-16: ๋ฐฐํฌ ์งํ ์ํ๋ SUCCESS(๋ฐฐํฌ ์๋ฃ) โ ์ดํ RUNNING/STOPPED ๋ก ๋ฐ๋. 1์ฐจ ์คํ์์ SUCCESS ๋ฅผ ๋ฐ๋ก ์ฝ๊ณ ํ์ โ ์ต๋ 5๋ถ ์ฌ์กฐํ
let es: any[] = [];
for (let i = 0; i < 6; i++) {
const resP = page.waitForResponse((r) => /\/api\/applications\/status\/groups/.test(r.url()), { timeout: 90_000 });
await gotoApp(page, STATUS_URL, /status/, NS);
const groups = ((await (await resP).json())?.data ?? []) as any[];
es = groups.filter((g) => /Elasticsearch/.test(g.applicationName) && g.mciId === INFRA);
test.info().annotations.push({ type: `status-${i}`, description: es.map((g) => `${g.vmId}:${g.status}`).join(', ') });
if (es.length && es.every((g) => /RUNNING|STOPPED|FAIL/i.test(g.status))) break;
await page.waitForTimeout(50_000);
}
expect(es.filter((g) => /RUNNING/i.test(g.status)).length).toBeGreaterThanOrEqual(2);
});
});tests/iv5/IV5_S24.spec.ts ยท 35์ค ยท ํด์ 57f7eb94 ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
// IV5_S24: TC-APP-024 ๋ฑ๋ก๋ Object Storage ์กฐํยท์ ํ [์ ์, Alibaba]
// v0.6 TC-ID: ์์(mc-application-manager ์ํธ์๋ง ์๋ TC, 2026-10-15 ์์ฑ)
// ํ๋ฉด: Applications > SW Catalogs โ Deploy โ Application Installation โ [JupyterLab for Object Storage] โ Object Storage Configuration
// ์์ ์ํ: admin(mcmp) ๋ก๊ทธ์ธ, ws01/default. ์ํ Project(default)์ Object Storage ๋ฑ๋ก โ Data Migrations ๋ก ๋ง๋ Alibaba bucket mcmp-e2e-v07-osam(Tumblebug objectStorage)
// ์ ์ฐจ: 1.Deploy โ VM ยท Infra mc3 ยท VM g3-1 ยท JupyterLab for Object Storage 2.Registered Object Storage Refresh(GET /applications/object-storages?namespace=default) 3.์ํ ๋ฒํท ์ ํ
// ์
๋ ฅ๊ฐ(์ํธ): Registered Object Storage ์ํ ๋ฒํท
// ๊ธฐ๋๊ฒฐ๊ณผ: ์ ํ Project ์ ๋ฑ๋ก ๋ฒํท ๋ชฉ๋ก ํ์, ์ฌ์ฉ ๊ฐ๋ฅํ ์ํ ๋ฒํท ์ ํ ๊ฐ๋ฅ
// ๋น๊ณ : ์ํ ๋ฒํท์ ์ค๋น ์ ํธ wsl/probe/obj-prep.spec.ts ๋ก ์์ฑ(2026-10-15, Alibaba ap-northeast-2, ๊ฐ์ฒด 10๊ฐ). Deploy ๋ ๋๋ฅด์ง ์์
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { openJupyterDeploy, refreshStorages, OBJ_BUCKET, OBJ_NS } from '../../lib/iv5-objstore';
test('IV5_S24_Alibaba TC-APP-024 ๋ฑ๋ก๋ Object Storage ์กฐํยท์ ํ', async ({ page }) => {
await loginAsAdmin(page);
const { m } = await test.step('1. Deploy โ VM mc3/g3-1 ยท JupyterLab for Object Storage', () => openJupyterDeploy(page, 'VM'));
await test.step('2. Registered Object Storage Refresh โ ๋ฑ๋ก ๋ฒํท ๋ชฉ๋ก', async () => {
const r = await refreshStorages(page, m);
test.info().annotations.push({ type: 'storages', description: `HTTP ${r.status} / API ${r.data.map((s) => `${s.id}(${s.provider}/${s.region}/${s.status})`).join(', ')} / ํ๋ฉด ${r.shown.join(', ')}` });
expect(r.status).toBe(200);
expect(r.shown.sort()).toEqual(r.data.map((s) => s.id).sort());
const mine = r.data.find((s) => s.id === OBJ_BUCKET);
expect(mine, `${OBJ_NS} ์ ๋ฑ๋ก๋ ${OBJ_BUCKET}`).toBeTruthy();
expect(String(mine.status)).toMatch(/available/i);
});
await test.step(`3. ์ํ ๋ฒํท ${OBJ_BUCKET} ์ ํ`, async () => {
await m.locator('#vmObjectStorageEnabled').check().catch(() => {});
const box = m.locator(`#vm-object-storage-${OBJ_BUCKET}`);
await expect(box).toBeEnabled();
await box.check();
await expect(box).toBeChecked();
});
await m.getByRole('button', { name: 'Cancel' }).first().click().catch(() => {});
});tests/iv5/IV5_S25.spec.ts ยท 57์ค ยท ํด์ ce7516d4 ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
// IV5_S25: TC-APP-025 Object Storage ์ฐ๊ฒฐ ์ฌ์ ํ์ธ(Storage Check) [์ ์, Alibaba]
// v0.6 TC-ID: ์์(mc-application-manager ์ํธ์๋ง ์๋ TC, 2026-10-15 ์์ฑ)
// ํ๋ฉด: SW Catalogs โ Deploy โ JupyterLab for Object Storage โ Object Storage Configuration โ Storage Check
// ์์ ์ํ: admin(mcmp) ๋ก๊ทธ์ธ, ws01/default. IV5_S24 ์๋ฃ(์ํ ๋ฒํท ๋ฑ๋ก), ๋์ VM mc3/g3-1 Running
// ์ ์ฐจ: 1.์ํ ๋ฒํท ยท Access Mode Read and write ยท Jupyter Access Token(12์ ์ด์) โ Storage Check 2.์ ๊ทผ ๋ถ๊ฐ ๋ฒํท(๋ฑ๋ก ์ํ Failed ์ธ ๋ฒํท)๋ง ์ ํ โ Storage Check 3.์ ๊ทผ ๋ถ๊ฐ ์ํ๋ก Deploy โ ์ฐจ๋จ
// ์
๋ ฅ๊ฐ(์ํธ): Access Mode Read and write / Token ์ํ์ฉ 12์ ์ด์ / ์ ์ ๋ฒํทยท์ ๊ทผ ๋ถ๊ฐ ์ํ ๋ฒํท
// ๊ธฐ๋๊ฒฐ๊ณผ: ์ ์ ๋ฒํท์ ๊ฒ์ฌ ํต๊ณผ, ์ ๊ทผ ๋ถ๊ฐ ์กฐ๊ฑด์ ์ค๋ฅ ํ์ยท๋ฐฐํฌ ์ฐจ๋จ
// ๋น๊ณ : ์ ๊ทผ ๋ถ๊ฐ ๋ฒํท์ default ์ ๋ฑ๋ก๋ ์ํ Failed ๋ฒํท(2026-10-15: mcmp-test-nhn-*). ์์ผ๋ฉด ๊ทธ ๋จ๊ณ๋ SKIP
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { openJupyterDeploy, refreshStorages, fillObjectStorage, storageCheck, OBJ_BUCKET } from '../../lib/iv5-objstore';
import { collectToasts, toastGone } from '../../lib/iv5-app';
test('IV5_S25_Alibaba TC-APP-025 Object Storage ์ฐ๊ฒฐ ์ฌ์ ํ์ธ(Storage Check)', async ({ page }) => {
test.setTimeout(10 * 60_000);
await loginAsAdmin(page);
const { f, m } = await openJupyterDeploy(page, 'VM');
const list = await refreshStorages(page, m);
const bad = list.data.find((s) => !/available/i.test(String(s.status)));
await test.step(`1. ์ ์ ๋ฒํท ${OBJ_BUCKET} ยท Read and write ยท Token โ Storage Check ํต๊ณผ`, async () => {
await fillObjectStorage(m, { buckets: [OBJ_BUCKET], mode: 'READ_WRITE' });
const r = await storageCheck(page, f, m);
test.info().annotations.push({ type: 'check-ok', description: `HTTP ${r.status} ${JSON.stringify(r.body).slice(0, 500)} / ${r.toasts.join(' | ')}` });
expect(r.toasts.join(' ')).toMatch(/Object Storage check succeeded/);
expect(r.body?.data?.success ?? r.body?.success).toBe(true);
});
await test.step(`2~3. ์ ๊ทผ ๋ถ๊ฐ ๋ฒํท${bad ? ` ${bad.id}(${bad.status})` : ''} โ Storage Check ์คํจ ยท Deploy ์ฐจ๋จ`, async () => {
test.skip(!bad, '์ ๊ทผ ๋ถ๊ฐ(์ํ ๋น์ ์) ๋ฑ๋ก ๋ฒํท ์์');
// ์ํ๊ฐ Failed ์ธ ๋ฒํท์ ์ฒดํฌ๋ฐ์ค๊ฐ ๋นํ์ฑ(2026-10-15) โ ์ ํ ์์ฒด๊ฐ ์ฐจ๋จ๋๋ฉด ๊ทธ๊ฒ์ '์ค๋ฅ ํ์ยท๋ฐฐํฌ ์ฐจ๋จ'์ผ๋ก ๋ณธ๋ค
const box = m.locator(`#vm-object-storage-${bad!.id}`);
const disabled = await box.isDisabled();
const label = (await m.locator(`label[for="vm-object-storage-${bad!.id}"]`).innerText().catch(() => '')).replace(/\s+/g, ' ');
test.info().annotations.push({ type: 'bad-bucket', description: `${bad!.id} ์ฒดํฌ๋ฐ์ค ${disabled ? '๋นํ์ฑ(์ ํ ๋ถ๊ฐ)' : '์ ํ ๊ฐ๋ฅ'} / ๋ผ๋ฒจ ${label}` });
if (!disabled) {
await fillObjectStorage(m, { buckets: [bad!.id], mode: 'READ_WRITE' });
const r = await storageCheck(page, f, m);
test.info().annotations.push({ type: 'check-bad', description: `HTTP ${r.status} ${JSON.stringify(r.body).slice(0, 500)} / ${r.toasts.join(' | ')}` });
expect(r.toasts.join(' ')).toMatch(/Object Storage check failed/);
} else {
await fillObjectStorage(m, { buckets: [], mode: 'READ_WRITE' }); // ์ ๊ทผ ๊ฐ๋ฅํ ๋ฒํท์ ํ๋๋ ๊ณ ๋ฅด์ง ๋ชปํ ์ํ
}
await toastGone(f);
const deploy = m.getByRole('button', { name: 'Deploy', exact: true });
if (await deploy.isDisabled()) {
test.info().annotations.push({ type: 'deploy-blocked', description: 'Deploy ๋ฒํผ ๋นํ์ฑ(๋ฐฐํฌ ์ฐจ๋จ)' });
} else {
await deploy.click();
const t = await collectToasts(page, f, 5000);
test.info().annotations.push({ type: 'deploy-blocked', description: t.join(' | ') || '(ํ ์คํธ ์์)' });
expect(t.join(' ')).toMatch(/Object Storage|Spec Check|before deploy|Select at least one/i);
}
await expect(m.getByText('Deployment completed.')).toHaveCount(0);
});
await m.getByRole('button', { name: 'Cancel' }).first().click().catch(() => {});
});tests/iv5/IV5_S26.spec.ts ยท 60์ค ยท ํด์ af800887 ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
// IV5_S26: TC-APP-026 VM Jupyter Object Storage ์ฐ๋ ๋ฐฐํฌยท์ฝ๊ธฐ/์ฐ๊ธฐ [์ ์, Alibaba]
// v0.6 TC-ID: ์์(mc-application-manager ์ํธ์๋ง ์๋ TC, 2026-10-15 ์์ฑ)
// ํ๋ฉด: SW Catalogs โ Deploy โ JupyterLab for Object Storage(VM mc3/g3-1) โ Storage Check โ Spec Check โ Deploy โ Jupyter(http://<VM ๊ณต์ธ IP>:<port>)
// ์์ ์ํ: admin(mcmp) ๋ก๊ทธ์ธ, ws01/default. IV5_S25 ์ ์ ์กฐ๊ฑด, ์ํ VM mc3/g3-1 Running(๊ธฐ์กด Infra ์ฌ์ฌ์ฉ โ ์ฌ์ฉ์ ๊ฒฐ์ 2026-10-15)
// ์ ์ฐจ: 1.A: Read only ยท ํฌํธ 8888 ยท ํ
์คํธ PC IP/32 ํ์ฉ์ผ๋ก ๋ฐฐํฌ 2.Jupyter(token) ์ปค๋์์ ๊ธฐ๋ณธ notebook helper(storages / list_objects / download / upload)๋ก A ์ฝ๊ธฐ ๊ฐ๋ฅยท์ฐ๊ธฐ ๊ฑฐ๋ถ
// 3.A Uninstall(๊ฐ์ VM ์ ๊ฐ์ ์นดํ๋ก๊ทธ๋ ํ๋๋ง ์ค์น ๊ฐ๋ฅ) 4.B: Read and write ยท Prefix am-test/ ยท ํฌํธ 8889 ๋ก ๋ฐฐํฌ 5.B ์ฝ๊ธฐยท์ฐ๊ธฐ ๊ฐ๋ฅ ํ์ธ
// ์
๋ ฅ๊ฐ(์ํธ): Target VM / ์ํ ๋ฒํท / Access Mode Read only ยท Read and write / Token 12์ ์ด์
// ๊ธฐ๋๊ฒฐ๊ณผ: VM Jupyter ๋ฐฐํฌยท์ ์ ์ฑ๊ณต, Read only ๋ ์ฝ๊ธฐ ๊ฐ๋ฅยท์ฐ๊ธฐ ๊ฑฐ๋ถ, Read and write ๋ ์ฝ๊ธฐยท์ฐ๊ธฐ ๊ฐ๋ฅ
// ๋น๊ณ : ๐ฐ VM ์ ๊ธฐ์กด mc3 ์ฌ์ฉ(์ถ๊ฐ VM ์์). B ๋ IV5_S28(Prefix ๊ถํ)์์ ์ด์ด ์ฐ๊ณ IV5_S29 ์์ Uninstall
// 2026-10-15 1์ฐจ: AยทB ๋์ ๋ฐฐํฌ ์๋ โ B ๊ฐ 'Skipping deployment on VMs with existing installations: g3-1' ๋ก ์คํจ(์ ํ ๋์: VM ๋น ์นดํ๋ก๊ทธ 1๊ฐ) โ ์์ฐจ๋ก ๋ณ๊ฒฝ
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { deployJupyterVm, jupyterRun, jupyterVmIp, myPublicIp, uninstallJupyterVm, jupyterTarget, JUPYTER_APP_NAME, APPS, OBJ_BUCKET, OBJ_MCI, OBJ_VM } from '../../lib/iv5-objstore';
import { appStatus } from '../../lib/iv5';
test('IV5_S26_Alibaba TC-APP-026 VM Jupyter Object Storage ์ฐ๋ ๋ฐฐํฌยท์ฝ๊ธฐ/์ฐ๊ธฐ', async ({ page }) => {
test.setTimeout(70 * 60_000);
await loginAsAdmin(page);
const ip = await jupyterVmIp(page);
const cidr = `${await myPublicIp(page)}/32`;
test.info().annotations.push({ type: 'target', description: `${OBJ_MCI}/${OBJ_VM} ip=${ip} / ํ์ฉ CIDR ${cidr}` });
expect(ip).toMatch(/^\d+\.\d+\.\d+\.\d+$/);
const deploy = async (k: 'ro' | 'rw') => {
const a = APPS[k];
const r = await deployJupyterVm(page, { buckets: [OBJ_BUCKET], mode: a.mode, prefix: a.prefix, port: a.port, cidr });
test.info().annotations.push({ type: `deploy-${k}`, description: `spec: ${r.specToasts.join(' | ')} / submit ${r.submit} / done=${r.done} ${r.status} / ${r.toasts.join(' | ')}` });
expect(r.done, `${k} Deployment completed`).toBe(true);
};
await test.step('1. A: Read only ยท ํฌํธ 8888 ๋ฐฐํฌ(์ด๋ฏธ Running ์ด๋ฉด ์ฌ์ฌ์ฉ)', async () => {
const st = await appStatus(page, jupyterTarget(), JUPYTER_APP_NAME, 1);
if (/running/i.test(st)) test.info().annotations.push({ type: 'deploy-ro', description: `๊ธฐ์กด A(Running) ์ฌ์ฌ์ฉ` });
else await deploy('ro');
});
await test.step('2. A Read only(8888): ์ฝ๊ธฐ ๊ฐ๋ฅ ยท ์ฐ๊ธฐ ๊ฑฐ๋ถ', async () => {
const code = `objs = list_objects(alias, '')\nprint('LIST', len(objs))\nkey = objs.iloc[0]['key'] if len(objs) else None\nprint('READ', len(download(alias, key)) if key else -1)\nimport tempfile, os\np = tempfile.mktemp(); open(p,'w').write('ro-test')\ntry:\n upload(alias, p, 'am-test/ro-write.txt'); print('WRITE OK')\nexcept Exception as e:\n print('WRITE DENIED', type(e).__name__, str(e)[:120])\n`;
const r = await jupyterRun(page, ip, APPS.ro.port, code);
test.info().annotations.push({ type: 'helper', description: `notebook helper ํจ์: ${[...r.helper.matchAll(/def (\w+)/g)].map((x) => x[1]).join(', ')}` });
test.info().annotations.push({ type: 'ro', description: `${r.out.slice(0, 800)} ${r.err}` });
expect(r.out).toMatch(/LIST [1-9]/);
expect(r.out).toMatch(/READ [1-9]/);
expect(r.out).toMatch(/WRITE DENIED/);
});
await test.step('3. A Uninstall(VM ๋น ๊ฐ์ ์นดํ๋ก๊ทธ 1๊ฐ)', async () => {
const u = await uninstallJupyterVm(page);
test.info().annotations.push({ type: 'uninstall-ro', description: u.seen.join(' โ ') });
expect(u.done).toBe(true);
});
await test.step('4. B: Read and write ยท Prefix am-test/ ยท ํฌํธ 8889 ๋ฐฐํฌ', () => deploy('rw'));
await test.step('5. B Read and write(8889, Prefix am-test/): ์ฐ๊ธฐ ยท ์ฝ๊ธฐ ๊ฐ๋ฅ', async () => {
const code = `import tempfile\np = tempfile.mktemp(); open(p,'w').write('rw-test-iv5s26')\nupload(alias, p, 'am-test/iv5_s26.txt'); print('WRITE OK')\nprint('READBACK', download(alias, 'am-test/iv5_s26.txt').decode())\nprint('LIST', len(list_objects(alias, 'am-test/')))\n`;
const r = await jupyterRun(page, ip, APPS.rw.port, code);
test.info().annotations.push({ type: 'rw', description: `${r.out.slice(0, 800)} ${r.err}` });
expect(r.err).toBe('');
expect(r.out).toMatch(/WRITE OK/);
expect(r.out).toMatch(/READBACK rw-test-iv5s26/);
});
});tests/iv5/IV5_S27.spec.ts ยท 68์ค ยท ํด์ 461f5265 ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
// IV5_S27: TC-APP-027 K8S Jupyter Object Storage ์ฐ๋ ๋ฐฐํฌยท์ฝ๊ธฐ/์ฐ๊ธฐ [์ ์, AWS]
// v0.6 TC-ID: ์์(mc-application-manager ์ํธ์๋ง ์๋ TC, 2026-10-15 ์์ฑ)
// ํ๋ฉด: SW Catalogs(Project p1) โ Deploy โ Target Infra K8S ยท ClusterName โ JupyterLab for Object Storage โ Storage Class/Notebook volume ยท Allowed IPv4 CIDR(์์ปค SG TCP 30880)
// โ Ingress Host(<node IP>.nip.io, DNS ์ด๋ฆ ํ์) โ Object Storage(๋ฒํทยทAccess ModeยทToken) โ Storage Check โ Spec Check โ Deploy โ Jupyter(http://<node IP>.nip.io:30880)
// ์์ ์ํ: admin(mcmp) ๋ก๊ทธ์ธ, ws01/p1. ํด๋ฌ์คํฐยท์์ปค Ready(K8s CSP ์ฌ์ดํด k1s/g1s โ ์ฌ์ฉ์ ๊ฒฐ์ 2026-10-15: CSP ํ๋์ฉ), ์ฌ์ฉ ๊ฐ๋ฅํ Storage Class(gp2)
// ์ ์ฐจ: 1.K8S ยท k1s ยท JupyterLab ์ ํ, Storage Class ์ฒซ ๊ฐ ยท 1GiB ยท ํ
์คํธ PC IP/32 2.์ํ ๋ฒํท Read only ยท Token โ Storage Check โ Spec Check โ Deploy
// 3.kubectl ๋ก Jupyter Pod Ready ํ์ธ(IV4_S19 kubeconfig + ํ ํฐ) 4.Jupyter(token) ์ปค๋์์ notebook helper ๋ก ๋ชฉ๋กยท์ฝ๊ธฐ ๊ฐ๋ฅ, ์ฐ๊ธฐ ๊ฑฐ๋ถ ํ์ธ
// ์
๋ ฅ๊ฐ(์ํธ): Target Infra K8S / ์ํ ๋ฒํท / Access Mode / Storage ClassยทSize(ํ์๋ ์ฌ์ฉ ๊ฐ๋ฅ ๊ฐ) / Token 12์ ์ด์
// ๊ธฐ๋๊ฒฐ๊ณผ: Pod Ready ๋ฐ Jupyter ์ ์ ์ฑ๊ณต, Access Mode ์ ๋ง๋ ์ฝ๊ธฐยท์ฐ๊ธฐ ๋์
// ๋น๊ณ : p1 ์ ๋ฑ๋ก๋ ๋ฒํท์ ๋ค๋ฅธ ์ฃผ์ฒด์ ๋ฒํท(mcmp07-*)๋ฟ์ด๊ณ Data Migrations ๋ก ๋ง๋ ์ํ ๋ฒํท์ ns default ์๋ง ๋ฑ๋ก๋๋ค(2026-10-15 ํ์ธ)
// โ K8S ๋ Read only ๋ก๋ง ํ์ธ(๋จ์ ๋ฒํท์ ์ฐ์ง ์์, OBJ_K8S_BUCKET ์ผ๋ก ๋ณ๊ฒฝ ๊ฐ๋ฅ). Read and write ๋์์ VM(IV5_S26)์์ ํ์ธ. ๋ฐฐํฌ ์ฑ์ IV5_S29 ์์ Uninstall
// 2026-10-15 AWS(k1s) 1์ฐจ: BLOCKED(ํ๊ฒฝ) โ Storage Check SUCCESS ยท ์ ์ถ OK, ๊ทธ๋ฌ๋ PVC mcmp-jupyter-37(gp2) Pending โ Pod ์คํจ.
// ์์ธ: EKS ์ ebs-csi-controller CrashLoopBackOff('no EC2 IMDS role found', SA ebs-csi-controller-sa ์ IRSA/Pod Identity ์์) โ ํด๋ฌ์คํฐ ์์ฑ ์ธก(tumblebug/spider EKS ์ ๋์จ IAM) ํ๊ณ
// ์ฑ ๋ก๊ทธ: 'K8s Jupyter installation failed; inspect Pod events and AM Gateway connectivity' / ์ ์ถ ์ํ: 'Cluster or registry access was denied'
// โ ๊ทผ๋ณธ์์ธ: spider ๊ฐ IMDS hop limit 2 ๋ฅผ NodeGroup ์์ฑ ์งํ node ์๋ง ์ ์ฉ โ Scale ๋ก ์๋ก ๋ฌ node ๋ 1. cb-spider ์์ (Launch Template, MZC-CSC fork fix/aws-eks-nodegroup-imds-hoplimit)์ stage ์ ๋ฐ์ ํ ์ฌ์คํ PASS
// ์ ์ node: Ingress Controller ๊ฐ source-IP ๋ณด์กด(Local) โ Ingress Controller Pod ๊ฐ ์๋ node IP ๋ก Host ์ง์
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { deployJupyterK8s, jupyterRun, myPublicIp, JUPYTER_APP_NAME } from '../../lib/iv5-objstore';
import { appStatus } from '../../lib/iv5';
import { copyKubeconfig, consoleOp, writeTokenKubeconfig, kubectl, K8S_CYCLE_CLUSTER } from '../../lib/iv4-kubectl';
import fs from 'node:fs';
const PRJ = process.env.K8S_PROJECT ?? 'p1';
const CLUSTER = process.env.K8S_CLUSTER ?? K8S_CYCLE_CLUSTER(1);
const BUCKET = process.env.OBJ_K8S_BUCKET ?? 'mcmp07-aws';
// GCP(2026-10-15): K8S_CSP=GCP K8S_PROJECT=p4 K8S_CLUSTER=k4s OBJ_K8S_BUCKET=mcmp-e2e-v07-os4-gke(p4 ์ ๋ฑ๋กํ ์ํ ๋ฒํท)
test(`IV5_S27_${process.env.K8S_CSP ?? 'AWS'} TC-APP-027 K8S Jupyter Object Storage ์ฐ๋ ๋ฐฐํฌยท์ฝ๊ธฐ/์ฐ๊ธฐ`, async ({ page }) => {
test.setTimeout(60 * 60_000);
await loginAsAdmin(page);
const k = await copyKubeconfig(page, PRJ, CLUSTER);
const t = await consoleOp(page, 'GetK8sClusterToken', { nsId: PRJ, k8sClusterId: CLUSTER });
const { dir, f } = writeTokenKubeconfig(k.clip, (t.data?.execCredential ?? t.data)?.status?.token);
try {
// Ingress Controller ๋ source-IP ๋ณด์กด(Local) โ Ingress Controller Pod ๊ฐ ์๋ node ๋ก๋ง ์ ์๋๋ค(ํ๋ฉด ์๋ด, 2026-10-15 ํ์ธ). ์์ง ์์ผ๋ฉด(์ฒซ ๋ฐฐํฌ) ์ฒซ node
const ingNode = kubectl(f, ['get', 'pods', '-A', '-l', 'app.kubernetes.io/name=ingress-nginx,app.kubernetes.io/component=controller', '-o', 'jsonpath={.items[*].spec.nodeName}']).trim().split(/\s+/)[0] ?? '';
const nodeIp = kubectl(f, ['get', 'nodes', ...(ingNode ? [ingNode] : []), '-o', ingNode ? 'jsonpath={.status.addresses[?(@.type=="ExternalIP")].address}' : 'jsonpath={.items[0].status.addresses[?(@.type=="ExternalIP")].address}']).trim();
const cidr = `${await myPublicIp(page)}/32`;
const host = `${nodeIp}.nip.io`; // Ingress Host(DNS ์ด๋ฆ ํ์) โ nip.io ๋ก node ๊ณต์ธ IP ์ ๋งคํ
test.info().annotations.push({ type: 'target', description: `${PRJ}/${CLUSTER} node ${nodeIp}(ingress ${ingNode || '์์'}) / ๋ฒํท ${BUCKET} / ํ์ฉ CIDR ${cidr}` });
expect(nodeIp).toMatch(/^\d+\.\d+\.\d+\.\d+$/);
await test.step('1~2. K8S Jupyter(Read only) ๋ฐฐํฌ(์ด๋ฏธ Running ์ด๋ฉด ์ฌ์ฌ์ฉ)', async () => {
const st = await appStatus(page, { key: `(${PRJ} / ${CLUSTER})`, project: PRJ }, JUPYTER_APP_NAME, 1);
if (/running/i.test(st)) { test.info().annotations.push({ type: 'deploy', description: '๊ธฐ์กด Jupyter(Running) ์ฌ์ฌ์ฉ' }); return; }
const r = await deployJupyterK8s(page, { project: PRJ, cluster: CLUSTER, buckets: [BUCKET], mode: 'READ_ONLY', sizeGi: '1', cidr, ingressHost: host });
test.info().annotations.push({ type: 'deploy', description: `StorageClass ${r.storageClasses.join(",")} ์ ํ ${r.scSelected} / spec: ${r.specToasts.join(' | ')} / submit ${r.submit} / done=${r.done} ${r.status} / ${r.toasts.join(' | ')}` });
expect(r.done, 'Deployment completed').toBe(true);
});
await test.step('3. Jupyter Pod Ready(kubectl)', async () => {
await expect.poll(() => kubectl(f, ['get', 'pods', '-A', '-o', 'wide']).split('\n').filter((l) => /jupyter/i.test(l)).join(' / '), { timeout: 10 * 60_000, intervals: [15_000] }).toMatch(/\s(\d+)\/\1\s+Running/); // Jupyter + gateway ์ฌ์ด๋์นด(2/2, 2026-10-15)
const pods = kubectl(f, ['get', 'pods,svc,pvc', '-A']).split('\n').filter((l) => /jupyter|NAME/i.test(l)).join(' / ');
test.info().annotations.push({ type: 'pods', description: pods.replace(/\s+/g, ' ').slice(0, 900) });
});
await test.step('4. Jupyter(30880): ์ฝ๊ธฐ ๊ฐ๋ฅ ยท ์ฐ๊ธฐ ๊ฑฐ๋ถ', async () => {
const code = `objs = list_objects(alias, '')\nprint('LIST', len(objs))\nkey = objs.iloc[0]['key'] if len(objs) else None\nprint('READ', len(download(alias, key)) if key else -1)\nimport tempfile\np = tempfile.mktemp(); open(p,'w').write('ro-test')\ntry:\n upload(alias, p, 'am-test/iv5_s27_ro.txt'); print('WRITE OK')\nexcept Exception as e:\n print('WRITE DENIED', type(e).__name__, str(e)[:120])\n`;
const r = await jupyterRun(page, host, '30880', code);
test.info().annotations.push({ type: 'ro', description: `${r.out.slice(0, 800)} ${r.err}` });
expect(r.out).toMatch(/STORAGES .*READ_ONLY/);
expect(r.out).toMatch(/LIST \d+/);
expect(r.out).toMatch(/WRITE DENIED/);
});
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
});tests/iv5/IV5_S28.spec.ts ยท 44์ค ยท ํด์ 8d072c8a ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
// IV5_S28: TC-APP-028 Object Storage ์ ๊ทผ ๋ชจ๋ยทPrefix ๊ถํ ํ์ธ [์ ์, Alibaba]
// v0.6 TC-ID: ์์(mc-application-manager ์ํธ์๋ง ์๋ TC, 2026-10-15 ์์ฑ)
// ํ๋ฉด: IV5_S26 ์์ ๋ฐฐํฌํ VM Jupyter B(Read and write ยท Allowed Object Prefix am-test/ ยท ํฌํธ 8889) โ Jupyter(token) ์ปค๋
// ์์ ์ํ: admin(mcmp) ๋ก๊ทธ์ธ, ws01/default. IV5_S26 ์๋ฃ(B Running, am-test/iv5_s26.txt ์กด์ฌ), ์ํ VM mc3/g3-1 Running
// ์ ์ฐจ: 1.Jupyter ์ปค๋์์ notebook helper ๋ก grant ํ์ธ(accessMode READ_WRITE, prefix am-test/) 2.ํ์ฉ Prefix: am-test/ ์ฐ๊ธฐยท์ฝ๊ธฐยท๋ชฉ๋ก
// 3.Prefix ๋ฐ: ๋ฒํท ๋ฃจํธ ๋ชฉ๋ก, ๋ฃจํธ ๊ฐ์ฒด ์ฝ๊ธฐ, outside/ ์ฐ๊ธฐ โ ๊ฑฐ๋ถ ํ์ธ
// ์
๋ ฅ๊ฐ(์ํธ): Allowed Object Prefix am-test/, Access Mode Read only / Read and write(Read only ๋์์ IV5_S26 A ์์ ํ์ธ)
// ๊ธฐ๋๊ฒฐ๊ณผ: Access Mode ์ ๋ง๊ฒ ์ฝ๊ธฐยท์ฐ๊ธฐ ํ์ฉ/์ฐจ๋จ, ํ์ฉ Prefix ๋ฐ ์ ๊ทผ ์ฐจ๋จ
// ๋น๊ณ : K8S ๋ p1 ์ ์ํ ๋ฒํท์ด ์์ด(๋ค๋ฅธ ์ฃผ์ฒด ๋ฒํท๋ง ๋ฑ๋ก) Read only ๋ก๋ง ํ์ธ(IV5_S27). ์ํ ํ์ผ์ IV5_S29 ์์ ์ ๋ฆฌ
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { jupyterRun, jupyterVmIp, APPS } from '../../lib/iv5-objstore';
const tryPy = (label: string, expr: string) => `try:\n _r = ${expr}\n print('${label} OK', (len(_r) if hasattr(_r, '__len__') else _r))\nexcept Exception as e:\n print('${label} DENIED', type(e).__name__, str(e)[:160])\n`;
test('IV5_S28_Alibaba TC-APP-028 Object Storage ์ ๊ทผ ๋ชจ๋ยทPrefix ๊ถํ ํ์ธ', async ({ page }) => {
test.setTimeout(20 * 60_000);
await loginAsAdmin(page);
const ip = await jupyterVmIp(page);
const code = [
"import tempfile, inspect\np = tempfile.mktemp(); open(p,'w').write('iv5-s28')\n",
"print('PRESIGNED_SIG', str(inspect.signature(_presigned)))\n",
tryPy('IN_WRITE', "upload(alias, p, 'am-test/iv5_s28.txt')"),
tryPy('IN_READ', "download(alias, 'am-test/iv5_s28.txt')"),
tryPy('IN_LIST', "list_objects(alias, 'am-test/')"),
tryPy('OUT_LIST', "list_objects(alias, '')"),
tryPy('OUT_READ', "download(alias, 'iv5_s28_outside_probe.txt')"),
tryPy('OUT_WRITE', "upload(alias, p, 'outside/iv5_s28.txt')"),
].join('');
const r = await jupyterRun(page, ip, APPS.rw.port, code);
test.info().annotations.push({ type: 'prefix', description: `${r.out.slice(0, 2000)} ${r.err}` });
await test.step('1. grant: READ_WRITE ยท am-test/', async () => expect(r.out).toMatch(/"accessMode": "READ_WRITE", "prefix": "am-test\/"/));
await test.step('2. ํ์ฉ Prefix ์ฐ๊ธฐยท์ฝ๊ธฐยท๋ชฉ๋ก', async () => {
expect(r.out).toMatch(/IN_WRITE OK/);
expect(r.out).toMatch(/IN_READ OK 7/);
expect(r.out).toMatch(/IN_LIST OK [1-9]/);
});
await test.step('3. Prefix ๋ฐ ์ ๊ทผ ์ฐจ๋จ', async () => {
expect(r.out).toMatch(/OUT_WRITE DENIED/);
expect(r.out).toMatch(/OUT_READ DENIED/);
// ๋ฃจํธ ๋ชฉ๋ก์ ๊ฑฐ๋ถ๋๊ฑฐ๋, ํ์ฉ Prefix ์๋๋ง ๋ณด์ฌ์ผ ํ๋ค
expect(r.out).toMatch(/OUT_LIST DENIED|OUT_LIST OK [0-9]/);
});
});tests/iv5/IV5_S29.spec.ts ยท 77์ค ยท ํด์ 3f81d492 ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
// IV5_S29: TC-APP-029 Jupyter ์ญ์ ยท์ฐ๋ ๊ถํ ํด์ ๋ฐ Object Storage ๋ฐ์ดํฐ ๋ณด์กด [์ ์, Alibaba]
// v0.6 TC-ID: ์์(mc-application-manager ์ํธ์๋ง ์๋ TC, 2026-10-15 ์์ฑ)
// ํ๋ฉด: Jupyter B(8889) ์ปค๋(์ํ ํ์ผ ์ ๋ฆฌ) โ Applications > App Status(Uninstall) โ Data Migrations > Generate(๋ฒํท Browse objects)
// ์์ ์ํ: admin(mcmp) ๋ก๊ทธ์ธ, ws01/default. IV5_S28 ์๋ฃ(B Running, am-test/ ์ํ ํ์ผ ์กด์ฌ)
// ์ ์ฐจ: 1.B ์ปค๋์์ am-test/ ์ํ ํ์ผ ๋ชฉ๋ก ๊ธฐ๋ก(์ญ์ ๋ gateway ๊ฐ download/upload ๋ง ์ง์ โ 'Operation must be download or upload.', 2026-10-15)
// 2.App Status ์์ JupyterLab for Object Storage(mc3/g3-1) Uninstall โ ์ ๊ฑฐ ํ์ธ 3.Jupyter(8889) ์ ์ ๋ถ๊ฐ(์ฐ๋ ๊ถํ ํด์ )
// 4.Data Migrations ์ ์ํ ๋ฒํท Browse objects โ ์๋ณธ ํ์ผ(์ํ ๋ฐ์ดํฐ Generate ๊ฒฐ๊ณผ) ๋ณด์กด ํ์ธ 5.๊ฐ์ ํ๋ฉด์์ am-test/ ์ํ ํ์ผ Delete(์ ๋ฆฌ)
// ์
๋ ฅ๊ฐ(์ํธ): TC-APP-026ยท027 ์ ์ํ Jupyter
// ๊ธฐ๋๊ฒฐ๊ณผ: ์ฑ ์ ๊ฑฐยท์ฐ๋ ๊ถํ ํด์ , Object Storage ์๋ณธ ํ์ผ ๋ณด์กด
// ๋น๊ณ : K8S Jupyter(IV5_S27, p1/k1s)๋ K8s ์ฌ์ดํด์ ํด๋ฌ์คํฐ ์ญ์ (CL_S01) ์ ์ App Status ์์ Uninstall โ ์ด spec ์ VM ๋์. ์ํ VM mc3 ๋ ๋๋๋ฉด ์ฌ์ฉ์ ํ์ธ ํ Suspend
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { CSPS } from '../../lib/data';
import { jupyterRun, jupyterVmIp, uninstallJupyterVm, jupyterTarget, JUPYTER_APP_NAME, APPS, OBJ_BUCKET } from '../../lib/iv5-objstore';
import { appStatus } from '../../lib/iv5';
import { openDm, selectCred, readObjects } from '../../lib/iv8-os';
test('IV5_S29_Alibaba TC-APP-029 Jupyter ์ญ์ ยท์ฐ๋ ๊ถํ ํด์ ๋ฐ Object Storage ๋ฐ์ดํฐ ๋ณด์กด', async ({ page }) => {
test.setTimeout(30 * 60_000);
await loginAsAdmin(page);
const ip = await jupyterVmIp(page);
const running = /running/i.test(await appStatus(page, jupyterTarget(), JUPYTER_APP_NAME, 1));
await test.step('1. ์ํ ํ์ผ ๋ชฉ๋ก ยท Jupyter ์ญ์ ์๋(gateway ๋ฏธ์ง์ ๊ธฐ๋ก)', async () => {
if (!running) { test.info().annotations.push({ type: 'jupyter-delete', description: 'B ๊ฐ ์ด๋ฏธ ์ ๊ฑฐ๋จ(์ฌ์คํ) โ ์ ๋ฆฌ๋ 5๋จ๊ณ์์' }); return; }
const code = "keys = list(list_objects(alias, 'am-test/')['key'])\nprint('KEYS', keys)\ntry:\n _presigned(alias, keys[0], 'delete'); print('DEL_PRESIGN OK')\nexcept Exception as e:\n print('DEL_PRESIGN', type(e).__name__, str(e)[:160])\n";
const r = await jupyterRun(page, ip, APPS.rw.port, code);
test.info().annotations.push({ type: 'jupyter-delete', description: `${r.out.slice(0, 1200)} ${r.err}` });
});
await test.step('2. Uninstall โ App Status ์ ๊ฑฐ', async () => {
if (!running) { test.info().annotations.push({ type: 'uninstall', description: '์ด๋ฏธ Uninstalled(์ฌ์คํ)' }); return; }
const u = await uninstallJupyterVm(page);
test.info().annotations.push({ type: 'uninstall', description: u.seen.join(' โ ') });
expect(u.done).toBe(true);
});
await test.step('3. Jupyter(8889) ์ ์ ๋ถ๊ฐ', async () => {
const res = await page.request.get(`http://${ip}:${APPS.rw.port}/api/status`, { timeout: 15_000 }).then((x) => `HTTP ${x.status()}`, (e) => `์ฐ๊ฒฐ ์คํจ ${String(e).split('\n')[0].slice(0, 120)}`);
test.info().annotations.push({ type: 'jupyter-after', description: res });
expect(res).not.toMatch(/HTTP 200/);
});
await test.step('4. ๋ฒํท ์๋ณธ ๋ณด์กด(Data Migrations Browse objects)', async () => {
const f = await openDm(page, 'Generate');
await selectCred(page, f, CSPS.find((c) => c.code === 'Alibaba')!);
const rows = await readObjects(page, f, OBJ_BUCKET);
expect(rows, '๊ฐ์ฒด ๋ชฉ๋ก').not.toBeNull();
expect(rows!.filter((x) => !/^am-test\//.test(x)).length, '์๋ณธ ํ์ผ ๋ณด์กด').toBeGreaterThan(0);
});
await test.step('5. ์ํ ํ์ผ(am-test/) ์ ๋ฆฌ โ Data Migrations Delete', async () => {
const f = await openDm(page, 'Generate');
await selectCred(page, f, CSPS.find((c) => c.code === 'Alibaba')!);
await f.locator(`button.bkt-browse-btn[data-name="${OBJ_BUCKET}"]`).first().click();
const table = f.locator('table:has(thead:has-text("Last Modified"))').first();
await table.waitFor({ state: 'visible', timeout: 30_000 });
await table.getByText('am-test/', { exact: false }).first().click();
await expect(table).toContainText('iv5_s2', { timeout: 15_000 });
const del = f.locator('button.bkt-object-delete-btn');
const done: string[] = [];
for (let i = 0; i < 5 && (await del.count()); i++) {
const row = del.first().locator('xpath=ancestor::tr[1]');
const key = (await row.innerText()).replace(/\s+/g, ' ').trim();
page.once('dialog', (d) => d.accept().catch(() => {}));
await del.first().click();
// ํ์ธ ๋ชจ๋ฌ #bktObjectDeleteModal(2026-10-15) โ ์ํ(btn-danger) ๋ฒํผ์ผ๋ก ํ์
const modal = f.locator('#bktObjectDeleteModal');
await modal.waitFor({ state: 'visible', timeout: 10_000 });
if (!i) test.info().annotations.push({ type: 'delete-modal', description: (await modal.innerText()).replace(/\s+/g, ' ').slice(0, 300) });
await modal.locator('button.btn-danger').last().click();
await modal.waitFor({ state: 'hidden', timeout: 30_000 });
await page.waitForTimeout(3000);
await f.locator('#bkt-object-refresh').click().catch(() => {});
await page.waitForTimeout(3000);
done.push(key);
}
test.info().annotations.push({ type: 'dm-delete', description: `์ญ์ ${done.join(' / ')} โ ๋จ์ Delete ๋ฒํผ ${await del.count()}๊ฐ` });
expect(await del.count(), 'am-test/ ์ํ ํ์ผ ์ ๋ฆฌ').toBe(0);
});
});