Test Specs IV0 - MZC-CSC/mc-web-console GitHub Wiki
์์ฑ: 2026-10-13 ยท ๊ธฐ์ค: v07 โ Test Scenarios ยท IV ํ๋ธ ยท ๊ธฐ๋ฅ TC ยท TC-ADMIN
Playwright(TypeScript) spec ์์ค์
๋๋ค. ์๋๋ฆฌ์ค๋ง๋ค ## <์๋๋ฆฌ์ค๋ฒํธ> ์น์
์ด ์๊ณ ์ต์ปค๋ #<์๋๋ฆฌ์ค๋ฒํธ ์๋ฌธ์> ์
๋๋ค.
IV0 ๋ ๋ธ๋ผ์ฐ์ ์์ด ์ค์น ๋์ ์๋ฒ์ ํฐ๋ฏธ๋ ๋ช
๋ น์ผ๋ก ํ์ธํ๋ CLI ํ
์คํธ์
๋๋ค. ์์กด์ @playwright/test ๋ฟ์ด๋ผ, ์ด๋ฏธ ์ค์น๋ Playwright ํ๋ก์ ํธ์ tests/iv0/ ์ ๊ณต์ฉ ํฌํผ _cli.ts ์ spec ํ์ผ์ ๋ณต์ฌํ๋ฉด ์คํ๋ฉ๋๋ค.
# ์๊ฒฉ ์๋ฒ(ssh)์์ ์คํ โ ์ํ๋ฅผ ๋ฐ๊พธ์ง ์๋ ์๋๋ฆฌ์ค๋ง ์คํ๋๊ณ ๋๋จธ์ง๋ ๊ฑด๋๋
ADMIN_CLI_SSH=ubuntu@<host> ADMIN_CLI_SSH_KEY=<key.pem> ADMIN_CLI_BIN=~/mc-admin-cli/bin npx playwright test tests/iv0
# ์๋๋ฆฌ์ค 1๊ฑด
ADMIN_CLI_SSH=ubuntu@<host> ADMIN_CLI_SSH_KEY=<key.pem> npx playwright test tests/iv0/IV0_S14.spec.ts
# ์ค์นยท์ญ์ ๋ฑ ์๋ฒ ์ํ๋ฅผ ๋ฐ๊พธ๋ ์๋๋ฆฌ์ค(์ ์ฉ ํ
์คํธ ์๋ฒ์์๋ง)
ADMIN_CLI_ALLOW_DESTRUCTIVE=1 ADMIN_CLI_SSH=ubuntu@<host> ADMIN_CLI_SSH_KEY=<key.pem> npx playwright test tests/iv0/IV0_S01.spec.ts| ํ๊ฒฝ๋ณ์ | ๊ธฐ๋ณธ๊ฐ | ์ฌ์ฉ ์๋๋ฆฌ์ค | ์ค๋ช |
|---|---|---|---|
ADMIN_CLI_SSH |
(์์ = ๋ก์ปฌ) | ์ ์ฒด | ์ค์น ๋์ ssh ์ฃผ์ |
ADMIN_CLI_SSH_KEY |
(์์) | ์ ์ฒด | ssh ๊ฐ์ธํค ๊ฒฝ๋ก |
ADMIN_CLI_BIN |
~/mc-admin-cli/bin |
์ ์ฒด | mc-admin-cli bin ๊ฒฝ๋ก |
ADMIN_CLI_ALLOW_DESTRUCTIVE |
(์์) | S01 S02 S05~S12 S24 S26 |
1 ์ด๋ฉด ์๋ฒ ์ํ๋ฅผ ๋ฐ๊พธ๋ ์๋๋ฆฌ์ค ์คํ |
ADMIN_CLI_ALLOW_REINSTALL |
(์์) | S01 S02 S10 |
1 ์ด๋ฉด ๊ธฐ์กด mc- ์ปจํ
์ด๋๊ฐ ์์ด๋ ์ค์น ์งํ |
ADMIN_CLI_DOMAIN |
(์์ = Enter, localhost) | S01 | ๋๋ฉ์ธ ์ ๋ ฅ๊ฐ. IP ๋ฉด ์์ฒด ์๋ช ์ธ์ฆ์ ๊ฒฝ๋ก |
ADMIN_CLI_FQDN |
(์์) | S02 S10 | ์ค์ ๋๋ฉ์ธ(Let's Encrypt) |
ADMIN_CLI_PUBLIC_HOST |
(์์) | S03 | ์ธ๋ถ์์ ์ ์ํ ์๋ฒ ์ฃผ์ |
ADMIN_CLI_PORTS |
80,443,3001,33002,7780,9090,9000,3000 |
S03 | ์ ๊ฒํ ํฌํธ |
ADMIN_CLI_DEFAULT_WORKSPACE / _PROJECT
|
ws01 / default
|
S04 | ์ด๊ธฐํ๋ก ์์ฑ๋๋ ๊ธฐ๋ณธ workspace/project |
ADMIN_CLI_TARGET_SERVICE |
mc-data-manager |
S07 S08 | ๋์ ์๋น์ค(all=์ ์ฒด, S08) |
ADMIN_CLI_K8S_PROVIDER / _VALUES
|
minikube / ../helm-chart/values.yaml
|
S11~S13 | K8s ๋ฐฐํฌ ๋์ |
ADMIN_CLI_ECHO_PORT |
18999 |
S14~S18 | ์์ echo ์๋ฒ ํฌํธ(๋์ ์๋ฒ 127.0.0.1) |
ADMIN_CLI_TB_AUTH |
default:default |
S15 S21~S25 | mc-infra-manager Basic ์ธ์ฆ |
ADMIN_CLI_API_CONFIG |
(์์ = api.yaml ํธ์คํธ๋ช โlocalhost ์ฌ๋ณธ) | S20~S22 | mcc api ์ค์ ํ์ผ |
ADMIN_CLI_CSP / ADMIN_CLI_CRED_FILE
|
aws / (์์) |
S24 | ๋ฑ๋กํ CSP, credential key=value ํ์ผ(์ด PC) |
ADMIN_CLI_INIT_INPUT |
y\n1\ny\n |
S26 | infra-init ๋ํํ ์ ๋ ฅ |
๊ฒฐ๊ณผ: ๋ช ๋ น ์ถ๋ ฅ์ด Playwright ๋ฆฌํฌํธ ์ฒจ๋ถ๋ก ๋จ์ต๋๋ค(๋น๋ฐ๋ฒํธยทcredential ๊ฐ์ ์ฒจ๋ถ ์ ์ ๊ฐ๋ฆผ). ํ๋ฉด์ด ์์ด ์์์ ์์ฑ๋์ง ์์ต๋๋ค.
| ์๋๋ฆฌ์ค | ํ์ผ | ์ค | ํด์ |
|---|---|---|---|
| ๊ณต์ฉ ํฌํผ | tests/iv0/_cli.ts |
179 | 1754fd79 |
| IV0_S01 | tests/iv0/IV0_S01.spec.ts |
37 | 36583587 |
| IV0_S02 | tests/iv0/IV0_S02.spec.ts |
48 | 1fda97b0 |
| IV0_S03 | tests/iv0/IV0_S03.spec.ts |
36 | 8210157a |
| IV0_S04 | tests/iv0/IV0_S04.spec.ts |
39 | 7159259d |
| IV0_S05 | tests/iv0/IV0_S05.spec.ts |
33 | cb875115 |
| IV0_S06 | tests/iv0/IV0_S06.spec.ts |
27 | a84ddad3 |
| IV0_S07 | tests/iv0/IV0_S07.spec.ts |
33 | 79f3ea35 |
| IV0_S08 | tests/iv0/IV0_S08.spec.ts |
38 | fcc2a71a |
| IV0_S09 | tests/iv0/IV0_S09.spec.ts |
35 | b47701dd |
| IV0_S10 | tests/iv0/IV0_S10.spec.ts |
44 | 279c16b7 |
| IV0_S11 | tests/iv0/IV0_S11.spec.ts |
39 | 6b23b35d |
| IV0_S12 | tests/iv0/IV0_S12.spec.ts |
23 | fd80da3a |
| IV0_S13 | tests/iv0/IV0_S13.spec.ts |
23 | c9c82ca1 |
| IV0_S14 | tests/iv0/IV0_S14.spec.ts |
26 | b6c58611 |
| IV0_S15 | tests/iv0/IV0_S15.spec.ts |
34 | 94094503 |
| IV0_S16 | tests/iv0/IV0_S16.spec.ts |
30 | 1caf97f6 |
| IV0_S17 | tests/iv0/IV0_S17.spec.ts |
28 | e71547ba |
| IV0_S18 | tests/iv0/IV0_S18.spec.ts |
25 | 087c2c71 |
| IV0_S19 | tests/iv0/IV0_S19.spec.ts |
37 | 9fe1e75f |
| IV0_S20 | tests/iv0/IV0_S20.spec.ts |
32 | 653f91eb |
| IV0_S21 | tests/iv0/IV0_S21.spec.ts |
35 | aed0c1ea |
| IV0_S22 | tests/iv0/IV0_S22.spec.ts |
24 | 9740de4b |
| IV0_S23 | tests/iv0/IV0_S23.spec.ts |
41 | 9f25ea19 |
| IV0_S24 | tests/iv0/IV0_S24.spec.ts |
59 | a55caa67 |
| IV0_S25 | tests/iv0/IV0_S25.spec.ts |
62 | 56b20ddb |
| IV0_S26 | tests/iv0/IV0_S26.spec.ts |
46 | 140a8e5b |
tests/iv0/_cli.ts ยท 179์ค ยท ํด์ 1754fd79
import { expect, test, type TestInfo } from '@playwright/test';
import { spawn } from 'node:child_process';
// IV0 ๊ณต์ฉ ํฌํผ โ mc-admin-cli(CLI) ํ
์คํธ๋ฅผ ์ค์น ๋์ ์๋ฒ์ ํฐ๋ฏธ๋ ๋ช
๋ น์ผ๋ก ์คํํ๋ค.
// ์ธ๋ถ ์์กด์ @playwright/test ๋ฟ์ด๋ค. ์ด๋ฏธ ์ค์น๋ Playwright ํ๋ก์ ํธ์ tests/iv0/ ์ ์ด ํ์ผ๊ณผ spec ์ ํจ๊ป ๋ณต์ฌํ๋ค.
//
// ๊ณตํต ํ๊ฒฝ๋ณ์
// ADMIN_CLI_SSH ์ค์น ๋์ ssh ์ฃผ์(์: [email protected]). ๋น์ฐ๋ฉด ๋ก์ปฌ์์ ์คํ
// ADMIN_CLI_SSH_KEY ssh ๊ฐ์ธํค ๊ฒฝ๋ก(์ ํ)
// ADMIN_CLI_BIN mc-admin-cli ์ bin ๊ฒฝ๋ก (๊ธฐ๋ณธ ~/mc-admin-cli/bin)
// ADMIN_CLI_TB_AUTH mc-infra-manager Basic ์ธ์ฆ user:password (๊ธฐ๋ณธ default:default)
// ADMIN_CLI_ALLOW_DESTRUCTIVE=1 ์ค์นยท์ญ์ ์ฒ๋ผ ์๋ฒ ์ํ๋ฅผ ๋ฐ๊พธ๋ TC ์คํ ํ์ฉ(๊ธฐ๋ณธ: ๊ฑด๋๋)
export const SSH = process.env.ADMIN_CLI_SSH ?? '';
export const KEY = process.env.ADMIN_CLI_SSH_KEY ?? '';
export const BIN = process.env.ADMIN_CLI_BIN ?? '~/mc-admin-cli/bin';
export const TB_AUTH = process.env.ADMIN_CLI_TB_AUTH ?? 'default:default';
export const TB = 'http://localhost:1323/tumblebug';
export type Run = { code: number; out: string };
const q = (s: string) => `'${s.replace(/'/g, `'\\''`)}'`;
// ๋์ ์๋ฒ์์ bash ๋ช
๋ น ์คํ. stdin ์ผ๋ก ๋ํํ ์
๋ ฅ์ ๋๊ธด๋ค. stdout+stderr ๋ฅผ ํฉ์ณ ๋๋ ค์ค๋ค.
export function sh(cmd: string, stdin = '', timeout = 120_000): Promise<Run> {
const args = SSH
? ['-o', 'StrictHostKeyChecking=accept-new', '-o', 'ServerAliveInterval=30', ...(KEY ? ['-i', KEY] : []), SSH, 'bash', '-lc', q(cmd)]
: ['-lc', cmd];
return new Promise((resolve) => {
const p = spawn(SSH ? 'ssh' : 'bash', args, { env: { ...process.env, TERM: 'dumb' } });
let out = '';
p.stdout.on('data', (d) => (out += d));
p.stderr.on('data', (d) => (out += d));
const t = setTimeout(() => p.kill('SIGTERM'), timeout);
p.on('close', (code) => { clearTimeout(t); resolve({ code: code ?? -1, out }); });
p.stdin.end(stdin);
});
}
// bin ํด๋์์ ์คํ (mcc, *.sh ๋ bin ๊ธฐ์ค ์๋๊ฒฝ๋ก๋ฅผ ์ด๋ค)
export const inBin = (cmd: string, stdin = '', timeout?: number) => sh(`cd ${BIN} && ${cmd}`, stdin, timeout);
// ๋ช
๋ น ์ถ๋ ฅ์ ๋ฆฌํฌํธ์ ์ฒจ๋ถ
export async function attach(testInfo: TestInfo, name: string, r: Run) {
await testInfo.attach(name, { body: `exit=${r.code}\n${r.out}`, contentType: 'text/plain' });
}
// ์๋ฒ ์ํ๋ฅผ ๋ฐ๊พธ๋ TC ๋ ๋ช
์์ ์ผ๋ก ํ์ฉํ์ ๋๋ง ์คํ
export function requireDestructive(what: string) {
test.skip(process.env.ADMIN_CLI_ALLOW_DESTRUCTIVE !== '1', `${what} โ ์๋ฒ ์ํ๋ฅผ ๋ฐ๊พธ๋ฏ๋ก ADMIN_CLI_ALLOW_DESTRUCTIVE=1 ์ผ ๋๋ง ์คํ`);
}
// installAll.sh ์ EXPECTED_CONTAINERS / NO_HEALTH_CHECK_CONTAINERS ๋ฐฐ์ด (๋ฒ์ ๋ง๋ค ๋ค๋ฅด๋ฏ๋ก ์คํฌ๋ฆฝํธ์์ ์ฝ์)
export async function readArray(name: string): Promise<string[]> {
const r = await inBin(`sed -n '/^${name}=(/,/^)/p' installAll.sh`);
return r.out.split('\n').map((l) => l.trim()).filter((l) => /^"[^"]+"$/.test(l)).map((l) => l.slice(1, -1));
}
// mcc infra info ์ถ๋ ฅ์์ ์ปจํ
์ด๋๋ณ ์ํ ํ์ ์ฐพ๋๋ค(์์ชฝ compose ps ํ๋ง ์ฌ์ฉ)
export function psLine(info: string, name: string): string | undefined {
return info.split('[v]Status of M-CMP runtime images')[0].split('\n').find((l) => new RegExp(`^${name}\\s`).test(l));
}
// ๊ธฐ๋ ์ปจํ
์ด๋ ์ ์ฒด๊ฐ Up(+health check ๊ฐ ์์ผ๋ฉด healthy)์ธ์ง ํ์ธ. ๋ฌธ์ ๋ชฉ๋ก์ ๋๋ ค์ค๋ค.
// docker compose ๋ฒ์ ์ ๋ฐ๋ผ ์ํ๊ฐ 'Up 5 minutes (healthy)' ๋๋ 'running (healthy)' ๋ก ๋์จ๋ค.
export async function unhealthy(names?: string[]): Promise<{ bad: string[]; info: Run }> {
const info = await inBin('./mcc infra info');
const expected = names ?? (await readArray('EXPECTED_CONTAINERS'));
const noHealth = new Set(await readArray('NO_HEALTH_CHECK_CONTAINERS'));
const bad: string[] = [];
for (const name of expected) {
const line = psLine(info.out, name);
if (!line) bad.push(`${name}: ์์`);
else if (!/\bUp\b|\brunning\b/.test(line) || /unhealthy|starting|restarting/i.test(line)) bad.push(`${name}: ${line.replace(/\s{2,}/g, ' ')}`);
else if (!noHealth.has(name) && !/\(healthy\)/.test(line)) bad.push(`${name}: healthy ํ์ ์์`);
}
return { bad, info };
}
// healthy ๊ฐ ๋ ๋๊น์ง ๋๊ธฐ
export async function waitHealthy(names?: string[], timeoutMs = 20 * 60_000) {
let last: string[] = [];
await expect.poll(async () => (last = (await unhealthy(names)).bad).length, { timeout: timeoutMs, intervals: [15_000] })
.toBe(0)
.catch(() => { throw new Error(`healthy ์๋ ์ปจํ
์ด๋:\n${last.join('\n')}`); });
}
// M-CMP(mcc compose ํ๋ก์ ํธ) ์ปจํ
์ด๋ ์ด๋ฆ ๋ชฉ๋ก โ ์ด๋ฆ์ด mc- ๋ก ์์ํ๋ ๋ค๋ฅธ ์ ํ ์ปจํ
์ด๋(mc-terrarium ๋ฑ)๋ ์ ์ธ
export async function mcContainers(all = true): Promise<string[]> {
const r = await sh(`docker ps ${all ? '-a' : ''} --filter label=com.docker.compose.project=mcc --format '{{.Names}}'`);
return r.out.split('\n').map((s) => s.trim()).filter(Boolean);
}
// ํด๋ฆฐ ์ค์น TC ์ ์ ํ ์ํ: mc- ์ปจํ
์ด๋๊ฐ ์์ด์ผ ํ๋ค
export async function requireClean() {
const existing = await mcContainers();
if (existing.length && process.env.ADMIN_CLI_ALLOW_REINSTALL !== '1') {
throw new Error(`M-CMP(mcc) ์ปจํ
์ด๋ ${existing.length}๊ฐ๊ฐ ์ด๋ฏธ ์์ต๋๋ค. ํด๋ฆฐ ์ค์น๊ฐ ์๋๋ฉด TC-ADMIN-005(cleanAll.sh) ํ ์คํํ๊ฑฐ๋ ADMIN_CLI_ALLOW_REINSTALL=1 ๋ก ์งํํ์ธ์.`);
}
}
// mc-infra-manager(tumblebug) REST ํธ์ถ โ ๋์ ์๋ฒ์ localhost ๊ธฐ์ค
export async function tb(path: string, method = 'GET', body?: unknown): Promise<{ status: number; json: any; raw: string }> {
const data = body === undefined ? '' : ` -H 'Content-Type: application/json' -d ${q(JSON.stringify(body))}`;
const r = await sh(`curl -s -u ${q(TB_AUTH)} -X ${method} -w '\\n%{http_code}' ${q(TB + path)}${data}`);
const lines = r.out.trimEnd().split('\n');
const status = Number(lines.pop());
const raw = lines.join('\n');
let json: any = null;
try { json = JSON.parse(raw); } catch { /* ๋ณธ๋ฌธ์ด JSON ์ด ์๋ */ }
return { status, json, raw };
}
// mc-iam-manager ํ๋ซํผ ๊ด๋ฆฌ์ ํ ํฐ โ ๊ณ์ ์ ๋์ ์๋ฒ .env ์์ ์ฝ๊ณ ํ๋ฉดยท๋ฆฌํฌํธ์ ๋จ๊ธฐ์ง ์๋๋ค
export async function iamToken(): Promise<string> {
const env = `${BIN}/../conf/docker/conf/mc-iam-manager/.env`;
const r = await sh(
`set -a; . ${env}; set +a; ` +
`curl -s -X POST http://localhost:5000/api/auth/login -H 'Content-Type: application/json' ` +
`-d "{\\"id\\":\\"$MC_IAM_MANAGER_PLATFORMADMIN_ID\\",\\"password\\":\\"$MC_IAM_MANAGER_PLATFORMADMIN_PASSWORD\\"}"`,
);
const token = (() => { try { return JSON.parse(r.out).access_token as string; } catch { return ''; } })();
expect(token, `ํ๋ซํผ ๊ด๋ฆฌ์ ๋ก๊ทธ์ธ ์คํจ: ${r.out.slice(0, 200)}`).toBeTruthy();
return token;
}
// mc-iam-manager POST */list ํธ์ถ
export async function iamList(token: string, path: string): Promise<any> {
const r = await sh(`curl -s -X POST ${q('http://localhost:5000' + path)} -H 'Content-Type: application/json' -H ${q('Authorization: Bearer ' + token)} -d '{}'`);
try { return JSON.parse(r.out); } catch { return r.out; }
}
// โโ mcc rest ๊ฒ์ฆ์ฉ echo ์๋ฒ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// ๋์ ์๋ฒ์ ๋ฐ์ ์์ฒญ(method, path, headers, body)์ JSON ์ผ๋ก ๋๋ ค์ฃผ๋ ์์ ์๋ฒ๋ฅผ ๋์ด๋ค.
// ?status=NNN ์ด๋ฉด ๊ทธ ์ํ ์ฝ๋๋ก ์๋ตํ๋ค. ํ
์คํธ ๋์ stopEcho() ๋ก ์ข
๋ฃ.
export const ECHO_PORT = Number(process.env.ADMIN_CLI_ECHO_PORT ?? 18999);
export const ECHO = `http://127.0.0.1:${ECHO_PORT}/echo`;
const ECHO_PY = `
import json, sys
from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import urlparse, parse_qs
class H(BaseHTTPRequestHandler):
def _h(self):
n = int(self.headers.get('Content-Length') or 0)
body = self.rfile.read(n).decode() if n else ''
st = int(parse_qs(urlparse(self.path).query).get('status', ['200'])[0])
out = json.dumps({'method': self.command, 'path': self.path, 'headers': dict(self.headers), 'body': body}).encode()
self.send_response(st); self.send_header('Content-Type', 'application/json'); self.send_header('X-Echo', 'iv0')
self.send_header('Content-Length', str(len(out))); self.end_headers()
if self.command != 'HEAD': self.wfile.write(out)
do_GET = do_POST = do_PUT = do_PATCH = do_DELETE = do_HEAD = _h
def log_message(self, *a): pass
HTTPServer(('127.0.0.1', int(sys.argv[1])), H).serve_forever()
`;
export async function startEcho() {
await sh(`cat > /tmp/iv0-echo.py <<'PYEOF'\n${ECHO_PY}\nPYEOF\nnohup python3 /tmp/iv0-echo.py ${ECHO_PORT} >/dev/null 2>&1 & echo $! > /tmp/iv0-echo.pid; sleep 1`);
expect((await sh(`curl -s -o /dev/null -w '%{http_code}' ${ECHO}`)).out, 'echo ์๋ฒ ๊ธฐ๋ ์คํจ').toBe('200');
}
export async function stopEcho() {
await sh('kill $(cat /tmp/iv0-echo.pid) 2>/dev/null; rm -f /tmp/iv0-echo.pid /tmp/iv0-echo.py');
}
// mcc ์ถ๋ ฅ์์ echo ์๋ต JSON ์ ์ฐพ๋๋ค
export function echoed(out: string): any {
const m = out.match(/\{"method".*\}/);
return m ? JSON.parse(m[0]) : null;
}
// โโ mcc api ์ค์ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// conf/api.yaml ์ baseurl ์ docker ๋ด๋ถ ํธ์คํธ๋ช
(http://mc-infra-manager:1323 โฆ)์ด๋ผ ์๋ฒ ์
ธ์์๋ ํ๋ฆฌ์ง ์๋๋ค.
// ๋ฌธ์ ์๋ด๋๋ก '์ค์ ์๋ฒ ์ฃผ์์ ๋ง๊ฒ' ํธ์คํธ๋ช
๋ง localhost ๋ก ๋ฐ๊พผ ์ฌ๋ณธ(/tmp/iv0-api.yaml)์ ๋ง๋ ๋ค.
// ADMIN_CLI_API_CONFIG ๋ฅผ ์ฃผ๋ฉด ๊ทธ ํ์ผ์ ๊ทธ๋๋ก ์ด๋ค.
export async function apiConfig(): Promise<string> {
if (process.env.ADMIN_CLI_API_CONFIG) return process.env.ADMIN_CLI_API_CONFIG;
await inBin(`sed -E 's#(baseurl: *"?https?://)[a-z0-9-]+:#\\1localhost:#' ../conf/api.yaml > /tmp/iv0-api.yaml`);
return '/tmp/iv0-api.yaml';
}
const [TBU, TBP] = TB_AUTH.split(':');
// mc-infra-manager ํธ์ถ ์ ์ธ์ฆ์ ๋ช
๋ น ์ธ์๋ก ๋ฎ์ด์ด๋ค(--authUser/--authPassword)
export const TB_AUTH_ARGS = `--authUser ${q(TBU)} --authPassword ${q(TBP)}`;tests/iv0/IV0_S01.spec.ts ยท 37์ค ยท ํด์ 36583587 ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '@playwright/test';
import { inBin, attach, requireDestructive, requireClean, unhealthy } from './_cli';
// IV0_S01 : Mode A ๊ฐ๋ฐ ์ค์น (installAll.sh ๋ํํ ์
๋ ฅ) โ TC-ADMIN-001
// ์
๋ ฅ: ๋ชจ๋ 1(Developer) / ๋๋ฉ์ธ Enter(๊ธฐ๋ณธ localhost) / ์คํ ๋ฐฉ์ 2(Background)
// ADMIN_CLI_DOMAIN ์ ์๋ฒ IP ๋ฅผ ๋ฃ์ผ๋ฉด ์์ฒด ์๋ช
์ธ์ฆ์(HTTPS) ๊ฒฝ๋ก๋ก ์ค์นํ๋ค(๋น์ฐ๋ฉด localhost, plain HTTP).
const DOMAIN = process.env.ADMIN_CLI_DOMAIN ?? '';
test('IV0_S01 Mode A ๊ฐ๋ฐ ์ค์น (installAll.sh โ mcc infra info ์ ์ฒด healthy)', async ({}, testInfo) => {
requireDestructive('ํด๋ฆฐ ์ค์น');
test.setTimeout(60 * 60_000);
await test.step('1. ์ ํ ์ํ ํ์ธ (docker compose, installAll.sh, ๊ธฐ์กด mc- ์ปจํ
์ด๋ ์์)', async () => {
expect((await inBin('docker compose version && ls installAll.sh mcc')).code).toBe(0);
await requireClean();
});
const install = await test.step(`2. installAll.sh ์คํ โ 1 / ${DOMAIN || 'Enter(localhost)'} / 2`, async () => {
const r = await inBin('./installAll.sh', `1\n${DOMAIN}\n2\n`, 45 * 60_000);
await attach(testInfo, 'installAll.log', r);
return r;
});
await test.step('3. ์ค์น ์๋ฃ ๋ฉ์์ง ํ์ธ', async () => {
expect(install.out).toContain(`Using domain: ${DOMAIN || 'localhost'}`);
expect(install.out).toMatch(DOMAIN ? /Developer mode configuration completed/ : /Local HTTP mode configuration completed/);
expect(install.out, '์ปจํ
์ด๋ ๋ชจ๋ํฐ๋ง์ด healthy ๋ก ๋๋์ง ์์').toContain('All environments have been set up!');
expect(install.out, 'mc-iam-manager-post-initial ๋ฏธ์๋ฃ').not.toContain('mc-iam-manager-post-initial did not complete');
expect(install.code, 'installAll.sh ์ข
๋ฃ ์ฝ๋').toBe(0);
});
await test.step('4. ./mcc infra info โ ์ ์ฒด framework healthy', async () => {
const { bad, info } = await unhealthy();
await attach(testInfo, 'mcc-infra-info.txt', info);
expect(bad, `healthy ์๋ ์ปจํ
์ด๋:\n${bad.join('\n')}`).toEqual([]);
});
});tests/iv0/IV0_S02.spec.ts ยท 48์ค ยท ํด์ 1fda97b0 ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '@playwright/test';
import { inBin, sh, attach, requireDestructive, requireClean, unhealthy } from './_cli';
// IV0_S02 : Mode B ์ด์ ์ค์น (Let's Encrypt) โ TC-ADMIN-002
// ์
๋ ฅ: ๋ชจ๋ 2(Production) / ๋๋ฉ์ธ = ์ค์ FQDN(์ด ์๋ฒ๋ฅผ ๊ฐ๋ฆฌํค๋ DNS A ๋ ์ฝ๋ ํ์) / ์คํ ๋ฐฉ์ 2(Background)
// ์ ์ : 80ยท443 ์ด ์ธ๋ถ์์ ์ด๋ ค ์์ด์ผ certbot(HTTP-01) ๋ฐ๊ธ์ด ๋๋ค. ์ฃผ: Let's Encrypt ๋ฐ๊ธ ํ์ ์ ํ(๊ฐ์ ๋๋ฉ์ธ ์ฃผ 5ํ)
const FQDN = process.env.ADMIN_CLI_FQDN ?? '';
test("IV0_S02 Mode B ์ด์ ์ค์น (Let's Encrypt ์ธ์ฆ์ ๋ฐ๊ธ โ HTTPS ์ ์ ยท ์ ์ฒด healthy)", async ({ request }, testInfo) => {
requireDestructive('์ด์ ๋ชจ๋ ํด๋ฆฐ ์ค์น');
test.skip(!FQDN, 'ADMIN_CLI_FQDN(์ค์ ๋๋ฉ์ธ) ๋ฏธ์ง์ ');
test.setTimeout(70 * 60_000);
await test.step('1. ์ ํ ์ํ ํ์ธ (ํด๋ฆฐ ์ํ, ๋๋ฉ์ธ DNS ๊ฐ ์ด ์๋ฒ๋ฅผ ๊ฐ๋ฆฌํด, 80 ๋ฏธ์ฌ์ฉ)', async () => {
await requireClean();
const dns = await sh(`getent hosts ${FQDN} | awk '{print $1}'; curl -s https://checkip.amazonaws.com`);
const [resolved, mine] = dns.out.trim().split('\n');
testInfo.annotations.push({ type: 'dns', description: `${FQDN} โ ${resolved} / ์๋ฒ ๊ณต์ธ IP ${mine}` });
expect(resolved, 'DNS A ๋ ์ฝ๋๊ฐ ์ด ์๋ฒ์ ๊ณต์ธ IP ์ ๋ค๋ฆ').toBe(mine);
expect((await sh(`ss -ltn '( sport = :80 )' | tail -n +2`)).out.trim(), '80 ํฌํธ๊ฐ ์ด๋ฏธ ์ฌ์ฉ ์ค').toBe('');
});
const install = await test.step(`2. installAll.sh ์คํ โ 2 / ${FQDN} / 2`, async () => {
const r = await inBin('./installAll.sh', `2\n${FQDN}\n2\n`, 55 * 60_000);
await attach(testInfo, 'installAll.log', r);
return r;
});
await test.step('3. ์ธ์ฆ์ ๋ฐ๊ธยท์ด์ ๋ชจ๋ ์ค์ ยท์ค์น ์๋ฃ ๋ฉ์์ง ํ์ธ', async () => {
expect(install.out).toContain('Certificate generation completed');
expect(install.out).toContain('Production mode configuration completed');
expect(install.out).toContain('All environments have been set up!');
expect(install.code).toBe(0);
});
await test.step('4. ์ธ๋ถ์์ HTTPS ์ ์ โ ๊ณต์ธ ์ธ์ฆ์ ๊ฒ์ฆ ํต๊ณผ (443, 3001)', async () => {
for (const url of [`https://${FQDN}/`, `https://${FQDN}:3001/`]) {
const res = await request.get(url, { ignoreHTTPSErrors: false, maxRedirects: 0 });
expect(res.status(), url).toBeLessThan(500);
}
});
await test.step('5. ./mcc infra info โ ์ ์ฒด framework healthy', async () => {
const { bad, info } = await unhealthy();
await attach(testInfo, 'mcc-infra-info.txt', info);
expect(bad, bad.join('\n')).toEqual([]);
});
});tests/iv0/IV0_S03.spec.ts ยท 36์ค ยท ํด์ 8210157a ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '@playwright/test';
import net from 'node:net';
import { sh, attach } from './_cli';
// IV0_S03 : ๋ฐฉํ๋ฒฝ ํฌํธ ์ ๊ฒ โ TC-ADMIN-003
// ํ์ ์ธ๋ถ ํฌํธ(README "Required Firewall Services" ๊ธฐ์ค, Cost FE ํ๋ก์๋ ํ์ฌ .env.setup ๊ฐ 7780 โ README ์ 7781 ์ ์ ๊ฐ)๊ฐ (1) ์๋ฒ์์ LISTEN ์ค์ด๊ณ (2) ์ธ๋ถ(์ด ํ
์คํธ๋ฅผ ์คํํ๋ PC)์์ TCP ์ ์๋๋์ง ํ์ธํ๋ค.
// ADMIN_CLI_PUBLIC_HOST: ์ธ๋ถ์์ ์ ์ํ ์๋ฒ ์ฃผ์(๋๋ฉ์ธ ๋๋ ๊ณต์ธ IP). ADMIN_CLI_PORTS ๋ก ๋ชฉ๋ก์ ๋ฐ๊ฟ ์ ์๋ค.
const HOST = process.env.ADMIN_CLI_PUBLIC_HOST ?? '';
const PORTS = (process.env.ADMIN_CLI_PORTS ?? '80,443,3001,33002,7780,9090,9000,3000').split(',').map(Number);
function connect(host: string, port: number, timeout = 5_000): Promise<boolean> {
return new Promise((resolve) => {
const s = net.connect({ host, port, timeout });
s.once('connect', () => { s.destroy(); resolve(true); });
s.once('timeout', () => { s.destroy(); resolve(false); });
s.once('error', () => resolve(false));
});
}
test('IV0_S03 ๋ฐฉํ๋ฒฝ ํฌํธ ์ ๊ฒ (ํ์ ํฌํธ LISTEN + ์ธ๋ถ TCP ์ ์)', async ({}, testInfo) => {
test.skip(!HOST, 'ADMIN_CLI_PUBLIC_HOST ๋ฏธ์ง์ ');
await test.step(`1. ์๋ฒ์์ ํ์ ํฌํธ LISTEN ํ์ธ (${PORTS.join(', ')})`, async () => {
const r = await sh(`ss -ltnH | awk '{print $4}' | sed 's/.*://' | sort -un`);
await attach(testInfo, 'listen-ports.txt', r);
const listening = new Set(r.out.split('\n').map(Number));
expect(PORTS.filter((p) => !listening.has(p)), 'LISTEN ํ์ง ์๋ ํฌํธ').toEqual([]);
});
await test.step(`2. ์ธ๋ถ(${HOST})์์ TCP ์ ์ ํ์ธ`, async () => {
const closed: number[] = [];
for (const p of PORTS) if (!(await connect(HOST, p))) closed.push(p);
testInfo.annotations.push({ type: 'ports', description: `์ด๋ฆผ ${PORTS.length - closed.length}/${PORTS.length}${closed.length ? `, ๋งํ: ${closed.join(', ')}` : ''}` });
expect(closed, '์ธ๋ถ์์ ์ ์๋์ง ์๋ ํฌํธ(๋ณด์๊ทธ๋ฃนยท๋ฐฉํ๋ฒฝ ํ์ธ)').toEqual([]);
});
});tests/iv0/IV0_S04.spec.ts ยท 39์ค ยท ํด์ 7159259d ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '@playwright/test';
import { sh, attach, tb, iamToken, iamList } from './_cli';
// IV0_S04 : IAM ์ด๊ธฐํ (iam_manager_init.sh / mc-iam-manager-post-initial) โ TC-ADMIN-004
// ์ ํ: TC-ADMIN-001(์ค์น) ์๋ฃ, mc-infra-manager healthy
// ํ์ธ: post-initial ๋ก๊ทธ์ ์ฑ๊ณต ๋ฉ์์ง โ IAM(๊ด๋ฆฌ์ยท์ญํ ยท๋ฉ๋ดยท๊ธฐ๋ณธ workspace/project) โ INFRA(namespace) ์์ฑ
const WS = process.env.ADMIN_CLI_DEFAULT_WORKSPACE ?? 'ws01';
const PJ = process.env.ADMIN_CLI_DEFAULT_PROJECT ?? 'default';
test('IV0_S04 IAM ์ด๊ธฐํ ๊ฒฐ๊ณผ ํ์ธ (post-initial ์ฑ๊ณต ๋ก๊ทธ ยท IAM ๊ธฐ๋ณธ ๋ฐ์ดํฐ ยท INFRA namespace)', async ({}, testInfo) => {
await test.step('1. docker logs mc-iam-manager-post-initial โ ์ฑ๊ณต ๋ฉ์์ง, ์ข
๋ฃ ์ฝ๋ 0', async () => {
const log = await sh('docker logs mc-iam-manager-post-initial 2>&1 | tail -n 60; echo "exit=$(docker inspect -f {{.State.ExitCode}} mc-iam-manager-post-initial)"');
await attach(testInfo, 'post-initial.log', log);
expect(log.out).toContain('[Success] MC-IAM-Manager initialization completed successfully!');
expect(log.out).toContain('Container will exit normally.');
expect(log.out).toContain('exit=0');
});
const token = await test.step('2. MC-IAM ํ๋ซํผ ๊ด๋ฆฌ์ ๋ก๊ทธ์ธ', iamToken);
await test.step('3. MC-IAM ์ญํ ยท๋ฉ๋ดยท๊ธฐ๋ณธ workspace/project ์์ฑ ํ์ธ', async () => {
const roles = JSON.stringify(await iamList(token, '/api/roles/list'));
const menus = JSON.stringify(await iamList(token, '/api/menus/list'));
const wss = JSON.stringify(await iamList(token, '/api/workspaces/list'));
const pjs = JSON.stringify(await iamList(token, '/api/projects/list'));
testInfo.annotations.push({ type: 'iam', description: `roles ${roles.length}B, menus ${menus.length}B, workspaces ${wss.length}B, projects ${pjs.length}B` });
expect(roles, 'ํ๋ซํผ ๊ด๋ฆฌ์ ์ญํ admin').toContain('"name":"admin"');
expect(menus.length, '๋ฉ๋ด ๋ชฉ๋ก').toBeGreaterThan(100);
expect(wss, `๊ธฐ๋ณธ workspace ${WS}`).toContain(`"${WS}"`);
expect(pjs, `๊ธฐ๋ณธ project ${PJ}`).toContain(`"${PJ}"`);
});
await test.step('4. MC-INFRA namespace ์์ฑ ํ์ธ (GET /ns)', async () => {
const ns = await tb('/ns');
await testInfo.attach('ns.json', { body: ns.raw, contentType: 'application/json' });
expect(ns.status).toBe(200);
expect((ns.json?.ns ?? []).map((n: any) => n.id), `project ${PJ} ์ namespace`).toContain(PJ);
});
});tests/iv0/IV0_S05.spec.ts ยท 33์ค ยท ํด์ cb875115 ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '@playwright/test';
import { sh, inBin, attach, requireDestructive } from './_cli';
// IV0_S05 : ํ๊ฒฝ ์ ์ฒด ์ ๋ฆฌ (cleanAll.sh) โ TC-ADMIN-005
// ์ฃผ์: cleanAll.sh ๋ M-CMP ๋ฟ ์๋๋ผ ์ด ์๋ฒ์ ๋ชจ๋ docker ์ด๋ฏธ์งยท์ปจํ
์ด๋ยท๋ณผ๋ฅจยท๋คํธ์ํฌ์ container-volume ํด๋๋ฅผ ์ง์ด๋ค.
// ์ ์ฉ ํ
์คํธ ์๋ฒ์์๋ง ์คํํ๋ค. sudo ๋น๋ฐ๋ฒํธ ์์ด ์คํ ๊ฐ๋ฅํด์ผ ํ๋ค(container-volume ์ญ์ ).
test('IV0_S05 ํ๊ฒฝ ์ ์ฒด ์ ๋ฆฌ (cleanAll.sh โ ์ด๋ฏธ์งยท์ปจํ
์ด๋ยท๋ณผ๋ฅจ ์ญ์ )', async ({}, testInfo) => {
requireDestructive('์ ์ฒด docker ์์ ์ญ์ ');
test.setTimeout(30 * 60_000);
await test.step('1. ์ ํ ์ํ ๊ธฐ๋ก (์ด๋ฏธ์งยท์ปจํ
์ด๋ยท๋ณผ๋ฅจ ์)', async () => {
const r = await sh('echo images=$(docker images -q | wc -l) containers=$(docker ps -aq | wc -l) volumes=$(docker volume ls -q | wc -l)');
testInfo.annotations.push({ type: 'before', description: r.out.trim() });
expect(r.out).not.toContain('images=0 ');
});
const clean = await test.step('2. bin ํด๋์์ ./cleanAll.sh ์คํ โ ํ์ธ ์ง๋ฌธ์ y', async () => {
const r = await inBin('./cleanAll.sh', 'y\n', 25 * 60_000);
await attach(testInfo, 'cleanAll.log', r);
return r;
});
await test.step('3. docker ์ด๋ฏธ์งยท์ปจํ
์ด๋ยท๋ณผ๋ฅจ์ด ๋ชจ๋ ์ญ์ ๋จ', async () => {
expect(clean.out).not.toContain('Aborted by user');
const r = await sh('echo images=$(docker images -q | wc -l) containers=$(docker ps -aq | wc -l) volumes=$(docker volume ls -q | wc -l)');
testInfo.annotations.push({ type: 'after', description: r.out.trim() });
expect(r.out).toContain('images=0 containers=0 volumes=0');
});
await test.step('4. container-volume ํด๋ ์ญ์ ๋จ', async () => {
expect((await inBin('test -d ../conf/docker/container-volume && echo exists || echo removed')).out.trim()).toBe('removed');
});
});tests/iv0/IV0_S06.spec.ts ยท 27์ค ยท ํด์ a84ddad3 ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '@playwright/test';
import { inBin, attach, requireDestructive, waitHealthy, unhealthy } from './_cli';
// IV0_S06 : ํ์คํ ์ด๋ฏธ์ง ํ ๋ฐ ๊ธฐ๋ (mcc infra pull โ run -d) โ TC-ADMIN-006
// ์ ํ: installAll.sh ์ค์ ๋จ๊ณ ์๋ฃ(.env, nginx.conf ์์ฑ) โ TC-ADMIN-001 ์ '์คํ ๋ฐฉ์ 3(Skip)' ๋๋ ๊ธฐ์กด ์ค์น
test('IV0_S06 ํ์คํ ์ด๋ฏธ์ง ํ ๋ฐ ๊ธฐ๋ (pull โ run -d โ ์ ์ฒด healthy)', async ({}, testInfo) => {
requireDestructive('์ ์ฒด ์คํ ์ด๋ฏธ์ง pullยท๊ธฐ๋');
test.setTimeout(60 * 60_000);
await test.step('1. ./mcc infra pull โ ์ด๋ฏธ์ง ๋ด๋ ค๋ฐ๊ธฐ ์ฑ๊ณต', async () => {
const r = await inBin('./mcc infra pull 2>&1', '', 40 * 60_000);
await attach(testInfo, 'pull.log', r);
expect(r.code).toBe(0);
expect(r.out).not.toMatch(/error|denied|not found/i);
});
await test.step('2. ./mcc infra run -d โ ๋ฐฑ๊ทธ๋ผ์ด๋ ๊ธฐ๋ (์ข
๋ฃ ์ฝ๋ 0)', async () => {
const r = await inBin('./mcc infra run -d 2>&1', '', 20 * 60_000);
await attach(testInfo, 'run.log', r);
expect(r.code).toBe(0);
});
await test.step('3. ./mcc infra info โ ์ ์ฒด framework healthy (์ต๋ 20๋ถ ๋๊ธฐ)', async () => {
await waitHealthy();
await attach(testInfo, 'mcc-infra-info.txt', (await unhealthy()).info);
});
});tests/iv0/IV0_S07.spec.ts ยท 33์ค ยท ํด์ 79f3ea35 ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '@playwright/test';
import { sh, inBin, attach, requireDestructive, waitHealthy } from './_cli';
// IV0_S07 : ๋จ์ผ ์๋น์ค ํ๊ฒ ๊ธฐ๋ (-s ํ๋๊ทธ) โ TC-ADMIN-007
// ๋์ ์๋น์ค 1๊ฐ๋ง ์ค์งํ๋ค๊ฐ -s ๋ก ๊ทธ ์๋น์ค๋ง ๋ค์ ๊ธฐ๋ํ๊ณ , ๋ค๋ฅธ ์๋น์ค๋ ์ฌ์์๋์ง ์์๋์ง ํ์ธํ๋ค.
// ADMIN_CLI_TARGET_SERVICE(๊ธฐ๋ณธ mc-data-manager), ๋น๊ต์ฉ ADMIN_CLI_OTHER_SERVICE(๊ธฐ๋ณธ mc-web-console-api)
const TARGET = process.env.ADMIN_CLI_TARGET_SERVICE ?? 'mc-data-manager';
const OTHER = process.env.ADMIN_CLI_OTHER_SERVICE ?? 'mc-web-console-api';
const startedAt = async (c: string) => (await sh(`docker inspect -f '{{.State.StartedAt}}' ${c}`)).out.trim();
const status = async (c: string) => (await sh(`docker inspect -f '{{.State.Status}}' ${c}`)).out.trim();
test(`IV0_S07 ๋จ์ผ ์๋น์ค ํ๊ฒ ๊ธฐ๋ (-s ${TARGET})`, async ({}, testInfo) => {
requireDestructive(`${TARGET} ์ค์งยท์ฌ๊ธฐ๋`);
test.setTimeout(20 * 60_000);
const otherBefore = await startedAt(OTHER);
await test.step(`1. ./mcc infra stop -s ${TARGET} โ ๋์๋ง ์ค์ง`, async () => {
await attach(testInfo, 'stop.log', await inBin(`./mcc infra stop -s ${TARGET} 2>&1`));
expect(await status(TARGET)).toBe('exited');
expect(await status(OTHER)).toBe('running');
});
await test.step(`2. ./mcc infra run -d -s ${TARGET} โ ๋์๋ง ๊ธฐ๋`, async () => {
const r = await inBin(`./mcc infra run -d -s ${TARGET} 2>&1`, '', 10 * 60_000);
await attach(testInfo, 'run.log', r);
expect(r.code).toBe(0);
});
await test.step('3. ๋์ healthy, ๋ค๋ฅธ ์๋น์ค๋ ์ฌ์์๋์ง ์์', async () => {
await waitHealthy([TARGET], 10 * 60_000);
expect(await startedAt(OTHER), `${OTHER} ๊ฐ ํจ๊ป ์ฌ์์๋จ`).toBe(otherBefore);
});
});tests/iv0/IV0_S08.spec.ts ยท 38์ค ยท ํด์ fcc2a71a ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '@playwright/test';
import { sh, inBin, attach, requireDestructive, waitHealthy } from './_cli';
// IV0_S08 : ์ค์ง โ ์ฌ๊ธฐ๋ โ ์ด๋ฏธ์ง ์
๋ฐ์ดํธ โ TC-ADMIN-008
// ADMIN_CLI_TARGET_SERVICE(๊ธฐ๋ณธ mc-data-manager)๋ก ๋ฒ์๋ฅผ ํ์ ํ๋ค. 'all' ์ด๋ฉด ์ ์ฒด ์คํ(-s ์์ด) ๋์.
// ์ฐธ๊ณ : v07 bin/mcc ์ update ๋ pull ํ 'docker compose up'(-d ์์)์ผ๋ก ํฌ๊ทธ๋ผ์ด๋์ ๋ถ์ด ๋๋์ง ์๋๋ค
// (MZC-CSC/mc-admin-cli#100 ์์ ํญ์ up -d ๋ก ์์ ). 15๋ถ timeout(124)ยท๊ฐ์ ์ข
๋ฃ(255 ๋ฑ) ๋ชจ๋ FAIL โ
// ๋ช
๋ น์ด ์ค์ค๋ก exit 0 ์ผ๋ก ๋๋์ผ PASS.
const TARGET = process.env.ADMIN_CLI_TARGET_SERVICE ?? 'mc-data-manager';
const S = TARGET === 'all' ? '' : ` -s ${TARGET}`;
// ์ค์ง ํ์ธ: ๋จ์ผ ์๋น์ค๋ exited, ์ ์ฒด๋ ์คํ ์ค์ธ mc- ์ปจํ
์ด๋ 0
const stopped = async () => TARGET === 'all'
? (await sh(`docker ps -q --filter label=com.docker.compose.project=mcc | wc -l`)).out.trim() === '0'
: (await sh(`docker inspect -f '{{.State.Status}}' ${TARGET}`)).out.trim() === 'exited';
test(`IV0_S08 ์ค์ง โ ์ฌ๊ธฐ๋ โ ์ด๋ฏธ์ง ์
๋ฐ์ดํธ (${TARGET})`, async ({}, testInfo) => {
requireDestructive(`${TARGET} ์ค์งยท์ฌ๊ธฐ๋ยท์
๋ฐ์ดํธ`);
test.setTimeout(60 * 60_000);
const targets = TARGET === 'all' ? undefined : [TARGET];
await test.step(`1. ./mcc infra stop${S} โ ์ค์ง๋จ`, async () => {
await attach(testInfo, 'stop.log', await inBin(`./mcc infra stop${S} 2>&1`, '', 15 * 60_000));
expect(await stopped(), '์ค์ง๋์ง ์์').toBe(true);
});
await test.step(`2. ./mcc infra run -d${S} โ ๋ค์ healthy`, async () => {
expect((await inBin(`./mcc infra run -d${S} 2>&1`, '', 15 * 60_000)).code).toBe(0);
await waitHealthy(targets);
});
await test.step(`3. ./mcc infra update${S} โ ์ต์ ์ด๋ฏธ์ง pull ํ ์ฌ๊ธฐ๋, ๋ช
๋ น์ด ์ข
๋ฃ๋จ`, async () => {
const r = await inBin(`timeout 900 ./mcc infra update${S} 2>&1`, '', 16 * 60_000);
await attach(testInfo, 'update.log', r);
expect(r.out).toContain('Install the latest Docker images');
expect(r.code, r.code === 124 ? 'update ๋ช
๋ น์ด 15๋ถ ์์ ๋๋์ง ์์(ํฌ๊ทธ๋ผ์ด๋ up)' : 'update ๋ช
๋ น์ด ์ ์ ์ข
๋ฃ(exit 0)ํ์ง ์์').toBe(0);
await waitHealthy(targets);
});
});tests/iv0/IV0_S09.spec.ts ยท 35์ค ยท ํด์ b47701dd ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '@playwright/test';
import { sh, inBin, attach, requireDestructive } from './_cli';
// IV0_S09 : ์คํ ์ ๋ฆฌ (mcc infra remove -v -i) โ TC-ADMIN-009
// ์ปจํ
์ด๋ยทnamed volumeยทcompose ์ด๋ฏธ์ง๋ฅผ ๋ชจ๋ ์ง์ด๋ค. ์ ์ฉ ํ
์คํธ ์๋ฒ์์๋ง ์คํ.
const COMPOSE = '../conf/docker/docker-compose.yaml';
test('IV0_S09 ์คํ ์ ๋ฆฌ (remove -v -i โ ์ปจํ
์ด๋ยท๋ณผ๋ฅจยท์ด๋ฏธ์ง ์ญ์ )', async ({}, testInfo) => {
requireDestructive('์คํ ์ ์ฒด ์ญ์ ');
test.setTimeout(30 * 60_000);
const images = await test.step('1. ์ ํ ์ํ ๊ธฐ๋ก (mcc ํ๋ก์ ํธ ์ปจํ
์ด๋ยท๋ณผ๋ฅจ, compose ์ด๋ฏธ์ง)', async () => {
const r = await inBin(`docker compose -f ${COMPOSE} config --images | sort -u`);
const list = r.out.split('\n').map((s) => s.trim()).filter(Boolean);
const c = (await sh(`docker ps -aq --filter label=com.docker.compose.project=mcc | wc -l`)).out.trim();
testInfo.annotations.push({ type: 'before', description: `์ปจํ
์ด๋ ${c}, compose ์ด๋ฏธ์ง ${list.length}` });
expect(Number(c), '์ ๋ฆฌํ ์คํ์ด ์์').toBeGreaterThan(0);
return list;
});
await test.step('2. ./mcc infra remove -v -i ์คํ', async () => {
await attach(testInfo, 'remove.log', await inBin('./mcc infra remove -v -i 2>&1', '', 25 * 60_000));
});
await test.step('3. mcc ํ๋ก์ ํธ ์ปจํ
์ด๋ยท๋ณผ๋ฅจ ์์', async () => {
const r = await sh(`echo c=$(docker ps -aq --filter label=com.docker.compose.project=mcc | wc -l) v=$(docker volume ls -q --filter label=com.docker.compose.project=mcc | wc -l)`);
expect(r.out.trim()).toBe('c=0 v=0');
});
await test.step('4. compose ์ด๋ฏธ์ง๊ฐ ๋ก์ปฌ์ ๋จ์ ์์ง ์์', async () => {
const left: string[] = [];
for (const img of images) if ((await sh(`docker image inspect ${img} >/dev/null 2>&1 && echo y || echo n`)).out.trim() === 'y') left.push(img);
expect(left, '์ญ์ ๋์ง ์์ ์ด๋ฏธ์ง').toEqual([]);
});
});tests/iv0/IV0_S10.spec.ts ยท 44์ค ยท ํด์ 279c16b7 ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '@playwright/test';
import { sh, inBin, attach, requireDestructive, requireClean, waitHealthy } from './_cli';
// IV0_S10 : Compose Variant ์ ํ (cert / mini) โ TC-ADMIN-010
// mini : docker-compose.mini.yaml ๋ก ์ต์ ๊ตฌ์ฑ(infra-connector/manager, iam, web-console)๋ง ๊ธฐ๋ โ healthy โ remove
// cert : docker-compose.cert.yaml ์ certbot ์ผ๋ก ์ธ์ฆ์ ๋ฐ๊ธ(ADMIN_CLI_FQDN ์ง์ ์์๋ง, 80 ํฌํธ ๋น์ด ์์ด์ผ ํจ)
const MINI = '../conf/docker/docker-compose.mini.yaml';
const CERT = '../conf/docker/docker-compose.cert.yaml';
const FQDN = process.env.ADMIN_CLI_FQDN ?? '';
test('IV0_S10 Compose Variant ์ ํ (mini ๊ธฐ๋ / cert ๋ฐ๊ธ)', async ({}, testInfo) => {
requireDestructive('variant ์คํ ๊ธฐ๋ยท์ญ์ ');
test.setTimeout(60 * 60_000);
await test.step('1. ์ ํ ์ํ: ๊ธฐ๋ณธ ์คํ์ด ์์ (์ปจํ
์ด๋ ์ด๋ฆ์ด ๊ฐ์ ํจ๊ป ๋์ธ ์ ์์)', requireClean);
const services = await test.step('2. ./mcc infra run -d -f mini โ ๊ธฐ๋', async () => {
const svc = (await inBin(`docker compose -f ${MINI} config --services`)).out.split('\n').map((s) => s.trim()).filter((s) => s && s !== 'mc-iam-manager-post-initial');
const r = await inBin(`./mcc infra run -d -f ${MINI} 2>&1`, '', 30 * 60_000);
await attach(testInfo, 'mini-run.log', r);
expect(r.code).toBe(0);
return svc;
});
await test.step(`3. mini ์๋น์ค ${services.length}๊ฐ healthy, ๊ทธ ๋ฐ์ mc- ์ปจํ
์ด๋ ์์`, async () => {
await waitHealthy(services);
const extra = (await sh(`docker ps --filter label=com.docker.compose.project=mcc --format '{{.Names}}'`)).out.split('\n').filter((n) => n && !services.includes(n) && n !== 'mc-iam-manager-post-initial');
expect(extra, 'mini ์ ์์ ์๋ ์ปจํ
์ด๋').toEqual([]);
});
await test.step('4. ./mcc infra remove -v -f mini โ ์ ๋ฆฌ', async () => {
await attach(testInfo, 'mini-remove.log', await inBin(`./mcc infra remove -v -f ${MINI} 2>&1`, '', 10 * 60_000));
expect((await sh(`docker ps -aq --filter label=com.docker.compose.project=mcc | wc -l`)).out.trim()).toBe('0');
});
await test.step('5. ./mcc infra run -f cert โ certbot ์ธ์ฆ์ ๋ฐ๊ธ (ADMIN_CLI_FQDN ์ง์ ์)', async () => {
if (!FQDN) { testInfo.annotations.push({ type: 'cert', description: 'ADMIN_CLI_FQDN ๋ฏธ์ง์ โ cert ๋จ๊ณ ๊ฑด๋๋' }); return; }
const r = await inBin(`./mcc infra run -f ${CERT} 2>&1`, '', 10 * 60_000);
await attach(testInfo, 'cert-run.log', r);
expect((await sh(`docker inspect -f '{{.State.ExitCode}}' mcmp-certbot`)).out.trim()).toBe('0');
const pem = await inBin(`sudo test -f ../conf/docker/container-volume/mc-iam-manager/certs/live/${FQDN}/fullchain.pem && echo ok`);
expect(pem.out.trim(), '์ธ์ฆ์ ํ์ผ ์์').toBe('ok');
});
});tests/iv0/IV0_S11.spec.ts ยท 39์ค ยท ํด์ 6b23b35d ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '@playwright/test';
import { inBin, sh, attach, requireDestructive } from './_cli';
// IV0_S11 : K8s ๋ฐฐํฌ (mcc k8s run, GKE/EKS/AKS/minikube) โ TC-ADMIN-011
// ํ ๋ฒ์ README: "K8S is not currently supported" โ k8s ์๋ธ์ปค๋งจ๋๊ฐ ๋ฑ๋ก๋์ง ์์ผ๋ฉด(src/cmd/k8s/root.go AddCommand ์ฃผ์) SKIP ์ผ๋ก ๋จ๊ธด๋ค.
// ์ง์๋ ๋์ ์ ์ฐจ: kubectl ์ปจํ
์คํธ๊ฐ ๋์ ํด๋ฌ์คํฐ / helm ์ค์น / bin/../helm-chart ์กด์ฌ
// ADMIN_CLI_K8S_PROVIDER = gke|eks|aks|minikube, ADMIN_CLI_K8S_VALUES = values ํ์ผ(๊ธฐ๋ณธ ../helm-chart/values.yaml)
const PROVIDER = process.env.ADMIN_CLI_K8S_PROVIDER ?? 'minikube';
const VALUES = process.env.ADMIN_CLI_K8S_VALUES ?? '../helm-chart/values.yaml';
test(`IV0_S11 K8s ๋ฐฐํฌ (mcc k8s run --k8sprovider ${PROVIDER})`, async ({}, testInfo) => {
const help = await inBin('./mcc k8s --help 2>&1');
test.skip(/unknown command "k8s"/.test(help.out), 'mcc k8s ๋ฏธ์ง์(ํ ๋ฒ์ ๋นํ์ฑ, README: K8S is not currently supported)');
requireDestructive('K8s ํด๋ฌ์คํฐ์ M-CMP ๋ฐฐํฌ');
test.setTimeout(60 * 60_000);
await test.step('1. ์ ํ ์ํ ํ์ธ (kubectl ์ ์, helm, helm-chart)', async () => {
expect((await sh('kubectl cluster-info && helm version')).code).toBe(0);
expect((await inBin(`test -f ${VALUES} && echo ok`)).out.trim(), 'values ํ์ผ ์์').toBe('ok');
});
await test.step(`2. ./mcc k8s run -f ${VALUES} --k8sprovider ${PROVIDER}`, async () => {
const r = await inBin(`./mcc k8s run -f ${VALUES} --k8sprovider ${PROVIDER} 2>&1`, '', 30 * 60_000);
await attach(testInfo, 'k8s-run.log', r);
expect(r.out).not.toMatch(/Error:|failed/i);
});
await test.step('3. ./mcc k8s info โ ๋ชจ๋ Pod Running/Completed', async () => {
await expect.poll(async () => {
const r = await sh(`kubectl get pods -n mcmp --no-headers | awk '$3!="Running" && $3!="Completed"' | wc -l`);
return r.out.trim();
}, { timeout: 25 * 60_000, intervals: [20_000] }).toBe('0');
await attach(testInfo, 'k8s-info.log', await inBin('./mcc k8s info 2>&1'));
});
await test.step('4. ./mcc k8s stop โ ์ ๋ฆฌ', async () => {
await attach(testInfo, 'k8s-stop.log', await inBin(`./mcc k8s stop -f ${VALUES} 2>&1`, '', 10 * 60_000));
});
});tests/iv0/IV0_S12.spec.ts ยท 23์ค ยท ํด์ fd80da3a ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '@playwright/test';
import { inBin, sh, attach, requireDestructive } from './_cli';
// IV0_S12 : Weave Scope ์ค์น/์ ๊ฑฐ (mcc k8s weave-scope install/uninstall) โ TC-ADMIN-012
// k8s ์๋ธ์ปค๋งจ๋๊ฐ ๋นํ์ฑ์ด๋ฉด SKIP. ์ฐธ๊ณ : ์ค์น ๋งค๋ํ์คํธ URL(cloud.weave.works)์ Weaveworks ์๋น์ค ์ข
๋ฃ๋ก ์ ์๋์ง ์์ ์ ์๋ค.
const PROVIDER = process.env.ADMIN_CLI_K8S_PROVIDER ?? 'minikube';
test(`IV0_S12 Weave Scope ์ค์น/์ ๊ฑฐ (--k8sprovider ${PROVIDER})`, async ({}, testInfo) => {
const help = await inBin('./mcc k8s --help 2>&1');
test.skip(/unknown command "k8s"/.test(help.out), 'mcc k8s ๋ฏธ์ง์(ํ ๋ฒ์ ๋นํ์ฑ)');
requireDestructive('ํด๋ฌ์คํฐ์ weave ๋ค์์คํ์ด์ค ์์ฑยท์ญ์ ');
test.setTimeout(20 * 60_000);
await test.step('1. install-weave-scope โ weave ๋ค์์คํ์ด์ค Pod Running', async () => {
await attach(testInfo, 'install.log', await inBin(`./mcc k8s weave-scope install-weave-scope --k8sprovider ${PROVIDER} 2>&1`, '', 10 * 60_000));
await expect.poll(async () => (await sh(`kubectl get pods -n weave --no-headers 2>/dev/null | grep -c Running || true`)).out.trim(), { timeout: 8 * 60_000 }).not.toBe('0');
});
await test.step('2. weave-scope uninstall โ weave ์์ ์ญ์ ', async () => {
await attach(testInfo, 'uninstall.log', await inBin(`./mcc k8s weave-scope uninstall --k8sprovider ${PROVIDER} 2>&1`, '', 10 * 60_000));
await expect.poll(async () => (await sh(`kubectl get ns weave --no-headers 2>/dev/null | wc -l`)).out.trim(), { timeout: 5 * 60_000 }).toBe('0');
});
});tests/iv0/IV0_S13.spec.ts ยท 23์ค ยท ํด์ c9c82ca1 ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '@playwright/test';
import { inBin, mcContainers, attach } from './_cli';
// IV0_S13 : Docker-K8s ์ํธ๋ฐฐํ ๊ฒ์ฆ (์์ธ) โ TC-ADMIN-013
// Docker Compose ์คํ์ด ์คํ ์ค์ผ ๋ mcc k8s run ์ ๊ฑฐ๋ถ๋์ด์ผ ํ๋ค(๊ฐ์ ํ๋ซํผ์ ์ด์ค ๋ฐฐํฌ ๋ฐฉ์ง).
// k8s ์๋ธ์ปค๋งจ๋๊ฐ ๋นํ์ฑ์ด๋ฉด SKIP.
const PROVIDER = process.env.ADMIN_CLI_K8S_PROVIDER ?? 'minikube';
test('IV0_S13 Docker-K8s ์ํธ๋ฐฐํ (Compose ์คํ ์ค k8s run ๊ฑฐ๋ถ)', async ({}, testInfo) => {
const help = await inBin('./mcc k8s --help 2>&1');
test.skip(/unknown command "k8s"/.test(help.out), 'mcc k8s ๋ฏธ์ง์(ํ ๋ฒ์ ๋นํ์ฑ)');
await test.step('1. ์ ํ ์ํ: Docker Compose ์คํ ์คํ ์ค', async () => {
expect((await mcContainers(false)).length, 'Compose ์คํ์ด ์คํ ์ค์ด ์๋').toBeGreaterThan(0);
});
await test.step('2. ./mcc k8s run ์คํ โ ๊ฑฐ๋ถ ๋ฉ์์ง, helm install ๋ฏธ์คํ', async () => {
const r = await inBin(`./mcc k8s run --k8sprovider ${PROVIDER} 2>&1`, '', 5 * 60_000);
await attach(testInfo, 'k8s-run.log', r);
expect(r.out, 'Docker ์คํ ์ค์ธ๋ฐ helm install ์ด ์คํ๋จ').not.toContain('helm install');
expect(r.out).toMatch(/docker|compose|already running|mutually exclusive/i);
});
});tests/iv0/IV0_S14.spec.ts ยท 26์ค ยท ํด์ b6c58611 ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '@playwright/test';
import { inBin, attach, startEcho, stopEcho, echoed, ECHO } from './_cli';
// IV0_S14 : mcc rest HTTP ๋ฉ์๋๋ณ ํธ์ถ (GET/POST/PUT/PATCH/DELETE) โ TC-ADMIN-014
// ๋์ ์๋ฒ์ ์์ echo ์๋ฒ(127.0.0.1:18999)๋ฅผ ๋์, ์๋ฒ๊ฐ ๋ฐ์ ๋ฉ์๋ยท๋ณธ๋ฌธ์ ๊ทธ๋๋ก ๋๋ ค๋ฐ์ ๋น๊ตํ๋ค.
const METHODS = ['get', 'post', 'put', 'patch', 'delete'] as const;
test.describe('IV0_S14', () => {
test.beforeAll(startEcho);
test.afterAll(stopEcho);
test('IV0_S14 HTTP ๋ฉ์๋๋ณ ํธ์ถ (5์ข
๋ฉ์๋๊ฐ ์๋ฒ์ ๊ทธ๋๋ก ์ ๋ฌ๋จ)', async ({}, testInfo) => {
for (const m of METHODS) {
await test.step(`./mcc rest ${m} ${ECHO}${m === 'get' || m === 'delete' ? '' : " -d '{\"k\":\"v\"}'"}`, async () => {
const data = m === 'get' || m === 'delete' ? '' : ` -d '{"k":"v"}'`;
const r = await inBin(`./mcc rest ${m} ${ECHO}${data}`);
await attach(testInfo, `${m}.txt`, r);
const e = echoed(r.out);
// 5์ข
์ ๋ชจ๋ ํ์ธํ๋๋ก soft ๋จ์ธ(ํ ๋ฉ์๋ ์คํจ๋ก ๋๋จธ์ง๋ฅผ ๊ฑด๋๋ฐ์ง ์์)
expect.soft(e?.method, `${m} ์์ฒญ์ด ๋ค๋ฅธ ๋ฉ์๋๋ก ์ ์ก๋จ`).toBe(m.toUpperCase());
if (data) expect.soft(e?.body, `${m} ๋ณธ๋ฌธ ๋ถ์ผ์น`).toBe('{"k":"v"}');
expect.soft(r.code).toBe(0);
});
}
});
});tests/iv0/IV0_S15.spec.ts ยท 34์ค ยท ํด์ 94094503 ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '@playwright/test';
import { inBin, attach, startEcho, stopEcho, echoed, ECHO, TB, TB_AUTH } from './_cli';
// IV0_S15 : mcc rest ์ธ์ฆ ๋ชจ๋ (Basic / Bearer) โ TC-ADMIN-015
// echo ์๋ฒ๋ก Authorization ํค๋ ๊ฐ์ ํ์ธํ๊ณ , ์ค์ ์๋น์ค(mc-infra-manager readyz)์ Basic ์ธ์ฆ์ผ๋ก ํธ์ถํ๋ค.
test.describe('IV0_S15', () => {
test.beforeAll(startEcho);
test.afterAll(stopEcho);
test('IV0_S15 ์ธ์ฆ ๋ชจ๋ (Basic -u/-p, Bearer --authToken, ์ฌ์ฉ์ scheme --authScheme)', async ({}, testInfo) => {
await test.step('1. Basic: -u iv0user -p iv0pass โ Authorization: Basic base64(iv0user:iv0pass)', async () => {
const r = await inBin(`./mcc rest get -u iv0user -p iv0pass ${ECHO}`);
expect(echoed(r.out)?.headers?.Authorization).toBe(`Basic ${Buffer.from('iv0user:iv0pass').toString('base64')}`);
});
await test.step('2. Bearer: --authToken iv0token โ Authorization: Bearer iv0token', async () => {
const r = await inBin(`./mcc rest get --authToken iv0token ${ECHO}`);
expect(echoed(r.out)?.headers?.Authorization).toBe('Bearer iv0token');
});
await test.step('3. ์ฌ์ฉ์ scheme: --authScheme OAuth --authToken iv0token โ Authorization: OAuth iv0token', async () => {
const r = await inBin(`./mcc rest get --authScheme OAuth --authToken iv0token ${ECHO}`);
expect(echoed(r.out)?.headers?.Authorization).toBe('OAuth iv0token');
});
await test.step('4. ์ค์ ์๋น์ค: mc-infra-manager /readyz ๋ฅผ Basic ์ธ์ฆ์ผ๋ก ํธ์ถ โ ready', async () => {
const [u, p] = TB_AUTH.split(':');
const r = await inBin(`./mcc rest get -u '${u}' -p '${p}' ${TB}/readyz`);
await attach(testInfo, 'tb-readyz.txt', { ...r, out: r.out.replace(p, '***') });
expect(r.out).toContain('"ready":true');
expect(r.code).toBe(0);
});
});
});tests/iv0/IV0_S16.spec.ts ยท 30์ค ยท ํด์ 1caf97f6 ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '@playwright/test';
import { sh, inBin, attach, startEcho, stopEcho, echoed, ECHO } from './_cli';
// IV0_S16 : mcc rest ํค๋ยท๋ฐ๋ ์ค์ (-H, -d, -f) โ TC-ADMIN-016
// ๋ณธ๋ฌธ ์ ๋ฌ์ PUT ์ผ๋ก ํ์ธํ๋ค(POST ์์ฒด์ ๋ฉ์๋ ๊ฒฐํจ์ IV0_S14 ์์ ํ์ ).
test.describe('IV0_S16', () => {
test.beforeAll(startEcho);
test.afterAll(stopEcho);
test('IV0_S16 ํค๋ยท๋ฐ๋ ์ค์ (-H ์ฌ๋ฌ ๊ฐ, -d ๋ฌธ์์ด, -f ํ์ผ)', async ({}, testInfo) => {
await test.step('1. -H "Content-Type:application/json" -H "X-Iv0:hello" โ ์๋ฒ๊ฐ ๋ ํค๋๋ฅผ ๋ฐ์', async () => {
const r = await inBin(`./mcc rest get -H "Content-Type:application/json" -H "X-Iv0:hello" ${ECHO}`);
const h = echoed(r.out)?.headers ?? {};
expect(h['Content-Type']).toBe('application/json');
expect(h['X-Iv0']).toBe('hello');
});
await test.step('2. -d ๋ก JSON ๋ณธ๋ฌธ ์ ๋ฌ(put) โ ์๋ฒ๊ฐ ๊ฐ์ ๋ณธ๋ฌธ์ ๋ฐ์', async () => {
const r = await inBin(`./mcc rest put -H "Content-Type:application/json" ${ECHO} -d '{"RegionName":"iv0"}'`);
expect(echoed(r.out)?.body).toBe('{"RegionName":"iv0"}');
});
await test.step('3. -f ํ์ผ๋ก JSON ๋ณธ๋ฌธ ์ ๋ฌ(put) โ ์๋ฒ๊ฐ ํ์ผ ๋ด์ฉ์ ๋ฐ์', async () => {
await sh(`echo -n '{"from":"file"}' > /tmp/iv0-body.json`);
const r = await inBin(`./mcc rest put -H "Content-Type:application/json" ${ECHO} -f /tmp/iv0-body.json`);
await attach(testInfo, 'post-file.txt', r);
expect(echoed(r.out)?.body, '-f ํ์ผ ๋ด์ฉ์ด ์ ์ก๋์ง ์์').toBe('{"from":"file"}');
});
});
});tests/iv0/IV0_S17.spec.ts ยท 28์ค ยท ํด์ e71547ba ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '@playwright/test';
import { inBin, attach, startEcho, stopEcho, ECHO } from './_cli';
// IV0_S17 : mcc rest HEAD(-I ์๋ต ํค๋) / verbose(-v ์ถ์ ์ ๋ณด) โ TC-ADMIN-017
test.describe('IV0_S17', () => {
test.beforeAll(startEcho);
test.afterAll(stopEcho);
test('IV0_S17 -I ์๋ต ํค๋ ํ์ / -v ์์ฒญ ์ถ์ ์ ๋ณด ํ์', async ({}, testInfo) => {
await test.step('1. ./mcc rest get -I โ ์๋ต ํค๋(X-Echo, Content-Type) ์ถ๋ ฅ', async () => {
const r = await inBin(`./mcc rest get -I ${ECHO}`);
await attach(testInfo, 'head.txt', r);
expect(r.out).toContain('Headers:');
expect(r.out).toMatch(/X-Echo: iv0/);
expect(r.out).toMatch(/Content-Type: application\/json/);
// ํ๋๊ทธ ์ค๋ช
์ "Show response headers only" โ ๋ณธ๋ฌธ๋ ํจ๊ป ์ถ๋ ฅ๋๋ฉด ๋น๊ณ ๋ก ๋จ๊ธด๋ค
if (r.out.includes('"method"')) testInfo.annotations.push({ type: '๊ฐ์ ๋ฉ๋ชจ', description: '-I ๊ฐ ํค๋์ ํจ๊ป ๋ณธ๋ฌธ๋ ์ถ๋ ฅ(์ค๋ช
: headers only)' });
});
await test.step('2. ./mcc rest get -v โ Request Trace Info ยท Status Code 200 ์ถ๋ ฅ', async () => {
const r = await inBin(`./mcc rest get -v ${ECHO}`);
await attach(testInfo, 'verbose.txt', r);
expect(r.out).toContain('Request Trace Info:');
expect(r.out).toContain('TotalTime');
expect(r.out).toMatch(/Status Code: 200/);
});
});
});tests/iv0/IV0_S18.spec.ts ยท 25์ค ยท ํด์ 087c2c71 ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '@playwright/test';
import { sh, inBin, attach, startEcho, stopEcho, ECHO } from './_cli';
// IV0_S18 : mcc rest ์ข
๋ฃ ์ฝ๋ = HTTP ์ํ ์ฒซ ์๋ฆฌ (Docker Compose healthcheck ์ฐ๋) โ TC-ADMIN-018
// 200 โ 0, 4xx โ 4, 5xx โ 5. compose healthcheck ๊ฐ ์ด ์ข
๋ฃ ์ฝ๋๋ก healthy ๋ฅผ ํ์ ํ๋ค.
test.describe('IV0_S18', () => {
test.beforeAll(startEcho);
test.afterAll(stopEcho);
test('IV0_S18 healthcheck ์ข
๋ฃ ์ฝ๋ (200โ0, 404โ4, 503โ5 ยท compose healthcheck ์ฐ๋)', async ({}, testInfo) => {
for (const [st, code] of [[200, 0], [404, 4], [503, 5]] as const) {
await test.step(`./mcc rest get ${ECHO}?status=${st} โ ์ข
๋ฃ ์ฝ๋ ${code}`, async () => {
const r = await inBin(`./mcc rest get '${ECHO}?status=${st}'`);
expect(r.code).toBe(code);
});
}
await test.step('compose healthcheck ๊ฐ mcc rest get ์ ์ฌ์ฉํ๊ณ healthy ๋ก ํ์ ๋จ (mc-infra-manager)', async () => {
const r = await sh(`docker inspect -f '{{json .Config.Healthcheck.Test}} {{.State.Health.Status}}' mc-infra-manager`);
await attach(testInfo, 'healthcheck.txt', r);
expect(r.out).toContain('"/app/tool/mcc","rest","get"');
expect(r.out.trim()).toMatch(/healthy$/);
});
});
});tests/iv0/IV0_S19.spec.ts ยท 37์ค ยท ํด์ 9fe1e75f ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '@playwright/test';
import { inBin, attach } from './_cli';
// IV0_S19 : mcc api ์๋น์คยท์ก์
์นดํ๋ก๊ทธ ์กฐํ (--list) โ TC-ADMIN-019
// ๊ธฐ์ค: bin/../conf/api.yaml ์ services / serviceActions ๋ชฉ๋ก๊ณผ ์ถ๋ ฅ์ด ์ผ์นํด์ผ ํ๋ค.
const yamlKeys = (section: string, svc = '') => inBin(
`python3 -c "import yaml,sys;d=yaml.safe_load(open('../conf/api.yaml'));s=d['${section}']${svc ? `['${svc}']` : ''};print('\\n'.join(sorted(s or {})))"`,
);
test('IV0_S19 ์๋น์คยท์ก์
์นดํ๋ก๊ทธ ์กฐํ (api --list, -s <service> --list)', async ({}, testInfo) => {
await test.step('1. ./mcc api --list โ api.yaml ์ ์๋น์ค ์ ์ฒด ์ถ๋ ฅ', async () => {
const r = await inBin('./mcc api --list');
await attach(testInfo, 'service-list.txt', r);
const expected = (await yamlKeys('services')).out.split('\n').filter(Boolean);
expect(expected.length).toBeGreaterThan(0);
const shown = r.out.split('\n').map((s) => s.trim());
expect(expected.filter((s) => !shown.includes(s)), '๋ชฉ๋ก์ ์๋ ์๋น์ค').toEqual([]);
});
await test.step('2. ./mcc api -s mc-infra-manager --list โ ์ก์
์ ์ฒด ์ถ๋ ฅ (GetReadyz ํฌํจ)', async () => {
const r = await inBin('./mcc api -s mc-infra-manager --list');
await attach(testInfo, 'action-list.txt', r);
const expected = (await yamlKeys('serviceActions', 'mc-infra-manager')).out.split('\n').filter(Boolean);
const shown = r.out.split('\n').map((s) => s.trim());
expect(r.out).toContain('[mc-infra-manager] Service Actions list');
// viper ๊ฐ ํค๋ฅผ ์๋ฌธ์๋ก ๋ฐ๊ฟ ์ถ๋ ฅํ๋ค(getreadyz) โ ๋์๋ฌธ์ ๋ฌด์ ๋น๊ต, ํธ์ถ์ ์๋ ์ด๋ฆ(-a GetReadyz)์ผ๋ก๋ ๋๋ค
const lower = new Set(shown.map((s) => s.toLowerCase()));
expect(lower.has('getreadyz')).toBe(true);
if (!shown.includes('GetReadyz')) testInfo.annotations.push({ type: '๊ฐ์ ๋ฉ๋ชจ', description: '์ก์
๋ชฉ๋ก์ด ์๋ฌธ์๋ก ์ถ๋ ฅ๋จ(api.yaml ์ด๋ฆ๊ณผ ๋ค๋ฆ)' });
expect(expected.filter((a) => !lower.has(a.toLowerCase())), '๋ชฉ๋ก์ ์๋ ์ก์
').toEqual([]);
});
await test.step('3. ์๋ ์๋น์ค ์ง์ โ ์ง์ ์๋น์ค๊ฐ ์๋๋ผ๋ ์๋ด', async () => {
const r = await inBin('./mcc api -s no-such-service -a X');
expect(r.out).toContain('is not on the list of supported services');
});
});tests/iv0/IV0_S20.spec.ts ยท 32์ค ยท ํด์ 653f91eb ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '@playwright/test';
import { inBin, attach, apiConfig } from './_cli';
// IV0_S20 : mcc api ์๋น์ค๋ณ ๊ธฐ๋ณธ ํธ์ถ (9์ข
) โ TC-ADMIN-020
// api.yaml ์ 9๊ฐ ์๋น์ค์ ๋ํด ์ธ์ฆยท๊ฒฝ๋ก ํ๋ผ๋ฏธํฐ๊ฐ ํ์ ์๋ ์ํ ํ์ธ ์ก์
์ ํธ์ถํ๋ค(์ค์ ํ์ผ์ ์ธ์ฆ๊ฐ ๊ทธ๋๋ก ์ฌ์ฉ).
// baseurl ์ docker ๋ด๋ถ ํธ์คํธ๋ช
์ localhost ๋ก ๋ฐ๊พผ ์ฌ๋ณธ์ ์ด๋ค(_cli.apiConfig). ํธ์คํธ์ ํฌํธ๊ฐ ๊ณต๊ฐ๋์ง ์์ ์๋น์ค๋ ์คํจ๋ก ๋จ๋๋ค.
const CALLS: [string, string, RegExp][] = [
['mc-infra-connector', 'Health-Check-Readyz', /ready/i],
['mc-infra-manager', 'GetReadyz', /"ready":\s*true/],
['mc-iam-manager', 'Readyz', /healthy|ready/i],
['mc-web-console', 'getSetupYamlCheck', /\{/],
['mc-observability', 'GetObsReadyz', /ready|ok|success/i],
['mc-application-manager', 'GetAppMgrReadyz', /ready|ok|success/i],
['mc-workflow-manager', 'GetWorkflowReadyz', /ready|ok|success/i],
['mc-cost-optimizer', 'GetCostReadyz', /ready|ok|success/i],
['mc-data-manager', 'GetDataMgrReadyz', /ready|ok|success/i],
];
test('IV0_S20 ์๋น์ค๋ณ ๊ธฐ๋ณธ ํธ์ถ (9์ข
์ํ ํ์ธ ์ก์
์๋ต)', async ({}, testInfo) => {
const cfg = await apiConfig();
const failed: string[] = [];
for (const [svc, action, ok] of CALLS) {
await test.step(`./mcc api -c ${cfg} -s ${svc} -a ${action}`, async () => {
const r = await inBin(`./mcc api -c ${cfg} -s ${svc} -a ${action}`, '', 60_000);
await attach(testInfo, `${svc}.txt`, r);
const body = r.out.split('service calling...').pop() ?? '';
if (!ok.test(body) || /connection refused|no such host|Unauthorized|dial tcp/i.test(body)) failed.push(`${svc}/${action}: ${body.trim().slice(0, 120)}`);
});
}
testInfo.annotations.push({ type: 'services', description: `์ฑ๊ณต ${CALLS.length - failed.length}/${CALLS.length}` });
expect(failed, `์๋ต์ด ๊ธฐ๋์ ๋ค๋ฅธ ์๋น์ค:\n${failed.join('\n')}`).toEqual([]);
});tests/iv0/IV0_S21.spec.ts ยท 35์ค ยท ํด์ aed0c1ea ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '@playwright/test';
import { inBin, attach, apiConfig, TB_AUTH_ARGS } from './_cli';
// IV0_S21 : mcc api ๊ฒฝ๋ก ํ๋ผ๋ฏธํฐยท์ฟผ๋ฆฌ ๋ฌธ์์ด ์นํ (-p, -q) โ TC-ADMIN-021
// mc-infra-manager ์ GetNs(/ns/{nsId}), GetAllNs(/ns) ์ฌ์ฉ. ์ธ์ฆ์ --authUser/--authPassword ๋ก ์ง์ (ADMIN_CLI_TB_AUTH).
const NS = process.env.ADMIN_CLI_TB_NS ?? 'default';
test('IV0_S21 ๊ฒฝ๋ก ํ๋ผ๋ฏธํฐยท์ฟผ๋ฆฌ ๋ฌธ์์ด ์นํ (-p nsId:<ns>, -q option=id)', async ({}, testInfo) => {
const cfg = await apiConfig();
const api = (args: string) => inBin(`./mcc api -c ${cfg} -s mc-infra-manager ${TB_AUTH_ARGS} ${args}`);
await test.step(`1. -a GetNs -p nsId:${NS} โ /ns/${NS} ํธ์ถ, ํด๋น namespace ๋ฐํ`, async () => {
const r = await api(`-a GetNs -p nsId:${NS}`);
await attach(testInfo, 'getns.txt', r);
expect(r.out).toContain(`"id":"${NS}"`);
});
await test.step('2. -p ์์ด ๊ฐ๋ณ ๊ฒฝ๋ก ์ก์
ํธ์ถ โ ๊ฒฝ๋ก ํ๋ผ๋ฏธํฐ ํ์ ์๋ด', async () => {
const r = await api('-a GetNs');
expect(r.out).toContain("couldn't find uri path parameter");
expect(r.out).toContain('/ns/{nsId}');
});
await test.step('3. -a GetAllNs -q option=id โ /ns?option=id ํธ์ถ, id ๋ชฉ๋ก(output) ๋ฐํ', async () => {
const r = await api('-a GetAllNs -q option=id');
await attach(testInfo, 'getallns-id.txt', r);
expect(r.out).toMatch(/"output":\s*\[/);
expect(r.out).toContain(`"${NS}"`);
});
await test.step('4. -q "?option=id" (๋ฌผ์ํ ํฌํจ) โ ๊ฐ์ ๊ฒฐ๊ณผ', async () => {
const r = await api(`-a GetAllNs -q '?option=id'`);
expect(r.out).toMatch(/"output":\s*\[/);
});
});tests/iv0/IV0_S22.spec.ts ยท 24์ค ยท ํด์ 9740de4b ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '@playwright/test';
import { sh, inBin, attach, apiConfig, TB_AUTH_ARGS } from './_cli';
// IV0_S22 : mcc api ์ถ๋ ฅ ํ์ผ ์ ์ฅ (-o) โ TC-ADMIN-022
const OUT = '/tmp/iv0-getallns.json';
test('IV0_S22 ์ถ๋ ฅ ํ์ผ ์ ์ฅ (-o ํ์ผ์ ์๋ต JSON ์ ์ฅ)', async ({}, testInfo) => {
const cfg = await apiConfig();
await sh(`rm -f ${OUT}`);
await test.step(`1. ./mcc api -s mc-infra-manager -a GetAllNs -o ${OUT}`, async () => {
const r = await inBin(`./mcc api -c ${cfg} -s mc-infra-manager ${TB_AUTH_ARGS} -a GetAllNs -o ${OUT}`);
await attach(testInfo, 'stdout.txt', r);
});
await test.step('2. ํ์ผ์ด ์์ฑ๋๊ณ ์๋ต JSON(ns ๋ชฉ๋ก)์ด ๋ค์ด ์์', async () => {
const r = await sh(`cat ${OUT}`);
await attach(testInfo, 'output-file.json', r);
expect(r.code, '์ถ๋ ฅ ํ์ผ ์์').toBe(0);
const json = JSON.parse(r.out);
expect(Array.isArray(json.ns), 'ns ๋ชฉ๋ก ์๋').toBe(true);
});
await sh(`rm -f ${OUT}`);
});tests/iv0/IV0_S23.spec.ts ยท 41์ค ยท ํด์ 9f25ea19 ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '@playwright/test';
import { sh, inBin, attach, TB, TB_AUTH } from './_cli';
// IV0_S23 : mcc api tool โ Swagger JSON โ api.yaml ์ก์
์ ์ ์์ฑ โ TC-ADMIN-023
// ์คํ ์ค์ธ mc-infra-manager ์ Swagger(/tumblebug/api/doc.json)๋ฅผ ๋ฐ์ tool ๋ก ๋ณํํ๊ณ ,
// (1) operationId ๋ง๋ค ์ก์
์ด ์์ฑ๋๋์ง (2) ํ์ฌ api.yaml ์ mc-infra-manager ์ก์
๊ณผ ์ผ๋ง๋ ์ผ์นํ๋์ง ํ์ธํ๋ค.
const SWAGGER = '/tmp/iv0-tb-swagger.json';
test('IV0_S23 api tool Swagger โ api.yaml ์ก์
์ฌ์์ฑ (operationId 1:1)', async ({}, testInfo) => {
await test.step('1. mc-infra-manager Swagger JSON ๋ด๋ ค๋ฐ๊ธฐ', async () => {
const r = await sh(`curl -s -u '${TB_AUTH}' ${TB}/api/doc.json -o ${SWAGGER} && python3 -c "import json;d=json.load(open('${SWAGGER}'));print(d.get('basePath'), sum(1 for p in d['paths'].values() for m in p.values() if isinstance(m,dict) and m.get('operationId')))"`);
testInfo.annotations.push({ type: 'swagger', description: `basePath, operationId ์: ${r.out.trim()}` });
expect(Number(r.out.trim().split(' ').pop()), 'operationId ์์').toBeGreaterThan(0);
});
const generated = await test.step(`2. ./mcc api tool -f ${SWAGGER} โ ์ก์
์ ์ ์ถ๋ ฅ`, async () => {
const r = await inBin(`./mcc api tool -f ${SWAGGER}`);
await attach(testInfo, 'tool-output.yaml', r);
expect(r.out).toContain('Base Path:');
return r.out;
});
await test.step('3. operationId ์ = ์์ฑ๋ ์ก์
์, ๊ฐ ์ก์
์ methodยทresourcePath ์กด์ฌ', async () => {
const ops = Number((await sh(`python3 -c "import json;d=json.load(open('${SWAGGER}'));print(sum(1 for p in d['paths'].values() for m in p.values() if isinstance(m,dict) and m.get('operationId')))"`)).out.trim());
const actions = generated.match(/^ {4}\S+:$/gm) ?? [];
expect(actions.length).toBe(ops);
expect((generated.match(/^ {6}method: /gm) ?? []).length).toBe(ops);
expect((generated.match(/^ {6}resourcePath: /gm) ?? []).length).toBe(ops);
});
await test.step('4. ํ์ฌ api.yaml(mc-infra-manager)๊ณผ ๋น๊ต โ ๋๋ฝยท์ถ๊ฐ ์ก์
์ ๊ธฐ๋ก', async () => {
const r = await inBin(`python3 -c "import yaml;print('\\n'.join(yaml.safe_load(open('../conf/api.yaml'))['serviceActions']['mc-infra-manager']))"`);
const current = new Set(r.out.split('\n').filter(Boolean));
const gen = new Set((generated.match(/^ {4}(\S+):$/gm) ?? []).map((s) => s.trim().slice(0, -1)));
const missing = [...gen].filter((a) => !current.has(a));
const stale = [...current].filter((a) => !gen.has(a));
testInfo.annotations.push({ type: 'diff', description: `api.yaml ์ ์๋ ์ ์ก์
${missing.length}, Swagger ์ ์๋ ์ ์ก์
${stale.length}` });
await testInfo.attach('diff.txt', { body: `[์ ์ก์
]\n${missing.join('\n')}\n\n[์ ์ก์
]\n${stale.join('\n')}`, contentType: 'text/plain' });
});
await sh(`rm -f ${SWAGGER}`);
});tests/iv0/IV0_S24.spec.ts ยท 59์ค ยท ํด์ a55caa67 ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '@playwright/test';
import fs from 'node:fs';
import { inBin, attach, tb, requireDestructive, TB_AUTH } from './_cli';
// IV0_S24 : CSP Credential ๋ฑ๋ก (mcc setup credential, RSA-OAEP + AES-CBC ์ํธํ) โ TC-ADMIN-024
// ์ ์ฐจ: CSP ์ ํ(--csp) โ ํ์ํ credential ํญ๋ชฉ ์
๋ ฅ(๋น๋ฐ๋ฒํธ ๋ฐฉ์, TTY ํ์) โ ๊ณต๊ฐํค๋ก ์ํธํ โ POST /credential
// ์
๋ ฅ๊ฐ: ADMIN_CLI_CSP(๊ธฐ๋ณธ aws), ADMIN_CLI_CRED_FILE = ์ด PC ์ key=value ํ์ผ
// ํญ๋ชฉ ์ด๋ฆ์ metainfo ์ CredentialCSP(์: aws_access_key_id / aws_secret_access_key) โ mc-infra-manager ๊ฐ ํ์ฉํ๋ ํค
// ์ฃผ์: ๊ฐ์ CSP ์ ๊ธฐ์กด credential(credentialHolder=admin)์ ๋ฎ์ด์ด๋ค. ์
๋ ฅ๊ฐ์ ๋ฆฌํฌํธ์ ๋จ๊ธฐ์ง ์๋๋ค.
// ์ธ์ฆ: api.yaml ์ mc-infra-manager ์ธ์ฆ๊ฐ(spider)์ ์ค์น ๊ธฐ๋ณธ๊ฐ(default)๊ณผ ๋ฌ๋ผ 401 ์ด ๋๋ฏ๋ก -u/-p ๋ก ์ง์ ํ๋ค(ADMIN_CLI_TB_AUTH).
const CSP = (process.env.ADMIN_CLI_CSP ?? 'aws').toLowerCase();
const CRED_FILE = process.env.ADMIN_CLI_CRED_FILE ?? '';
test(`IV0_S24 CSP Credential ๋ฑ๋ก (${CSP}, ์ํธํ ํ mc-infra-manager ๋ฑ๋ก โ connConfig verified)`, async ({}, testInfo) => {
requireDestructive(`${CSP} credential ๋ฑ๋ก(๊ธฐ์กด ๊ฐ ๋ฎ์ด์)`);
test.skip(!CRED_FILE, 'ADMIN_CLI_CRED_FILE(credential key=value ํ์ผ) ๋ฏธ์ง์ ');
test.setTimeout(15 * 60_000);
const keys = await test.step(`1. ํ์ํ credential ํญ๋ชฉ ์กฐํ (POST /forward/cloudos/metainfo/${CSP} โ CredentialCSP)`, async () => {
const meta = await tb(`/forward/cloudos/metainfo/${CSP}`, 'POST', {});
const list: string[] = meta.json?.CredentialCSP ?? meta.json?.Credential ?? [];
testInfo.annotations.push({ type: 'keys', description: list.join(', ') });
expect(list.length, `metainfo ์กฐํ ์คํจ: ${meta.raw.slice(0, 200)}`).toBeGreaterThan(0);
return list;
});
const values = Object.fromEntries(fs.readFileSync(CRED_FILE, 'utf8').split('\n').filter((l) => l.includes('=')).map((l) => [l.slice(0, l.indexOf('=')).trim(), l.slice(l.indexOf('=') + 1).trim()]));
const before = (await tb(`/connConfig?filterCredentialHolder=admin`)).json?.connectionconfig?.filter((c: any) => c.providerName?.toLowerCase() === CSP).length ?? 0;
const r = await test.step(`2. ./mcc setup credential --csp ${CSP} โ ํญ๋ชฉ ์
๋ ฅ โ ๊ฒํ no โ ํ์ธ yes`, async () => {
const missing = keys.filter((k) => !(k in values));
expect(missing, 'credential ํ์ผ์ ์๋ ํญ๋ชฉ').toEqual([]);
const stdin = keys.map((k) => values[k]).join('\n') + '\nno\nyes\n'; // ๊ฒํ no โ ํ์ธ yes
// ๊ฐ์ ํฐ๋ฏธ๋(script)์ด ์
๋ ฅ์ ํ๋ฉด์ ๋๋๋ ค ์ฐ์ง ์๋๋ก stty -echo. ์ถ๋ ฅ์ ํญ์ ๊ฐ์ ๊ฐ๋ฆฐ ๋ค์๋ง ์ด๋ค(๋จ์ธ ๋ฉ์์ง ํฌํจ).
const res = await inBin(`script -qec 'stty -echo; ./mcc setup credential --csp ${CSP} -u ${TB_AUTH.split(':')[0]} -p ${TB_AUTH.split(':')[1]}' /dev/null`, stdin, 10 * 60_000);
let masked = res.out;
for (const v of Object.values(values)) if (v) masked = masked.split(v).join('***');
await attach(testInfo, 'credential.log', { ...res, out: masked });
return { code: res.code, out: masked };
});
await test.step('3. ๊ณต๊ฐํค ์กฐํยท์ํธํยท๋ฑ๋ก ๊ฒฐ๊ณผ ํ์ธ', async () => {
expect(r.out).toContain(`Processing authentication information for selected [${CSP}] CSP`);
expect(r.out, '๋ฑ๋ก ์๋ฃ ๋ฉ์์ง ์์').toContain('CSP Credential registration completed successfully');
expect(r.out).toContain('Result:');
expect(r.out).not.toMatch(/^Error:/m);
// ์๋ฒ ๊ฑฐ๋ถ ์ฌ๋ถ์ ์ด๋ฒ ๋ฑ๋ก ๊ฒฐ๊ณผ๋ฅผ ์ง์ ํ์ธํ๋ค(๊ธฐ์กด ์ฐ๊ฒฐ์ด ๋จ์ ์์ผ๋ฉด 4๋จ๊ณ๋ง์ผ๋ก๋ ๋ฑ๋ก ์คํจ๋ฅผ ๋์น๋ค)
expect(r.out, '์๋ฒ๊ฐ credential ๋ฑ๋ก์ ๊ฑฐ๋ถํจ').not.toMatch(/invalid credential|"message":\s*"[^"]*(invalid|error|fail)/i);
expect(r.out, '๋ฑ๋ก ๊ฒฐ๊ณผ๊ฐ ๋น์ด ์์(Result: map[])').not.toMatch(/Result: map\[\]\s*$/m);
});
await test.step('4. mc-infra-manager connConfig ์ ํด๋น CSP ์ฐ๊ฒฐ์ด ์๊ธฐ๊ณ verified', async () => {
const list = (await tb(`/connConfig?filterCredentialHolder=admin`)).json?.connectionconfig ?? [];
const mine = list.filter((c: any) => c.providerName?.toLowerCase() === CSP);
testInfo.annotations.push({ type: 'connConfig', description: `${CSP} ์ฐ๊ฒฐ ${before} โ ${mine.length}, verified ${mine.filter((c: any) => c.verified).length}` });
expect(mine.length).toBeGreaterThan(0);
expect(mine.some((c: any) => c.verified), 'verified ์ฐ๊ฒฐ ์์').toBe(true);
});
});tests/iv0/IV0_S25.spec.ts ยท 62์ค ยท ํด์ 56b20ddb ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '@playwright/test';
import { sh, inBin, attach, tb } from './_cli';
// IV0_S25 : ์ํธํ cross-check (Python PyCryptodome) โ TC-ADMIN-025
// mcc setup credential ๊ณผ ๊ฐ์ ๋ฐฉ์(RSA-OAEP(SHA-256)๋ก AES-256 ํค ์ํธํ + ๊ฐ๋ง๋ค AES-CBC(IV ์๋ถ์, PKCS7))์
// ์ ์ฅ์์ test_encryption.py ๋ก ์ฌํํ๊ณ , (1) ์คํฌ๋ฆฝํธ ์คํ (2) ์์ ํค์์ผ๋ก ๋ณตํธํ ์๋ณต (3) mc-infra-manager ๊ณต๊ฐํค๋ก ์ํธํ ํ์ ํ์ธ.
// ์ ํ: python3 + pycryptodome. ์์ผ๋ฉด uv ๋ก ์์ ํ๊ฒฝ์ ๋ง๋ค์ด ์คํํ๋ค(uv run --with pycryptodome, ์์คํ
ํจํค์ง ์ค์น ์์).
const PY = String.raw`
import base64, json, sys, runpy, io, contextlib
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP, AES
from Crypto.Hash import SHA256
with contextlib.redirect_stdout(io.StringIO()):
mod = runpy.run_path('../test_encryption.py')
enc = mod['encrypt_credential_value_with_publickey']
creds = {'ClientId': 'iv0-id', 'ClientSecret': 'iv0-secret-โ', 'Region': 'ap-northeast-2'}
mode = sys.argv[1]
if mode == 'roundtrip':
key = RSA.generate(4096)
ec, ek = enc(key.publickey().export_key().decode(), creds)
aes = PKCS1_OAEP.new(key, hashAlgo=SHA256).decrypt(base64.b64decode(ek))
out = {}
for k, v in ec.items():
raw = base64.b64decode(v); c = AES.new(aes, AES.MODE_CBC, raw[:16]).decrypt(raw[16:])
out[k] = c[:-c[-1]].decode()
print(json.dumps({'aesKeyLen': len(aes), 'match': out == creds}))
else:
pub = sys.stdin.read()
ec, ek = enc(pub, creds)
print(json.dumps({'encKeyBytes': len(base64.b64decode(ek)), 'valueBlocksOk': all((len(base64.b64decode(v)) - 16) % 16 == 0 for v in ec.values())}))
`;
test('IV0_S25 ์ํธํ cross-check (test_encryption.py ยท ๋ณตํธํ ์๋ณต ยท ์ค์ ๊ณต๊ฐํค ํ์)', async ({}, testInfo) => {
const py = await test.step('1. ์ ํ: pycryptodome ์ฌ์ฉ ๊ฐ๋ฅ (python3 ๋๋ uv)', async () => {
if ((await sh(`python3 -c "import Crypto"`)).code === 0) return 'python3';
const uv = 'PATH=$HOME/.local/bin:$PATH uv run -q --no-project --with pycryptodome python3';
expect((await sh(`${uv} -c "import Crypto"`, '', 5 * 60_000)).code, 'pycryptodome ์์, uv ๋ ์์').toBe(0);
testInfo.annotations.push({ type: 'python', description: 'uv ์์ ํ๊ฒฝ(pycryptodome) ์ฌ์ฉ' });
return uv;
});
await sh(`cat > /tmp/iv0-crypto.py <<'PYEOF'\n${PY}\nPYEOF`);
await test.step('2. python3 test_encryption.py ์คํ โ 4๊ฐ ๊ฐ ์ํธํ ์๋ฃ', async () => {
const r = await inBin(`${py} ../test_encryption.py`);
await attach(testInfo, 'test_encryption.log', r);
expect(r.code).toBe(0);
expect(r.out).toContain('์ด 4 ๊ฐ์ credentials๊ฐ ์ํธํ๋์์ต๋๋ค.');
});
await test.step('3. ์์ RSA-4096 ํค์์ผ๋ก ์ํธํ โ ๋ณตํธํ ์๋ณต ๊ฒฐ๊ณผ๊ฐ ์๋ฌธ๊ณผ ๊ฐ์ (AES-256)', async () => {
const r = await inBin(`${py} /tmp/iv0-crypto.py roundtrip`, '', 5 * 60_000);
expect(JSON.parse(r.out.trim().split('\n').pop()!)).toEqual({ aesKeyLen: 32, match: true });
});
await test.step('4. mc-infra-manager ๊ณต๊ฐํค(GET /credential/publicKey)๋ก ์ํธํ โ RSA 4096 ํค ๊ธธ์ดยทAES ๋ธ๋ก ํ์', async () => {
const pk = await tb('/credential/publicKey');
expect(pk.json?.publicKey, `๊ณต๊ฐํค ์กฐํ ์คํจ: ${pk.raw.slice(0, 200)}`).toBeTruthy();
const r = await inBin(`${py} /tmp/iv0-crypto.py live`, pk.json.publicKey);
expect(JSON.parse(r.out.trim().split('\n').pop()!)).toEqual({ encKeyBytes: 512, valueBlocksOk: true });
});
await sh('rm -f /tmp/iv0-crypto.py');
});tests/iv0/IV0_S26.spec.ts ยท 46์ค ยท ํด์ 140a8e5b ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '@playwright/test';
import { sh, inBin, attach, tb, requireDestructive } from './_cli';
// IV0_S26 : CB-Tumblebug infra-init (mcc setup infra-init: ๋ฒ์ ํ์ธ โ clone โ init.sh) โ TC-ADMIN-026
// ์ ํ: mc-infra-manager healthy, ์ํธํ๋ credential ํ์ผ(~/.cloud-barista/credentials.yaml.enc) ์ค๋น, uv ์ค์น, docker ๊ทธ๋ฃน ์ฌ์ฉ์
// ๋ํํ ์
๋ ฅ์ ADMIN_CLI_INIT_INPUT ๋ก ๋๊ธด๋ค(๊ธฐ๋ณธ: ์ค๋น ํ์ธ y โ ํด๋ ๋ฉ๋ด 1(์๋ก ๋ฐ๊ธฐ) โ init.sh ์งํ y).
// init.sh ๋ CSP ์ฐ๊ฒฐ ๋ฑ๋กยท์์ฐ(spec/image) ์ ์ฌ๋ก ์์ญ ๋ถ ๊ฑธ๋ฆด ์ ์๋ค.
const INPUT = (process.env.ADMIN_CLI_INIT_INPUT ?? 'y\\n1\\ny\\n').replace(/\\n/g, '\n');
const TB_DIR = '~/go/src/github.com/cloud-barista/cb-tumblebug';
test('IV0_S26 CB-Tumblebug infra-init (์คํ ๋ฒ์ ๊ณผ ๊ฐ์ ํ๊ทธ clone โ init.sh โ initialized)', async ({}, testInfo) => {
requireDestructive('mc-infra-manager ์ด๊ธฐํ(์ฐ๊ฒฐยท์์ฐ ๋ฑ๋ก)');
test.setTimeout(90 * 60_000);
await test.step('1. ์ ํ: credential ์ํธํ ํ์ผ ์กด์ฌ', async () => {
expect((await sh('test -f ~/.cloud-barista/credentials.yaml.enc && echo ok')).out.trim(), 'credentials.yaml.enc ์์').toBe('ok');
});
const r = await test.step('2. ./mcc setup infra-init ์คํ', async () => {
const res = await inBin('script -qec "./mcc setup infra-init" /dev/null', INPUT, 80 * 60_000);
await attach(testInfo, 'infra-init.log', res);
return res;
});
const version = await test.step('3. ์คํ ์ํยท๋ฒ์ ยทhealth ํ์ธ ๋ฉ์์ง', async () => {
expect(r.out).toContain('MC-Infra-Manager is running.');
expect(r.out).toContain('MC-Infra-Manager is healthy.');
const v = r.out.match(/Version confirmed: (\d+\.\d+\.\d+)/)?.[1];
expect(v, '์คํ ๋ฒ์ ํ์ธ ์คํจ(์ด๋ฏธ์ง ํ๊ทธ๊ฐ x.y.z ๊ฐ ์๋๋ฉด ๊ฐ์ง ๋ชปํจ)').toBeTruthy();
expect(r.out).toContain('Executing init.sh');
return v!;
});
await test.step(`4. cb-tumblebug ํด๋๊ฐ ์คํ ๋ฒ์ ํ๊ทธ(v${version})๋ก checkout`, async () => {
const tag = (await sh(`cd ${TB_DIR} && git describe --tags --exact-match HEAD`)).out.trim();
expect(tag.replace(/^v/, '')).toBe(version);
});
await test.step('5. mc-infra-manager initialized=true, ์ฐ๊ฒฐ ์ ๋ณด(connConfig) ๋ฑ๋ก๋จ', async () => {
const ready = await tb('/readyz');
expect(ready.json?.initialized).toBe(true);
const conns = (await tb('/connConfig')).json?.connectionconfig ?? [];
testInfo.annotations.push({ type: 'connConfig', description: `์ฐ๊ฒฐ ${conns.length}๊ฐ, verified ${conns.filter((c: any) => c.verified).length}๊ฐ` });
expect(conns.length).toBeGreaterThan(0);
});
});