Files
Gnuboard7/tests/Playwright/specs/admin/settings-env-priority.spec.ts
HeuJung 381fcc4e0b feat(core,admin_basic): .env 키 단위 우선 모드 추가
관리자 환경설정 저장값이 config 를 덮는 기존 설계는 공유호스팅을 1차 대상으로
삼은 결과다. .env 를 배포 기준값으로 관리하는 설치에서는 그 설계가 반대로
작동해, 설치기가 defaults 를 시드하는 순간부터 .env 의 28개 항목이 오류도
로그도 없이 사문화된다.

G7_ENV_PRIORITY 옵트인으로 그 소유권을 키 단위로 되돌린다. 스위치를 켜지 않은
설치는 전 경로가 조기 return 이라 동작이 100% 동일하다.

명시 여부는 config 빌드 시점에 캡처한다 — 런타임 env 는 config:cache 환경에서
null 로 고정되어 판정이 영구 미발동한다(attachment.disk_explicit 와 같은 함정).
판정은 strict 라 APP_DEBUG=false·REDIS_DB=0 같은 falsy 명시도 명시로 본다.

잠금은 그 키의 주입만 건너뛰는 것이지만, 제거된 키의 부재를 값으로 읽는 자리가
있으면 형제 설정까지 무너진다. 게이트(웹소켓 토글·메일 드라이버·디버그 모드)와
기본값 주입(mailgun.endpoint·ses.region), 형제 폴백(reverb server endpoint),
파생 판정(storage_driver→attachment.disk)을 유효값으로 보정했다.
2026-09-04 10:08:30 +09:00

142 lines
5.8 KiB
TypeScript

