수정: 관리자 e2e 인증 흐름 안정화
This commit is contained in:
+15
-15
@@ -3,15 +3,15 @@
|
|||||||
> 이 문서는 현재 WBS 기준의 검증 문서가 아니라, 과거 배포 요약의 기록이다.
|
> 이 문서는 현재 WBS 기준의 검증 문서가 아니라, 과거 배포 요약의 기록이다.
|
||||||
> 최신 상태는 `ROADMAP_WBS.md`와 CI 로그를 기준으로 판단한다.
|
> 최신 상태는 `ROADMAP_WBS.md`와 CI 로그를 기준으로 판단한다.
|
||||||
|
|
||||||
## 📊 최종 완성 현황
|
## 📊 과거 기록 현황
|
||||||
|
|
||||||
### ⚠️ 과거 기준 기록
|
### ⚠️ 과거 기준 기록
|
||||||
|
|
||||||
| 단계 | 항목 | 상태 |
|
| 단계 | 항목 | 상태 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| W0 | 프로젝트 기반 구축 | ✅ 완료 |
|
| W0 | 프로젝트 기반 구축 | 과거 기록 |
|
||||||
| W1 | LLM 개발 지침 (CLAUDE.md) | ✅ 완료 |
|
| W1 | LLM 개발 지침 (CLAUDE.md) | 과거 기록 |
|
||||||
| W2 | 도메인/인프라/서비스 레이어 | ✅ 완료 |
|
| W2 | 도메인/인프라/서비스 레이어 | 과거 기록 |
|
||||||
| **W3** | **공개 홈페이지 (Razor Pages SSR)** | 과거 기록 |
|
| **W3** | **공개 홈페이지 (Razor Pages SSR)** | 과거 기록 |
|
||||||
| **W4** | **관리자 백오피스 (Blazor Server)** | 과거 기록 |
|
| **W4** | **관리자 백오피스 (Blazor Server)** | 과거 기록 |
|
||||||
| **W5** | **스타일링 및 모바일 UX** | 과거 기록 |
|
| **W5** | **스타일링 및 모바일 UX** | 과거 기록 |
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
### 관리자
|
### 관리자
|
||||||
- 🔐 **로그인**: http://178.104.200.7/taxbaik/admin/login
|
- 🔐 **로그인**: http://178.104.200.7/taxbaik/admin/login
|
||||||
- 📊 **대시보드**: http://178.104.200.7/taxbaik/admin/dashboard
|
- 📊 **대시보드**: http://178.104.200.7/taxbaik/admin/dashboard
|
||||||
- 👤 **기본 계정**: admin / admin123
|
- 계정 정보는 문서에 기록하지 않고 Gitea Secrets 또는 서버 환경변수로만 관리한다.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -61,9 +61,9 @@
|
|||||||
## 📊 과거 데이터베이스 기록
|
## 📊 과거 데이터베이스 기록
|
||||||
|
|
||||||
### 초기 데이터
|
### 초기 데이터
|
||||||
- ✅ **5개 카테고리**: 사업자세무, 부동산세금, 종합소득세, 부가가치세, 가족자산증여
|
- 5개 카테고리: 사업자세무, 부동산세금, 종합소득세, 부가가치세, 가족자산증여
|
||||||
- ✅ **5개 블로그 포스트**: 초기 콘텐츠 포함
|
- 5개 블로그 포스트: 초기 콘텐츠 포함
|
||||||
- ✅ **1개 관리자 계정**: admin/admin123
|
- 관리자 계정: 비밀번호는 문서화하지 않는다.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -101,13 +101,13 @@ e7e01d0 마이그레이션 및 보안 수정
|
|||||||
|
|
||||||
## ✨ 주요 특징
|
## ✨ 주요 특징
|
||||||
|
|
||||||
- ✅ SEO 최적화 (Server-Side Rendering)
|
- SEO 항목 (Server-Side Rendering)
|
||||||
- ✅ 무중단 배포 (Shadow Copy)
|
- 심링크 기반 배포
|
||||||
- ✅ 반응형 모바일 UI
|
- 반응형 모바일 UI
|
||||||
- ✅ 한국어 완전 지원
|
- 한국어 UI
|
||||||
- ✅ 자동 마이그레이션
|
- 자동 마이그레이션
|
||||||
- ✅ 안전한 인증 (쿠키 + 인증)
|
- 인증 항목
|
||||||
- ✅ 체계적인 레이어 구조
|
- 레이어 구조
|
||||||
- 기록용 요약일 뿐, 현재 완료 판정 기준은 아니다.
|
- 기록용 요약일 뿐, 현재 완료 판정 기준은 아니다.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
+60
-61
@@ -8,10 +8,10 @@
|
|||||||
|
|
||||||
## 📌 프로젝트 개요
|
## 📌 프로젝트 개요
|
||||||
|
|
||||||
### 비즈니스 목표
|
### 비즈니스 목표 기록
|
||||||
- ✅ 온라인 전문성 표현
|
- 온라인 전문성 표현
|
||||||
- ✅ 블로그 SEO 유입
|
- 블로그 SEO 유입
|
||||||
- ✅ 전국 고객 확보
|
- 전국 고객 확보
|
||||||
|
|
||||||
### 핵심 포지셔닝
|
### 핵심 포지셔닝
|
||||||
> "사업자 세금 + 부동산 + 가족자산 = 맞춤형 세무 파트너"
|
> "사업자 세금 + 부동산 + 가족자산 = 맞춤형 세무 파트너"
|
||||||
@@ -95,24 +95,23 @@ TaxBaik.Admin/ 95 KB (Blazor Server)
|
|||||||
## ✨ 주요 기능
|
## ✨ 주요 기능
|
||||||
|
|
||||||
### 공개 사이트
|
### 공개 사이트
|
||||||
- ✅ SEO 최적화 블로그 (5개 카테고리)
|
- SEO 블로그
|
||||||
- ✅ 온라인 상담 신청 폼
|
- 온라인 상담 신청 폼
|
||||||
- ✅ 반응형 디자인 (모바일 375px+)
|
- 반응형 디자인
|
||||||
- ✅ 성능 최적화 (gzip, lazy load)
|
- 성능 최적화 항목
|
||||||
|
|
||||||
### 관리자 백오피스
|
### 관리자 백오피스
|
||||||
- ✅ 대시보드 (KPI 카드)
|
- 대시보드
|
||||||
- ✅ 블로그 CRUD
|
- 블로그 관리
|
||||||
- ✅ 문의 관리 (상태 변경)
|
- 문의 관리
|
||||||
- ✅ 사이트 설정
|
- 사이트 설정
|
||||||
|
|
||||||
### 보안 & 성능
|
### 보안 & 성능
|
||||||
- ✅ SQL Injection 방지 (파라미터화 쿼리)
|
- SQL Injection 방지 항목
|
||||||
- ✅ CSRF 보호 ([ValidateAntiForgeryToken])
|
- 인증/인가 항목
|
||||||
- ✅ Cookie 기반 인증 (8시간 세션)
|
- gzip 응답 압축
|
||||||
- ✅ gzip 응답 압축
|
- 이미지 lazy load
|
||||||
- ✅ 이미지 lazy load
|
- 폰트 preconnect
|
||||||
- ✅ 폰트 preconnect
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -130,7 +129,7 @@ Gitea Actions 트리거
|
|||||||
4. 심링크 스왑
|
4. 심링크 스왑
|
||||||
5. systemctl restart
|
5. systemctl restart
|
||||||
↓
|
↓
|
||||||
배포 완료 (무중단)
|
배포 기록 생성
|
||||||
```
|
```
|
||||||
|
|
||||||
### 자동 마이그레이션
|
### 자동 마이그레이션
|
||||||
@@ -143,7 +142,7 @@ schema_migrations 테이블 확인
|
|||||||
↓
|
↓
|
||||||
미실행 마이그레이션 자동 실행
|
미실행 마이그레이션 자동 실행
|
||||||
↓
|
↓
|
||||||
DB 준비 완료
|
DB 준비 기록 생성
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -164,32 +163,32 @@ DB 준비 완료
|
|||||||
## 🎯 과거 수락 기준 기록
|
## 🎯 과거 수락 기준 기록
|
||||||
|
|
||||||
### 기술적 요구사항
|
### 기술적 요구사항
|
||||||
- [x] ASP.NET Core 8 + C#11 기반
|
- ASP.NET Core 기반
|
||||||
- [x] Dapper + PostgreSQL 사용
|
- Dapper + PostgreSQL 사용
|
||||||
- [x] Razor Pages SSR (공개 사이트)
|
- Razor Pages SSR (공개 사이트)
|
||||||
- [x] Blazor Server (관리자)
|
- Blazor Server (관리자)
|
||||||
- [x] 계층화된 아키텍처 (Domain → Infrastructure → Application → Web/Admin)
|
- 계층화된 아키텍처
|
||||||
- [x] 모든 UI 문자열 한국어
|
- UI 문자열 한국어
|
||||||
|
|
||||||
### 기능 요구사항
|
### 기능 요구사항
|
||||||
- [x] 블로그 (5개 카테고리, SEO 최적화)
|
- 블로그
|
||||||
- [x] 온라인 문의 폼
|
- 온라인 문의 폼
|
||||||
- [x] 관리자 백오피스 (블로그 + 문의 관리)
|
- 관리자 백오피스
|
||||||
- [x] 반응형 디자인
|
- 반응형 디자인
|
||||||
- [x] 성능 최적화
|
- 성능 최적화
|
||||||
|
|
||||||
### 배포 요구사항
|
### 배포 요구사항
|
||||||
- [x] CI/CD 파이프라인 (Gitea Actions)
|
- CI/CD 파이프라인
|
||||||
- [x] 자동 마이그레이션
|
- 자동 마이그레이션
|
||||||
- [x] 무중단 배포 (심링크 스왑)
|
- 심링크 배포
|
||||||
- [x] systemd 서비스 파일
|
- systemd 서비스 파일
|
||||||
- [x] Nginx 리버스 프록시 설정
|
- Nginx 리버스 프록시 설정
|
||||||
|
|
||||||
### 문서 요구사항
|
### 문서 요구사항
|
||||||
- [x] CLAUDE.md (개발 지침)
|
- CLAUDE.md
|
||||||
- [x] DEPLOYMENT_GUIDE.md (배포 가이드)
|
- DEPLOYMENT_GUIDE.md
|
||||||
- [x] README.md (프로젝트 개요)
|
- README.md
|
||||||
- [x] 서버 설치 스크립트
|
- 서버 설치 스크립트
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -231,31 +230,31 @@ b300cd7 완성: 빌드 성공 및 최종 통합 (W0~W6 완료)
|
|||||||
|
|
||||||
## 과거 체크리스트 기록
|
## 과거 체크리스트 기록
|
||||||
|
|
||||||
### 개발 완료
|
### 개발 기록
|
||||||
- [x] 코드 작성
|
- 코드 작성 기록
|
||||||
- [x] 로컬 빌드 성공
|
- 로컬 빌드 기록
|
||||||
- [x] Git 커밋/푸시
|
- Git 커밋/푸시 기록
|
||||||
|
|
||||||
### 검증 완료
|
### 검증 기록
|
||||||
- [x] 아키텍처 검증
|
- 아키텍처 검토 기록
|
||||||
- [x] 코드 구조 검증
|
- 코드 구조 검토 기록
|
||||||
- [x] 보안 검증
|
- 보안 검토 기록
|
||||||
- [x] 성능 검증
|
- 성능 검토 기록
|
||||||
- [x] SEO 검증
|
- SEO 검토 기록
|
||||||
|
|
||||||
### 배포 준비
|
### 배포 준비
|
||||||
- [x] CI/CD 파이프라인
|
- CI/CD 파이프라인
|
||||||
- [x] 자동 마이그레이션
|
- 자동 마이그레이션
|
||||||
- [x] 배포 스크립트
|
- 배포 스크립트
|
||||||
- [x] 배포 가이드
|
- 배포 가이드
|
||||||
- [x] 모니터링 설정
|
- 모니터링 설정
|
||||||
|
|
||||||
### 문서 완성
|
### 문서 기록
|
||||||
- [x] README.md
|
- README.md
|
||||||
- [x] CLAUDE.md
|
- CLAUDE.md
|
||||||
- [x] DEPLOYMENT_GUIDE.md
|
- DEPLOYMENT_GUIDE.md
|
||||||
- [x] PRODUCTION_CHECKLIST.md
|
- PRODUCTION_CHECKLIST.md
|
||||||
- [x] SERVER_SETUP.sh
|
- SERVER_SETUP.sh
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+6
-6
@@ -98,12 +98,12 @@ Todo:
|
|||||||
Todo:
|
Todo:
|
||||||
- [x] README 테스트/배포 섹션 갱신
|
- [x] README 테스트/배포 섹션 갱신
|
||||||
- [x] CLAUDE.md E2E 기준 갱신
|
- [x] CLAUDE.md E2E 기준 갱신
|
||||||
- [ ] 오래된 최종 보고 문서의 허위 완료 표현 정정
|
- [x] 오래된 최종 보고 문서의 허위 완료 표현 정정
|
||||||
|
|
||||||
### 현재 검증 메모
|
### 현재 검증 메모
|
||||||
- 로컬 빌드 성공
|
- `dotnet build TaxBaik.sln` 성공
|
||||||
- 관리자 smoke 성공
|
- 시크릿 없는 로컬 Playwright 전체 실행: 공개 smoke, blog SEO 통과 / 관리자 시나리오는 자격 증명 미설정으로 스킵
|
||||||
- 공개 smoke 성공
|
- 배포본 `version.txt`: `8f0cb69`
|
||||||
- 블로그 상세 SEO는 원격 배포본 반영 대기
|
- 배포본 블로그 상세: HTTP 200
|
||||||
- 문의 제출 E2E는 원격 배포 반영 대기
|
- CI Playwright 전체 통과는 최신 커밋 배포 후 재확인 필요
|
||||||
- 비밀번호 변경 E2E는 배포 환경 자격 증명 확인 대기
|
- 비밀번호 변경 E2E는 배포 환경 자격 증명 확인 대기
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
@page "/admin"
|
@page "/admin"
|
||||||
|
@attribute [Authorize]
|
||||||
@inject NavigationManager NavigationManager
|
@inject NavigationManager NavigationManager
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
@page "/admin/blog/create"
|
@page "/admin/blog/create"
|
||||||
|
@attribute [Authorize]
|
||||||
@using TaxBaik.Application.DTOs
|
@using TaxBaik.Application.DTOs
|
||||||
@using TaxBaik.Application.Services
|
@using TaxBaik.Application.Services
|
||||||
@using TaxBaik.Domain.Interfaces
|
@using TaxBaik.Domain.Interfaces
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
@page "/admin/blog"
|
@page "/admin/blog"
|
||||||
|
@attribute [Authorize]
|
||||||
@inject IApiClient ApiClient
|
@inject IApiClient ApiClient
|
||||||
@inject ISnackbar Snackbar
|
@inject ISnackbar Snackbar
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
@page "/admin/dashboard"
|
@page "/admin/dashboard"
|
||||||
|
@attribute [Authorize]
|
||||||
@using TaxBaik.Application.Services
|
@using TaxBaik.Application.Services
|
||||||
@inject AdminDashboardService DashboardService
|
@inject AdminDashboardService DashboardService
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
@page "/admin/inquiries/{InquiryId:int}"
|
@page "/admin/inquiries/{InquiryId:int}"
|
||||||
|
@attribute [Authorize]
|
||||||
@using TaxBaik.Application.Services
|
@using TaxBaik.Application.Services
|
||||||
@inject InquiryService InquiryService
|
@inject InquiryService InquiryService
|
||||||
@inject NavigationManager Navigation
|
@inject NavigationManager Navigation
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
@page "/admin/inquiries"
|
@page "/admin/inquiries"
|
||||||
|
@attribute [Authorize]
|
||||||
@using TaxBaik.Domain.Interfaces
|
@using TaxBaik.Domain.Interfaces
|
||||||
@inject IInquiryRepository InquiryRepository
|
@inject IInquiryRepository InquiryRepository
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
@page "/admin/settings"
|
@page "/admin/settings"
|
||||||
|
@attribute [Authorize]
|
||||||
@using System.ComponentModel.DataAnnotations
|
@using System.ComponentModel.DataAnnotations
|
||||||
@using System.Collections.Generic
|
@using System.Collections.Generic
|
||||||
@using TaxBaik.Web.Services
|
@using TaxBaik.Web.Services
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
@inject NavigationManager Navigation
|
||||||
|
|
||||||
|
@code {
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
var returnUrl = Uri.EscapeDataString(Navigation.ToBaseRelativePath(Navigation.Uri));
|
||||||
|
Navigation.NavigateTo($"/taxbaik/admin/login?returnUrl={returnUrl}", replace: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,11 @@
|
|||||||
|
|
||||||
<Router AppAssembly="@typeof(Program).Assembly">
|
<Router AppAssembly="@typeof(Program).Assembly">
|
||||||
<Found Context="routeData">
|
<Found Context="routeData">
|
||||||
<RouteView RouteData="@routeData" DefaultLayout="@typeof(TaxBaik.Web.Components.Admin.Layout.MainLayout)" />
|
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(TaxBaik.Web.Components.Admin.Layout.MainLayout)">
|
||||||
|
<NotAuthorized>
|
||||||
|
<RedirectToLogin />
|
||||||
|
</NotAuthorized>
|
||||||
|
</AuthorizeRouteView>
|
||||||
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
|
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
|
||||||
</Found>
|
</Found>
|
||||||
<NotFound>
|
<NotFound>
|
||||||
|
|||||||
@@ -22,22 +22,10 @@ test.describe('admin authentication', () => {
|
|||||||
await expect(page.locator('input[placeholder="사용자명"]')).toBeVisible();
|
await expect(page.locator('input[placeholder="사용자명"]')).toBeVisible();
|
||||||
await expect(page.locator('input[placeholder="비밀번호"]')).toBeVisible();
|
await expect(page.locator('input[placeholder="비밀번호"]')).toBeVisible();
|
||||||
|
|
||||||
const token = await page.evaluate(async ({ baseUrl, username, password }) => {
|
await page.locator('input[placeholder="사용자명"]').fill(username);
|
||||||
const response = await fetch(`${baseUrl}/api/auth/login`, {
|
await page.locator('input[placeholder="비밀번호"]').fill(password);
|
||||||
method: 'POST',
|
await page.getByRole('button', { name: '로그인' }).click();
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ username, password }),
|
|
||||||
});
|
|
||||||
if (!response.ok) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const body = await response.json();
|
|
||||||
return body?.token ?? null;
|
|
||||||
}, { baseUrl, username, password });
|
|
||||||
expect(token, 'login API should return a token').toBeTruthy();
|
|
||||||
|
|
||||||
await page.addInitScript(value => localStorage.setItem('auth_token', value), token);
|
|
||||||
await page.goto(`${baseUrl}/admin/dashboard`);
|
|
||||||
await expect(page).toHaveURL(/\/taxbaik\/admin\/dashboard$/);
|
await expect(page).toHaveURL(/\/taxbaik\/admin\/dashboard$/);
|
||||||
await expect(page.locator('text=대시보드')).toBeVisible({ timeout: 20_000 });
|
await expect(page.locator('text=대시보드')).toBeVisible({ timeout: 20_000 });
|
||||||
await expect(page.getByRole('link', { name: /로그아웃/ })).toBeVisible();
|
await expect(page.getByRole('link', { name: /로그아웃/ })).toBeVisible();
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { expect, test } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
|
import { getAdminToken, installAdminToken } from './helpers/admin-auth';
|
||||||
|
|
||||||
const username = process.env.E2E_ADMIN_USERNAME ?? 'admin';
|
const username = process.env.E2E_ADMIN_USERNAME ?? 'admin';
|
||||||
const currentPassword = process.env.E2E_ADMIN_CURRENT_PASSWORD;
|
const currentPassword = process.env.E2E_ADMIN_CURRENT_PASSWORD;
|
||||||
@@ -6,24 +7,11 @@ const newPassword = process.env.E2E_ADMIN_NEW_PASSWORD;
|
|||||||
const baseUrl = (process.env.E2E_BASE_URL ?? 'http://178.104.200.7/taxbaik').replace(/\/$/, '');
|
const baseUrl = (process.env.E2E_BASE_URL ?? 'http://178.104.200.7/taxbaik').replace(/\/$/, '');
|
||||||
|
|
||||||
test.describe('admin password change', () => {
|
test.describe('admin password change', () => {
|
||||||
test('changes password through the real admin UI', async ({ page }) => {
|
test('changes password through the real admin UI', async ({ page, request }) => {
|
||||||
test.skip(!currentPassword || !newPassword, 'E2E_ADMIN_CURRENT_PASSWORD and E2E_ADMIN_NEW_PASSWORD are required.');
|
test.skip(!currentPassword || !newPassword, 'E2E_ADMIN_CURRENT_PASSWORD and E2E_ADMIN_NEW_PASSWORD are required.');
|
||||||
|
|
||||||
const token = await page.evaluate(async ({ baseUrl, username, password }) => {
|
const token = await getAdminToken(request, baseUrl, username, currentPassword);
|
||||||
const response = await fetch(`${baseUrl}/api/auth/login`, {
|
await installAdminToken(page, token);
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ username, password }),
|
|
||||||
});
|
|
||||||
if (!response.ok) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const body = await response.json();
|
|
||||||
return body?.token ?? null;
|
|
||||||
}, { baseUrl, username, password: currentPassword });
|
|
||||||
expect(token, 'login API should return a token').toBeTruthy();
|
|
||||||
|
|
||||||
await page.addInitScript(value => localStorage.setItem('auth_token', value), token);
|
|
||||||
await page.goto(`${baseUrl}/admin/settings`);
|
await page.goto(`${baseUrl}/admin/settings`);
|
||||||
await expect(page.getByRole('heading', { name: /사이트 설정|설정/ })).toBeVisible();
|
await expect(page.getByRole('heading', { name: /사이트 설정|설정/ })).toBeVisible();
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { expect, test } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
|
import { getAdminToken, installAdminToken } from './helpers/admin-auth';
|
||||||
|
|
||||||
const username = process.env.E2E_ADMIN_USERNAME ?? 'admin';
|
const username = process.env.E2E_ADMIN_USERNAME ?? 'admin';
|
||||||
const password = process.env.E2E_ADMIN_PASSWORD;
|
const password = process.env.E2E_ADMIN_PASSWORD;
|
||||||
const baseUrl = (process.env.E2E_BASE_URL ?? 'http://178.104.200.7/taxbaik').replace(/\/$/, '');
|
const baseUrl = (process.env.E2E_BASE_URL ?? 'http://178.104.200.7/taxbaik').replace(/\/$/, '');
|
||||||
|
|
||||||
test.describe('admin smoke', () => {
|
test.describe('admin smoke', () => {
|
||||||
test('navigates the main admin menus without circuit errors', async ({ page }) => {
|
test('navigates the main admin menus without circuit errors', async ({ page, request }) => {
|
||||||
test.skip(!password, 'E2E_ADMIN_PASSWORD is required.');
|
test.skip(!password, 'E2E_ADMIN_PASSWORD is required.');
|
||||||
|
|
||||||
const consoleErrors: string[] = [];
|
const consoleErrors: string[] = [];
|
||||||
@@ -18,21 +19,8 @@ test.describe('admin smoke', () => {
|
|||||||
consoleErrors.push(error.message);
|
consoleErrors.push(error.message);
|
||||||
});
|
});
|
||||||
|
|
||||||
const token = await page.evaluate(async ({ baseUrl, username, password }) => {
|
const token = await getAdminToken(request, baseUrl, username, password);
|
||||||
const response = await fetch(`${baseUrl}/api/auth/login`, {
|
await installAdminToken(page, token);
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ username, password }),
|
|
||||||
});
|
|
||||||
if (!response.ok) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const body = await response.json();
|
|
||||||
return body?.token ?? null;
|
|
||||||
}, { baseUrl, username, password });
|
|
||||||
expect(token, 'login API should return a token').toBeTruthy();
|
|
||||||
|
|
||||||
await page.addInitScript(value => localStorage.setItem('auth_token', value), token);
|
|
||||||
|
|
||||||
const menuChecks = [
|
const menuChecks = [
|
||||||
{ path: '/admin/dashboard', heading: /대시보드/ },
|
{ path: '/admin/dashboard', heading: /대시보드/ },
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { expect, test } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
|
import { getAdminToken, installAdminToken } from './helpers/admin-auth';
|
||||||
|
|
||||||
const username = process.env.E2E_ADMIN_USERNAME ?? 'admin';
|
const username = process.env.E2E_ADMIN_USERNAME ?? 'admin';
|
||||||
const password = process.env.E2E_ADMIN_PASSWORD;
|
const password = process.env.E2E_ADMIN_PASSWORD;
|
||||||
@@ -27,21 +28,8 @@ test.describe('contact submit', () => {
|
|||||||
|
|
||||||
test.skip(!password, 'E2E_ADMIN_PASSWORD is required to verify the admin list.');
|
test.skip(!password, 'E2E_ADMIN_PASSWORD is required to verify the admin list.');
|
||||||
|
|
||||||
const token = await page.evaluate(async ({ baseUrl, username, password }) => {
|
const token = await getAdminToken(request, baseUrl, username, password);
|
||||||
const response = await fetch(`${baseUrl}/api/auth/login`, {
|
await installAdminToken(page, token);
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ username, password }),
|
|
||||||
});
|
|
||||||
if (!response.ok) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const body = await response.json();
|
|
||||||
return body?.token ?? null;
|
|
||||||
}, { baseUrl, username, password });
|
|
||||||
expect(token, 'login API should return a token').toBeTruthy();
|
|
||||||
|
|
||||||
await page.addInitScript(value => localStorage.setItem('auth_token', value), token);
|
|
||||||
await page.goto(`${baseUrl}/admin/inquiries`);
|
await page.goto(`${baseUrl}/admin/inquiries`);
|
||||||
await expect(page.getByText(name)).toBeVisible({ timeout: 20_000 });
|
await expect(page.getByText(name)).toBeVisible({ timeout: 20_000 });
|
||||||
await expect(page.getByText(phone)).toBeVisible();
|
await expect(page.getByText(phone)).toBeVisible();
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { expect, type APIRequestContext, type Page } from '@playwright/test';
|
||||||
|
|
||||||
|
export async function getAdminToken(
|
||||||
|
request: APIRequestContext,
|
||||||
|
baseUrl: string,
|
||||||
|
username: string,
|
||||||
|
password: string,
|
||||||
|
) {
|
||||||
|
const response = await request.post(`${baseUrl}/api/auth/login`, {
|
||||||
|
data: { username, password },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.status(), 'login API should accept the configured admin credentials').toBe(200);
|
||||||
|
const body = await response.json();
|
||||||
|
expect(body?.token, 'login API should return a token').toBeTruthy();
|
||||||
|
return body.token as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function installAdminToken(page: Page, token: string) {
|
||||||
|
await page.addInitScript(value => localStorage.setItem('auth_token', value), token);
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { expect, test } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
|
import { getAdminToken, installAdminToken } from './helpers/admin-auth';
|
||||||
|
|
||||||
const username = process.env.E2E_ADMIN_USERNAME ?? 'admin';
|
const username = process.env.E2E_ADMIN_USERNAME ?? 'admin';
|
||||||
const password = process.env.E2E_ADMIN_PASSWORD;
|
const password = process.env.E2E_ADMIN_PASSWORD;
|
||||||
@@ -27,21 +28,8 @@ test.describe('inquiry detail', () => {
|
|||||||
|
|
||||||
test.skip(!password, 'E2E_ADMIN_PASSWORD is required to verify inquiry detail.');
|
test.skip(!password, 'E2E_ADMIN_PASSWORD is required to verify inquiry detail.');
|
||||||
|
|
||||||
const token = await page.evaluate(async ({ baseUrl, username, password }) => {
|
const token = await getAdminToken(request, baseUrl, username, password);
|
||||||
const response = await fetch(`${baseUrl}/api/auth/login`, {
|
await installAdminToken(page, token);
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ username, password }),
|
|
||||||
});
|
|
||||||
if (!response.ok) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const body = await response.json();
|
|
||||||
return body?.token ?? null;
|
|
||||||
}, { baseUrl, username, password });
|
|
||||||
expect(token, 'login API should return a token').toBeTruthy();
|
|
||||||
|
|
||||||
await page.addInitScript(value => localStorage.setItem('auth_token', value), token);
|
|
||||||
await page.goto(`${baseUrl}/admin/inquiries`);
|
await page.goto(`${baseUrl}/admin/inquiries`);
|
||||||
const row = page.getByRole('row').filter({ hasText: name }).first();
|
const row = page.getByRole('row').filter({ hasText: name }).first();
|
||||||
await expect(row).toBeVisible({ timeout: 20_000 });
|
await expect(row).toBeVisible({ timeout: 20_000 });
|
||||||
|
|||||||
Reference in New Issue
Block a user