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'); + }); +});