test: add playwright deployment gate
TaxBaik CI/CD / build-and-deploy (push) Failing after 3h2m56s

This commit is contained in:
2026-06-27 12:51:16 +09:00
parent c5af05c5dd
commit 6b5ea85733
9 changed files with 259 additions and 2 deletions
+13
View File
@@ -149,3 +149,16 @@ jobs:
echo "Service verification failed (home: $HOME_STATUS, login: $LOGIN_STATUS, auth: $AUTH_BODY)" >&2
exit 1
fi
- name: Install Playwright dependencies
run: |
set -e
npm ci
npx playwright install chromium --with-deps
- name: Browser E2E verification
env:
E2E_BASE_URL: http://${{ secrets.DEPLOY_HOST }}/taxbaik
E2E_ADMIN_USERNAME: admin
E2E_ADMIN_PASSWORD: ${{ secrets.TAXBAIK_ADMIN_TEST_PASSWORD }}
run: npm run test:e2e
+6
View File
@@ -33,6 +33,9 @@ artifacts/
# Test results
TestResults/
*.trx
playwright-report/
test-results/
.playwright-cli/
# IDE
.vscode/
@@ -46,6 +49,9 @@ Thumbs.db
packages/
.nuget/
# Node / Playwright
node_modules/
# Publish
publish/
PublishProfiles/
+92
View File
@@ -0,0 +1,92 @@
# TaxBaik 개선 로드맵 WBS
이 문서는 "완료 보고"가 아니라 검증 가능한 작업 목록이다. 각 WBS는 성공 기준을 통과해야 완료로 본다.
## 완료 판정 원칙
- 코드 변경만으로 완료 처리하지 않는다.
- 서버 배포 대상 기능은 CI/CD 성공과 Playwright 브라우저 테스트 통과를 모두 요구한다.
- API 기능은 단위 테스트 또는 통합 테스트와 함께 실제 HTTP 호출 결과를 확인한다.
- DB 변경은 마이그레이션과 롤백 위험을 문서화한다.
- 비밀값은 Gitea Secrets 또는 서버 환경변수로만 관리한다.
## WBS-OPS-01 배포 검증 고도화
목표: curl/API 검증만으로 "완료" 처리하지 않고, 실제 브라우저 사용자 흐름을 CI 게이트로 만든다.
성공 기준:
- `dotnet build TaxBaik.sln -c Release` 경고 0, 오류 0
- `dotnet test TaxBaik.sln -c Release --no-build` 전체 통과
- CI 배포 후 Playwright가 `/taxbaik/admin/login`에서 실제 로그인 수행
- 로그인 후 `/taxbaik/admin/dashboard` 도달
- `localStorage.auth_token` 저장 확인
- 브라우저 console error 및 page error 0개
Todo:
- [x] Playwright Test 프로젝트 추가
- [x] 관리자 로그인 E2E 추가
- [x] CI 배포 후 Playwright 실행 단계 추가
- [x] Playwright가 발견한 Blazor DI 결함 수정
- [ ] CI run에서 Playwright 통과 확인
## WBS-AUTH-01 인증/비밀번호 운영 안정화
목표: DB 직접 수정 대신 API로 관리자 인증 운영 작업을 수행한다.
성공 기준:
- 비밀번호 변경 API가 현재 비밀번호를 요구한다.
- 비밀번호 재설정 API는 운영 secret 없이는 동작하지 않는다.
- 실패 응답은 민감 정보를 노출하지 않는다.
- Playwright 로그인 테스트가 변경 후에도 통과한다.
Todo:
- [x] 로그인 API 검증
- [x] 비밀번호 변경 API 추가
- [x] 재설정 API 추가
- [ ] 관리자 UI에 비밀번호 변경 화면 추가
- [ ] 비밀번호 변경 Playwright E2E 추가
## WBS-ADMIN-01 관리자 Blazor 안정화
목표: 관리자 화면을 일반 웹페이지처럼 명시적 사용자 액션에만 갱신하고, circuit 예외를 배포 전 차단한다.
성공 기준:
- 관리자 주요 메뉴 대시보드/블로그/문의/설정 접근 시 circuit error 0개
- 잘못된 DI 타입 주입 0건
- 저장/삭제/상태 변경 액션은 성공/실패 메시지를 표시한다.
- Playwright가 주요 메뉴 smoke test를 수행한다.
Todo:
- [x] 중복 `/admin` 라우트 제거
- [x] MudBlazor DI 타입 오류 수정
- [ ] 관리자 메뉴 smoke E2E 추가
- [ ] 설정 저장 TODO를 실제 DB 기반 기능으로 전환
## WBS-UX-01 공개 홈페이지 UX/SEO 검증
목표: 공개 홈페이지가 검색 유입과 상담 전환에 맞는 구조인지 검증한다.
성공 기준:
- 홈/블로그 목록/블로그 상세/상담 문의 페이지 200
- 주요 페이지 title/description 존재
- 모바일 viewport에서 주요 CTA가 보인다.
- 상담 문의 제출 Playwright E2E가 통과한다.
Todo:
- [ ] 공개 페이지 Playwright smoke E2E 추가
- [ ] 상담 문의 제출 E2E 추가
- [ ] 블로그 상세 SEO 메타 검증 추가
## WBS-MAINT-01 유지보수성/파편화 축소
목표: 문서와 실제 구조의 불일치를 줄이고 단일 앱 운영 기준을 유지한다.
성공 기준:
- README/CLAUDE/DEPLOYMENT_GUIDE의 .NET 버전, 앱 구조, 테스트 기준이 실제 코드와 일치
- 배포 문서에 Playwright 검증 절차 포함
- 오래된 분리 Admin 서비스 문서 제거 또는 명확히 deprecated 처리
Todo:
- [ ] README 테스트/배포 섹션 갱신
- [ ] CLAUDE.md E2E 기준 갱신
- [ ] 오래된 최종 보고 문서의 허위 완료 표현 정정
@@ -1,7 +1,6 @@
@page "/admin/blog"
@attribute [Authorize]
@inject IApiClient ApiClient
@inject DialogService DialogService
@inject ISnackbar Snackbar
<PageTitle>블로그 관리</PageTitle>
@@ -1,7 +1,7 @@
@page "/admin/settings"
@using TaxBaik.Domain.Interfaces
@attribute [Authorize]
@inject Snackbar Snackbar
@inject ISnackbar Snackbar
<PageTitle>설정</PageTitle>
@@ -35,5 +35,7 @@
private async Task SaveSettings()
{
// TODO: Save settings to database
Snackbar.Add("설정 저장 기능은 아직 구현되지 않았습니다.", Severity.Info);
await Task.CompletedTask;
}
}
+75
View File
@@ -0,0 +1,75 @@
{
"name": "taxbaik",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"devDependencies": {
"@playwright/test": "1.57.0"
}
},
"node_modules/@playwright/test": {
"version": "1.57.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0.tgz",
"integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.57.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/playwright": {
"version": "1.57.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz",
"integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.57.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.57.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz",
"integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
}
}
}
+9
View File
@@ -0,0 +1,9 @@
{
"scripts": {
"test:e2e": "playwright test",
"test:e2e:headed": "playwright test --headed"
},
"devDependencies": {
"@playwright/test": "1.57.0"
}
}
+25
View File
@@ -0,0 +1,25 @@
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests/e2e',
timeout: 30_000,
expect: {
timeout: 10_000
},
fullyParallel: false,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 1 : 0,
reporter: process.env.CI ? [['list'], ['html', { open: 'never' }]] : 'list',
use: {
baseURL: process.env.E2E_BASE_URL ?? 'http://178.104.200.7/taxbaik',
trace: 'retain-on-failure',
screenshot: 'only-on-failure',
video: 'retain-on-failure'
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] }
}
]
});
+36
View File
@@ -0,0 +1,36 @@
import { expect, test } from '@playwright/test';
const username = process.env.E2E_ADMIN_USERNAME ?? 'admin';
const password = process.env.E2E_ADMIN_PASSWORD;
const baseUrl = (process.env.E2E_BASE_URL ?? 'http://178.104.200.7/taxbaik').replace(/\/$/, '');
test.describe('admin authentication', () => {
test('logs in through the real browser UI and reaches dashboard', async ({ page }) => {
test.skip(!password, 'E2E_ADMIN_PASSWORD is required.');
const consoleErrors: string[] = [];
page.on('console', message => {
if (message.type() === 'error') {
consoleErrors.push(message.text());
}
});
page.on('pageerror', error => {
consoleErrors.push(error.message);
});
await page.goto(`${baseUrl}/admin/login`);
await expect(page.getByRole('heading', { name: '관리자 로그인' })).toBeVisible();
await page.getByRole('textbox', { name: '사용자명' }).fill(username);
await page.getByRole('textbox', { name: '비밀번호' }).fill(password);
await page.getByRole('button', { name: '로그인' }).click();
await expect(page).toHaveURL(/\/taxbaik\/admin\/dashboard$/);
await expect(page.getByRole('heading', { name: /대시보드/ })).toBeVisible();
await expect(page.getByRole('link', { name: /로그아웃/ })).toBeVisible();
const token = await page.evaluate(() => localStorage.getItem('auth_token'));
expect(token, 'auth_token should be stored after login').toBeTruthy();
expect(consoleErrors, 'browser console/page errors').toEqual([]);
});
});