From 7a7455e56d1f6afa20c1cad1589255ec444b777c Mon Sep 17 00:00:00 2001 From: kjh2064 Date: Sat, 11 Jul 2026 18:45:15 +0900 Subject: [PATCH] =?UTF-8?q?docs:=20=EB=A1=9C=EC=BB=AC=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=ED=95=84=EC=88=98=20=EC=A1=B0=EA=B1=B4=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(SSH=20=ED=84=B0=EB=84=90=EB=A7=81,=20=EB=B0=B0?= =?UTF-8?q?=ED=8F=AC=20=EC=A0=84=20=EA=B2=80=EC=A6=9D=20=EA=B0=80=EB=93=9C?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 변경사항 ### CLAUDE.md - '로컬 개발 & 테스트' 섹션 신규 추가 * SSH 터널링 설정 (Docker 사용 금지) * appsettings.Development.json 설정 * 로컬 서비스 시작 방법 - 배포 전 필수 체크리스트 * Build (0 errors, 0 warnings) * 서비스 시작 확인 * 로그인 테스트 * 모든 Admin 페이지 검증 (200 상태, 500 에러 없음) * E2E 테스트 통과 - 배포 게이트: 로컬 테스트 통과 전 절대 배포 금지 ### E2E 테스트 - complete-admin-flow.spec.ts 신규 추가 * 모든 Admin 페이지 접근 테스트 * 500 에러 감지 * Authorization 검증 ## 교훈 Authorization Policy 500 오류가 로컬에서 먼저 발견되었어야 했음. Docker 없이 SSH 터널로 원격 DB 접속하는 현실을 반영하여 지침화. Co-Authored-By: Claude Haiku 4.5 --- CLAUDE.md | 69 +++++++++++ tests/e2e/complete-admin-flow.spec.ts | 159 ++++++++++++++++++++++++++ 2 files changed, 228 insertions(+) create mode 100644 tests/e2e/complete-admin-flow.spec.ts diff --git a/CLAUDE.md b/CLAUDE.md index 85cabdfb..d92bc4e1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -264,6 +264,74 @@ UI: `Pages/Admin/Collection/Index.cshtml` — status 값에 따라 배지 색상 4. **GetDailyItemChartPriceAsync** (FHKST03010100) — Daily OHLCV data 5. **GetInvestorTrendAsync** (FHKST01010900) — Investor sentiment (개인/외국인/기관) +## Local Development & Testing (2026-07-11) + +### ⚠️ CRITICAL: SSH Tunnel for Remote Database Access + +**Never use Docker locally.** Always use SSH tunneling to connect to remote PostgreSQL: + +```powershell +# 1. Setup SSH tunnel (Terminal 1) — forwards local 5432 to remote DB +ssh -L 127.0.0.1:5432:localhost:5432 kjh2064@178.104.200.7 -N + +# 2. Configure appsettings.Development.json +{ + "ConnectionStrings": { + "DefaultConnection": "Host=127.0.0.1;Database=quantenginedb;Username=quantengine_app;Password=quantengine_app;Search Path=quantengine;" + } +} + +# 3. Start service locally (Terminal 2) +cd src/dotnet +dotnet watch run --project QuantEngine.Web + +# 4. Access locally +http://localhost:5265/Account/Login +``` + +### Mandatory Pre-Deployment Checklist + +**EVERY code change must pass:** + +1. ✅ **Local build (0 errors, 0 warnings)** + ```powershell + dotnet build src/dotnet/QuantEngine.Web/QuantEngine.Web.csproj -c Release + ``` + +2. ✅ **Local service startup with SSH tunnel** + - Service must start without DB connection errors + - DbUp migrations must succeed + +3. ✅ **Login test (admin/quant123!)** + - `/Account/Login` must return 200 + - Authentication flow must complete + - Cookie must be set + +4. ✅ **All Admin pages must load** + - `/Admin/Dashboard` → 200 (NOT 500) + - `/Admin/Users` → 200 (NOT 500) + - `/Admin/Collection` → 200 (NOT 500) + - `/Admin/Monitoring` → 200 (NOT 500) + - `/Admin/Operations` → 200 (NOT 500) + - **No 500 errors in response body** + +5. ✅ **Playwright E2E tests pass** + ```powershell + npx playwright test tests/e2e/complete-admin-flow.spec.ts + ``` + +### Deployment Gates + +**NEVER deploy without:** +- ❌ Local testing complete +- ❌ All Admin pages verified (200 status, no 500 errors) +- ❌ E2E tests passing +- ❌ Authorization Policy configured (if changes made to Program.cs) + +**Deployment failure is better than service outage.** Halt and investigate if local tests fail. + +--- + ## Notes for Contributors (2026-07-11) - **SQL Safety**: Whitelist-only table access (enum switch in Repository) @@ -275,3 +343,4 @@ UI: `Pages/Admin/Collection/Index.cshtml` — status 값에 따라 배지 색상 - **Legacy Code**: `QuantEngine.Web.Client` folder kept for reference (not in .sln, not built) - **Newtonsoft.Json**: Known high-severity vulnerability (GHSA-5crp-9r3c-p9vr); update or replace when feasible - **Release Authority**: Python gates (`full-gate`, `prepare-upload-zip`) remain authority; .NET Admin fully operational as of 2026-07-11 +- **Testing Requirement**: All code changes must pass local testing with SSH tunnel to remote DB before deployment (see "Local Development & Testing" above) diff --git a/tests/e2e/complete-admin-flow.spec.ts b/tests/e2e/complete-admin-flow.spec.ts new file mode 100644 index 00000000..8fa949d2 --- /dev/null +++ b/tests/e2e/complete-admin-flow.spec.ts @@ -0,0 +1,159 @@ +import { test, expect } from '@playwright/test'; + +test.describe('Complete Admin Flow - All Pages', () => { + const BASE_URL = 'http://localhost:5000'; + const LOGIN_URL = `${BASE_URL}/Account/Login`; + const ADMIN_DASHBOARD = `${BASE_URL}/Admin/Dashboard`; + + test.beforeEach(async ({ page }) => { + // 1. 로그인 페이지 접근 + await page.goto(LOGIN_URL); + await expect(page).toHaveTitle(/Login|로그인/i); + + // 2. 로그인 시도 + await page.fill('#username', 'admin'); + await page.fill('#password', 'quant123!'); + await page.click('button[type="submit"]'); + + // 3. 로그인 후 리다이렉트 대기 + await page.waitForURL(/Admin|Dashboard/); + }); + + test('Admin Dashboard 접근 가능', async ({ page }) => { + console.log('Testing: Admin Dashboard'); + await page.goto(ADMIN_DASHBOARD); + + // 페이지 로드 확인 + await expect(page).toHaveTitle(/Dashboard|대시보드/i); + + // 주요 엘리먼트 확인 + const body = await page.content(); + expect(body).toContain('Dashboard'); + + console.log('✓ Dashboard loaded successfully'); + }); + + test('Admin Users 페이지 접근', async ({ page }) => { + console.log('Testing: Users Page'); + await page.goto(`${BASE_URL}/Admin/Users`); + + // 페이지 로드 확인 + const statusCode = (await page.goto(`${BASE_URL}/Admin/Users`)).status(); + expect([200, 301, 302]).toContain(statusCode); + + const content = await page.content(); + expect(content).not.toContain('500'); + expect(content).not.toContain('error'); + + console.log('✓ Users page accessible'); + }); + + test('Admin Collection 페이지 접근', async ({ page }) => { + console.log('Testing: Collection Page'); + const response = await page.goto(`${BASE_URL}/Admin/Collection`); + + expect(response?.status()).toBeLessThan(400); + const content = await page.content(); + expect(content).not.toContain('500'); + + console.log('✓ Collection page accessible'); + }); + + test('Admin Monitoring 페이지 접근', async ({ page }) => { + console.log('Testing: Monitoring Page'); + const response = await page.goto(`${BASE_URL}/Admin/Monitoring`); + + expect(response?.status()).toBeLessThan(400); + const content = await page.content(); + expect(content).not.toContain('500'); + + console.log('✓ Monitoring page accessible'); + }); + + test('Admin Operations 페이지 접근', async ({ page }) => { + console.log('Testing: Operations Page'); + const response = await page.goto(`${BASE_URL}/Admin/Operations`); + + expect(response?.status()).toBeLessThan(400); + const content = await page.content(); + expect(content).not.toContain('500'); + + console.log('✓ Operations page accessible'); + }); + + test('Users Create 페이지 접근', async ({ page }) => { + console.log('Testing: Users Create Page'); + const response = await page.goto(`${BASE_URL}/Admin/Users/Create`); + + expect(response?.status()).toBeLessThan(400); + const content = await page.content(); + expect(content).not.toContain('500'); + expect(content).toContain('Create'); + + console.log('✓ Users Create page accessible'); + }); + + test('모든 Admin 페이지가 200 이상 500 미만 상태코드 반환', async ({ page }) => { + console.log('Testing: All Admin Pages Status Codes'); + + const adminPages = [ + '/Admin/Dashboard', + '/Admin/Users', + '/Admin/Collection', + '/Admin/Monitoring', + '/Admin/Operations', + '/Admin/Users/Create', + ]; + + const results = []; + + for (const path of adminPages) { + const fullUrl = `${BASE_URL}${path}`; + const response = await page.goto(fullUrl); + const status = response?.status() || 0; + + results.push({ + path, + status, + success: status >= 200 && status < 500, + }); + + console.log(` ${path} → ${status} ${status >= 200 && status < 500 ? '✓' : '❌'}`); + + // 각 페이지에서 500 에러가 없는지 확인 + const content = await page.content(); + expect(content).not.toContain('500'); + expect(content).not.toContain('System.InvalidOperationException'); + } + + // 모든 페이지가 정상인지 확인 + const allSuccess = results.every(r => r.success); + expect(allSuccess).toBe(true); + + console.log('\n✅ All Admin pages passed'); + }); + + test('로그아웃 후 보호된 페이지 접근 불가', async ({ page }) => { + console.log('Testing: Access Control'); + + // 대시보드 접근 가능 확인 + let response = await page.goto(ADMIN_DASHBOARD); + expect(response?.status()).toBeLessThan(400); + + // 로그아웃 (쿠키 삭제) + await page.context().clearCookies(); + + // 보호된 페이지 접근 시도 + response = await page.goto(ADMIN_DASHBOARD); + + // 로그인 페이지로 리다이렉트되거나 401/403 에러 + const url = page.url(); + expect( + url.includes('/Account/Login') || + url.includes('/Account/AccessDenied') || + (response?.status() && [401, 403].includes(response.status())) + ).toBe(true); + + console.log('✓ Access control working correctly'); + }); +});