-
Notifications
You must be signed in to change notification settings - Fork 0
test: auth 관련 유닛 테스트 추가 #174
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
3432639
test: 전화번호/비밀번호/인증코드 스키마 유효성 테스트 추가
g-hyxn 7192626
test: 토큰 저장·삭제·조회 및 로그인 상태 테스트 추가
g-hyxn e3dd284
test: 미들웨어 라우팅 보호·어드민·슬로건 게이트 테스트 추가
g-hyxn f2d5b82
test: 로그인 폼 제출 및 리다이렉트 테스트 추가
g-hyxn f16bdde
test: 회원가입 폼 제출 성공/실패 테스트 추가
g-hyxn b391e89
chore: test 규칙 생성
g-hyxn eb23b35
test: 코드 리뷰 반영 (afterEach, 에러 메시지 단언, 헬퍼 함수, toast 단언 추가)
g-hyxn b92f3cc
test: publicIn27 축제 날짜 접근 제한 테스트 추가
g-hyxn f812496
test: 쿠키 초기화 및 role 쿠키 저장 단언 추가
g-hyxn File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| Write unit tests for the specified file or feature: $ARGUMENTS | ||
|
|
||
| ## Project Testing Context | ||
|
|
||
| - **Framework**: Vitest + React Testing Library | ||
| - **Coverage**: `@vitest/coverage-istanbul` | ||
| - **Setup file**: `vitest.setup.ts` (`@testing-library/jest-dom` 자동 적용) | ||
| - **Path alias**: `@/` maps to `src/` | ||
| - **Run tests**: `npm run test:run src/path/to/file.test.ts` | ||
| - **Run all**: `npm run test:run` | ||
| - **Coverage**: `npm run test:coverage` | ||
|
|
||
| ## Instructions | ||
|
|
||
| 1. **Read the target file first** before writing any tests. Understand what it actually does. | ||
|
|
||
| 2. **Follow existing test patterns** — check nearby `__tests__/` files for conventions used in this codebase. | ||
|
|
||
| 3. **Test structure**: | ||
| - Place test file in `__tests__/` directory next to the source file | ||
| - `foo.ts` → `__tests__/foo.test.ts` | ||
| - Group with `describe`, name cases with `it` | ||
| - Import `describe`, `it`, `expect`, `vi`, `beforeEach`, `afterEach` from `vitest` (globals: false) | ||
|
|
||
| 4. **Mocking**: | ||
| - API 함수: `vi.mock("@/entities/.../api/...")` | ||
| - 외부 라이브러리: `vi.mock("sonner")`, `vi.mock("next/navigation")` 등 | ||
| - `vi.mocked(fn)`으로 타입 안전하게 사용 | ||
| - 각 테스트 전 `vi.clearAllMocks()` 호출 | ||
|
|
||
| 5. **React components**: `render`, `screen`, `userEvent` from `@testing-library/react` | ||
| ```ts | ||
| import { render, screen } from "@testing-library/react"; | ||
| import userEvent from "@testing-library/user-event"; | ||
| ``` | ||
|
|
||
| 6. **Next.js middleware**: `NextRequest`를 직접 생성해서 테스트 | ||
| ```ts | ||
| import { NextRequest } from "next/server"; | ||
| new NextRequest(new URL("/path", "http://localhost"), { | ||
| headers: { Cookie: "accessToken=abc; refreshToken=xyz" }, | ||
| }); | ||
| ``` | ||
|
|
||
| 7. **Date-dependent logic**: `vi.setSystemTime(new Date(...))` + `vi.useRealTimers()` | ||
|
|
||
| 8. **cookies (jsdom)**: `document.cookie`로 직접 설정/초기화 | ||
| ```ts | ||
| beforeEach(() => { | ||
| document.cookie.split(";").forEach(c => { | ||
| const name = c.split("=")[0].trim(); | ||
| if (name) document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`; | ||
| }); | ||
| }); | ||
| ``` | ||
|
|
||
| 9. **What to test**: | ||
| - Happy path (정상 동작) | ||
| - Error states (API 실패, 유효성 오류) | ||
| - Edge cases (오픈 리다이렉트 방지, 권한 분기 등) | ||
| - User interactions (UI 컴포넌트) | ||
|
|
||
| 10. **What NOT to test**: | ||
| - 구현 세부사항 (내부 상태, private 메서드) | ||
| - 서드파티 라이브러리 내부 동작 | ||
| - 단순 pass-through 코드 | ||
|
|
||
| 11. After writing, run the tests to confirm they pass: | ||
| ```bash | ||
| npm run test:run src/path/to/__tests__/file.test.ts | ||
| ``` | ||
| Fix any failures before finishing. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,165 @@ | ||
| import { describe, it, expect, vi, afterEach } from "vitest"; | ||
| import { NextRequest } from "next/server"; | ||
| import { middleware } from "../middleware"; | ||
|
|
||
| vi.mock("@/shared/config/authConfig", () => ({ | ||
| publicPages: ["/home", "/signin", "/signup", "/admin", "/vote", "/slogan"], | ||
| publicIn18: ["/booking"], | ||
| publicIn27: ["/vote"], | ||
| ticketOpenDate: new Date("2025-09-18T20:00:00"), | ||
| performerTicketOpenDate: new Date("2025-09-15T20:00:00"), | ||
| festivalDate: new Date("2025-09-27T00:00:00"), | ||
| sloganStartDate: new Date("2026-05-18T00:00:00+09:00"), | ||
| sloganEndDate: new Date("2026-05-28T23:59:59+09:00"), | ||
| })); | ||
|
|
||
| function makeRequest(path: string, cookies: Record<string, string> = {}) { | ||
| const cookieHeader = Object.entries(cookies) | ||
| .map(([k, v]) => `${k}=${v}`) | ||
| .join("; "); | ||
|
|
||
| return new NextRequest(new URL(path, "http://localhost"), { | ||
| headers: cookieHeader ? { Cookie: cookieHeader } : {}, | ||
| }); | ||
| } | ||
|
|
||
| function getLocation(res: Response) { | ||
| return res.headers.get("location"); | ||
| } | ||
|
|
||
| describe("middleware - 라우팅 보호", () => { | ||
| it("토큰 없이 보호된 경로 접근 시 /signin으로 리다이렉트한다", () => { | ||
| const res = middleware(makeRequest("/my-page")); | ||
| expect(res.status).toBe(307); | ||
| expect(getLocation(res)).toContain("/signin"); | ||
| }); | ||
|
|
||
| it("리다이렉트 URL에 next 파라미터가 포함된다", () => { | ||
| const res = middleware(makeRequest("/my-page")); | ||
| expect(getLocation(res)).toContain("next=%2Fmy-page"); | ||
| }); | ||
|
|
||
| it("토큰이 있으면 보호된 경로를 통과시킨다", () => { | ||
| const res = middleware(makeRequest("/my-page", { accessToken: "abc", refreshToken: "xyz" })); | ||
| expect(res.status).toBe(200); | ||
| }); | ||
|
|
||
| it("/home은 토큰 없이 접근 가능하다", () => { | ||
| expect(middleware(makeRequest("/home")).status).toBe(200); | ||
| }); | ||
|
|
||
| it("/signin은 토큰 없이 접근 가능하다", () => { | ||
| expect(middleware(makeRequest("/signin")).status).toBe(200); | ||
| }); | ||
| }); | ||
|
|
||
| describe("middleware - /signin 리다이렉트", () => { | ||
| it("로그인 상태에서 /signin 접근 시 /home으로 리다이렉트한다", () => { | ||
| const res = middleware(makeRequest("/signin", { accessToken: "abc", refreshToken: "xyz" })); | ||
| expect(res.status).toBe(307); | ||
| expect(getLocation(res)).toContain("/home"); | ||
| }); | ||
|
|
||
| it("올바른 next 파라미터가 있으면 해당 경로로 리다이렉트한다", () => { | ||
| const res = middleware( | ||
| makeRequest("/signin?next=%2Fvote", { accessToken: "abc", refreshToken: "xyz" }), | ||
| ); | ||
| expect(getLocation(res)).toContain("/vote"); | ||
| }); | ||
|
|
||
| it("오픈 리다이렉트 방지: next가 /signin이면 /home으로 이동한다", () => { | ||
| const res = middleware( | ||
| makeRequest("/signin?next=%2Fsignin", { accessToken: "abc", refreshToken: "xyz" }), | ||
| ); | ||
| expect(getLocation(res)).toContain("/home"); | ||
| }); | ||
|
|
||
| it("오픈 리다이렉트 방지: next가 외부 URL이면 /home으로 이동한다", () => { | ||
| const res = middleware( | ||
| makeRequest("/signin?next=https%3A%2F%2Fevil.com", { accessToken: "abc", refreshToken: "xyz" }), | ||
| ); | ||
| expect(getLocation(res)).toContain("/home"); | ||
| }); | ||
| }); | ||
|
|
||
| describe("middleware - 어드민 접근 제어", () => { | ||
| it("ROLE_ADMIN이 아니면 /admin 접근 시 /home으로 리다이렉트한다", () => { | ||
| const res = middleware( | ||
| makeRequest("/admin/dashboard", { accessToken: "abc", refreshToken: "xyz", role: "ROLE_USER" }), | ||
| ); | ||
| expect(res.status).toBe(307); | ||
| expect(getLocation(res)).toContain("/home"); | ||
| }); | ||
|
|
||
| it("ROLE_ADMIN이면 /admin 접근이 허용된다", () => { | ||
| const res = middleware( | ||
| makeRequest("/admin/dashboard", { accessToken: "abc", refreshToken: "xyz", role: "ROLE_ADMIN" }), | ||
| ); | ||
| expect(res.status).toBe(200); | ||
| }); | ||
|
|
||
| it("/admin/lottery/:id는 비어드민도 접근 가능하다", () => { | ||
| const res = middleware( | ||
| makeRequest("/admin/lottery/123", { accessToken: "abc", refreshToken: "xyz", role: "ROLE_USER" }), | ||
| ); | ||
| expect(res.status).toBe(200); | ||
| }); | ||
|
|
||
| it("/admin/score/:id는 비어드민도 접근 가능하다", () => { | ||
| const res = middleware( | ||
| makeRequest("/admin/score/456", { accessToken: "abc", refreshToken: "xyz", role: "ROLE_USER" }), | ||
| ); | ||
| expect(res.status).toBe(200); | ||
| }); | ||
| }); | ||
|
|
||
| describe("middleware - publicIn27 (축제 날짜 접근 제한)", () => { | ||
| afterEach(() => { | ||
| vi.useRealTimers(); | ||
| }); | ||
|
|
||
| it("축제 이전에는 비어드민의 /vote 접근을 /home으로 리다이렉트한다", () => { | ||
| vi.setSystemTime(new Date("2025-09-20T00:00:00")); | ||
| const res = middleware(makeRequest("/vote", { role: "ROLE_USER" })); | ||
| expect(res.status).toBe(307); | ||
| expect(getLocation(res)).toContain("/home"); | ||
| }); | ||
|
|
||
| it("축제 이후에는 비어드민도 /vote 접근이 허용된다", () => { | ||
| vi.setSystemTime(new Date("2025-09-28T00:00:00")); | ||
| const res = middleware(makeRequest("/vote", { role: "ROLE_USER" })); | ||
| expect(res.status).toBe(200); | ||
| }); | ||
|
|
||
| it("어드민은 축제 이전에도 /vote 접근이 허용된다", () => { | ||
| vi.setSystemTime(new Date("2025-09-20T00:00:00")); | ||
| const res = middleware(makeRequest("/vote", { accessToken: "abc", refreshToken: "xyz", role: "ROLE_ADMIN" })); | ||
| expect(res.status).toBe(200); | ||
| }); | ||
| }); | ||
|
|
||
| describe("middleware - 슬로건 게이트", () => { | ||
| afterEach(() => { | ||
| vi.useRealTimers(); | ||
| }); | ||
|
|
||
| it("슬로건 기간 이전에는 /slogan 접근 시 /home으로 리다이렉트한다", () => { | ||
| vi.setSystemTime(new Date("2026-04-01T00:00:00")); | ||
| const res = middleware(makeRequest("/slogan", { accessToken: "abc", refreshToken: "xyz" })); | ||
| expect(res.status).toBe(307); | ||
| expect(getLocation(res)).toContain("/home"); | ||
| }); | ||
|
|
||
| it("슬로건 기간 내에는 /slogan 접근이 허용된다", () => { | ||
| vi.setSystemTime(new Date("2026-05-20T00:00:00")); | ||
| const res = middleware(makeRequest("/slogan", { accessToken: "abc", refreshToken: "xyz" })); | ||
| expect(res.status).toBe(200); | ||
| }); | ||
|
|
||
| it("슬로건 기간 이후에는 /slogan 접근 시 /home으로 리다이렉트한다", () => { | ||
| vi.setSystemTime(new Date("2026-06-01T00:00:00")); | ||
| const res = middleware(makeRequest("/slogan", { accessToken: "abc", refreshToken: "xyz" })); | ||
| expect(res.status).toBe(307); | ||
| expect(getLocation(res)).toContain("/home"); | ||
| }); | ||
| }); | ||
|
g-hyxn marked this conversation as resolved.
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| import { describe, it, expect } from "vitest"; | ||
| import { | ||
| passwordSchema, | ||
| verificationCodeSchema, | ||
| signinSchema, | ||
| signUpSchema, | ||
| } from "../schema"; | ||
|
|
||
| describe("passwordSchema", () => { | ||
| it("8자 이상 영문+숫자 포함이면 유효하다", () => { | ||
| expect(passwordSchema.safeParse("abc12345").success).toBe(true); | ||
| }); | ||
|
|
||
| it("8자 미만이면 실패한다", () => { | ||
| const result = passwordSchema.safeParse("ab1234"); | ||
| expect(result.success).toBe(false); | ||
| expect(result.error?.errors[0].message).toBe("비밀번호는 최소 8자 이상이어야 합니다."); | ||
| }); | ||
|
|
||
| it("숫자가 없으면 실패한다", () => { | ||
| const result = passwordSchema.safeParse("abcdefgh"); | ||
| expect(result.success).toBe(false); | ||
| expect(result.error?.errors[0].message).toBe("비밀번호는 영문과 숫자를 포함해야 합니다."); | ||
| }); | ||
|
|
||
| it("영문이 없으면 실패한다", () => { | ||
| const result = passwordSchema.safeParse("12345678"); | ||
| expect(result.success).toBe(false); | ||
| expect(result.error?.errors[0].message).toBe("비밀번호는 영문과 숫자를 포함해야 합니다."); | ||
| }); | ||
| }); | ||
|
|
||
| describe("verificationCodeSchema", () => { | ||
| it("6자리면 유효하다", () => { | ||
| expect(verificationCodeSchema.safeParse("123456").success).toBe(true); | ||
| }); | ||
|
|
||
| it("빈 값이면 실패한다", () => { | ||
| const result = verificationCodeSchema.safeParse(""); | ||
| expect(result.success).toBe(false); | ||
| expect(result.error?.errors[0].message).toBe("인증번호를 입력해주세요."); | ||
| }); | ||
|
|
||
| it("6자리 미만이면 실패한다", () => { | ||
| const result = verificationCodeSchema.safeParse("12345"); | ||
| expect(result.success).toBe(false); | ||
| expect(result.error?.errors[0].message).toBe("인증번호는 6자리여야 합니다."); | ||
| }); | ||
|
|
||
| it("6자리 초과면 실패한다", () => { | ||
| const result = verificationCodeSchema.safeParse("1234567"); | ||
| expect(result.success).toBe(false); | ||
| expect(result.error?.errors[0].message).toBe("인증번호는 6자리여야 합니다."); | ||
| }); | ||
|
g-hyxn marked this conversation as resolved.
|
||
| }); | ||
|
|
||
| describe("signinSchema", () => { | ||
| it("올바른 전화번호와 비밀번호면 유효하다", () => { | ||
| expect(signinSchema.safeParse({ phoneNumber: "01012345678", password: "pass1234" }).success).toBe(true); | ||
| }); | ||
|
|
||
| it("전화번호 형식이 잘못되면 실패한다", () => { | ||
| expect(signinSchema.safeParse({ phoneNumber: "010-1234-5678", password: "pass1234" }).success).toBe(false); | ||
| }); | ||
|
|
||
| it("비밀번호가 짧으면 실패한다", () => { | ||
| expect(signinSchema.safeParse({ phoneNumber: "01012345678", password: "ab1" }).success).toBe(false); | ||
| }); | ||
| }); | ||
|
|
||
| describe("signUpSchema", () => { | ||
| it("모든 필드가 올바르면 유효하다", () => { | ||
| expect( | ||
| signUpSchema.safeParse({ | ||
| phoneNumber: "01012345678", | ||
| verificationCode: "123456", | ||
| password: "pass1234", | ||
| passwordConfirm: "pass1234", | ||
| }).success, | ||
| ).toBe(true); | ||
| }); | ||
|
|
||
| it("인증코드가 없으면 실패한다", () => { | ||
| expect( | ||
| signUpSchema.safeParse({ | ||
| phoneNumber: "01012345678", | ||
| verificationCode: "", | ||
| password: "pass1234", | ||
| passwordConfirm: "pass1234", | ||
| }).success, | ||
| ).toBe(false); | ||
| }); | ||
| }); | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.