Test Specs IV8 - MZC-CSC/mc-web-console GitHub Wiki
์์ฑ: 2026-10-15 ยท ๊ธฐ์ค: v07 ์ค์น ํ ํตํฉ ๊ฒ์ฆ(IV) ยท ์๋ณธ: ์ฌ์ ๋ณ ๊ณํ์(Excel) โ Test Scenarios ยท IV ํ๋ธ ยท ๊ธฐ๋ฅ TC
Playwright(TypeScript) spec ์์ค์
๋๋ค. ์๋๋ฆฌ์ค๋ง๋ค ## <์๋๋ฆฌ์ค๋ฒํธ> ์น์
์ด ์๊ณ ์ต์ปค๋ #<์๋๋ฆฌ์ค๋ฒํธ ์๋ฌธ์> ์
๋๋ค. ๊ณต์ฉ ํฌํผ๋ Test-Specs-Common. ๋น๋ฐ๋ฒํธ ๋ฑ์ ๋ง์คํนํ์ต๋๋ค.
| ์๋๋ฆฌ์ค | ํ์ผ | ์ค | ํด์ |
|---|---|---|---|
| IV8_S01 | tests/iv8/IV8_S01.spec.ts |
18 | 4ea3ecd6 |
| IV8_S02 | tests/iv8/IV8_S02.spec.ts |
15 | 8f0d208d |
| IV8_S03 | tests/iv8/IV8_S03.spec.ts |
29 | 68f09f08 |
| IV8_S04 | tests/iv8/IV8_S04.spec.ts |
70 | 0053808b |
| IV8_S05 | tests/iv8/IV8_S05.spec.ts |
77 | c0646226 |
| IV8_S06 | tests/iv8/IV8_S06.spec.ts |
67 | 7e508888 |
| IV8_S07 | tests/iv8/IV8_S07.spec.ts |
81 | 36d69f1e |
| IV8_S08 | tests/iv8/IV8_S08.spec.ts |
51 | 4f16b484 |
| IV8_S09 | tests/iv8/IV8_S09.spec.ts |
52 | c1b0949f |
| IV8_S10 | tests/iv8/IV8_S10.spec.ts |
48 | e1d80c9b |
| IV8_S11 | tests/iv8/IV8_S11.spec.ts |
37 | 7d592bd5 |
| IV8_S12 | tests/iv8/IV8_S12.spec.ts |
98 | 43e4bb43 |
| IV8_S13 | tests/iv8/IV8_S13.spec.ts |
40 | f76297ec |
| IV8_S14 | tests/iv8/IV8_S14.spec.ts |
59 | e3b90577 |
| IV8_S15 | tests/iv8/IV8_S15.spec.ts |
34 | aebfcf7f |
| IV8_S16 | tests/iv8/IV8_S16.spec.ts |
40 | a1b5437c |
| IV8_S17 | tests/iv8/IV8_S17.spec.ts |
126 | b125a4ea |
| IV8_S18 | tests/iv8/IV8_S18.spec.ts |
42 | 785837a0 |
| IV8_S19 | tests/iv8/IV8_S19.spec.ts |
61 | 71e29ab0 |
| IV8_S20 | tests/iv8/IV8_S20.spec.ts |
65 | 83d022b3 |
tests/iv8/IV8_S01.spec.ts ยท 18์ค ยท ํด์ 4ea3ecd6 ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
// IV8_S01 : Data Migrations ์ง์
(iframe ํธ๋์
ฐ์ดํฌ). ๋งํฌ: /webconsole/_view/mcdatamigration
test('IV8_S01 Data Migrations ์ง์
(iframe ํธ๋์
ฐ์ดํฌ)', async ({ page }) => {
const t0 = Date.now();
await loginAsAdmin(page);
test.info().annotations.push({ type: 'login-ms', description: String(Date.now() - t0) });
await page.getByText('Data Migrations', { exact: true }).first().click();
await page.getByRole('link', { name: 'Migration', exact: true }).first().click();
// stage ๋์ ๋ถํ ์ ๋ฉ๋ด ํด๋ฆญ ํ 15์ด ๋๊ฒ ๋น ํ๋ฉด์ด ์ ์ง๋ ์ ์์(2026-10-02) โ URL ์ ํ ํ์ธ ํ iframe ์ ๋๋ํ ๋๊ธฐ
await page.waitForURL(/mcdatamigration/, { timeout: 60_000 });
await expect(page.locator('iframe').first()).toBeVisible({ timeout: 60_000 });
const frame = page.frameLocator('iframe').first();
await expect(frame.locator('#root, body').first()).toBeVisible({ timeout: 60_000 });
test.info().annotations.push({ type: 'iframe-src', description: (await page.locator('iframe').first().getAttribute('src')) ?? '' });
});tests/iv8/IV8_S02.spec.ts ยท 15์ค ยท ํด์ 8f0d208d ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '../../lib/fixtures';
import { logout } from '../../lib/ui';
// IV8_S02 : ๋ฏธ๋ก๊ทธ์ธ ์ํ์์ Data Migrations ์ง์ ์ ๊ทผ (์์ ์คํจ)
test('IV8_S02 ๋ฏธ๋ก๊ทธ์ธ ์ํ์์ Data Migrations ์ง์ ์ ๊ทผ (์์์คํจ)', async ({ page }) => {
await logout(page);
const res = await page.goto('/webconsole/_view/mcdatamigration');
test.info().annotations.push({ type: 'landed', description: `${res?.status()} ${page.url()}` });
// ๊ธฐ๋: ๋ก๊ทธ์ธ/Unauthorized ํ๋ฉด์ผ๋ก ์ด๋ํ๊ณ ์ฝํ
์ธ (iframe) ๋ฏธ๋
ธ์ถ.
// 2026-10-01 Playwright ์คํ: 1ํ์ฐจ /auth/unauthorized#cookieError, ์ดํ 3ํ '502 Bad Gateway'(๊ฐ์ URL) โ ํ์ฌ ์ด ๋จ์ธ์ ์คํจํ๋ค(์ ํ ํ์ธ ํ์).
await expect(page).toHaveURL(/\/auth\/(login|unauthorized)/);
await expect(page.getByRole('heading', { name: 'Unauthorized' }).or(page.getByPlaceholder('Your ID'))).toBeVisible();
await expect(page.locator('iframe')).toHaveCount(0);
});tests/iv8/IV8_S03.spec.ts ยท 29์ค ยท ํด์ 68f09f08 ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '../../lib/fixtures';
import { activeCsps, tid } from '../../lib/data';
import { loginAsAdmin } from '../../lib/ui';
import { apiRecorder, openDm, selectCred, ensureBucket, bucketName, dmBlocker, dmRegion } from '../../lib/iv8-os';
// IV8_S03 : Object Storage: Bucket ์์ฑ โ Bucket List ํ์ [์ ์, CSP๋ณ]
// Data Migrations > Generate > Object Storage โ Target credential ์ ํ โ + Bucket(mcmp-e2e-v07-os{n}) โ Bucket ListยทTarget Bucket ๋๋กญ๋ค์ด ํ์ธ
// ์ฌ์คํ ๊ฐ๋ฅ: ์ด๋ฏธ ์์ผ๋ฉด ์์ฑ์ ๊ฑด๋๋ฐ๊ณ ๋ชฉ๋ก ํ์๋ง ํ์ธํ๋ค.
for (const c of activeCsps(['AWS', 'Azure', 'Alibaba', 'GCP', 'Tencent', 'NHN Cloud', 'NCP', 'IBM', 'KT'])) {
test(`${tid('IV8_S03', c)} Object Storage: Bucket ์์ฑ โ Bucket List ํ์`, async ({ page }) => {
const why = dmBlocker(c);
test.skip(!!why, why);
const api = apiRecorder(page);
await loginAsAdmin(page);
const f = await openDm(page, 'Generate');
await selectCred(page, f, c);
api();
const name = bucketName(c);
const row = await ensureBucket(page, f, name, api);
expect(row, 'Bucket List ํ').toContain(name);
// ๋ชฉ๋ก์ ์ฌ๋ผ์๋ Status ๊ฐ Failed ๋ฉด CSP ์์ฑ ์คํจ(2026-10-12 KT: MCMP ์ Failed ๋ก ๋ฑ๋ก, cspResourceId ๋น ๊ฐ)
expect(row, 'Bucket Status').not.toMatch(/\bFailed\b/i);
// Region ์ง์ CSP ๋ ์ง์ ๋ฆฌ์ ์ ์์ฑ๋๋์ง ํ์ธ(2026-10-12: ์๋ ์ ํ af-south-1 ๋ก ์์ฑ๋ ์ด๋ ฅ)
if (dmRegion(c)) expect(row, 'Bucket Region').toContain(dmRegion(c));
// Target Bucket ๋๋กญ๋ค์ด์๋ ํ์
await expect.poll(() => f.locator('select#newBucket option').allTextContents().then((o) => o.join(',')), { timeout: 30_000 }).toContain(name);
});
}tests/iv8/IV8_S04.spec.ts ยท 70์ค ยท ํด์ 0053808b ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '../../lib/fixtures';
import { activeCsps, tid } from '../../lib/data';
import { loginAsAdmin } from '../../lib/ui';
import { apiRecorder, openDm, selectCred, selectBucket, bucketName, bucketRowText, readObjects, logHistory, dmBlocker, dmRegion } from '../../lib/iv8-os';
// IV8_S04 : Object Storage: ์ํ ๋ฐ์ดํฐ ์์ฑ (Generate) [์ ์, CSP๋ณ]
// Generate > Object Storage โ Target credentialยทRegion โ Target Bucket(mcmp-e2e-v07-os{n}, IV8_S03) โ TXT ๋ง 1GB โ Submit
// โ ๊ฒฐ๊ณผ(Result)ยทAPI ์๋ต, bucket Objects, Log Management(Object Storage / Generate) ํ์ธ
const TYPES = ['SQL', 'CSV', 'TXT', 'PNG', 'GIF', 'ZIP', 'JSON', 'XML', 'ServerJSON', 'ServerSQL'];
const SIZE_GB = '1'; // ๊ธฐ๋ณธ 10GB โ 1GB(์๊ฐยท์ ์ก ๋น์ฉ)
for (const c of activeCsps(['AWS', 'Azure', 'Alibaba', 'GCP', 'Tencent', 'NHN Cloud', 'NCP', 'IBM', 'KT'])) {
test(`${tid('IV8_S04', c)} Object Storage: ์ํ ๋ฐ์ดํฐ ์์ฑ (Generate)`, async ({ page }) => {
test.setTimeout(45 * 60_000);
const why = dmBlocker(c);
test.skip(!!why, why);
const api = apiRecorder(page);
await loginAsAdmin(page);
let f = await openDm(page, 'Generate');
await selectCred(page, f, c);
const name = bucketName(c);
const row = await bucketRowText(f, name);
test.skip(!row || /Failed/i.test(row), `์ ํ IV8_S03_${c.code} bucket ${name} ์์/์คํจ: ${row || '(์์)'}`);
if (dmRegion(c)) expect(row, 'bucket Region').toContain(dmRegion(c));
await selectBucket(f, name);
// TXT ๋ง ์ฒดํฌ, ํฌ๊ธฐ 1GB
for (const t of TYPES) {
const cb = f.locator(`#check${t}`);
if (await cb.count()) await cb.setChecked(t === 'TXT', { force: true });
}
await f.locator('#sizeTXT').fill(SIZE_GB);
const checked = await f.evaluate((ts) => ts.filter((t) => (document.getElementById(`check${t}`) as HTMLInputElement | null)?.checked), TYPES);
test.info().annotations.push({ type: 'generate-input', description: `bucket=${name}, checked=${checked.join(',')}, TXT=${SIZE_GB}GB` });
expect(checked).toEqual(['TXT']);
api();
let genResp = '';
page.on('response', async (r) => {
if (/:3300\/generate\//i.test(r.url()) && r.request().method() !== 'GET') genResp = `${r.status()} ${r.url().replace(/^https?:\/\/[^/]+/, '')}`;
});
const t0 = Date.now();
await f.locator('#submitBtn').click();
// ์๋ฃ ํ๋จ: generate API(POST) ์๋ต ๋๋ Result ์์ญ์ ์๋ฃ/์คํจ ๋ฌธ๊ตฌ. 1GB ์์ฑยท์
๋ก๋๋ ์ ๋ถ ๊ฑธ๋ฆด ์ ์์
const result = f.locator('#resultText');
await expect.poll(async () => {
const v = await result.inputValue().catch(() => '');
return genResp || /(success|fail|error|complete|finished|done)/i.test(v) ? 'done' : '';
}, { timeout: 40 * 60_000, intervals: [5000, 10_000, 30_000] }).toBe('done');
await page.waitForTimeout(3000); // ์๋ต ํ Result ๊ฐฑ์
test.info().annotations.push({ type: 'generate-response', description: genResp || '(POST ์๋ต ๋ฏธํ์ธ, Result ๋ฌธ๊ตฌ๋ก ํ๋จ)' });
const text = (await result.inputValue()).trim();
test.info().annotations.push({ type: 'generate-sec', description: String(Math.round((Date.now() - t0) / 1000)) });
test.info().annotations.push({ type: 'generate-result', description: text.slice(-1200) });
test.info().annotations.push({ type: 'generate-api', description: api().slice(0, 1500) || '(API ๊ธฐ๋ก ์์)' });
expect(text, 'Generate ๊ฒฐ๊ณผ').not.toMatch(/\b(fail(ed)?|error)\b/i);
// bucket ๊ฐ์ฒด ํ์ธ
f = await openDm(page, 'Generate');
await selectCred(page, f, c);
const objs = await readObjects(page, f, name);
if (objs) expect(objs.length, 'bucket ๊ฐ์ฒด ์').toBeGreaterThan(0);
// Log Management ์ด๋ ฅ
const logs = await logHistory(page, 'Object Storage', 'Generate');
// ์ต์ ์ด๋ ฅ(์ฒซ ํ)์ด ์ด bucket ์ generate success ์ฌ์ผ ํ๋ค
expect(logs[0] ?? '', 'Log Management ์ต์ Generate ์ด๋ ฅ').toMatch(new RegExp(`generate .*${dmRegion(c) ? dmRegion(c) + ' ' : ''}${name} .*success`, 'i'));
});
}tests/iv8/IV8_S05.spec.ts ยท 77์ค ยท ํด์ c0646226 ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '../../lib/fixtures';
import { activeCsps, tid, CSPS } from '../../lib/data';
import { loginAsAdmin } from '../../lib/ui';
import {
apiRecorder, openDm, selectCred, selectBucket, ensureBucket, bucketName, bucketRowText, readObjects, logHistory,
nextStep, diagnose, dmBlocker, dmRegion,
} from '../../lib/iv8-os';
// IV8_S05 : Object Storage ๋ง์ด๊ทธ๋ ์ด์
(Migration) [์ ์, CSP๋ณ]
// Migration > Object Storage 4๋จ๊ณ: 1 Source(ํด๋น CSP, mcmp-e2e-v07-os{n}) โ 2 Diagnose โ 3 Target(AWS os1m, AWS ํ์ GCP os4m. ์์ผ๋ฉด + Bucket) โ 4 Diagnose โ Submit
// โ ResultยทAPI, ๋์ bucket ๊ฐ์ฒด ์ = ์๋ณธ Diagnose Object Count, Log Management(Migrate) ํ์ธ
for (const c of activeCsps(['AWS', 'Azure', 'Alibaba', 'GCP', 'Tencent', 'NHN Cloud', 'NCP', 'IBM', 'KT'])) {
test(`${tid('IV8_S05', c)} Object Storage ๋ง์ด๊ทธ๋ ์ด์
(Migration)`, async ({ page }) => {
test.setTimeout(60 * 60_000);
const why = dmBlocker(c);
test.skip(!!why, why);
const tc = CSPS.find((x) => x.code === (c.code === 'AWS' ? 'GCP' : 'AWS'))!; // ๋์ CSP
const src = bucketName(c);
const dst = bucketName(tc, 'm');
test.info().annotations.push({ type: 'route', description: `${c.code} ${src} โ ${tc.code} ${dst}` });
const api = apiRecorder(page);
await loginAsAdmin(page);
let f = await openDm(page, 'Migration');
// 1. Source
await selectCred(page, f, c, 'sourceCredentialSelect');
const srow = await bucketRowText(f, src);
test.skip(!srow || /Failed/i.test(srow), `์ ํ IV8_S03/S04_${c.code} ์๋ณธ bucket ${src} ์์/์คํจ: ${srow || '(์์)'}`);
await selectBucket(f, src, 'sourceBucket');
await nextStep(page, f, '#collapseOne');
// 2. Diagnose Source
const sd = await diagnose(page, f, 'src');
expect(Number(sd.count), '์๋ณธ ๊ฐ์ฒด ์(IV8_S04 ์ ํ)').toBeGreaterThan(0);
await nextStep(page, f, '#collapseTwo');
// 3. Target (์์ผ๋ฉด + Bucket ์ผ๋ก ์์ฑ)
await selectCred(page, f, tc, 'targetCredentialSelect');
api();
const trow = await ensureBucket(page, f, dst, api, '#tgt-bkt-createBtn');
expect(trow, '๋์ bucket').not.toMatch(/\bFailed\b/i);
if (dmRegion(tc)) expect(trow, '๋์ bucket Region').toContain(dmRegion(tc));
await selectBucket(f, dst, 'targetBucket');
await nextStep(page, f, '#collapseThree');
// 4. Diagnose Target (์คํ ์ )
const td = await diagnose(page, f, 'tgt');
// Submit
api();
let migResp = '';
page.on('response', (r) => { if (/:3300\/migrat/i.test(r.url()) && r.request().method() !== 'GET') migResp = `${r.status()} ${r.url().replace(/^https?:\/\/[^/]+/, '')}`; });
const t0 = Date.now();
await f.locator('#submitBtn').click();
const result = f.locator('#resultText');
await expect.poll(async () => {
const v = await result.inputValue().catch(() => '');
return migResp || /(success|fail|error|complete|finished)/i.test(v) ? 'done' : '';
}, { timeout: 55 * 60_000, intervals: [5000, 10_000, 30_000] }).toBe('done');
await page.waitForTimeout(3000);
const text = (await result.inputValue()).trim();
test.info().annotations.push({ type: 'migrate-sec', description: String(Math.round((Date.now() - t0) / 1000)) });
test.info().annotations.push({ type: 'migrate-response', description: migResp || '(POST ์๋ต ๋ฏธํ์ธ)' });
test.info().annotations.push({ type: 'migrate-result', description: text.slice(-1500) });
test.info().annotations.push({ type: 'migrate-api', description: api().slice(0, 1500) || '(API ๊ธฐ๋ก ์์)' });
expect(text, 'Migration ๊ฒฐ๊ณผ').not.toMatch(/\b(fail(ed)?|error)\b/i);
// ๋์ bucket ๊ฐ์ฒด ํ์ธ (Generate ํ๋ฉด Browse objects)
f = await openDm(page, 'Generate');
await selectCred(page, f, tc);
const objs = await readObjects(page, f, dst);
if (objs) expect(objs.length, '๋์ bucket ๊ฐ์ฒด(ํด๋) ์').toBeGreaterThan(0);
test.info().annotations.push({ type: 'count-compare', description: `์๋ณธ Diagnose count=${sd.count}, ๋์ Diagnose(์คํ ์ ) count=${td.count}` });
// Log Management ์ต์ Migrate ์ด๋ ฅ
const logs = await logHistory(page, 'Object Storage', 'Migrate');
expect(logs[0] ?? '', 'Log Management ์ต์ Migrate ์ด๋ ฅ').toMatch(/migrat.*success/i);
});
}tests/iv8/IV8_S06.spec.ts ยท 67์ค ยท ํด์ 7e508888 ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '../../lib/fixtures';
import { activeCsps, tid } from '../../lib/data';
import { loginAsAdmin } from '../../lib/ui';
import {
apiRecorder, openDm, selectCred, selectBucket, bucketName, bucketRowText, backupName, backupRowText, logHistory, dmBlocker, dmRegion,
} from '../../lib/iv8-os';
// IV8_S06 : Object Storage ๋ฐฑ์
(Back Up) [์ ์, CSP๋ณ]
// Back Up > Object Storage โ Source credentialยทRegionยทBucket(mcmp-e2e-v07-os{n}) โ Backup Name(mcmp-e2e-v07-bk{n}) โ Submit
// โ ResultยทAPI, Backup List(Back UpยทRestore ํ๋ฉด) ํ์, Log Management(Backup) ํ์ธ
// ์ฌ์คํ ๊ฐ๋ฅ: ๊ฐ์ ์ด๋ฆ์ ๋ฐฑ์
์ด ์ด๋ฏธ ์์ผ๋ฉด ์์ฑ์ ๊ฑด๋๋ฐ๊ณ ๋ชฉ๋ก ํ์๋ง ํ์ธํ๋ค.
for (const c of activeCsps(['AWS', 'Azure', 'Alibaba', 'GCP', 'Tencent', 'NHN Cloud', 'NCP', 'IBM', 'KT'])) {
test(`${tid('IV8_S06', c)} Object Storage ๋ฐฑ์
(Back Up)`, async ({ page }) => {
test.setTimeout(60 * 60_000);
const why = dmBlocker(c);
test.skip(!!why, why);
const src = bucketName(c);
const bk = backupName(c);
const api = apiRecorder(page);
await loginAsAdmin(page);
let f = await openDm(page, 'Back Up');
await selectCred(page, f, c, 'sourceCredentialSelect');
const srow = await bucketRowText(f, src);
test.skip(!srow || /Failed/i.test(srow), `์ ํ IV8_S03/S04_${c.code} ์๋ณธ bucket ${src} ์์/์คํจ: ${srow || '(์์)'}`);
if (dmRegion(c)) expect(srow, '์๋ณธ bucket Region').toContain(dmRegion(c));
const before = await backupRowText(f, bk);
if (before) {
test.info().annotations.push({ type: 'backup-exists', description: `${bk} ์ด๋ฏธ ์์ โ ์์ฑ ๊ฑด๋๋: ${before}` });
} else {
await selectBucket(f, src, 'sourceBucket');
await f.locator('#backup-name').fill(bk);
api();
let bkResp = '';
page.on('response', (r) => { if (/:3300\/backup/i.test(r.url()) && r.request().method() === 'POST') bkResp = `${r.status()} ${r.url().replace(/^https?:\/\/[^/]+/, '')}`; });
const t0 = Date.now();
await f.locator('#submitBtn').click();
const result = f.locator('#resultText');
await expect.poll(async () => {
const v = await result.inputValue().catch(() => '');
return bkResp || /(success|fail|error|complete|finished)/i.test(v) ? 'done' : '';
}, { timeout: 55 * 60_000, intervals: [5000, 10_000, 30_000] }).toBe('done');
await page.waitForTimeout(3000);
const text = (await result.inputValue()).trim();
test.info().annotations.push({ type: 'backup-sec', description: String(Math.round((Date.now() - t0) / 1000)) });
test.info().annotations.push({ type: 'backup-response', description: bkResp || '(POST ์๋ต ๋ฏธํ์ธ)' });
test.info().annotations.push({ type: 'backup-result', description: text.slice(-1500) });
test.info().annotations.push({ type: 'backup-api', description: api().slice(0, 1500) || '(API ๊ธฐ๋ก ์์)' });
expect(text, 'Back Up ๊ฒฐ๊ณผ').not.toMatch(/\b(fail(ed)?|error)\b/i);
}
// Backup List: Back Up ํ๋ฉด(์๋ก ์ด๊ธฐ) / Restore ํ๋ฉด
f = await openDm(page, 'Back Up');
await expect.poll(() => backupRowText(f, bk), { timeout: 60_000 }).not.toBe('');
const r1 = await backupRowText(f, bk);
test.info().annotations.push({ type: 'backup-list(backup)', description: r1 });
expect(r1, '๋ฐฑ์
์ํ').not.toMatch(/\bFailed\b/i);
f = await openDm(page, 'Restore');
await expect.poll(() => backupRowText(f, bk), { timeout: 60_000 }).not.toBe('');
test.info().annotations.push({ type: 'backup-list(restore)', description: await backupRowText(f, bk) });
// Log Management ์ต์ Backup ์ด๋ ฅ
const logs = await logHistory(page, 'Object Storage', 'Backup');
expect(logs[0] ?? '', 'Log Management ์ต์ Backup ์ด๋ ฅ').toMatch(/backup.*success/i);
});
}tests/iv8/IV8_S07.spec.ts ยท 81์ค ยท ํด์ 36d69f1e ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '../../lib/fixtures';
import { activeCsps, tid } from '../../lib/data';
import { loginAsAdmin } from '../../lib/ui';
import {
apiRecorder, openDm, selectCred, selectBucket, ensureBucket, bucketName, backupName, backupRowText, readObjects, listFolder, logHistory, dmBlocker, dmRegion,
} from '../../lib/iv8-os';
// IV8_S07 : Object Storage ๋ณต๊ตฌ (Restore) [์ ์, CSP๋ณ]
// Restore > Object Storage โ Source Backup List ์์ mcmp-e2e-v07-bk{n}(IV8_S06) ํ ์ ํ โ Target credentialยทRegion โ + Bucket mcmp-e2e-v07-os{n}r โ Submit
// โ ResultยทAPI, ๋์ bucket ๊ฐ์ฒด(์๋ณธ os{n} ๊ณผ ๊ฐ์ datamold ํด๋), Log Management(Restore) ํ์ธ
for (const c of activeCsps(['AWS', 'Azure', 'Alibaba', 'GCP', 'Tencent', 'NHN Cloud', 'NCP', 'IBM', 'KT'])) {
test(`${tid('IV8_S07', c)} Object Storage ๋ณต๊ตฌ (Restore)`, async ({ page }) => {
test.setTimeout(60 * 60_000);
const why = dmBlocker(c);
test.skip(!!why, why);
const bk = backupName(c);
const dst = bucketName(c, 'r');
const api = apiRecorder(page);
await loginAsAdmin(page);
let f = await openDm(page, 'Restore');
// Source: Backup List ํ ์ ํ
await expect.poll(() => backupRowText(f, bk), { timeout: 60_000 }).not.toBe('');
const brow = await backupRowText(f, bk);
test.skip(!/completed/i.test(brow), `์ ํ IV8_S06_${c.code} ๋ฐฑ์
${bk} ์์/๋ฏธ์๋ฃ: ${brow}`);
const row = f.locator(`tr.backup-row[data-name="${bk}"]`).first();
await row.locator('td', { hasText: bk }).first().click();
await page.waitForTimeout(1500);
const sel = await row.evaluate((r) => ({ cls: r.className, checked: (r.querySelector('input[type=checkbox]') as HTMLInputElement | null)?.checked ?? null, id: (r as HTMLElement).dataset.id ?? '' }));
const label = await f.locator('#sourcePointLabel').inputValue().catch(() => '');
test.info().annotations.push({ type: 'backup-select', description: `${brow} | row class=${sel.cls} checked=${sel.checked} | Source label=${label}` });
// Target: credentialยทRegion โ + Bucket(res) โ Target Bucket
await selectCred(page, f, c, 'targetCredentialSelect');
api();
const trow = await ensureBucket(page, f, dst, api, '#res-bkt-createBtn');
expect(trow, '๋์ bucket').not.toMatch(/\bFailed\b/i);
if (dmRegion(c)) expect(trow, '๋์ bucket Region').toContain(dmRegion(c));
await selectBucket(f, dst, 'targetBucket');
// Submit
api();
let rsResp = '';
page.on('response', (r) => { if (/:3300\/restore/i.test(r.url()) && r.request().method() === 'POST') rsResp = `${r.status()} ${r.url().replace(/^https?:\/\/[^/]+/, '')}`; });
const t0 = Date.now();
await f.locator('#submitBtn').click();
const result = f.locator('#resultText');
await expect.poll(async () => {
const v = await result.inputValue().catch(() => '');
return rsResp || /(success|fail|error|complete|finished)/i.test(v) ? 'done' : '';
}, { timeout: 55 * 60_000, intervals: [5000, 10_000, 30_000] }).toBe('done');
await page.waitForTimeout(3000);
const text = (await result.inputValue()).trim();
test.info().annotations.push({ type: 'restore-sec', description: String(Math.round((Date.now() - t0) / 1000)) });
test.info().annotations.push({ type: 'restore-response', description: rsResp || '(POST ์๋ต ๋ฏธํ์ธ)' });
test.info().annotations.push({ type: 'restore-result', description: text.slice(-1500) });
test.info().annotations.push({ type: 'restore-api', description: api().slice(0, 1500) || '(API ๊ธฐ๋ก ์์)' });
expect(text, 'Restore ๊ฒฐ๊ณผ').not.toMatch(/\b(fail(ed)?|error)\b/i);
// ๋์ bucket ๊ฐ์ฒด: ์๋ณธ(os{n})๊ณผ ๊ฐ์ datamold ํด๋๊ฐ ์์ด์ผ ํ๋ค.
// 2026-10-12 AWS ํ์ธ: ๋ณต๊ตฌ ๋ฐ์ดํฐ๋ ์๋ ๊ฒฝ๋ก๊ฐ ์๋๋ผ '{๋ฐฑ์
id}/' ์ ๋ ํด๋ ์๋์ ์๋ณธ ๊ตฌ์กฐ ๊ทธ๋๋ก ๋ค์ด๊ฐ๋ค โ ์ ๋ ํด๋๊ฐ ์์ผ๋ฉด ๊ทธ ์์ ๋น๊ต
f = await openDm(page, 'Generate');
await selectCred(page, f, c);
const srcObjs = await readObjects(page, f, bucketName(c));
let dstObjs = await readObjects(page, f, dst);
const top = (dstObjs ?? []).map((t) => t.split(' ')[0]);
if (sel.id && top.some((k) => k.startsWith(sel.id))) {
test.info().annotations.push({ type: 'restore-prefix', description: `๋ณต๊ตฌ ์์น: ${dst}/${sel.id}/ (์๋ ๊ฒฝ๋ก ์๋)` });
dstObjs = await listFolder(page, f, dst, sel.id);
}
const folders = (o: string[] | null) => (o ?? []).filter((t) => /datamold/.test(t)).sort();
test.info().annotations.push({ type: 'compare', description: `์๋ณธ ${folders(srcObjs).join(', ')} / ๋ณต๊ตฌ ${folders(dstObjs).join(', ')}` });
expect(folders(dstObjs), '๋ณต๊ตฌ๋ datamold ํด๋(์ด๋ฆยทํญ๋ชฉ ์) = ์๋ณธ').toEqual(folders(srcObjs));
expect(folders(dstObjs).length, '๋ณต๊ตฌ๋ ํด๋ ์').toBeGreaterThan(0);
// Log Management ์ต์ Restore ์ด๋ ฅ
const logs = await logHistory(page, 'Object Storage', 'Restore');
expect(logs[0] ?? '', 'Log Management ์ต์ Restore ์ด๋ ฅ').toMatch(/restore.*success/i);
});
}tests/iv8/IV8_S08.spec.ts ยท 51์ค ยท ํด์ 4f16b484 ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '../../lib/fixtures';
import { activeCsps, tid, CSPS } from '../../lib/data';
import { loginAsAdmin } from '../../lib/ui';
import { apiRecorder, openDm, selectCred, deleteBucket, deleteBackup, bucketName, backupName, bucketRowText, backupRowText, dmBlocker } from '../../lib/iv8-os';
// IV8_S08 : Object Storage: ํ
์คํธ ์์ ์ ๋ฆฌ โ Bucketยท๋ฐฑ์
์ญ์ [์ ์, CSP๋ณ]
// ์ญ์ ๋์(์ด ํ์ด S03~S07 ์์ ๋ง๋ ๊ฒ):
// โ ์๊ธฐ CSP bucket mcmp-e2e-v07-os{n}(S03), os{n}r(S07)
// โก S05 Migration ๋์ bucket: AWS ํ โ GCP os4m, ๋ค๋ฅธ ํ โ AWS os1m (2026-10-12 ์ฌ์ฉ์ ์ง์๋ก ์ ๋ฆฌ ๋ฒ์ ํ์ฅ)
// โข S06 ๋ฐฑ์
mcmp-e2e-v07-bk{n} (Restore ํ๋ฉด Backup List)
// Generate > Object Storage ์ Bucket List ์์ ์ฒดํฌ โ Delete โ ์ด๋ฆ ์
๋ ฅ ํ์ธ. ์๋ ์์์ ๊ฑด๋๋ด๋ค(์ต์ 1๊ฐ๋ ์ญ์ ๋ผ์ผ PASS).
for (const c of activeCsps(['AWS', 'Azure', 'Alibaba', 'GCP', 'Tencent', 'NHN Cloud', 'NCP', 'IBM', 'KT'])) {
test(`${tid('IV8_S08', c)} Object Storage: Bucket ์ญ์ (ํ
์คํธ ์์ ์ ๋ฆฌ)`, async ({ page }) => {
const why = dmBlocker(c);
test.skip(!!why, why);
const tc = CSPS.find((x) => x.code === (c.code === 'AWS' ? 'GCP' : 'AWS'))!; // S05 ๋์ CSP
const api = apiRecorder(page);
await loginAsAdmin(page);
const deleted: string[] = [];
// โข ๋ฐฑ์
๋จผ์ (์๋ณธ bucket ์ ๋ณด๋ฅผ ์ฐธ์กฐํ๋ฏ๋ก bucket ๋ณด๋ค ๋จผ์ ์ง์ด๋ค)
let f = await openDm(page, 'Restore');
await page.waitForTimeout(3000);
const bk = backupName(c);
if (await deleteBackup(page, f, bk, api)) deleted.push(`backup ${bk}`);
// โ ์๊ธฐ CSP bucket
f = await openDm(page, 'Generate');
await selectCred(page, f, c);
api();
for (const n of [bucketName(c), bucketName(c, 'r')]) if (await deleteBucket(page, f, n, api)) deleted.push(`${c.code} ${n}`);
// โก S05 ๋์ bucket(๋ค๋ฅธ CSP)
f = await openDm(page, 'Generate');
await selectCred(page, f, tc);
api();
const mig = bucketName(tc, 'm');
if (await deleteBucket(page, f, mig, api)) deleted.push(`${tc.code} ${mig}`);
test.info().annotations.push({ type: 'deleted', description: deleted.join(', ') || '(์์)' });
expect(deleted.length, '์ญ์ ํ ์์ ์(ํ๋๋ ์์ผ๋ฉด IV8_S03~S07 ์ ํ ํ์)').toBeGreaterThan(0);
expect(await bucketRowText(f, mig), `${mig} ๋ชฉ๋ก์์ ์ ๊ฑฐ`).toBe('');
// ์ต์ข
ํ์ธ: ๋จ์ ์์ ์์
f = await openDm(page, 'Generate');
await selectCred(page, f, c);
for (const n of [bucketName(c), bucketName(c, 'r')]) expect(await bucketRowText(f, n), `${n} ๋ชฉ๋ก์์ ์ ๊ฑฐ`).toBe('');
f = await openDm(page, 'Restore');
await page.waitForTimeout(3000);
expect(await backupRowText(f, bk), `${bk} Backup List ์์ ์ ๊ฑฐ`).toBe('');
});
}tests/iv8/IV8_S09.spec.ts ยท 52์ค ยท ํด์ c1b0949f ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '../../lib/fixtures';
import { activeCsps, tid } from '../../lib/data';
import { loginAsAdmin } from '../../lib/ui';
import { apiRecorder } from '../../lib/iv8-os';
import { openSql, ensureRdb, rdbsOf, refreshRow, PENDING, RDB_CSPS } from '../../lib/iv8-rdb';
// IV8_S09 : SQL Database: RDBMS ์ธ์คํด์ค ์์ฑ โ Instance List ํ์ [์ ์, CSP๋ณ AWSยทGCP] (TC-DATA-018)
// Data Migrations > Generate > SQL Database ํญ โ credential ์ ํ(Region ์ง์ ) โ + Instance โ ์์ง/๋ฒ์ /Class/ID/๊ณ์ ์
๋ ฅ โ Create โ Instance List ์ ํ์
// ์
๋ ฅ๊ฐ์ Drive mc-data-manager ์ํธ ๊ธฐ์ค(2026-10-14): AWS mysql 8.4.11 / db.m5.large / mcmp โ rdb1aws, rdb3aws 2๊ฐ.
// GCP MYSQL 8_4 / db-g1-small / root(ํ๋ฉด์์ root ๋ง ํ์ฉ) โ rdb2gpc. ์ฌ์คํ ๊ฐ๋ฅ(์ด๋ฏธ ์์ผ๋ฉด ์์ฑ ์๋ต).
// CSP ์ ์ธ์คํด์ค๋ฅผ ๋ชจ๋ ์์ฑ ์์ฒญํ ๋ค ํจ๊ป ์๋ฃ๋ฅผ ๊ธฐ๋ค๋ฆฐ๋ค(AWS 1๊ฐ ์ฝ 12๋ถ).
// 2026-10-15: ๋์ํ RDB = AWSยทGCPยทAlibabaยทNCP (AlibabaยทNCP ์ต์ ์ฌ์, ์ฌ์ฉ์ ์ง์)
for (const c of activeCsps(RDB_CSPS)) {
test(`${tid('IV8_S09', c)} SQL Database: RDBMS ์ธ์คํด์ค ์์ฑ โ Instance List ํ์`, async ({ page }) => {
test.setTimeout(50 * 60_000);
const list = rdbsOf(c);
const api = apiRecorder(page);
await loginAsAdmin(page);
let f = await openSql(page, c);
api();
for (const r of list) {
// Alibaba API ํธ์ถ ์ ํ("user flow control")์ผ๋ก Class ๋ชฉ๋ก์ด ๋น๋ ๊ฒฝ์ฐ๊ฐ ์์ด ํ๋ฉด์ ๋ค์ ์ด๊ณ ์ต๋ 3ํ ์๋
let row = '';
for (let i = 1; ; i++) {
try { row = await ensureRdb(page, f, c, r, api); break; } catch (e) {
if (i >= 3) throw e;
test.info().annotations.push({ type: 'rdb-retry', description: `${r.id} ${i}ํ์ฐจ ์คํจ โ 60s ํ ์ฌ์๋: ${String(e).slice(0, 300)}` });
await page.waitForTimeout(60_000);
f = await openSql(page, c);
}
}
expect(row, `${r.id} Instance List ํ`).toContain(r.id);
expect(row, `${r.id} Instance Class ํ์`).toContain(r.classText);
f = await openSql(page, c); // ์์ฑ ํจ๋์ ๋ซ๊ณ ๋ชฉ๋ก์ ์๋ก ์ฝ๋๋ค
}
// ์์ฑ ์งํ ์ํ(creating ๋ฑ)๊ฐ ๋๋ ๋๊น์ง ๊ฐฑ์ ํ๋ฉฐ ๋๊ธฐ(์ต๋ 30๋ถ)
const cur: Record<string, string> = {};
const t0 = Date.now();
const pending = () => list.filter((r) => !cur[r.id] || PENDING.test(cur[r.id])); // ๋น ๊ฐ(๋ชฉ๋ก ์ฌ์กฐํ ์ค)์ ์๋ฃ๋ก ๋ณด์ง ์๋๋ค
while (pending().length && Date.now() - t0 < 30 * 60_000) {
await page.waitForTimeout(30_000);
for (const r of pending()) cur[r.id] = (await refreshRow(page, c, r.id)).text || cur[r.id] || '';
}
for (const r of list) {
test.info().annotations.push({ type: 'rdb-final', description: `${r.id} โ ${Math.round((Date.now() - t0) / 1000)}s ๋๊ธฐ ํ: ${cur[r.id]}` });
expect(cur[r.id], `${r.id} ์ต์ข
Instance List ํ`).toContain(r.id);
expect(cur[r.id], `${r.id} ์ธ์คํด์ค ์ํ`).not.toMatch(/fail|error/i);
expect(cur[r.id], `${r.id} ์์ฑ ์๋ฃ ์ํ(creating ์ข
๋ฃ)`).not.toMatch(PENDING);
}
});
}tests/iv8/IV8_S10.spec.ts ยท 48์ค ยท ํด์ e1d80c9b ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '../../lib/fixtures';
import { activeCsps, tid } from '../../lib/data';
import { loginAsAdmin } from '../../lib/ui';
import { apiRecorder, openDm, deleteBackup, backupRowText } from '../../lib/iv8-os';
import { openSql, deleteRdb, rdbsOf, refreshRow, rdbFlowOf, RDB_CSPS } from '../../lib/iv8-rdb';
// IV8_S10 : SQL Database: RDBMS ์ธ์คํด์ค ์ญ์ (ํ
์คํธ ์์ ์ ๋ฆฌ) [์ ์, CSP๋ณ AWSยทGCP] (TC-DATA-019)
// 1.Restore > SQL Database โ Backup List ์์ 'mcmp-e2e-v07-bk2'(IV8_S13) ์ฒดํฌ โ Delete โ ํ์ธ ํ์
์ ๋ฐฑ์
์ด๋ฆ ์
๋ ฅ โ Delete (AWS ์คํ์์ 1ํ)
// 2.Generate > SQL Database โ credential ์ ํ โ Instance List ์์ S09 ์ธ์คํด์ค ์ฒดํฌ โ Delete โ ํ์ธ ํ์
์ ์ธ์คํด์ค ์ด๋ฆ ์
๋ ฅ โ Delete
// 3.Instance ListยทBackup List ์์ ์ฌ๋ผ์ง ํ์ธ. ๋์: AWS rdb1awsยทrdb3aws / GCP rdb2gpc (Drive mc-data-manager ์ํธ ๊ธฐ์ค, 2026-10-14)
// 2026-10-15: ๋์ํ RDB = AWSยทGCPยทAlibabaยทNCP, ๋ฐฑ์
์ CSP ๋ณ(rdbFlowOf(c).backup)
for (const c of activeCsps(RDB_CSPS)) {
test(`${tid('IV8_S10', c)} SQL Database: RDBMS ์ธ์คํด์ค ์ญ์ (ํ
์คํธ ์์ ์ ๋ฆฌ)`, async ({ page }) => {
test.setTimeout(40 * 60_000);
const list = rdbsOf(c);
const api = apiRecorder(page);
await loginAsAdmin(page);
// AWS ๋ 2026-10-14 ํ๋ฆ์ ์ ๋ฐฑ์
(mcmp-e2e-v07-bk2)๋ ๋จ์ ์์ผ๋ฉด ํจ๊ป ์ ๋ฆฌ
for (const RDB_BACKUP of [rdbFlowOf(c).backup, ...(c.code === 'AWS' ? ['mcmp-e2e-v07-bk2'] : [])]) {
const r = await openDm(page, 'Restore');
await r.getByText(/SQL Database/i).first().click();
await page.waitForTimeout(3000);
api();
if (await backupRowText(r, RDB_BACKUP)) {
expect(await deleteBackup(page, r, RDB_BACKUP, api), `${RDB_BACKUP} ๋ฐฑ์
์ญ์ `).toBe(true);
} else test.info().annotations.push({ type: 'backup-missing', description: `${RDB_BACKUP} Backup List ์ ์์(์ด๋ฏธ ์ ๋ฆฌ๋จ)` });
expect(await backupRowText(r, RDB_BACKUP), `${RDB_BACKUP} Backup List ์์ ์ ๊ฑฐ`).toBe('');
}
for (const r of list) {
const f = await openSql(page, c);
api();
const deleted = await deleteRdb(page, f, r, api);
expect(deleted, `${r.id} ๊ฐ Instance List ์ ์์ด์ผ ํ๋ค(IV8_S09 ์ ํ)`).toBe(true);
}
// ์ญ์ ๋ CSP ์์ ์ ๋ถ ๊ฑธ๋ฆฐ๋ค: ๋ชฉ๋ก์ ๊ฐฑ์ ํ๋ฉฐ ํ์ด ์ฌ๋ผ์ง ๋๊น์ง ๋๊ธฐ(์ต๋ 30๋ถ)
const t0 = Date.now();
const cur: Record<string, string> = Object.fromEntries(list.map((r) => [r.id, 'pending']));
while (list.some((r) => cur[r.id]) && Date.now() - t0 < 30 * 60_000) {
await page.waitForTimeout(30_000);
for (const r of list.filter((x) => cur[x.id])) {
cur[r.id] = (await refreshRow(page, c, r.id)).text;
test.info().annotations.push({ type: 'rdb-poll', description: `${r.id} ${Math.round((Date.now() - t0) / 1000)}s: ${cur[r.id] || '(ํ ์์)'}` });
}
}
for (const r of list) expect(cur[r.id], `${r.id} ๋ชฉ๋ก์์ ์ ๊ฑฐ`).toBe('');
});
}tests/iv8/IV8_S11.spec.ts ยท 37์ค ยท ํด์ 7d592bd5 ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '../../lib/fixtures';
import { activeCsps, tid } from '../../lib/data';
import { loginAsAdmin } from '../../lib/ui';
import { apiRecorder, openDm, selectCred } from '../../lib/iv8-os';
import { openSql, pickRdbRow, createSampleData, loadDatabases, hasRdbDb, rdbFlowOf, RDB_CSPS, RDB_DB } from '../../lib/iv8-rdb';
// IV8_S11 : SQL Database: RDBMS ์ํ ๋ฐ์ดํฐ ์์ฑ (Generate) [์ ์, CSP๋ณ AWS] (TC-DATA-008)
// 1.Data Migrations > Generate โ SQL Database ํญ 2.credential 'AWS - aws' / Region 'ap-northeast-2' โ Instance List ์์ mcmp-e2e-v07-rdb1aws ์ ํ
// 3.Sample Data โ Username 'mcmp' / Password ์
๋ ฅ โ Submit 4.ํ๋จ Result ์ฑ๊ณต ๋ฉ์์ง
// 5.Back Up โ SQL Database ํญ์์ ๊ฐ์ ์ธ์คํด์ค ์ ํ โ ์ ์์ ๋ณด โ Load Databases ์ LibraryManagement_0 ํ์
// ์ ํ: IV8_S09_AWS(rdb1aws available)
// 2026-10-15: ๋์ํ(RDB = AWSยทGCPยทAlibabaยทNCP)์ ๋ง์ถฐ CSP ๋ณ ์๋ณธ(rdbFlowOf(c).src)์์ ์คํ
for (const c of activeCsps(RDB_CSPS)) {
test(`${tid('IV8_S11', c)} SQL Database: RDBMS ์ํ ๋ฐ์ดํฐ ์์ฑ (Generate)`, async ({ page }) => {
test.setTimeout(30 * 60_000);
const r = rdbFlowOf(c).src;
const api = apiRecorder(page);
await loginAsAdmin(page);
const f = await openSql(page, c);
api();
await pickRdbRow(page, f, r.id);
const result = await createSampleData(page, f, r, api);
expect(result, 'Result ์ฑ๊ณต ๋ฉ์์ง').toMatch(/success|complete/i);
expect(result, 'Result ์ค๋ฅ ์์').not.toMatch(/fail|error/i);
// Back Up > SQL Database ์์ ์์ฑ๋ DB ์กฐํ
const b = await openDm(page, 'Back Up');
await b.getByText(/SQL Database/i).first().click();
await selectCred(page, b, c, 'sourceCredentialSelect');
await page.waitForTimeout(3000);
api();
await pickRdbRow(page, b, r.id, '#mysql-srcHost');
const dbs = await loadDatabases(page, b, r, '#rdbms-load-db-btn', api);
expect(hasRdbDb(dbs), `Back Up ํ๋ฉด DB ๋ชฉ๋ก์ ${RDB_DB}(๋์๋ฌธ์ ๋ฌด์)`).toBe(true);
});
}tests/iv8/IV8_S12.spec.ts ยท 98์ค ยท ํด์ 43e4bb43 ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '../../lib/fixtures';
import { activeCsps, tid } from '../../lib/data';
import { loginAsAdmin } from '../../lib/ui';
import { apiRecorder, openDm, selectCred, dmCred, dmRegion } from '../../lib/iv8-os';
import {
pickRdbRow, loadDatabases, resultText, waitResult, stepSummary, clickIn, diagnoseIn, DIAG_ERR,
hasRdbDb, selectRdbDb, rdbFlowOf, RDB_CSPS, RDB_DB, RDB_PW,
} from '../../lib/iv8-rdb';
// IV8_S12 : SQL Database: RDBMS ๋ง์ด๊ทธ๋ ์ด์
[์ ์, CSP๋ณ AWS] (TC-DATA-009)
// 1.Data Migrations > Migration โ SQL Database ํญ
// 2.Source: credential 'AWS - aws' / Region 'ap-northeast-2' โ mcmp-e2e-v07-rdb1aws ์ ํ โ Username 'mcmp' / Password โ Load Databases โ LibraryManagement_0 ์ ํ โ Verify โ Next
// 3.Diagnose Source โ Next 4.Target: 'AWS - aws' / 'ap-northeast-2' โ mcmp-e2e-v07-rdb3aws ์ ํ โ ์ ์์ ๋ณด โ Verify โ Next
// 5.Diagnose Target โ Submit 6.ํ๋จ Result ์ฑ๊ณต ๋ฉ์์ง 7.Back Up โ SQL Database ์์ rdb3aws ์ DB ๋ชฉ๋ก์ LibraryManagement_0
// ์ ํ: IV8_S09_AWS(rdb1awsยทrdb3aws), IV8_S11_AWS(rdb1aws ์ํ ๋ฐ์ดํฐ)
const P1 = '#collapseOne', P2 = '#collapseTwo', P3 = '#collapseThree', P4 = '#collapseFour';
// 2026-10-15: CSP ๋ณ srcโmig(rdbFlowOf) โ ๋์ํ RDB = AWSยทGCPยทAlibabaยทNCP
for (const c of activeCsps(RDB_CSPS)) {
test(`${tid('IV8_S12', c)} SQL Database: RDBMS ๋ง์ด๊ทธ๋ ์ด์
`, async ({ page }) => {
test.setTimeout(40 * 60_000);
const note = (t: string, d: string) => test.info().annotations.push({ type: t, description: d.slice(0, 1500) });
const { src: RDB_SRC, mig: RDB_MIG } = rdbFlowOf(c);
const api = apiRecorder(page);
await loginAsAdmin(page);
const f = await openDm(page, 'Migration');
await f.getByText(/SQL Database/i).first().click();
await test.step('1. Source ์ ์์ ๋ณด โ Database ์ ํ โ Verify โ Next', async () => {
await selectCred(page, f, c, 'sourceCredentialSelect');
await page.waitForTimeout(3000);
api();
await pickRdbRow(page, f, RDB_SRC.id, '#mysql-srcHost');
await loadDatabases(page, f, RDB_SRC, '#src-load-db-btn', api);
await selectRdbDb(f, '#mysql-srcDatabase');
await f.locator('#sourceVerifyBtn').click();
await page.waitForTimeout(5000);
note('source-verify', `${await stepSummary(f, P1)} | api: ${api()}`);
await f.locator(`button[data-current="${P1}"]`).click();
await expect(f.locator(P2)).toBeVisible({ timeout: 30_000 });
});
await test.step('2. Diagnose Source โ Next', async () => {
note('step2', await stepSummary(f, P2));
const d = await diagnoseIn(page, f, P2, api);
expect(d, 'Diagnose Source ๊ฒฐ๊ณผ ์ค๋ฅ ์์').not.toMatch(DIAG_ERR);
await f.locator(`button[data-current="${P2}"]`).click();
await expect(f.locator(P3)).toBeVisible({ timeout: 30_000 });
});
await test.step('3. Target ์ ์์ ๋ณด โ Verify โ Next', async () => {
note('step3-before', await stepSummary(f, P3));
const cred = f.locator(`${P3} select[id*="Credential"]`).first();
await cred.selectOption(dmCred(c)!);
const region = f.locator(`${P3} select[id*="Region"]`).first();
await page.waitForTimeout(2000);
await region.selectOption(dmRegion(c));
await page.waitForTimeout(4000);
const row = f.locator(P3).locator('table tbody tr').filter({ has: f.locator('td', { hasText: new RegExp(`^\\s*${RDB_MIG.id}\\s*$`) }) }).first();
await expect(row, `${RDB_MIG.id} Target Instance List ํ`).toBeVisible({ timeout: 60_000 });
await row.locator('td').nth(2).click();
await page.waitForTimeout(2000);
const user = f.locator(P3).getByPlaceholder('Username').first();
const pw = f.locator(P3).getByPlaceholder('Password').first();
await user.fill(RDB_MIG.user);
await pw.fill(RDB_PW);
api();
await clickIn(f, P3, /verify/i);
await page.waitForTimeout(5000);
note('target-verify', `${await stepSummary(f, P3)} | api: ${api()}`);
await f.locator(`button[data-current="${P3}"]`).click();
await expect(f.locator(P4)).toBeVisible({ timeout: 30_000 });
});
await test.step('4. Diagnose Target โ Submit โ Result', async () => {
note('step4', await stepSummary(f, P4));
const d = await diagnoseIn(page, f, P4, api);
expect(d, 'Diagnose Target ๊ฒฐ๊ณผ ์ค๋ฅ ์์').not.toMatch(DIAG_ERR);
const before = await resultText(page, f);
await f.locator('#submitBtn').click();
const result = await waitResult(page, f, before, 30 * 60_000);
note('submit-api', api() || '(API ๊ธฐ๋ก ์์)');
expect(result, 'Result ์ฑ๊ณต ๋ฉ์์ง').toMatch(/success|complete/i);
expect(result, 'Result ์ค๋ฅ ์์').not.toMatch(/\bfail|\berror/i);
});
await test.step('5. Back Up > SQL Database ์์ Target(rdb3aws) DB ๋ชฉ๋ก ํ์ธ', async () => {
const b = await openDm(page, 'Back Up');
await b.getByText(/SQL Database/i).first().click();
await selectCred(page, b, c, 'sourceCredentialSelect');
await page.waitForTimeout(3000);
api();
await pickRdbRow(page, b, RDB_MIG.id, '#mysql-srcHost');
const dbs = await loadDatabases(page, b, RDB_MIG, '#rdbms-load-db-btn', api);
expect(hasRdbDb(dbs), `Target(${RDB_MIG.id}) DB ๋ชฉ๋ก์ ${RDB_DB}(๋์๋ฌธ์ ๋ฌด์)`).toBe(true);
});
});
}tests/iv8/IV8_S13.spec.ts ยท 40์ค ยท ํด์ f76297ec ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '../../lib/fixtures';
import { activeCsps, tid } from '../../lib/data';
import { loginAsAdmin } from '../../lib/ui';
import { apiRecorder, openDm, selectCred, backupRowText } from '../../lib/iv8-os';
import { pickRdbRow, loadDatabases, submitBackup, stepSummary, hasRdbDb, selectRdbDb, rdbFlowOf, RDB_CSPS, RDB_DB } from '../../lib/iv8-rdb';
// IV8_S13 : SQL Database: RDBMS ๋ฐฑ์
[์ ์, CSP๋ณ AWS] (TC-DATA-010)
// 1.Data Migrations > Back Up โ SQL Database ํญ 2.Source: 'AWS - aws' / 'ap-northeast-2' โ mcmp-e2e-v07-rdb1aws ์ ํ โ Username 'mcmp' / Password
// โ Load Databases(๊ฒ์ฆ) 3.Database 'LibraryManagement_0' ์ ํ 4.Backup Name 'mcmp-e2e-v07-bk2' โ Submit 5.Result ์ฑ๊ณต ยท Backup List ์ ํ์
// ์ ํ: IV8_S11_AWS(rdb1aws ์ํ ๋ฐ์ดํฐ). ์ด๋ฏธ ๊ฐ์ ์ด๋ฆ ๋ฐฑ์
์ด ์์ผ๋ฉด ์คํจ๋ก ๋จ๊ธด๋ค(IV8_S10 ์์ ์ ๋ฆฌ ํ ์ฌ์คํ).
// 2026-10-15: CSP ๋ณ ์๋ณธยท๋ฐฑ์
์ด๋ฆ(rdbFlowOf) โ ๋์ํ RDB = AWSยทGCPยทAlibabaยทNCP
for (const c of activeCsps(RDB_CSPS)) {
test(`${tid('IV8_S13', c)} SQL Database: RDBMS ๋ฐฑ์
`, async ({ page }) => {
test.setTimeout(40 * 60_000);
const note = (t: string, d: string) => test.info().annotations.push({ type: t, description: d.slice(0, 1500) });
const { src: RDB_SRC, backup: RDB_BACKUP } = rdbFlowOf(c);
const api = apiRecorder(page);
await loginAsAdmin(page);
const f = await openDm(page, 'Back Up');
await f.getByText(/SQL Database/i).first().click();
await selectCred(page, f, c, 'sourceCredentialSelect');
await page.waitForTimeout(3000);
expect(await backupRowText(f, RDB_BACKUP), `${RDB_BACKUP} ๊ฐ ์ด๋ฏธ Backup List ์ ์์(์ ๋ฆฌ ํ์)`).toBe('');
api();
await pickRdbRow(page, f, RDB_SRC.id, '#mysql-srcHost');
const dbs = await loadDatabases(page, f, RDB_SRC, '#rdbms-load-db-btn', api);
expect(hasRdbDb(dbs), `DB ๋ชฉ๋ก์ ${RDB_DB}(๋์๋ฌธ์ ๋ฌด์)`).toBe(true);
const dbSel = f.locator('select[name$="[databaseName]"], select[id*="Database"]').first();
const dbId = await dbSel.getAttribute('id');
await selectRdbDb(f, dbId ? `select#${dbId}` : 'select[id*="Database"]');
note('form', await stepSummary(f, 'body'));
const { result, row } = await submitBackup(page, f, RDB_BACKUP, api);
expect(result, 'Result ์ฑ๊ณต ๋ฉ์์ง').toMatch(/success|complete/i);
expect(result, 'Result ์ค๋ฅ ์์').not.toMatch(/\bfail|\berror/i);
expect(row, 'Backup List ์ ๋ฐฑ์
ํ์').toContain(RDB_BACKUP);
expect(row.toLowerCase(), 'Backup List ํ์ DB(๋์๋ฌธ์ ๋ฌด์)').toContain(RDB_DB.toLowerCase());
expect(row, '๋ฐฑ์
์ํ').not.toMatch(/fail|error/i);
});
}tests/iv8/IV8_S14.spec.ts ยท 59์ค ยท ํด์ e3b90577 ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '../../lib/fixtures';
import { activeCsps, tid } from '../../lib/data';
import { loginAsAdmin } from '../../lib/ui';
import { apiRecorder, openDm, selectCred, backupRowText, logHistory } from '../../lib/iv8-os';
import { rdbRow, pickRdbRow, loadDatabases, resultText, waitResult, stepSummary, hasRdbDb, rdbFlowOf, RDB_CSPS, RDB_DB, RDB_PW } from '../../lib/iv8-rdb';
// IV8_S14 : SQL Database: RDBMS ๋ณต๊ตฌ [์ ์, Target GCP] (TC-DATA-011)
// 1.Data Migrations > Restore โ SQL Database ํญ 2.Backup List ์์ 'mcmp-e2e-v07-bk2'(IV8_S13, AWS rdb1aws ๋ฐฑ์
) ์ ํ
// 3.Target: 'GCP - gcp' / 'asia-northeast3' โ mcmp-e2e-v07-rdb2gpc ์ ํ โ Username 'root' / Password โ Submit 4.ํ๋จ Result ์ฑ๊ณต
// 5.Back Up โ SQL Database ์์ rdb2gpc ์ DB ๋ชฉ๋ก์ LibraryManagement_0. ์ ํ: IV8_S09_GCP, IV8_S13_AWS
// 2026-10-15: CSP ๋จ๋
ํ๋ฆ โ ๊ทธ CSP ์๋ณธ ๋ฐฑ์
(rdbFlowOf(c).backup) โ ๊ฐ์ CSP ์ rst ๋ก ๋ณต๊ตฌ (2026-10-14 GCP PASS ๋ AWS ๋ฐฑ์
โ rdb2gpc)
for (const c of activeCsps(RDB_CSPS)) {
test(`${tid('IV8_S14', c)} SQL Database: RDBMS ๋ณต๊ตฌ`, async ({ page }) => {
test.setTimeout(40 * 60_000);
const { rst: RDB_RST, backup: RDB_BACKUP } = rdbFlowOf(c);
const note = (t: string, d: string) => test.info().annotations.push({ type: t, description: d.slice(0, 1500) });
const api = apiRecorder(page);
await loginAsAdmin(page);
const f = await openDm(page, 'Restore');
await f.getByText(/SQL Database/i).first().click();
await page.waitForTimeout(3000);
await expect.poll(() => backupRowText(f, RDB_BACKUP), { timeout: 60_000 }).not.toBe('');
const brow = await backupRowText(f, RDB_BACKUP);
expect(brow, `${RDB_BACKUP} ๋ฐฑ์
์๋ฃ ์ํ(IV8_S13 ์ ํ)`).toMatch(/completed/i);
const row = f.locator(`tr.backup-row[data-name="${RDB_BACKUP}"]`).first();
await (await row.count() ? row : f.locator('table tbody tr').filter({ hasText: RDB_BACKUP }).first()).locator('td', { hasText: RDB_BACKUP }).first().click();
await page.waitForTimeout(1500);
note('backup-select', `${brow} | Source=${await f.locator('#sourcePointLabel').inputValue().catch(() => '')}`);
await selectCred(page, f, c, 'targetCredentialSelect');
await page.waitForTimeout(3000);
await expect(rdbRow(f, RDB_RST.id), `${RDB_RST.id} Target Instance List ํ`).toBeVisible({ timeout: 60_000 });
await rdbRow(f, RDB_RST.id).locator('td').nth(2).click();
await page.waitForTimeout(2000);
await f.locator('input[placeholder="Username"]:visible').first().fill(RDB_RST.user);
await f.locator('input[placeholder="Password"]:visible').first().fill(RDB_PW);
note('form', await stepSummary(f, 'body'));
api();
const before = await resultText(page, f);
await f.locator('#submitBtn').click();
const result = await waitResult(page, f, before, 30 * 60_000);
note('submit-api', api() || '(API ๊ธฐ๋ก ์์)');
expect(result, 'Result ์ฑ๊ณต ๋ฉ์์ง').toMatch(/success|complete/i);
expect(result, 'Result ์ค๋ฅ ์์').not.toMatch(/\bfail|\berror/i);
// ๋ณต๊ตฌ ๋์์์ DB ์กฐํ
const b = await openDm(page, 'Back Up');
await b.getByText(/SQL Database/i).first().click();
await selectCred(page, b, c, 'sourceCredentialSelect');
await page.waitForTimeout(3000);
api();
await pickRdbRow(page, b, RDB_RST.id, '#mysql-srcHost');
const dbs = await loadDatabases(page, b, RDB_RST, '#rdbms-load-db-btn', api);
expect(hasRdbDb(dbs), `Target(${RDB_RST.id}) DB ๋ชฉ๋ก์ ${RDB_DB}(๋์๋ฌธ์ ๋ฌด์)`).toBe(true);
const log = await logHistory(page, 'SQL Database', 'Restore').catch(() => [] as string[]);
note('log', log.slice(0, 3).join(' / ') || '(์ด๋ ฅ ์กฐํ ์ ๋จ)');
});
}tests/iv8/IV8_S15.spec.ts ยท 34์ค ยท ํด์ aebfcf7f ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '../../lib/fixtures';
import { activeCsps, tid } from '../../lib/data';
import { loginAsAdmin } from '../../lib/ui';
import { apiRecorder } from '../../lib/iv8-os';
import { PENDING } from '../../lib/iv8-rdb';
import { openNosql, ensureNrdb, waitNrdbReady, nrdbsOf, NRDB_CSPS } from '../../lib/iv8-nrdb';
// IV8_S15 : No-SQL: NRDB ์ธ์คํด์ค ์์ฑ โ Instance List ํ์ [์ ์, CSP๋ณ GCP] (TC-DATA-020)
// Data Migrations > Generate โ No-SQL ํญ โ credential 'GCP - gcp' / Region 'asia-northeast3' โ + Instance โ Instance ID ์
๋ ฅ โ Create
// โ Instance List ์ ํ์. ์ธ์คํด์ค mcmp-e2e-v07-nrdb1gcp(์๋ณธ), mcmp-e2e-v07-nrdb2gcp(Migration ๋์). ์ฌ์คํ ๊ฐ๋ฅ(์ด๋ฏธ ์์ผ๋ฉด ์์ฑ ์๋ต).
// 2026-10-15: ๋์ํ(NRDB) ์ค ์ธ์คํด์ค๊ฐ ์๋ GCPยทAlibabaยทNCP. AlibabaยทNCP MongoDB ์ต์ ์ฌ์(์ฌ์ฉ์ ์ง์) โ Alibaba ์ค๋ฉ ํด๋ฌ์คํฐ๋ ์์ฑ์ ์์ญ ๋ถ
for (const c of activeCsps(NRDB_CSPS)) {
test(`${tid('IV8_S15', c)} No-SQL: NRDB ์ธ์คํด์ค ์์ฑ โ Instance List ํ์`, async ({ page }) => {
test.setTimeout(70 * 60_000);
const list = nrdbsOf(c);
const NRDB_IDS = list.map((s) => s.id);
const api = apiRecorder(page);
await loginAsAdmin(page);
for (const s of list) {
const id = s.id;
const f = await openNosql(page, c);
api();
const row = await ensureNrdb(page, f, s, api);
expect(row, `${id} Instance List ํ`).toContain(id);
}
const cur = await waitNrdbReady(page, c, NRDB_IDS, 55 * 60_000);
for (const id of NRDB_IDS) {
expect(cur[id], `${id} ์ต์ข
Instance List ํ`).toContain(id);
expect(cur[id], `${id} ์ธ์คํด์ค ์ํ`).not.toMatch(/fail|error/i);
expect(cur[id], `${id} ์์ฑ ์๋ฃ ์ํ`).not.toMatch(PENDING);
}
});
}tests/iv8/IV8_S16.spec.ts ยท 40์ค ยท ํด์ a1b5437c ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '../../lib/fixtures';
import { activeCsps, tid } from '../../lib/data';
import { loginAsAdmin } from '../../lib/ui';
import { apiRecorder, logHistory } from '../../lib/iv8-os';
import { openNosql, pickNrdbRow, createNosqlSample, createMongoSample, createDynamoSample, nrdbFlowOf, nrdbKind, NRDB_FLOW_CSPS } from '../../lib/iv8-nrdb';
// IV8_S16 : No-SQL: NRDBMS ์ํ ๋ฐ์ดํฐ ์์ฑ (Generate) [์ ์, CSP๋ณ] (TC-DATA-012)
// 1.Data Migrations > Generate โ No-SQL ํญ 2.credential / Region ์ ํ โ Instance List ์์ ์๋ณธ ์ ํ
// 3.Sample Data โ ์
๋ ฅ โ Submit 4.ํ๋จ Result ์ฑ๊ณต ๋ฉ์์ง 5.Log Management(No-SQL / Generate) ์ฑ๊ณต ์ด๋ ฅ
// GCP(Firestore): mcmp-e2e-v07-nrdb1gcp ์ ํ โ Database ID(= Instance ID, ์๋ ์
๋ ฅ) ํ์ธ โ Submit
// AlibabaยทNCP(MongoDB, 2026-10-15): nrdb1ali / mcmpndb1 ์ ํ โ UsernameยทPasswordยทDatabase Name(mcmpe2e) โ Submit
// AWS(DynamoDB, 2026-10-15): ์ธ์คํด์ค ๋ชฉ๋ก ์์ โ credentialยทRegion ์ ํ ํ 'Sample Data'(#nrdb-aws-sampleBtn)
// ํด๋ผ์ฐ๋ ์ฝ์ ๋ฐ์ดํฐ ํ์ธ์ ์ด spec ๋ฒ์ ๋ฐ โ ์ํ ๋ฐ์ดํฐ๋ S17(Migration)ยทS18(Backup) ๊ฒฐ๊ณผ๋ก ๊ฐ์ ํ์ธ
// ์ ํ: IV8_S15_<CSP>(์๋ณธ available, AWS ์ ์ธ)
// 2026-10-15: ์ฌ์ฉ์ ๋์ํ(NRDB = AWSยทGCPยทAlibabaยทNCP)
for (const c of activeCsps(NRDB_FLOW_CSPS)) {
test(`${tid('IV8_S16', c)} No-SQL: NRDBMS ์ํ ๋ฐ์ดํฐ ์์ฑ (Generate)`, async ({ page }) => {
test.setTimeout(30 * 60_000);
const kind = nrdbKind(c);
// NRDB_SAMPLE_ON=mig: ๋์(mig)์ ์ํ ๋ฐ์ดํฐ โ MongoDB MigrationยทRestore ์ฌ์ ์กฐ๊ฑด(๋์์ Database ๊ฐ ์์ด์ผ ๋์ DB ๋ฅผ ๊ณ ๋ฅผ ์ ์์).
// Sample Data ์์ฑ์ด ๋์ Database ๋ฅผ ๋ง๋๋ ์์
์ ๋์ ํ๋ค(2026-10-15 ์ฌ์ฉ์ ํ์ธ: ์ฐํ ์๋)
const src = process.env.NRDB_SAMPLE_ON === 'mig' ? nrdbFlowOf(c).mig : nrdbFlowOf(c).src;
if (process.env.NRDB_SAMPLE_ON === 'mig') test.info().annotations.push({ type: 'sample-target', description: `์ฌ์ ์กฐ๊ฑด: ๋์ ${src.id} ์ Database ์์ฑ(Sample Data)` });
const api = apiRecorder(page);
await loginAsAdmin(page);
const f = await openNosql(page, c);
api();
let result = '';
if (kind === 'dynamo') result = await createDynamoSample(page, f, '#nrdb-aws-sampleBtn', api);
else {
await pickNrdbRow(page, f, src.id);
result = kind === 'mongo' ? await createMongoSample(page, f, src, api) : await createNosqlSample(page, f, api);
}
expect(result, 'Result ์ฑ๊ณต ๋ฉ์์ง').toMatch(/success|complete/i);
expect(result, 'Result ์ค๋ฅ ์์').not.toMatch(/fail|error/i);
const log = await logHistory(page, 'No-SQL', 'Generate');
expect(log.join(' / '), 'Log Management ์ Generate ์ด๋ ฅ').toMatch(/generate/i);
});
}tests/iv8/IV8_S17.spec.ts ยท 126์ค ยท ํด์ b125a4ea ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '../../lib/fixtures';
import { activeCsps, tid } from '../../lib/data';
import { loginAsAdmin, watchToasts } from '../../lib/ui';
import { apiRecorder, openDm, selectCred, logHistory, dmCred, dmRegion } from '../../lib/iv8-os';
import { resultText, waitResult, stepSummary, diagnoseIn, DIAG_ERR, RDB_PW } from '../../lib/iv8-rdb';
import { pickNrdbRow, mongoLoadDb, nrdbFlowOf, nrdbKind, NRDB_FLOW_CSPS, NRDB_DB } from '../../lib/iv8-nrdb';
// IV8_S17 : No-SQL: NRDBMS ๋ง์ด๊ทธ๋ ์ด์
[์ ์, CSP๋ณ] (TC-DATA-013)
// 1.Data Migrations > Migration โ No-SQL ํญ 2.Source: credential / Region โ ์๋ณธ ์ ํ โ Next
// 3.Diagnose Source โ Next 4.Target: ๊ฐ์ CSP credential / Region โ ๋์ ์ ํ โ Next 5.Diagnose Target โ Submit 6.ํ๋จ Result ์ฑ๊ณต 7.Log Management(No-SQL) ์ด๋ ฅ
// GCP(Firestore): nrdb1gcp โ nrdb2gcp, Instance IDยทDatabase ID ์๋ ์
๋ ฅ. Diagnose ๋ GCP ๋ฏธ์ง์(๋ฒํผ ๋นํ์ฑ)
// AlibabaยทNCP(MongoDB, 2026-10-15): ์๋ณธ ํ ์ ํ โ UsernameยทPassword โ Load Databases โ mcmpe2e ์ ํ. ๋์ ํ ์ ํ โ UsernameยทPassword
// AWS(DynamoDB, 2026-10-15): ์ธ์คํด์ค ์์ด SourceยทTarget ๋ชจ๋ aws / ap-northeast-2 (๊ฐ์ Region ์ DynamoDB)
// ์ ํ: IV8_S15_<CSP>(์๋ณธยท๋์, AWS ์ ์ธ), IV8_S16_<CSP>(์๋ณธ ์ํ ๋ฐ์ดํฐ)
const P1 = '#collapseOne', P2 = '#collapseTwo', P3 = '#collapseThree', P4 = '#collapseFour';
// 2026-10-15: ์ฌ์ฉ์ ๋์ํ(NRDB = AWSยทGCPยทAlibabaยทNCP)
for (const c of activeCsps(NRDB_FLOW_CSPS)) {
test(`${tid('IV8_S17', c)} No-SQL: NRDBMS ๋ง์ด๊ทธ๋ ์ด์
`, async ({ page }) => {
test.setTimeout(40 * 60_000);
const note = (t: string, d: string) => test.info().annotations.push({ type: t, description: d.slice(0, 1500) });
const kind = nrdbKind(c);
const { src, mig } = nrdbFlowOf(c);
const api = apiRecorder(page);
// Diagnose: ๋ฒํผ์ด ๋นํ์ฑ(GCP ๋ฏธ์ง์)ยท์์์ด๋ฉด ์ํ๋ง ๊ธฐ๋ก, ํ์ฑ์ด๋ฉด ์คํํ๊ณ ๊ฒฐ๊ณผ์ ์ค๋ฅ๊ฐ ์์ด์ผ ํ๋ค
const diag = async (panel: string) => {
const b = f.locator(panel).getByRole('button', { name: /diagnose/i }).first();
const state = !(await b.isVisible().catch(() => false)) ? '๋ฒํผ ์์' : (await b.isEnabled()) ? 'ํ์ฑ' : '๋นํ์ฑ';
note(`diagnose-state ${panel}`, `${state} | ${await stepSummary(f, panel)}`);
if (state === 'ํ์ฑ') expect(await diagnoseIn(page, f, panel, api), `${panel} Diagnose ๊ฒฐ๊ณผ ์ค๋ฅ ์์`).not.toMatch(DIAG_ERR);
};
const clickIfVisible = async (scope: string, name: RegExp, tag: string) => {
const b = f.locator(scope).getByRole('button', { name }).first();
if (await b.isVisible().catch(() => false)) { await b.click(); await page.waitForTimeout(5000); note(tag, `${await stepSummary(f, scope)} | api: ${api() || '(์์)'}`); }
};
await loginAsAdmin(page);
const f = await openDm(page, 'Migration');
await f.getByText(/No.?SQL/i).first().click();
await test.step('1. Source ์ ํ โ Next', async () => {
await selectCred(page, f, c, 'sourceCredentialSelect');
await page.waitForTimeout(3000);
api();
if (kind === 'firestore') await pickNrdbRow(page, f, src.id, '#src-nrdb-gcp-databaseId');
if (kind === 'mongo') {
await pickNrdbRow(page, f, src.id, '#mig-source-mongodb-host');
expect(await mongoLoadDb(page, f, src, 'mig-source-mongodb-', '#src-nrdb-load-db-btn', api, P1), 'Source Database ์ ํ').not.toBe('');
await clickIfVisible(P1, /verify/i, 'source-verify');
}
note('step1', await stepSummary(f, P1));
await f.locator(`button[data-current="${P1}"]`).click();
await expect(f.locator(P2)).toBeVisible({ timeout: 30_000 });
});
await test.step('2. Diagnose Source โ Next', async () => {
await diag(P2);
await f.locator(`button[data-current="${P2}"]`).click();
await expect(f.locator(P3)).toBeVisible({ timeout: 30_000 });
});
await test.step('3. Target ์ ํ โ Next', async () => {
await f.locator(`${P3} select[id*="Credential"]`).first().selectOption(dmCred(c)!);
await page.waitForTimeout(2000);
// AWS: ๊ฐ์ Region ์ ๊ฐ์ ํ
์ด๋ธ๋ก ์ฎ๊ธฐ๋ฉด 'BatchWriteItem โฆ key element does not match the schema'(500, 2026-10-15) โ ๋์ Region ์ ap-northeast-1 ๋ก ๋ถ๋ฆฌ
await f.locator(`${P3} select[id*="Region"]`).first().selectOption(kind === 'dynamo' ? 'ap-northeast-1' : dmRegion(c));
await page.waitForTimeout(4000);
if (kind !== 'dynamo') {
const row = f.locator(P3).locator('table tbody tr').filter({ has: f.locator('td', { hasText: new RegExp(`^\\s*${mig.id}\\s*$`) }) }).first();
await expect(row, `${mig.id} Target Instance List ํ`).toBeVisible({ timeout: 60_000 });
await row.locator('td').nth(2).click();
await page.waitForTimeout(2000);
}
if (kind === 'mongo') {
note('step3-after-pick', await stepSummary(f, P3));
const user = f.locator(`${P3} input[id*="sername"]:visible`).first();
const pw = f.locator(`${P3} input[type="password"]:visible`).first();
if (await user.count()) await user.fill(mig.user!);
if (await pw.count()) await pw.fill(RDB_PW);
await clickIfVisible(P3, /load databases/i, 'target-load-db');
// Load Databases ๋ค ๋์ DB select(#mig-target-โฆ)๊ฐ ์๊ธด๋ค โ ๋น์ ๋๋ฉด Submit ์ด ์์ฒญ์ ๋ณด๋ด์ง ์์(2026-10-15 AlibabaยทNCP).
// ์ฌ์ ์กฐ๊ฑด: ๋์์ Database ๊ฐ ์์ด์ผ ํ๋ค โ IV8_S16 ์ NRDB_SAMPLE_ON=mig ๋ก ๋์์๋ ์คํ(Sample Data ๊ฐ Database ์์ฑ์ ๋์ )
note('target-db-options', await f.locator(P3).evaluate((p) => [...p.querySelectorAll('select')].filter((e) => (e as HTMLElement).offsetParent)
.map((s) => `#${s.id}=${(s as HTMLSelectElement).value} [${[...(s as HTMLSelectElement).options].map((o) => `${o.value}:${(o.textContent || '').trim()}`).join('|')}]`).join(' / ')));
const tsel = f.locator(`${P3} select[id^="mig-target"]`).first();
if (await tsel.count()) {
const opts = await tsel.locator('option').evaluateAll((os) => os.map((o) => (o as HTMLOptionElement).value));
if (opts.includes(NRDB_DB)) await tsel.selectOption(NRDB_DB);
note('target-db', `${await tsel.getAttribute('id')} = ${await tsel.inputValue()} (์ต์
${opts.join('|')})`);
}
await clickIfVisible(P3, /verify/i, 'target-verify');
}
note('step3', await stepSummary(f, P3));
await f.locator(`button[data-current="${P3}"]`).click();
await expect(f.locator(P4)).toBeVisible({ timeout: 30_000 });
});
await test.step('4. Diagnose Target โ Submit โ Result', async () => {
await diag(P4);
const before = await resultText(page, f);
api();
const toasts = await watchToasts(page);
note('submit-state', `#submitBtn enabled=${await f.locator('#submitBtn').isEnabled()} visible=${await f.locator('#submitBtn').isVisible()}`);
// Result ์์์ ์ค๋ฅ๊ฐ ์ ๋์ค๋ ๊ฒฝ์ฐ(2026-10-15 AWS 500)๊ฐ ์์ด /migrate ์๋ต์ ํจ๊ป ๊ธฐ๋ค๋ฆฐ๋ค
const resp = page.waitForResponse((r) => /\/migrate\/nrdbms/i.test(r.url()) && r.request().method() === 'POST', { timeout: 30 * 60_000 }).catch(() => null);
await f.locator('#submitBtn').click();
await page.waitForTimeout(5000);
const early = await toasts();
note('submit-toasts', early.join(' | ') || '(์์)');
note('submit-invalid', await f.evaluate(() => [...document.querySelectorAll('.is-invalid, .invalid-feedback, .text-danger')].filter((e) => (e as HTMLElement).offsetParent).map((e) => `#${(e as HTMLElement).id} ${(e.textContent || '').trim()}`).join(' | ').slice(0, 600)) || '(์์)');
const first = await Promise.race([resp, page.waitForTimeout(60_000).then(() => 'none' as const)]);
if (first === 'none') note('submit-api-wait', '60์ด ์์ /migrate/nrdbms ์์ฒญ ์์');
const r = first === 'none' ? null : first; // ์์ฒญ์ด ์ ๋๊ฐ๋ฉด 30๋ถ์ ๊ธฐ๋ค๋ฆฌ์ง ์๋๋ค
expect(r, 'Submit ํ /migrate/nrdbms ์์ฒญ ์ ์ก(๋์ MongoDB ์ DB ๊ฐ ์์ผ๋ฉด ๋์ DB ๋ฅผ ๊ณ ๋ฅผ ์ ์์ด ์์ฒญ์ด ๋๊ฐ์ง ์์)').not.toBeNull();
if (r) { note('submit-response', `${r.status()} ${(await r.text().catch(() => '')).slice(0, 800)}`); expect(r.status(), '/migrate/nrdbms ์๋ต ์ํ').toBeLessThan(400); }
const result = await waitResult(page, f, before, r ? 5 * 60_000 : 60_000);
note('submit-api', api() || '(API ๊ธฐ๋ก ์์)');
expect(result, 'Result ์ฑ๊ณต ๋ฉ์์ง').toMatch(/success|complete/i);
expect(result, 'Result ์ค๋ฅ ์์').not.toMatch(/\bfail|\berror/i);
});
await test.step('5. Log Management(No-SQL) ์ด๋ ฅ', async () => {
const rows = await logHistory(page, 'No-SQL', 'Migrate').catch(async () => logHistory(page, 'No-SQL', 'Migration').catch(() => [] as string[]));
note('log', rows.slice(0, 3).join(' / ') || '(์ด๋ ฅ ์กฐํ ์ ๋จ)');
});
});
}tests/iv8/IV8_S18.spec.ts ยท 42์ค ยท ํด์ 785837a0 ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '../../lib/fixtures';
import { activeCsps, tid } from '../../lib/data';
import { loginAsAdmin } from '../../lib/ui';
import { apiRecorder, openDm, selectCred, backupRowText } from '../../lib/iv8-os';
import { submitBackup, stepSummary } from '../../lib/iv8-rdb';
import { pickNrdbRow, mongoLoadDb, nrdbFlowOf, nrdbKind, NRDB_FLOW_CSPS } from '../../lib/iv8-nrdb';
// IV8_S18 : No-SQL: NRDBMS ๋ฐฑ์
[์ ์, CSP๋ณ] (TC-DATA-014)
// 1.Data Migrations > Back Up โ No-SQL ํญ 2.Source: credential / Region โ ์๋ณธ ์ ํ 3.Backup Name โ Submit 4.Result ์ฑ๊ณต ยท Backup List ์ ํ์
// GCP(Firestore): nrdb1gcp ์ ํ(Instance IDยทDatabase ID ์๋) โ 'mcmp-e2e-v07-bk3'
// AlibabaยทNCP(MongoDB, 2026-10-15): ์๋ณธ ์ ํ โ UsernameยทPassword โ Load Databases โ mcmpe2e ์ ํ โ 'mcmp-e2e-v07-bk3ali' / 'bk3ncp'
// AWS(DynamoDB, 2026-10-15): ์ธ์คํด์ค ์์ด aws / ap-northeast-2 โ 'mcmp-e2e-v07-bk3aws'
// ์ ํ: IV8_S16_<CSP>(์๋ณธ ์ํ ๋ฐ์ดํฐ). ๊ฐ์ ์ด๋ฆ ๋ฐฑ์
์ด ์์ผ๋ฉด ์คํจ๋ก ๋จ๊ธด๋ค(IV8_S20 ์์ ์ ๋ฆฌ ํ ์ฌ์คํ).
// 2026-10-15: ์ฌ์ฉ์ ๋์ํ(NRDB = AWSยทGCPยทAlibabaยทNCP), ๋ฐฑ์
์ด๋ฆ์ CSP ๋ณ(nrdbFlowOf(c).backup)
for (const c of activeCsps(NRDB_FLOW_CSPS)) {
test(`${tid('IV8_S18', c)} No-SQL: NRDBMS ๋ฐฑ์
`, async ({ page }) => {
test.setTimeout(40 * 60_000);
const note = (t: string, d: string) => test.info().annotations.push({ type: t, description: d.slice(0, 1500) });
const kind = nrdbKind(c);
const { src, backup } = nrdbFlowOf(c);
const api = apiRecorder(page);
await loginAsAdmin(page);
const f = await openDm(page, 'Back Up');
await f.getByText(/No.?SQL/i).first().click();
await selectCred(page, f, c, 'sourceCredentialSelect');
await page.waitForTimeout(3000);
expect(await backupRowText(f, backup), `${backup} ๊ฐ ์ด๋ฏธ Backup List ์ ์์(์ ๋ฆฌ ํ์)`).toBe('');
api();
if (kind === 'firestore') await pickNrdbRow(page, f, src.id, '#nrdbms-gcp-databaseId');
if (kind === 'mongo') {
await pickNrdbRow(page, f, src.id, '#nosql-srcHost');
expect(await mongoLoadDb(page, f, src, 'nosql-src', '#nrdb-load-db-btn', api), 'Backup ๋์ Database ์ ํ').not.toBe('');
}
note('form', await stepSummary(f, 'body'));
const { result, row } = await submitBackup(page, f, backup, api);
expect(result, 'Result ์ฑ๊ณต ๋ฉ์์ง').toMatch(/success|complete/i);
expect(result, 'Result ์ค๋ฅ ์์').not.toMatch(/\bfail|\berror/i);
expect(row, 'Backup List ์ ๋ฐฑ์
ํ์').toContain(backup);
expect(row, '๋ฐฑ์
์ํ').not.toMatch(/fail|error/i);
});
}tests/iv8/IV8_S19.spec.ts ยท 61์ค ยท ํด์ 71e29ab0 ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '../../lib/fixtures';
import { activeCsps, tid } from '../../lib/data';
import { loginAsAdmin } from '../../lib/ui';
import { apiRecorder, openDm, selectCred, backupRowText, logHistory } from '../../lib/iv8-os';
import { resultText, waitResult, stepSummary, RDB_PW } from '../../lib/iv8-rdb';
import { pickNrdbRow, mongoLoadDb, nrdbFlowOf, nrdbKind, NRDB_FLOW_CSPS } from '../../lib/iv8-nrdb';
// IV8_S19 : No-SQL: NRDBMS ๋ณต๊ตฌ [์ ์, CSP๋ณ] (TC-DATA-015)
// 1.Data Migrations > Restore โ No-SQL ํญ 2.Backup List ์์ ๊ทธ CSP ๋ฐฑ์
(IV8_S18) ์ ํ
// 3.Target: ๊ฐ์ CSP credential / Region โ ๋ณต๊ตฌ ๋์ ์ ํ โ Submit 4.ํ๋จ Result ์ฑ๊ณต 5.Log Management(No-SQL / Restore) ์ด๋ ฅ
// AWS(DynamoDB): ์ธ์คํด์ค ์์ด credentialยทRegion ๋ง (2026-10-14 PASS ๋ GCP bk3 โ AWS, 2026-10-15 ๋ถํฐ AWS bk3aws โ AWS)
// GCP(Firestore): bk3 โ nrdb2gcp
// AlibabaยทNCP(MongoDB, 2026-10-15): bk3ali โ nrdb2ali / bk3ncp โ mcmpndb2, ๋์ ์ ํ ํ UsernameยทPassword
// ํด๋ผ์ฐ๋ ์ฝ์ ๋ฐ์ดํฐ ํ์ธ์ ์ด spec ๋ฒ์ ๋ฐ. ์ ํ: IV8_S18_<CSP>
// 2026-10-15: ์ฌ์ฉ์ ๋์ํ(NRDB = AWSยทGCPยทAlibabaยทNCP) โ CSP ๋จ๋
ํ๋ฆ
for (const c of activeCsps(NRDB_FLOW_CSPS)) {
test(`${tid('IV8_S19', c)} No-SQL: NRDBMS ๋ณต๊ตฌ`, async ({ page }) => {
test.setTimeout(40 * 60_000);
const note = (t: string, d: string) => test.info().annotations.push({ type: t, description: d.slice(0, 1500) });
const kind = nrdbKind(c);
const { rst, backup } = nrdbFlowOf(c);
const api = apiRecorder(page);
await loginAsAdmin(page);
const f = await openDm(page, 'Restore');
await f.getByText(/No.?SQL/i).first().click();
await page.waitForTimeout(3000);
await expect.poll(() => backupRowText(f, backup), { timeout: 60_000 }).not.toBe('');
const brow = await backupRowText(f, backup);
expect(brow, `${backup} ๋ฐฑ์
์๋ฃ ์ํ(IV8_S18 ์ ํ)`).toMatch(/completed/i);
const row = f.locator(`tr.backup-row[data-name="${backup}"]`).first();
await (await row.count() ? row : f.locator('table tbody tr').filter({ hasText: backup }).first()).locator('td', { hasText: backup }).first().click();
await page.waitForTimeout(1500);
note('backup-select', `${brow} | Source=${await f.locator('#sourcePointLabel').inputValue().catch(() => '')}`);
await selectCred(page, f, c, 'targetCredentialSelect');
await page.waitForTimeout(3000);
if (kind !== 'dynamo') {
await pickNrdbRow(page, f, rst.id, kind === 'mongo' ? '#nosql-srcHost' : undefined);
if (kind === 'mongo') {
// ๋์ ๊ณ์ โ Load Databases โ mcmpe2e ์ ํ. DB ๋ฅผ ๊ณ ๋ฅด์ง ์์ผ๋ฉด Submit ํด๋ ์์ฒญ์ด ๋๊ฐ์ง ์๋๋ค(2026-10-15, S17 ๊ณผ ๊ฐ์ ๋์)
expect(await mongoLoadDb(page, f, rst, 'nosql-src', '#nrdb-load-db-btn', api), '๋ณต๊ตฌ ๋์ Database ์ ํ').not.toBe('');
}
}
note('form', await stepSummary(f, 'body'));
api();
const before = await resultText(page, f);
const resp = page.waitForResponse((r) => /\/restore\/nrdbms/i.test(r.url()) && r.request().method() === 'POST', { timeout: 30 * 60_000 }).catch(() => null);
await f.locator('#submitBtn').click();
const first = await Promise.race([resp, page.waitForTimeout(60_000).then(() => 'none' as const)]);
const r = first === 'none' ? null : first;
expect(r, 'Submit ํ /restore/nrdbms ์์ฒญ ์ ์ก').not.toBeNull();
note('submit-response', `${r!.status()} ${(await r!.text().catch(() => '')).slice(0, 800)}`);
expect(r!.status(), '/restore/nrdbms ์๋ต ์ํ').toBeLessThan(400);
const result = await waitResult(page, f, before, 5 * 60_000);
note('submit-api', api() || '(API ๊ธฐ๋ก ์์)');
expect(result, 'Result ์ฑ๊ณต ๋ฉ์์ง').toMatch(/success|complete/i);
expect(result, 'Result ์ค๋ฅ ์์').not.toMatch(/\bfail|\berror/i);
const log = await logHistory(page, 'No-SQL', 'Restore').catch(() => [] as string[]);
note('log', log.slice(0, 3).join(' / ') || '(์ด๋ ฅ ์กฐํ ์ ๋จ)');
});
}tests/iv8/IV8_S20.spec.ts ยท 65์ค ยท ํด์ 83d022b3 ยท ์๋๋ฆฌ์ค ๋ณด๊ธฐ
import { test, expect } from '../../lib/fixtures';
import { activeCsps, tid } from '../../lib/data';
import { loginAsAdmin } from '../../lib/ui';
import { apiRecorder, openDm, deleteBackup, backupRowText } from '../../lib/iv8-os';
import { deleteRdb, rdbRowText } from '../../lib/iv8-rdb';
import { openNosql, refreshNrdb, nrdbsOf, nrdbFlowOf, NRDB_CSPS } from '../../lib/iv8-nrdb';
// AWS(DynamoDB)๋ ์ธ์คํด์ค๊ฐ ์์ด TC-DATA-021 ๋น๋์ โ IV8_S18_AWS ๋ฐฑ์
(bk3aws)๋ง ์ ๋ฆฌํ๋ค(๊ณํ ์์
ํ ์์)
for (const c of activeCsps(['AWS'])) {
test(`${tid('IV8_S20', c)} No-SQL: ๋ฐฑ์
์ ๋ฆฌ(AWS DynamoDB, ์ธ์คํด์ค ์์)`, async ({ page }) => {
const backup = nrdbFlowOf(c).backup;
const api = apiRecorder(page);
await loginAsAdmin(page);
const r = await openDm(page, 'Restore');
await r.getByText(/No.?SQL/i).first().click();
await page.waitForTimeout(3000);
if (await backupRowText(r, backup)) expect(await deleteBackup(page, r, backup, api), `${backup} ๋ฐฑ์
์ญ์ `).toBe(true);
else test.info().annotations.push({ type: 'backup-missing', description: `${backup} Backup List ์ ์์(์ด๋ฏธ ์ ๋ฆฌ๋จ)` });
expect(await backupRowText(r, backup), `${backup} Backup List ์์ ์ ๊ฑฐ`).toBe('');
});
}
// IV8_S20 : No-SQL: NRDB ์ธ์คํด์ค ์ญ์ (ํ
์คํธ ์์ ์ ๋ฆฌ) [์ ์, CSP๋ณ GCP] (TC-DATA-021)
// 1.Restore > No-SQL โ Backup List ์์ 'mcmp-e2e-v07-bk3'(IV8_S18) ์ฒดํฌ โ Delete โ ํ์ธ ํ์
์ ๋ฐฑ์
์ด๋ฆ ์
๋ ฅ โ Delete
// 2.Generate > No-SQL โ 'GCP - gcp' / 'asia-northeast3' โ Instance List ์์ mcmp-e2e-v07-nrdb1gcp, nrdb2gcp ๊ฐ๊ฐ ์ฒดํฌ โ Delete โ ์ด๋ฆ ์
๋ ฅ โ Delete
// 3.Instance ListยทBackup List ์์ ์ฌ๋ผ์ง ํ์ธ
// (Drive ์ ์ฐจ 2 ์ 'Generate > SQL Database' ๋ No-SQL ์ธ์คํด์ค๋ผ No-SQL ํญ์์ ์ญ์ ํ๋ค)
// 2026-10-15: ๋์ํ(NRDB) ์ค ์ธ์คํด์ค๊ฐ ์๋ GCPยทAlibabaยทNCP, ๋ฐฑ์
์ด๋ฆ์ CSP ๋ณ(nrdbFlowOf(c).backup)
for (const c of activeCsps(NRDB_CSPS)) {
test(`${tid('IV8_S20', c)} No-SQL: NRDB ์ธ์คํด์ค ์ญ์ (ํ
์คํธ ์์ ์ ๋ฆฌ)`, async ({ page }) => {
test.setTimeout(45 * 60_000);
const NRDB_IDS = nrdbsOf(c).map((s) => s.id);
const NRDB_BACKUP = nrdbFlowOf(c).backup;
const api = apiRecorder(page);
await loginAsAdmin(page);
const r = await openDm(page, 'Restore');
await r.getByText(/No.?SQL/i).first().click();
await page.waitForTimeout(3000);
api();
if (await backupRowText(r, NRDB_BACKUP)) {
expect(await deleteBackup(page, r, NRDB_BACKUP, api), `${NRDB_BACKUP} ๋ฐฑ์
์ญ์ `).toBe(true);
} else test.info().annotations.push({ type: 'backup-missing', description: `${NRDB_BACKUP} Backup List ์ ์์(์ด๋ฏธ ์ ๋ฆฌ๋จ)` });
expect(await backupRowText(r, NRDB_BACKUP), `${NRDB_BACKUP} Backup List ์์ ์ ๊ฑฐ`).toBe('');
for (const id of NRDB_IDS) {
const f = await openNosql(page, c);
api();
// ๋ชฉ๋ก์ด ๋ฆ๊ฒ ๊ทธ๋ ค์ง ์ ์์ด ํ์ ์ต๋ 30์ด ๊ธฐ๋ค๋ฆฐ๋ค(2026-10-14: 3์ด๋ง ๋ณด๊ณ '๋ชฉ๋ก์ ์์'์ผ๋ก ํ๋จํ ์ฌ๋ก). ์ด๋ฏธ ์ง์์ก์ผ๋ฉด ๊ธฐ๋ก๋ง ๋จ๊ธด๋ค
const seen = await expect.poll(() => rdbRowText(f, id), { timeout: 30_000, intervals: [3000] }).not.toBe('').then(() => true, () => false);
if (seen) expect(await deleteRdb(page, f, { id } as any, api), `${id} ์ญ์ ์์ฒญ`).toBe(true);
else test.info().annotations.push({ type: 'nrdb-missing', description: `${id} Instance List ์ ์์(์ด๋ฏธ ์ญ์ ๋จ)` });
}
const t0 = Date.now();
const cur: Record<string, string> = Object.fromEntries(NRDB_IDS.map((id) => [id, 'pending']));
while (NRDB_IDS.some((id) => cur[id]) && Date.now() - t0 < 35 * 60_000) {
await page.waitForTimeout(20_000);
for (const id of NRDB_IDS.filter((x) => cur[x])) {
cur[id] = await refreshNrdb(page, c, id);
test.info().annotations.push({ type: 'nrdb-poll', description: `${id} ${Math.round((Date.now() - t0) / 1000)}s: ${cur[id] || '(ํ ์์)'}` });
}
}
for (const id of NRDB_IDS) expect(cur[id], `${id} ๋ชฉ๋ก์์ ์ ๊ฑฐ`).toBe('');
});
}