diff --git a/.gitignore b/.gitignore index 8129dd9f..5810062d 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ docs/work/ # Playwright docs/e2e-ui/ +e2e/.auth/*.json playwright-report/ test-results/ .playwright-mcp/ diff --git a/e2e/.auth/.gitkeep b/e2e/.auth/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/e2e/auth.setup.ts b/e2e/auth.setup.ts new file mode 100644 index 00000000..0046a0a0 --- /dev/null +++ b/e2e/auth.setup.ts @@ -0,0 +1,8 @@ +import { test as setup } from "@playwright/test"; + +import { AUTH_FILE, BACKEND_URL } from "./constants"; + +setup("authenticate", async ({ request }) => { + await request.post(`${BACKEND_URL}/auth/e2e-login`); + await request.storageState({ path: AUTH_FILE }); +}); diff --git a/e2e/authenticated.auth.spec.ts b/e2e/authenticated.auth.spec.ts new file mode 100644 index 00000000..2c3c39ba --- /dev/null +++ b/e2e/authenticated.auth.spec.ts @@ -0,0 +1,23 @@ +import { expect, test } from "@playwright/test"; + +import { E2E_USER_DISPLAY_NAME } from "./constants"; + +test.describe("로그인 상태 테스트", () => { + test("로그인 후 사용자 정보가 헤더에 표시됨", async ({ page }) => { + await page.goto("/"); + + const userMenu = page.getByRole("button", { name: new RegExp(E2E_USER_DISPLAY_NAME) }); + await expect(userMenu).toBeVisible(); + }); + + test("로그인 후 컨텐츠 상세에서 보상 수정 버튼이 활성화됨", async ({ page }) => { + await page.goto("/"); + + const firstRow = page.locator("table tbody tr").first(); + await firstRow.click(); + + const settingsButton = page.getByTestId("content-reward-section").getByRole("button"); + await expect(settingsButton).toBeVisible(); + await expect(settingsButton).toBeEnabled(); + }); +}); diff --git a/e2e/constants.ts b/e2e/constants.ts new file mode 100644 index 00000000..583bc24f --- /dev/null +++ b/e2e/constants.ts @@ -0,0 +1,4 @@ +export const AUTH_FILE = "e2e/.auth/user.json"; +export const BACKEND_URL = "http://localhost:3001"; +export const E2E_USER_DISPLAY_NAME = "E2E Test User"; +export const FRONTEND_URL = "http://localhost:3000"; diff --git a/e2e/home.spec.ts b/e2e/home.spec.ts index 28cea2fb..914462d1 100644 --- a/e2e/home.spec.ts +++ b/e2e/home.spec.ts @@ -1,6 +1,8 @@ import { expect, test } from "@playwright/test"; test.describe("홈 페이지", () => { + test.use({ storageState: { cookies: [], origins: [] } }); // 비로그인 상태로 테스트 + test.beforeEach(async ({ page }) => { await page.goto("/"); }); @@ -10,18 +12,14 @@ test.describe("홈 페이지", () => { }); test("백엔드에서 골드 환율을 표시함", async ({ page }) => { - // 골드 환율 설정 버튼이 표시되는지 확인 (백엔드 API 호출 필요) const exchangeRateButton = page.getByRole("button", { name: /골드 환율 설정/, }); await expect(exchangeRateButton).toBeVisible(); - - // 환율 정보가 포함되어 있는지 확인 (예: "500:650") await expect(exchangeRateButton).toContainText(/\d+:\d+/); }); test("백엔드에서 컨텐츠 시급 목록을 로드함", async ({ page }) => { - // 테이블 행이 존재하는지 확인 (백엔드에서 데이터를 받아옴) const tableRows = page.locator("table tbody tr"); await expect(tableRows).not.toHaveCount(0); }); diff --git a/playwright.config.ts b/playwright.config.ts index d9cdfcb7..c09369cf 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,7 +1,7 @@ import { defineConfig, devices } from "@playwright/test"; -const FRONTEND_URL = "http://localhost:3000"; -const BACKEND_URL = "http://localhost:3001"; +import { AUTH_FILE, BACKEND_URL, FRONTEND_URL } from "./e2e/constants"; + const WEBSERVER_TIMEOUT_MS = 3 * 60 * 1000; export default defineConfig({ @@ -19,8 +19,22 @@ export default defineConfig({ video: process.env.CI ? "retain-on-failure" : "on", }, projects: [ + { + name: "setup", + testMatch: /.*\.setup\.ts/, + }, { name: "chromium", + dependencies: ["setup"], + testMatch: /.*\.auth\.spec\.ts/, + use: { + ...devices["Desktop Chrome"], + storageState: AUTH_FILE, + }, + }, + { + name: "chromium-no-auth", + testIgnore: /.*\.auth\.spec\.ts/, use: { ...devices["Desktop Chrome"] }, }, ], diff --git a/src/backend/src/auth/auth.controller.ts b/src/backend/src/auth/auth.controller.ts index f76fdd31..73b4c42b 100644 --- a/src/backend/src/auth/auth.controller.ts +++ b/src/backend/src/auth/auth.controller.ts @@ -1,5 +1,6 @@ import { Controller, + ForbiddenException, Get, HttpException, HttpStatus, @@ -10,11 +11,23 @@ import { } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; import { AuthGuard } from "@nestjs/passport"; +import { AuthProvider } from "@prisma/client"; import { Request, Response } from "express"; +import { PrismaService } from "src/prisma"; + +const E2E_TEST_USER = { + displayName: "E2E Test User", + email: "e2e-test@example.com", + provider: AuthProvider.GOOGLE, + refId: "e2e-test-user-ref-id", +}; @Controller("auth") export class AuthController { - constructor(private configService: ConfigService) {} + constructor( + private configService: ConfigService, + private prisma: PrismaService + ) {} @Get("check") async check(@Req() req: Request) { @@ -37,6 +50,23 @@ export class AuthController { return req.user; } + @Post("e2e-login") + async e2eLogin(@Req() req: Request) { + if (process.env.NODE_ENV === "production") { + throw new ForbiddenException("E2E login is disabled in production"); + } + + const user = await this.prisma.user.upsert({ + create: E2E_TEST_USER, + update: {}, + where: { refId: E2E_TEST_USER.refId }, + }); + + req.session["passport"] = { user }; + + return user; + } + @Get("google/callback") @UseGuards(AuthGuard("google")) async googleCallback(@Req() req: Request, @Res() res: Response) { diff --git a/src/frontend/src/components/section/section.tsx b/src/frontend/src/components/section/section.tsx index 3f04cd49..165bc9c4 100644 --- a/src/frontend/src/components/section/section.tsx +++ b/src/frontend/src/components/section/section.tsx @@ -4,10 +4,11 @@ import { ReactNode } from "react"; import { ErrorBoundary } from "~/components/error"; export type SectionProps = Omit & { + testId?: string; title?: ReactNode; }; -export const Section = ({ children, title, ...props }: SectionProps) => { +export const Section = ({ children, testId, title, ...props }: SectionProps) => { return ( { borderColor="border.subtle" borderRadius="md" boxShadow="md" + data-testid={testId} p={{ base: 2, md: 4 }} w="100%" {...props} diff --git a/src/frontend/src/domains/content/components/content-reward-section.tsx b/src/frontend/src/domains/content/components/content-reward-section.tsx index d26b91bf..279fbe4b 100644 --- a/src/frontend/src/domains/content/components/content-reward-section.tsx +++ b/src/frontend/src/domains/content/components/content-reward-section.tsx @@ -38,6 +38,7 @@ export const ContentRewardSection = ({ return (
보상 정보