마이페이지, 리포트, 메일인증
CI / test (push) Failing after 13m23s
CI / e2e (push) Has been cancelled
CI / pr_and_merge (push) Has been cancelled

This commit is contained in:
2025-12-15 21:59:43 +09:00
parent a8e1a4a960
commit 87cdc1868b
30 changed files with 5488 additions and 16 deletions
+195 -4
View File
@@ -9,11 +9,25 @@ const BASE_URL = "http://localhost";
// 이렇게 하면 CI/로컬 어디에서도 DATABASE_URL 이나 실제 DB 없이 Auth API 테스트를 실행할 수 있다.
const inMemoryUsers: any[] = [];
// 이메일 인증 토큰 발송 유틸을 목 처리해, 실제 SMTP 로 메일을 보내지 않고 호출 여부/인자를 검증한다.
const sendVerificationEmailMock = vi.fn();
vi.mock("@prisma/client", () => {
class PrismaClientMock {
user = {
findUnique: async ({ where: { email } }: any) => {
return inMemoryUsers.find((u) => u.email === email) ?? null;
// where.email 또는 where.id 기준으로 단일 유저를 찾는다.
findUnique: async ({ where }: any) => {
if (!where) return null;
if (where.email) {
return inMemoryUsers.find((u) => u.email === where.email) ?? null;
}
if (where.id) {
return inMemoryUsers.find((u) => u.id === where.id) ?? null;
}
if (where.emailVerifyToken) {
return inMemoryUsers.find((u) => u.emailVerifyToken === where.emailVerifyToken) ?? null;
}
return null;
},
create: async ({ data }: any) => {
const now = new Date();
@@ -26,17 +40,34 @@ vi.mock("@prisma/client", () => {
inMemoryUsers.push(user);
return user;
},
// change-password / change-email 에서 사용할 update 목
update: async ({ where, data }: any) => {
const target = inMemoryUsers.find((u) => u.id === where.id);
if (!target) {
throw new Error("User not found in inMemoryUsers");
}
Object.assign(target, data, { updatedAt: new Date() });
return target;
},
};
}
return { PrismaClient: PrismaClientMock };
});
vi.mock("@/features/email/sendVerificationEmail", () => {
return {
sendVerificationEmail: (...args: any[]) => sendVerificationEmailMock(...args),
};
});
beforeEach(() => {
// 각 테스트 전에 메모리 유저 저장소를 초기화한다.
inMemoryUsers.length = 0;
process.env.AUTH_JWT_SECRET = "test-jwt-secret-api";
process.env.AUTH_ENCRYPTION_KEY = "0123456789abcdef0123456789abcdef";
sendVerificationEmailMock.mockReset();
});
describe("/api/auth", () => {
@@ -232,8 +263,22 @@ describe("/api/auth", () => {
it("유효한 JWT 쿠키가 있을 때 현재 로그인 유저 정보를 반환해야 한다", async () => {
const { GET: me } = await import("@/app/api/auth/me/route");
const user = { id: "user-1", email: "me@example.com", tokenVersion: 1 };
const token = await signAccessToken(user);
const user = { id: "user-1", email: "me@example.com", tokenVersion: 1, emailVerified: true };
// /api/auth/me 가 DB(User) 기준으로 동작하므로, Prisma 목(inMemoryUsers)에 동일한 유저를 추가한다.
inMemoryUsers.push({
id: user.id,
email: user.email,
passwordHash: "hashed",
tokenVersion: user.tokenVersion,
emailVerified: user.emailVerified,
emailVerifyToken: null,
emailVerifyTokenExpiresAt: null,
createdAt: new Date(),
updatedAt: new Date(),
});
const token = await signAccessToken(user as any);
const res = await me(
new Request(`${BASE_URL}/api/auth/me`, {
@@ -247,6 +292,7 @@ describe("/api/auth", () => {
const json = (await res.json()) as any;
expect(json.id).toBe(user.id);
expect(json.email).toBe(user.email);
expect(json.emailVerified).toBe(true);
expect(json.passwordHash).toBeUndefined();
});
@@ -264,4 +310,149 @@ describe("/api/auth", () => {
expect(res2.status).toBe(401);
});
});
describe("POST /api/auth/request-email-verification", () => {
it("로그인된 사용자가 호출하면 emailVerifyToken 과 만료 시간이 설정되어야 한다", async () => {
const { POST: requestEmailVerification } = await import(
"@/app/api/auth/request-email-verification/route",
);
const email = "verify@example.com";
inMemoryUsers.push({
id: "user-20",
email,
passwordHash: "hashed",
tokenVersion: 1,
emailVerified: false,
emailVerifyToken: null,
emailVerifyTokenExpiresAt: null,
createdAt: new Date(),
updatedAt: new Date(),
});
const token = await signAccessToken({ id: "user-20", email, tokenVersion: 1 });
const res = await requestEmailVerification(
new Request(`${BASE_URL}/api/auth/request-email-verification`, {
method: "POST",
headers: {
cookie: `pb_access=${token}`,
},
}),
);
expect(res.status).toBe(200);
const json = (await res.json()) as any;
expect(json.message).toBeDefined();
expect(typeof json.verificationToken).toBe("string");
expect(json.verificationToken.length).toBeGreaterThan(0);
// in-memory 유저에 토큰과 만료 시간이 설정되었는지 확인한다.
const user = inMemoryUsers[0];
expect(typeof user.emailVerifyToken).toBe("string");
expect(user.emailVerifyToken.length).toBeGreaterThan(0);
expect(user.emailVerifyTokenExpiresAt instanceof Date).toBe(true);
// 이메일 발송 유틸이 올바른 인자로 한 번 호출되었는지 확인한다.
expect(sendVerificationEmailMock).toHaveBeenCalledTimes(1);
expect(sendVerificationEmailMock).toHaveBeenCalledWith(
expect.objectContaining({
to: email,
token: json.verificationToken,
}),
);
});
it("인증 정보가 없으면 401 을 반환해야 한다", async () => {
const { POST: requestEmailVerification } = await import(
"@/app/api/auth/request-email-verification/route",
);
const res = await requestEmailVerification(
new Request(`${BASE_URL}/api/auth/request-email-verification`, {
method: "POST",
}),
);
expect(res.status).toBe(401);
});
});
describe("GET /api/auth/verify-email", () => {
it("유효한 토큰으로 호출하면 emailVerified 가 true 로 설정되고 토큰 필드가 제거되어야 한다", async () => {
const { GET: verifyEmail } = await import("@/app/api/auth/verify-email/route");
const token = "verify-token-123";
const future = new Date(Date.now() + 60 * 60 * 1000);
inMemoryUsers.push({
id: "user-21",
email: "verify2@example.com",
passwordHash: "hashed",
tokenVersion: 1,
emailVerified: false,
emailVerifyToken: token,
emailVerifyTokenExpiresAt: future,
createdAt: new Date(),
updatedAt: new Date(),
});
const res = await verifyEmail(
new Request(`${BASE_URL}/api/auth/verify-email?token=${encodeURIComponent(token)}`),
);
// 리다이렉트 응답이어야 한다.
expect(res.status).toBeGreaterThanOrEqual(300);
expect(res.status).toBeLessThan(400);
const location = res.headers.get("location");
expect(location).toContain("/mypage");
// in-memory 유저 상태가 업데이트되었는지 확인한다.
const user = inMemoryUsers[0];
expect(user.emailVerified).toBe(true);
expect(user.emailVerifyToken).toBeNull();
expect(user.emailVerifyTokenExpiresAt).toBeNull();
});
it("토큰이 없거나 잘못된 경우 400 을 반환해야 한다", async () => {
const { GET: verifyEmail } = await import("@/app/api/auth/verify-email/route");
// 쿼리 파라미터 없이 호출
const res1 = await verifyEmail(
new Request(`${BASE_URL}/api/auth/verify-email`),
);
expect(res1.status).toBe(400);
// 잘못된 토큰으로 호출
const res2 = await verifyEmail(
new Request(`${BASE_URL}/api/auth/verify-email?token=unknown-token`),
);
expect(res2.status).toBe(400);
});
it("만료된 토큰으로 호출하면 400 을 반환해야 한다", async () => {
const { GET: verifyEmail } = await import("@/app/api/auth/verify-email/route");
const token = "expired-token-123";
const past = new Date(Date.now() - 60 * 60 * 1000);
inMemoryUsers.push({
id: "user-22",
email: "expired@example.com",
passwordHash: "hashed",
tokenVersion: 1,
emailVerified: false,
emailVerifyToken: token,
emailVerifyTokenExpiresAt: past,
createdAt: new Date(),
updatedAt: new Date(),
});
const res = await verifyEmail(
new Request(`${BASE_URL}/api/auth/verify-email?token=${encodeURIComponent(token)}`),
);
expect(res.status).toBe(400);
const json = (await res.json()) as any;
expect(json.message).toBeDefined();
});
});
});