관리자 환경설정 저장값이 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)을 유효값으로 보정했다.
142 lines
5.8 KiB
TypeScript
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();
|
|
}
|
|
});
|