/**
* E2E: 환경설정 — `.env` 키 단위 우선 (G7_ENV_PRIORITY) (#641)
*
* 시나리오 매니페스트: tests/scenarios/settings-env-priority.yaml — 마킹은 각 테스트의
* scenario(k=v 조합)·effects 주석이 담당하며, 헤더 요약 마킹은 파서 형식이 아니다
*
* 잠금 축(스위치 ON)은 서버의 `.env` 를 바꾸고 설정 캐시를 다시 구워야 재현되므로 CI 의
* 일반 실행에서는 다루지 않는다. 그 축은 레이아웃 테스트
* (admin-settings-env-locked.test.tsx)와 Chrome MCP 실측 매트릭스가 커버한다.
* 잠금 축을 켜 둔 서버에서 돌리려면 `G7_E2E_ENV_PRIORITY=1` 을 주면 된다.
*
* 여기서 상시 지키는 것은 **회귀 가드**다 — 스위치를 켜지 않은 설치에서 화면이 종전과
* 똑같이 동작하는지(배지 0 · 배너 0 · 저장 왕복 유지).
*/
import { test, expect, issueToken, authenticatePage } from '../../fixtures/auth';
const ENV_PRIORITY_ON = process.env.G7_E2E_ENV_PRIORITY === '1';
/** 관리자 환경설정 일반 탭 진입 (설정 시드 완료까지 대기) */
async function gotoGeneralTab(page: import('@playwright/test').Page): Promise<void> {
await page.goto('/admin/settings?tab=general');
await page.waitForLoadState('domcontentloaded', { timeout: 30_000 });
await expect(page.locator('input[name="general.site_name"]').first()).toBeAttached({ timeout: 20_000 });
await expect
.poll(
() =>
page.evaluate(
() => Object.keys((window as any).G7Core?.state?.getLocal?.()?.form?.general ?? {}).length
),
{ timeout: 20_000 }
)
.toBeGreaterThan(0);
}
/** 설정 응답의 `_meta` 를 읽는다 */
async function readSettingsMeta(page: import('@playwright/test').Page): Promise<any> {
return page.evaluate(() => (window as any).G7Core?.state?.getLocal?.()?.form?._meta ?? null);
}
// @scenario switch=off, key_state=unlocked, surface=get_meta
// @effects switch_off_behaves_identically
test('@smoke #641 - 스위치가 꺼진 설치는 응답 메타에 잠금이 비어 있다', async ({ page }) => {
test.skip(ENV_PRIORITY_ON, '이 블록은 스위치가 꺼진 서버를 전제로 한다');
const token = issueToken('core.settings.read', 'core.settings.update');
await authenticatePage(page, token);
await gotoGeneralTab(page);
const meta = await readSettingsMeta(page);
expect(meta).not.toBeNull();
expect(meta.env_priority_enabled).toBe(false);
expect(Object.keys(meta.env_locked ?? {})).toHaveLength(0);
});
// @scenario switch=off, key_state=unlocked, surface=ui_lock
// @effects switch_off_behaves_identically
test('#641 - 스위치가 꺼진 설치는 배너도 잠금 배지도 렌더하지 않는다', async ({ page }) => {
test.skip(ENV_PRIORITY_ON, '이 블록은 스위치가 꺼진 서버를 전제로 한다');
const token = issueToken('core.settings.read', 'core.settings.update');
await authenticatePage(page, token);
await gotoGeneralTab(page);
await expect(page.locator('#env_priority_banner')).toHaveCount(0);
await expect(page.locator('input[name="general.site_name"]').first()).toBeEnabled();
// site_description 은 Textarea 로 렌더된다 — input 셀렉터로는 잡히지 않는다.
await expect(page.locator('textarea[name="general.site_description"]').first()).toBeEnabled();
});
// @scenario switch=off, key_state=unlocked, surface=post_filter
// @effects switch_off_behaves_identically
test('#641 - 스위치가 꺼진 설치의 저장 왕복이 종전과 같다', async ({ page }) => {
test.skip(ENV_PRIORITY_ON, '이 블록은 스위치가 꺼진 서버를 전제로 한다');
const token = issueToken('core.settings.read', 'core.settings.update');
await authenticatePage(page, token);
await gotoGeneralTab(page);
const description = page.locator('textarea[name="general.site_description"]').first();
await expect(description).toBeAttached();
const original = (await description.inputValue()) ?? '';
const probe = `env-priority-e2e-${Date.now()}`;
const save = async (value: string) => {
await description.fill(value);
const saved = page.waitForResponse(
(response) =>
response.request().method() === 'POST' &&
/\/api\/admin\/settings\/?$/.test(new URL(response.url()).pathname),
{ timeout: 20_000 }
);
await page.getByRole('button', { name: /저장|Save/ }).first().click();
return saved;
};
const response = await save(probe);
expect(response.status()).toBe(200);
const body = await response.json();
expect(body?.data?.settings?._meta?.env_priority_enabled).toBe(false);
expect(body?.data?.settings?.general?.site_description).toBe(probe);
// 원복 — 검수 후 상태를 남기지 않는다
const restore = await save(original);
expect(restore.status()).toBe(200);
});
// @scenario switch=on, key_state=locked_plain, surface=ui_lock
// @effects badge_and_disabled_rendered, meta_env_locked_uses_frontend_keys
test('#641 - 스위치가 켜진 서버는 잠긴 필드를 배지와 함께 잠근다', async ({ page }) => {
test.skip(!ENV_PRIORITY_ON, '서버 `.env` 조작이 필요한 축 — G7_E2E_ENV_PRIORITY=1 일 때만 실행');
const token = issueToken('core.settings.read', 'core.settings.update');
await authenticatePage(page, token);
await gotoGeneralTab(page);
const meta = await readSettingsMeta(page);
expect(meta.env_priority_enabled).toBe(true);
const lockedKeys = Object.keys(meta.env_locked ?? {});
expect(lockedKeys.length).toBeGreaterThan(0);
await expect(page.locator('#env_priority_banner')).toBeVisible();
for (const key of lockedKeys.filter((k) => k.startsWith('general.'))) {
const field = page.locator(`[name="${key}"]`).first();
if ((await field.count()) === 0) {
continue;
}
await expect(field).toBeDisabled();
}
});