From e90083a692b7d4fb01faeab33f8cef5a9828f8c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=B5=E1=86=B7=E1=84=8B=E1=85=AD=E1=86=BC?= =?UTF-8?q?=E1=84=86=E1=85=B5=E1=86=AB?= Date: Thu, 5 Feb 2026 18:29:49 +0900 Subject: [PATCH 1/3] =?UTF-8?q?refactor(api):=20DEFAULT=5FCATEGORIES=20?= =?UTF-8?q?=EC=A4=91=EB=B3=B5=20=EC=A0=9C=EA=B1=B0=20=EB=B0=8F=20OAuth=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80=20(#114)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - auth.service.ts에서 로컬 DEFAULT_CATEGORIES 제거, 공통 타입에서 import - oauth.service.ts에서 로컬 DEFAULT_CATEGORIES 제거, 공통 타입에서 import - oauth.service.spec.ts에 기본 카테고리 생성 Unit 테스트 6개 추가 - Apple/Google/Kakao/Naver 로그인 시 기본 카테고리 2개 생성 검증 - 기존 사용자 재로그인 시 카테고리 미생성 검증 - oauth.integration-spec.ts에 기본 카테고리 생성 Integration 테스트 5개 추가 - 실제 DB에 카테고리 생성 및 속성(이름, 색상, 순서) 검증 - 기존 사용자 재로그인 시 중복 생성 방지 검증 - .gitignore 업데이트 --- .gitignore | 1 + .../src/modules/auth/services/auth.service.ts | 6 +- .../auth/services/oauth.service.spec.ts | 244 ++++++++++++++++++ .../modules/auth/services/oauth.service.ts | 6 +- .../integration/oauth.integration-spec.ts | 165 ++++++++++++ 5 files changed, 412 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index 85a1e265..fcf7bf81 100644 --- a/.gitignore +++ b/.gitignore @@ -74,3 +74,4 @@ pnpm-debug.log* myDocs/ PLAN.md settings.local.json +my-docs diff --git a/apps/api/src/modules/auth/services/auth.service.ts b/apps/api/src/modules/auth/services/auth.service.ts index d8f66d3d..8671b57e 100644 --- a/apps/api/src/modules/auth/services/auth.service.ts +++ b/apps/api/src/modules/auth/services/auth.service.ts @@ -18,6 +18,7 @@ import { BusinessExceptions } from "@/common/exception/services/business-excepti import { DatabaseService } from "@/database"; import type { UserStatus } from "@/generated/prisma/client"; import { TodoCategoryRepository } from "../../todo-category/todo-category.repository"; +import { DEFAULT_CATEGORIES } from "../../todo-category/types/todo-category.types"; import { AUTH_DEFAULTS, LOGIN_FAILURE_REASON, @@ -43,11 +44,6 @@ import { PasswordService } from "./password.service"; import { TokenService } from "./token.service"; import { VerificationService } from "./verification.service"; -const DEFAULT_CATEGORIES = [ - { name: "중요한 일", color: "#FFB3B3", sortOrder: 0 }, - { name: "할 일", color: "#FF6B43", sortOrder: 1 }, -] as const; - // Re-export types for backward compatibility export type { CurrentUserResult, diff --git a/apps/api/src/modules/auth/services/oauth.service.spec.ts b/apps/api/src/modules/auth/services/oauth.service.spec.ts index ac882ed0..80fba91a 100644 --- a/apps/api/src/modules/auth/services/oauth.service.spec.ts +++ b/apps/api/src/modules/auth/services/oauth.service.spec.ts @@ -1725,4 +1725,248 @@ describe("OAuthService", () => { }); }); }); + + describe("기본 카테고리 생성", () => { + describe("신규 소셜 로그인 사용자", () => { + it("Apple 로그인 시 기본 카테고리 2개가 생성되어야 한다", async () => { + // Given - 신규 Apple 사용자 + const appleProfile: AppleVerifiedProfile = { + id: "apple-new-user", + email: "apple@example.com", + emailVerified: true, + }; + const mockUser = UserBuilder.create() + .withId("new-apple-user") + .withEmail("apple@example.com") + .verified() + .build(); + + setupSuccessfulOAuthLogin(mockUser); + tokenVerifier.verifyAppleToken.mockResolvedValue(appleProfile); + accountRepo.findByProviderAccountId.mockResolvedValue(null); + userRepo.findByEmail.mockResolvedValue(null); + userRepo.create.mockResolvedValue(mockUser); + accountRepo.createOAuthAccount.mockResolvedValue({} as any); + userRepo.createProfile.mockResolvedValue({} as any); + todoCategoryRepo.createMany.mockResolvedValue(2); + + // When + await service.handleAppleMobileLogin("valid-id-token"); + + // Then - 기본 카테고리 2개 생성 확인 + expect(todoCategoryRepo.createMany).toHaveBeenCalledWith( + expect.arrayContaining([ + expect.objectContaining({ + userId: "new-apple-user", + name: "중요한 일", + color: "#FFB3B3", + sortOrder: 0, + }), + expect.objectContaining({ + userId: "new-apple-user", + name: "할 일", + color: "#FF6B43", + sortOrder: 1, + }), + ]), + expect.anything(), + ); + }); + + it("Google 로그인 시 기본 카테고리 2개가 생성되어야 한다", async () => { + // Given - 신규 Google 사용자 + const googleProfile: OAuthProfile = { + id: "google-new-user", + email: "google@example.com", + emailVerified: true, + name: "Google User", + }; + const mockUser = UserBuilder.create() + .withId("new-google-user") + .withEmail("google@example.com") + .verified() + .build(); + + setupSuccessfulOAuthLogin(mockUser); + tokenVerifier.verifyGoogleToken.mockResolvedValue(googleProfile); + accountRepo.findByProviderAccountId.mockResolvedValue(null); + userRepo.findByEmail.mockResolvedValue(null); + userRepo.create.mockResolvedValue(mockUser); + accountRepo.createOAuthAccount.mockResolvedValue({} as any); + userRepo.createProfile.mockResolvedValue({} as any); + todoCategoryRepo.createMany.mockResolvedValue(2); + + // When + await service.handleGoogleMobileLogin("valid-id-token"); + + // Then - 기본 카테고리 2개 생성 확인 + expect(todoCategoryRepo.createMany).toHaveBeenCalledWith( + expect.arrayContaining([ + expect.objectContaining({ + userId: "new-google-user", + name: "중요한 일", + color: "#FFB3B3", + sortOrder: 0, + }), + expect.objectContaining({ + userId: "new-google-user", + name: "할 일", + color: "#FF6B43", + sortOrder: 1, + }), + ]), + expect.anything(), + ); + }); + + it("Kakao 로그인 시 기본 카테고리 2개가 생성되어야 한다", async () => { + // Given - 신규 Kakao 사용자 + const kakaoProfile: OAuthProfile = { + id: "kakao-new-user", + email: "kakao@example.com", + emailVerified: false, + name: "Kakao User", + }; + const mockUser = UserBuilder.create() + .withId("new-kakao-user") + .withEmail("kakao@example.com") + .build(); + + setupSuccessfulOAuthLogin(mockUser); + tokenVerifier.verifyKakaoToken.mockResolvedValue(kakaoProfile); + accountRepo.findByProviderAccountId.mockResolvedValue(null); + userRepo.findByEmail.mockResolvedValue(null); + userRepo.create.mockResolvedValue(mockUser); + accountRepo.createOAuthAccount.mockResolvedValue({} as any); + userRepo.createProfile.mockResolvedValue({} as any); + todoCategoryRepo.createMany.mockResolvedValue(2); + + // When + await service.handleKakaoMobileLogin("valid-access-token"); + + // Then - 기본 카테고리 2개 생성 확인 + expect(todoCategoryRepo.createMany).toHaveBeenCalledWith( + expect.arrayContaining([ + expect.objectContaining({ + userId: "new-kakao-user", + name: "중요한 일", + color: "#FFB3B3", + sortOrder: 0, + }), + expect.objectContaining({ + userId: "new-kakao-user", + name: "할 일", + color: "#FF6B43", + sortOrder: 1, + }), + ]), + expect.anything(), + ); + }); + + it("Naver 로그인 시 기본 카테고리 2개가 생성되어야 한다", async () => { + // Given - 신규 Naver 사용자 + const naverProfile: OAuthProfile = { + id: "naver-new-user", + email: "naver@example.com", + emailVerified: false, + name: "Naver User", + }; + const mockUser = UserBuilder.create() + .withId("new-naver-user") + .withEmail("naver@example.com") + .build(); + + setupSuccessfulOAuthLogin(mockUser); + tokenVerifier.verifyNaverToken.mockResolvedValue(naverProfile); + accountRepo.findByProviderAccountId.mockResolvedValue(null); + userRepo.findByEmail.mockResolvedValue(null); + userRepo.create.mockResolvedValue(mockUser); + accountRepo.createOAuthAccount.mockResolvedValue({} as any); + userRepo.createProfile.mockResolvedValue({} as any); + todoCategoryRepo.createMany.mockResolvedValue(2); + + // When + await service.handleNaverMobileLogin("valid-access-token"); + + // Then - 기본 카테고리 2개 생성 확인 + expect(todoCategoryRepo.createMany).toHaveBeenCalledWith( + expect.arrayContaining([ + expect.objectContaining({ + userId: "new-naver-user", + name: "중요한 일", + color: "#FFB3B3", + sortOrder: 0, + }), + expect.objectContaining({ + userId: "new-naver-user", + name: "할 일", + color: "#FF6B43", + sortOrder: 1, + }), + ]), + expect.anything(), + ); + }); + }); + + describe("기존 소셜 로그인 사용자", () => { + it("기존 사용자 로그인 시 카테고리가 추가 생성되지 않아야 한다", async () => { + // Given - 기존 Apple 사용자 + const appleProfile: AppleVerifiedProfile = { + id: "apple-existing-user", + email: "existing@example.com", + emailVerified: true, + }; + const existingUser = UserBuilder.create() + .withId("existing-user-id") + .withEmail("existing@example.com") + .verified() + .build(); + const existingAccount = AccountBuilder.create(existingUser.id) + .asApple("apple-existing-user") + .build(); + + setupSuccessfulOAuthLogin(existingUser); + tokenVerifier.verifyAppleToken.mockResolvedValue(appleProfile); + accountRepo.findByProviderAccountId.mockResolvedValue(existingAccount); + userRepo.findById.mockResolvedValue(existingUser); + + // When + await service.handleAppleMobileLogin("valid-id-token"); + + // Then - 카테고리 생성 호출되지 않음 + expect(todoCategoryRepo.createMany).not.toHaveBeenCalled(); + }); + + it("기존 사용자 재로그인 시 새 카테고리가 생성되지 않아야 한다 (Google)", async () => { + // Given - 기존 Google 사용자 + const googleProfile: OAuthProfile = { + id: "google-existing-user", + email: "existing-google@example.com", + emailVerified: true, + name: "Existing Google User", + }; + const existingUser = UserBuilder.create() + .withId("existing-google-user-id") + .withEmail("existing-google@example.com") + .verified() + .build(); + const existingAccount = AccountBuilder.create(existingUser.id) + .asGoogle("google-existing-user") + .build(); + + setupSuccessfulOAuthLogin(existingUser); + tokenVerifier.verifyGoogleToken.mockResolvedValue(googleProfile); + accountRepo.findByProviderAccountId.mockResolvedValue(existingAccount); + userRepo.findById.mockResolvedValue(existingUser); + + // When + await service.handleGoogleMobileLogin("valid-id-token"); + + // Then - 카테고리 생성 호출되지 않음 + expect(todoCategoryRepo.createMany).not.toHaveBeenCalled(); + }); + }); + }); }); diff --git a/apps/api/src/modules/auth/services/oauth.service.ts b/apps/api/src/modules/auth/services/oauth.service.ts index ea9e3d85..f045fbf7 100644 --- a/apps/api/src/modules/auth/services/oauth.service.ts +++ b/apps/api/src/modules/auth/services/oauth.service.ts @@ -5,6 +5,7 @@ import { BusinessExceptions } from "@/common/exception/services/business-excepti import { DatabaseService } from "@/database"; import type { AccountProvider } from "@/generated/prisma/client"; import { TodoCategoryRepository } from "@/modules/todo-category/todo-category.repository"; +import { DEFAULT_CATEGORIES } from "@/modules/todo-category/types/todo-category.types"; import { AUTH_DEFAULTS, @@ -22,11 +23,6 @@ import type { LoginResult, RequestMetadata } from "../types"; import { OAuthTokenVerifierService } from "./oauth-token-verifier.service"; import { TokenService } from "./token.service"; -const DEFAULT_CATEGORIES = [ - { name: "중요한 일", color: "#FFB3B3", sortOrder: 0 }, - { name: "할 일", color: "#FF6B43", sortOrder: 1 }, -] as const; - // Apple, Google, Kakao, Naver OAuth 소셜 로그인 처리 @Injectable() export class OAuthService { diff --git a/apps/api/test/integration/oauth.integration-spec.ts b/apps/api/test/integration/oauth.integration-spec.ts index 4326bfae..1b613578 100644 --- a/apps/api/test/integration/oauth.integration-spec.ts +++ b/apps/api/test/integration/oauth.integration-spec.ts @@ -1184,4 +1184,169 @@ describe("OAuth 통합 테스트 (실제 DB)", () => { expect(secondResult.userId).toBe(firstResult.userId); }); }); + + // =========================================================================== + // 기본 카테고리 생성 테스트 + // =========================================================================== + + describe("기본 카테고리 생성 (실제 DB)", () => { + it("Apple 로그인 시 기본 카테고리가 DB에 생성되어야 한다", async () => { + // Given: Apple 토큰 설정 + const token = "apple-category-test-token"; + fakeTokenVerifier.setCustomProfile("apple", token, { + id: "apple-category-user", + email: "apple-category@example.com", + emailVerified: true, + }); + + // When: Apple 로그인 + const result = await oauthService.handleAppleMobileLogin(token); + + // Then: DB에서 카테고리 2개 확인 + const categories = await databaseService.todoCategory.findMany({ + where: { userId: result.userId }, + orderBy: { sortOrder: "asc" }, + }); + + expect(categories).toHaveLength(2); + expect(categories[0]).toMatchObject({ + name: "중요한 일", + color: "#FFB3B3", + sortOrder: 0, + }); + expect(categories[1]).toMatchObject({ + name: "할 일", + color: "#FF6B43", + sortOrder: 1, + }); + }); + + it("Google 로그인 시 기본 카테고리가 DB에 생성되어야 한다", async () => { + // Given: Google 토큰 설정 + const token = "google-category-test-token"; + fakeTokenVerifier.setCustomProfile("google", token, { + id: "google-category-user", + email: "google-category@example.com", + emailVerified: true, + name: "Google Category User", + }); + + // When: Google 로그인 + const result = await oauthService.handleGoogleMobileLogin(token); + + // Then: DB에서 카테고리 2개 확인 + const categories = await databaseService.todoCategory.findMany({ + where: { userId: result.userId }, + orderBy: { sortOrder: "asc" }, + }); + + expect(categories).toHaveLength(2); + expect(categories[0]).toMatchObject({ + name: "중요한 일", + color: "#FFB3B3", + sortOrder: 0, + }); + expect(categories[1]).toMatchObject({ + name: "할 일", + color: "#FF6B43", + sortOrder: 1, + }); + }); + + it("Kakao 로그인 시 기본 카테고리가 DB에 생성되어야 한다", async () => { + // Given: Kakao 토큰 설정 + const token = "kakao-category-test-token"; + fakeTokenVerifier.setCustomProfile("kakao", token, { + id: "kakao-category-user", + email: "kakao-category@example.com", + emailVerified: false, + name: "Kakao Category User", + }); + + // When: Kakao 로그인 + const result = await oauthService.handleKakaoMobileLogin(token); + + // Then: DB에서 카테고리 2개 확인 + const categories = await databaseService.todoCategory.findMany({ + where: { userId: result.userId }, + orderBy: { sortOrder: "asc" }, + }); + + expect(categories).toHaveLength(2); + expect(categories[0]).toMatchObject({ + name: "중요한 일", + color: "#FFB3B3", + sortOrder: 0, + }); + expect(categories[1]).toMatchObject({ + name: "할 일", + color: "#FF6B43", + sortOrder: 1, + }); + }); + + it("Naver 로그인 시 기본 카테고리가 DB에 생성되어야 한다", async () => { + // Given: Naver 토큰 설정 + const token = "naver-category-test-token"; + fakeTokenVerifier.setCustomProfile("naver", token, { + id: "naver-category-user", + email: "naver-category@example.com", + emailVerified: false, + name: "Naver Category User", + }); + + // When: Naver 로그인 + const result = await oauthService.handleNaverMobileLogin(token); + + // Then: DB에서 카테고리 2개 확인 + const categories = await databaseService.todoCategory.findMany({ + where: { userId: result.userId }, + orderBy: { sortOrder: "asc" }, + }); + + expect(categories).toHaveLength(2); + expect(categories[0]).toMatchObject({ + name: "중요한 일", + color: "#FFB3B3", + sortOrder: 0, + }); + expect(categories[1]).toMatchObject({ + name: "할 일", + color: "#FF6B43", + sortOrder: 1, + }); + }); + + it("기존 사용자 재로그인 시 카테고리가 중복 생성되지 않아야 한다", async () => { + // Given: 첫 번째 로그인으로 사용자 생성 + const token = "duplicate-category-test-token"; + fakeTokenVerifier.setCustomProfile("google", token, { + id: "duplicate-category-user", + email: "duplicate-category@example.com", + emailVerified: true, + name: "Duplicate Test User", + }); + + const firstResult = await oauthService.handleGoogleMobileLogin(token); + + // 첫 로그인 후 카테고리 개수 확인 + const categoriesAfterFirst = await databaseService.todoCategory.findMany({ + where: { userId: firstResult.userId }, + }); + expect(categoriesAfterFirst).toHaveLength(2); + + // When: 같은 사용자로 재로그인 + const secondResult = await oauthService.handleGoogleMobileLogin(token); + + // Then: 같은 사용자이고 카테고리 개수가 여전히 2개 + expect(secondResult.userId).toBe(firstResult.userId); + + const categoriesAfterSecond = await databaseService.todoCategory.findMany( + { + where: { userId: secondResult.userId }, + }, + ); + expect(categoriesAfterSecond).toHaveLength(2); + }); + }); }); From aa85f296d5738473fd82dcd2cea42d51d8891671 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=B5=E1=86=B7=E1=84=8B=E1=85=AD=E1=86=BC?= =?UTF-8?q?=E1=84=86=E1=85=B5=E1=86=AB?= Date: Thu, 5 Feb 2026 18:32:55 +0900 Subject: [PATCH 2/3] =?UTF-8?q?chore(api):=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=20=ED=8C=8C=EC=9D=BC=EC=97=90=EC=84=9C=20noExplicitAny=20?= =?UTF-8?q?=EA=B7=9C=EC=B9=99=20=EB=B9=84=ED=99=9C=EC=84=B1=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - biome.json overrides 설정 추가 - *.spec.ts, *.test.ts 파일에서 any 타입 사용 허용 - 테스트 mock 객체 생성 시 필요한 유연성 확보 --- apps/api/biome.json | 14 +- .../auth/services/auth.service.spec.ts | 126 +++++++++--------- .../auth/services/oauth.service.spec.ts | 94 ++++++------- 3 files changed, 123 insertions(+), 111 deletions(-) diff --git a/apps/api/biome.json b/apps/api/biome.json index 13290568..473f4a26 100644 --- a/apps/api/biome.json +++ b/apps/api/biome.json @@ -17,5 +17,17 @@ "noThisInStatic": "off" } } - } + }, + "overrides": [ + { + "includes": ["**/*.spec.ts", "**/*.test.ts"], + "linter": { + "rules": { + "suspicious": { + "noExplicitAny": "off" + } + } + } + } + ] } diff --git a/apps/api/src/modules/auth/services/auth.service.spec.ts b/apps/api/src/modules/auth/services/auth.service.spec.ts index 9b557f15..1e7eb69e 100644 --- a/apps/api/src/modules/auth/services/auth.service.spec.ts +++ b/apps/api/src/modules/auth/services/auth.service.spec.ts @@ -115,18 +115,18 @@ describe("AuthService", () => { userConsent: { create: jest.fn() }, userPreference: { create: jest.fn() }, }; - return callback(mockTx as any); + return callback(mockTx as never); }, ); userRepo.create.mockResolvedValue(mockUser); - userRepo.createProfile.mockResolvedValue({} as any); - accountRepo.createCredentialAccount.mockResolvedValue({} as any); + userRepo.createProfile.mockResolvedValue({} as never); + accountRepo.createCredentialAccount.mockResolvedValue({} as never); verificationService.createEmailVerification.mockResolvedValue({ code: "123456", expiresAt: new Date(), }); verificationService.sendVerificationEmail.mockResolvedValue(undefined); - securityLogRepo.create.mockResolvedValue({} as any); + securityLogRepo.create.mockResolvedValue({} as never); todoCategoryRepo.createMany.mockResolvedValue(2); }; @@ -281,24 +281,24 @@ describe("AuthService", () => { ) => { userRepo.findByEmail.mockResolvedValue(mockUser); database.$transaction.mockImplementation( - async (callback: TransactionCallback) => callback({} as any), + async (callback: TransactionCallback) => callback({} as never), ); - verificationService.verifyCode.mockResolvedValue(true as any); - userRepo.markEmailVerified.mockResolvedValue({} as any); + verificationService.verifyCode.mockResolvedValue(true as never); + userRepo.markEmailVerified.mockResolvedValue({} as never); tokenService.generateTokenFamily.mockReturnValue("family-id"); tokenService.getRefreshTokenExpiresInSeconds.mockReturnValue(604800); sessionRepo.create.mockResolvedValue({ id: "session-id", userId: mockUser.id, - } as any); + } as never); tokenService.generateTokenPair.mockResolvedValue(mockTokens); tokenService.hashRefreshToken.mockReturnValue("hashed-refresh-token"); - sessionRepo.updateRefreshTokenHash.mockResolvedValue({} as any); - securityLogRepo.create.mockResolvedValue({} as any); + sessionRepo.updateRefreshTokenHash.mockResolvedValue({} as never); + securityLogRepo.create.mockResolvedValue({} as never); userRepo.findByIdWithProfile.mockResolvedValue({ ...mockUser, profile: { name: "Test User", profileImage: null }, - } as any); + } as never); }; it("올바른 코드로 이메일 인증에 성공한다", async () => { @@ -425,26 +425,26 @@ describe("AuthService", () => { userId: mockUser.id, type: "CREDENTIAL", password: "hashed-password", - } as any); + } as never); passwordService.verify.mockResolvedValue(true); database.$transaction.mockImplementation( - async (callback: TransactionCallback) => callback({} as any), + async (callback: TransactionCallback) => callback({} as never), ); tokenService.generateTokenFamily.mockReturnValue("family-id"); tokenService.getRefreshTokenExpiresInSeconds.mockReturnValue(604800); sessionRepo.create.mockResolvedValue({ id: "session-id", userId: mockUser.id, - } as any); + } as never); tokenService.generateTokenPair.mockResolvedValue(mockTokens); tokenService.hashRefreshToken.mockReturnValue("hashed-refresh-token"); - sessionRepo.updateRefreshTokenHash.mockResolvedValue({} as any); - loginAttemptRepo.create.mockResolvedValue({} as any); - securityLogRepo.create.mockResolvedValue({} as any); + sessionRepo.updateRefreshTokenHash.mockResolvedValue({} as never); + loginAttemptRepo.create.mockResolvedValue({} as never); + securityLogRepo.create.mockResolvedValue({} as never); userRepo.findByIdWithProfile.mockResolvedValue({ ...mockUser, profile: { name: "Test User", profileImage: null }, - } as any); + } as never); }; it("올바른 자격 증명으로 토큰을 반환한다", async () => { @@ -470,7 +470,7 @@ describe("AuthService", () => { // Given loginAttemptRepo.countRecentFailuresByEmail.mockResolvedValue(0); userRepo.findByEmail.mockResolvedValue(null); - loginAttemptRepo.create.mockResolvedValue({} as any); + loginAttemptRepo.create.mockResolvedValue({} as never); // When & Then await expect(service.login(loginInput)).rejects.toThrow( @@ -494,9 +494,9 @@ describe("AuthService", () => { id: "account-123", userId: mockUser.id, password: "hashed-password", - } as any); + } as never); passwordService.verify.mockResolvedValue(false); - loginAttemptRepo.create.mockResolvedValue({} as any); + loginAttemptRepo.create.mockResolvedValue({} as never); // When & Then await expect(service.login(loginInput)).rejects.toThrow( @@ -520,7 +520,7 @@ describe("AuthService", () => { id: "account-123", userId: pendingUser.id, password: "hashed-password", - } as any); + } as never); passwordService.verify.mockResolvedValue(true); // When & Then @@ -602,7 +602,7 @@ describe("AuthService", () => { id: "account-123", userId: lockedUser.id, password: "hashed-password", - } as any); + } as never); passwordService.verify.mockResolvedValue(true); // When & Then @@ -627,8 +627,8 @@ describe("AuthService", () => { .build(); sessionRepo.findById.mockResolvedValue(mockSession); - sessionRepo.revoke.mockResolvedValue({} as any); - securityLogRepo.create.mockResolvedValue({} as any); + sessionRepo.revoke.mockResolvedValue({} as never); + securityLogRepo.create.mockResolvedValue({} as never); cacheService.invalidateSession.mockResolvedValue(undefined); // When @@ -649,8 +649,8 @@ describe("AuthService", () => { .build(); sessionRepo.findById.mockResolvedValue(mockSession); - sessionRepo.revoke.mockResolvedValue({} as any); - securityLogRepo.create.mockResolvedValue({} as any); + sessionRepo.revoke.mockResolvedValue({} as never); + securityLogRepo.create.mockResolvedValue({} as never); cacheService.invalidateSession.mockResolvedValue(undefined); // When @@ -715,7 +715,7 @@ describe("AuthService", () => { it("사용자의 모든 세션을 비활성화한다", async () => { // Given sessionRepo.revokeAllByUserId.mockResolvedValue(3); - securityLogRepo.create.mockResolvedValue({} as any); + securityLogRepo.create.mockResolvedValue({} as never); // When const result = await service.logoutAll(userId); @@ -731,7 +731,7 @@ describe("AuthService", () => { it("보안 이벤트를 기록한다", async () => { // Given sessionRepo.revokeAllByUserId.mockResolvedValue(3); - securityLogRepo.create.mockResolvedValue({} as any); + securityLogRepo.create.mockResolvedValue({} as never); // When await service.logoutAll(userId); @@ -777,7 +777,7 @@ describe("AuthService", () => { .withTokenFamily("family-id") .build(); - tokenService.verifyRefreshToken.mockResolvedValue(mockPayload as any); + tokenService.verifyRefreshToken.mockResolvedValue(mockPayload as never); tokenService.hashRefreshToken.mockReturnValue("hashed-token"); sessionRepo.findByRefreshTokenHash.mockResolvedValue(mockSession); sessionRepo.findByPreviousTokenHash.mockResolvedValue(null); @@ -785,8 +785,8 @@ describe("AuthService", () => { sessionRepo.rotateToken.mockResolvedValue({ ...mockSession, tokenVersion: 2, - } as any); - securityLogRepo.create.mockResolvedValue({} as any); + } as never); + securityLogRepo.create.mockResolvedValue({} as never); // When const result = await service.refreshTokens(refreshToken); @@ -804,7 +804,7 @@ describe("AuthService", () => { .withTokenFamily("family-id") .build(); - tokenService.verifyRefreshToken.mockResolvedValue(mockPayload as any); + tokenService.verifyRefreshToken.mockResolvedValue(mockPayload as never); tokenService.hashRefreshToken.mockReturnValue("hashed-token"); sessionRepo.findByRefreshTokenHash.mockResolvedValue(mockSession); sessionRepo.findByPreviousTokenHash.mockResolvedValue(null); @@ -812,8 +812,8 @@ describe("AuthService", () => { sessionRepo.rotateToken.mockResolvedValue({ ...mockSession, tokenVersion: 2, - } as any); - securityLogRepo.create.mockResolvedValue({} as any); + } as never); + securityLogRepo.create.mockResolvedValue({} as never); // When await service.refreshTokens(refreshToken); @@ -845,12 +845,12 @@ describe("AuthService", () => { .withTokenFamily("family-id") .build(); - tokenService.verifyRefreshToken.mockResolvedValue(mockPayload as any); + tokenService.verifyRefreshToken.mockResolvedValue(mockPayload as never); tokenService.hashRefreshToken.mockReturnValue("hashed-token"); sessionRepo.findByRefreshTokenHash.mockResolvedValue(null); sessionRepo.findByPreviousTokenHash.mockResolvedValue(mockSession); sessionRepo.revokeByTokenFamily.mockResolvedValue(1); - securityLogRepo.create.mockResolvedValue({} as any); + securityLogRepo.create.mockResolvedValue({} as never); // When & Then await expect(service.refreshTokens(refreshToken)).rejects.toThrow( @@ -876,7 +876,7 @@ describe("AuthService", () => { .revoked() .build(); - tokenService.verifyRefreshToken.mockResolvedValue(mockPayload as any); + tokenService.verifyRefreshToken.mockResolvedValue(mockPayload as never); tokenService.hashRefreshToken.mockReturnValue("hashed-token"); sessionRepo.findByRefreshTokenHash.mockResolvedValue(revokedSession); @@ -893,7 +893,7 @@ describe("AuthService", () => { .expired() .build(); - tokenService.verifyRefreshToken.mockResolvedValue(mockPayload as any); + tokenService.verifyRefreshToken.mockResolvedValue(mockPayload as never); tokenService.hashRefreshToken.mockReturnValue("hashed-token"); sessionRepo.findByRefreshTokenHash.mockResolvedValue(expiredSession); @@ -911,7 +911,7 @@ describe("AuthService", () => { .withTokenFamily("family-id") .build(); - tokenService.verifyRefreshToken.mockResolvedValue(mockPayload as any); + tokenService.verifyRefreshToken.mockResolvedValue(mockPayload as never); tokenService.hashRefreshToken.mockReturnValue("hashed-token"); sessionRepo.findByRefreshTokenHash.mockResolvedValue(mockSession); sessionRepo.findByPreviousTokenHash.mockResolvedValue(null); @@ -993,15 +993,15 @@ describe("AuthService", () => { id: "account-123", userId: mockUser.id, password: "old-hashed-password", - } as any); + } as never); passwordService.hash.mockResolvedValue("new-hashed-password"); database.$transaction.mockImplementation( - async (callback: TransactionCallback) => callback({} as any), + async (callback: TransactionCallback) => callback({} as never), ); - verificationService.verifyCode.mockResolvedValue(true as any); - accountRepo.updatePassword.mockResolvedValue({} as any); + verificationService.verifyCode.mockResolvedValue(true as never); + accountRepo.updatePassword.mockResolvedValue({} as never); sessionRepo.revokeAllByUserId.mockResolvedValue(2); - securityLogRepo.create.mockResolvedValue({} as any); + securityLogRepo.create.mockResolvedValue({} as never); // When const result = await service.resetPassword(email, code, newPassword); @@ -1052,14 +1052,14 @@ describe("AuthService", () => { id: "account-123", userId, password: "current-hashed-password", - } as any); + } as never); passwordService.verify.mockResolvedValue(true); passwordService.hash.mockResolvedValue("new-hashed-password"); database.$transaction.mockImplementation( - async (callback: TransactionCallback) => callback({} as any), + async (callback: TransactionCallback) => callback({} as never), ); - accountRepo.updatePassword.mockResolvedValue({} as any); - securityLogRepo.create.mockResolvedValue({} as any); + accountRepo.updatePassword.mockResolvedValue({} as never); + securityLogRepo.create.mockResolvedValue({} as never); // When const result = await service.changePassword( @@ -1079,7 +1079,7 @@ describe("AuthService", () => { id: "account-123", userId, password: "current-hashed-password", - } as any); + } as never); passwordService.verify.mockResolvedValue(false); // When & Then @@ -1104,14 +1104,14 @@ describe("AuthService", () => { id: "account-123", userId, password: "current-hashed-password", - } as any); + } as never); passwordService.verify.mockResolvedValue(true); passwordService.hash.mockResolvedValue("new-hashed-password"); database.$transaction.mockImplementation( - async (callback: TransactionCallback) => callback({} as any), + async (callback: TransactionCallback) => callback({} as never), ); - accountRepo.updatePassword.mockResolvedValue({} as any); - securityLogRepo.create.mockResolvedValue({} as any); + accountRepo.updatePassword.mockResolvedValue({} as never); + securityLogRepo.create.mockResolvedValue({} as never); // When await service.changePassword(userId, currentPassword, newPassword); @@ -1191,9 +1191,9 @@ describe("AuthService", () => { .build(); sessionRepo.findById.mockResolvedValue(mockSession); - sessionRepo.revoke.mockResolvedValue({} as any); + sessionRepo.revoke.mockResolvedValue({} as never); cacheService.invalidateSession.mockResolvedValue(undefined); - securityLogRepo.create.mockResolvedValue({} as any); + securityLogRepo.create.mockResolvedValue({} as never); // When const result = await service.revokeSession(userId, sessionId); @@ -1213,9 +1213,9 @@ describe("AuthService", () => { .build(); sessionRepo.findById.mockResolvedValue(mockSession); - sessionRepo.revoke.mockResolvedValue({} as any); + sessionRepo.revoke.mockResolvedValue({} as never); cacheService.invalidateSession.mockResolvedValue(undefined); - securityLogRepo.create.mockResolvedValue({} as any); + securityLogRepo.create.mockResolvedValue({} as never); // When await service.revokeSession(userId, sessionId); @@ -1272,7 +1272,7 @@ describe("AuthService", () => { userRepo.findByEmail.mockResolvedValue(mockUser); database.$transaction.mockImplementation( - async (callback: TransactionCallback) => callback({} as any), + async (callback: TransactionCallback) => callback({} as never), ); verificationService.createEmailVerification.mockResolvedValue({ code: "123456", @@ -1328,7 +1328,7 @@ describe("AuthService", () => { userRepo.findByEmail.mockResolvedValue(mockUser); database.$transaction.mockImplementation( - async (callback: TransactionCallback) => callback({} as any), + async (callback: TransactionCallback) => callback({} as never), ); verificationService.createEmailVerification.mockResolvedValue({ code: "654321", @@ -1445,7 +1445,7 @@ describe("AuthService", () => { userRepo.updateProfile.mockResolvedValue({ name: "Updated Name", profileImage: null, - } as any); + } as never); cacheService.invalidateUserProfile.mockResolvedValue(undefined); // When @@ -1462,7 +1462,7 @@ describe("AuthService", () => { userRepo.updateProfile.mockResolvedValue({ name: "Updated Name", profileImage: null, - } as any); + } as never); cacheService.invalidateUserProfile.mockResolvedValue(undefined); // When @@ -1484,7 +1484,7 @@ describe("AuthService", () => { userRepo.updateProfile.mockResolvedValue({ name: "Test User", profileImage: "https://example.com/new-image.jpg", - } as any); + } as never); cacheService.invalidateUserProfile.mockResolvedValue(undefined); // When diff --git a/apps/api/src/modules/auth/services/oauth.service.spec.ts b/apps/api/src/modules/auth/services/oauth.service.spec.ts index 80fba91a..01b71016 100644 --- a/apps/api/src/modules/auth/services/oauth.service.spec.ts +++ b/apps/api/src/modules/auth/services/oauth.service.spec.ts @@ -286,8 +286,8 @@ describe("OAuthService", () => { accountRepo.findByProviderAccountId.mockResolvedValue(null); userRepo.findByEmail.mockResolvedValue(null); userRepo.create.mockResolvedValue(mockUser); - accountRepo.createOAuthAccount.mockResolvedValue({} as any); - userRepo.createProfile.mockResolvedValue({} as any); + accountRepo.createOAuthAccount.mockResolvedValue({} as never); + userRepo.createProfile.mockResolvedValue({} as never); todoCategoryRepo.createMany.mockResolvedValue(2); // When @@ -340,8 +340,8 @@ describe("OAuthService", () => { tokenVerifier.verifyAppleToken.mockResolvedValue(profileWithoutEmail); accountRepo.findByProviderAccountId.mockResolvedValue(null); userRepo.create.mockResolvedValue(mockUser); - accountRepo.createOAuthAccount.mockResolvedValue({} as any); - userRepo.createProfile.mockResolvedValue({} as any); + accountRepo.createOAuthAccount.mockResolvedValue({} as never); + userRepo.createProfile.mockResolvedValue({} as never); todoCategoryRepo.createMany.mockResolvedValue(2); // When @@ -371,11 +371,11 @@ describe("OAuthService", () => { tokenVerifier.verifyAppleToken.mockResolvedValue(appleVerifiedProfile); accountRepo.findByProviderAccountId.mockResolvedValue(null); userRepo.findByEmail.mockResolvedValue(existingUser); - accountRepo.createOAuthAccount.mockResolvedValue({} as any); + accountRepo.createOAuthAccount.mockResolvedValue({} as never); userRepo.findByIdWithProfile.mockResolvedValue({ ...existingUser, profile: { name: "기존유저", profileImage: null }, - } as any); + } as never); // When const result = await service.handleAppleMobileLogin("valid-id-token"); @@ -415,7 +415,7 @@ describe("OAuthService", () => { tokenVerifier.verifyAppleToken.mockResolvedValue(appleVerifiedProfile); accountRepo.findByProviderAccountId.mockResolvedValue(existingAccount); userRepo.findById.mockResolvedValue(lockedUser); - loginAttemptRepo.create.mockResolvedValue({} as any); + loginAttemptRepo.create.mockResolvedValue({} as never); // When & Then await expect( @@ -438,7 +438,7 @@ describe("OAuthService", () => { tokenVerifier.verifyAppleToken.mockResolvedValue(appleVerifiedProfile); accountRepo.findByProviderAccountId.mockResolvedValue(existingAccount); userRepo.findById.mockResolvedValue(suspendedUser); - loginAttemptRepo.create.mockResolvedValue({} as any); + loginAttemptRepo.create.mockResolvedValue({} as never); // When & Then await expect( @@ -456,7 +456,7 @@ describe("OAuthService", () => { it("새로운 소셜 계정을 연결한다", async () => { // Given accountRepo.findByProviderAccountId.mockResolvedValue(null); - accountRepo.createOAuthAccount.mockResolvedValue({} as any); + accountRepo.createOAuthAccount.mockResolvedValue({} as never); // When const result = await service.linkAccount( @@ -529,7 +529,7 @@ describe("OAuthService", () => { appleAccount, credentialAccount, ]); - accountRepo.deleteAccount.mockResolvedValue({} as any); + accountRepo.deleteAccount.mockResolvedValue({} as never); // When const result = await service.unlinkAccount("user-123", "APPLE"); @@ -716,7 +716,7 @@ describe("OAuthService", () => { it("aido-dev://auth/kakao를 허용한다", async () => { // Given const redirectUri = "aido-dev://auth/kakao"; - oauthStateRepo.create.mockResolvedValue({} as any); + oauthStateRepo.create.mockResolvedValue({} as never); // When await service.generateKakaoAuthUrlWithState(testState, redirectUri); @@ -732,7 +732,7 @@ describe("OAuthService", () => { it("aido-dev://auth/google을 허용한다", async () => { // Given const redirectUri = "aido-dev://auth/google"; - oauthStateRepo.create.mockResolvedValue({} as any); + oauthStateRepo.create.mockResolvedValue({} as never); // When await service.generateGoogleAuthUrlWithState(testState, redirectUri); @@ -748,7 +748,7 @@ describe("OAuthService", () => { it("aido-dev://auth/naver를 허용한다", async () => { // Given const redirectUri = "aido-dev://auth/naver"; - oauthStateRepo.create.mockResolvedValue({} as any); + oauthStateRepo.create.mockResolvedValue({} as never); // When await service.generateNaverAuthUrlWithState(testState, redirectUri); @@ -767,7 +767,7 @@ describe("OAuthService", () => { it("aido-dev://auth/callback을 허용한다", async () => { // Given const redirectUri = "aido-dev://auth/callback"; - oauthStateRepo.create.mockResolvedValue({} as any); + oauthStateRepo.create.mockResolvedValue({} as never); // When await service.generateKakaoAuthUrlWithState(testState, redirectUri); @@ -785,7 +785,7 @@ describe("OAuthService", () => { it("aido://auth/kakao를 허용한다", async () => { // Given const redirectUri = "aido://auth/kakao"; - oauthStateRepo.create.mockResolvedValue({} as any); + oauthStateRepo.create.mockResolvedValue({} as never); // When await service.generateKakaoAuthUrlWithState(testState, redirectUri); @@ -801,7 +801,7 @@ describe("OAuthService", () => { it("aido://auth/callback을 허용한다", async () => { // Given const redirectUri = "aido://auth/callback"; - oauthStateRepo.create.mockResolvedValue({} as any); + oauthStateRepo.create.mockResolvedValue({} as never); // When await service.generateKakaoAuthUrlWithState(testState, redirectUri); @@ -819,7 +819,7 @@ describe("OAuthService", () => { it("잘못된 scheme은 기본값으로 대체된다", async () => { // Given const invalidUri = "invalid-scheme://auth/kakao"; - oauthStateRepo.create.mockResolvedValue({} as any); + oauthStateRepo.create.mockResolvedValue({} as never); // When await service.generateKakaoAuthUrlWithState(testState, invalidUri); @@ -835,7 +835,7 @@ describe("OAuthService", () => { it("잘못된 경로는 기본값으로 대체된다", async () => { // Given const invalidUri = "aido://wrong/path"; - oauthStateRepo.create.mockResolvedValue({} as any); + oauthStateRepo.create.mockResolvedValue({} as never); // When await service.generateKakaoAuthUrlWithState(testState, invalidUri); @@ -850,7 +850,7 @@ describe("OAuthService", () => { it("URI가 제공되지 않으면 기본값을 사용한다", async () => { // Given - oauthStateRepo.create.mockResolvedValue({} as any); + oauthStateRepo.create.mockResolvedValue({} as never); // When await service.generateKakaoAuthUrlWithState(testState); @@ -898,7 +898,7 @@ describe("OAuthService", () => { name: "카카오사용자", profileImage: "https://kakao.com/profile.jpg", }, - } as any); + } as never); tokenVerifier.verifyKakaoToken.mockResolvedValue(mockKakaoProfile); accountRepo.findByProviderAccountId.mockResolvedValue(existingAccount); userRepo.findById.mockResolvedValue(mockUser); @@ -1015,8 +1015,8 @@ describe("OAuthService", () => { accountRepo.findByProviderAccountId.mockResolvedValue(null); userRepo.findByEmail.mockResolvedValue(null); userRepo.create.mockResolvedValue(mockUser); - accountRepo.createOAuthAccount.mockResolvedValue({} as any); - userRepo.createProfile.mockResolvedValue({} as any); + accountRepo.createOAuthAccount.mockResolvedValue({} as never); + userRepo.createProfile.mockResolvedValue({} as never); todoCategoryRepo.createMany.mockResolvedValue(2); const mockTokenResponse = { @@ -1060,7 +1060,7 @@ describe("OAuthService", () => { tokenVerifier.verifyAppleToken.mockRejectedValue( new Error("Invalid token"), ); - loginAttemptRepo.create.mockResolvedValue({} as any); + loginAttemptRepo.create.mockResolvedValue({} as never); // When & Then await expect( @@ -1131,7 +1131,7 @@ describe("OAuthService", () => { tokenVerifier.verifyGoogleToken.mockRejectedValue( new Error("Invalid token"), ); - loginAttemptRepo.create.mockResolvedValue({} as any); + loginAttemptRepo.create.mockResolvedValue({} as never); // When & Then await expect( @@ -1204,7 +1204,7 @@ describe("OAuthService", () => { tokenVerifier.verifyKakaoToken.mockRejectedValue( new Error("Invalid token"), ); - loginAttemptRepo.create.mockResolvedValue({} as any); + loginAttemptRepo.create.mockResolvedValue({} as never); // When & Then await expect( @@ -1277,7 +1277,7 @@ describe("OAuthService", () => { tokenVerifier.verifyNaverToken.mockRejectedValue( new Error("Invalid token"), ); - loginAttemptRepo.create.mockResolvedValue({} as any); + loginAttemptRepo.create.mockResolvedValue({} as never); // When & Then await expect( @@ -1350,7 +1350,7 @@ describe("OAuthService", () => { tokenVerifier.verifyAppleToken.mockRejectedValue( new Error("Invalid token"), ); - loginAttemptRepo.create.mockResolvedValue({} as any); + loginAttemptRepo.create.mockResolvedValue({} as never); // When & Then await expect( @@ -1395,7 +1395,7 @@ describe("OAuthService", () => { tokenVerifier.verifyGoogleToken.mockResolvedValue(googleProfile); accountRepo.findByProviderAccountId.mockResolvedValue(null); userRepo.findByEmail.mockResolvedValue(existingUser); - accountRepo.createOAuthAccount.mockResolvedValue({} as any); + accountRepo.createOAuthAccount.mockResolvedValue({} as never); // When const result = await service.handleGoogleMobileLogin( @@ -1447,8 +1447,8 @@ describe("OAuthService", () => { tokenVerifier.verifyGoogleToken.mockResolvedValue(unverifiedProfile); accountRepo.findByProviderAccountId.mockResolvedValue(null); userRepo.findByEmail.mockResolvedValue(existingUser); - securityLogRepo.create.mockResolvedValue({} as any); - loginAttemptRepo.create.mockResolvedValue({} as any); + securityLogRepo.create.mockResolvedValue({} as never); + loginAttemptRepo.create.mockResolvedValue({} as never); // When & Then await expect( @@ -1495,7 +1495,7 @@ describe("OAuthService", () => { tokenVerifier.verifyAppleToken.mockResolvedValue(appleProfile); accountRepo.findByProviderAccountId.mockResolvedValue(null); userRepo.findByEmail.mockResolvedValue(existingUser); - accountRepo.createOAuthAccount.mockResolvedValue({} as any); + accountRepo.createOAuthAccount.mockResolvedValue({} as never); // When const result = await service.handleAppleMobileLogin( @@ -1552,8 +1552,8 @@ describe("OAuthService", () => { tokenVerifier.verifyKakaoToken.mockResolvedValue(kakaoProfile); accountRepo.findByProviderAccountId.mockResolvedValue(null); userRepo.findByEmail.mockResolvedValue(existingUser); - securityLogRepo.create.mockResolvedValue({} as any); - loginAttemptRepo.create.mockResolvedValue({} as any); + securityLogRepo.create.mockResolvedValue({} as never); + loginAttemptRepo.create.mockResolvedValue({} as never); // When & Then await expect( @@ -1592,8 +1592,8 @@ describe("OAuthService", () => { tokenVerifier.verifyKakaoToken.mockResolvedValue(unverifiedProfile); accountRepo.findByProviderAccountId.mockResolvedValue(null); userRepo.findByEmail.mockResolvedValue(existingUser); - securityLogRepo.create.mockResolvedValue({} as any); - loginAttemptRepo.create.mockResolvedValue({} as any); + securityLogRepo.create.mockResolvedValue({} as never); + loginAttemptRepo.create.mockResolvedValue({} as never); // When & Then await expect( @@ -1636,8 +1636,8 @@ describe("OAuthService", () => { tokenVerifier.verifyNaverToken.mockResolvedValue(naverProfile); accountRepo.findByProviderAccountId.mockResolvedValue(null); userRepo.findByEmail.mockResolvedValue(existingUser); - securityLogRepo.create.mockResolvedValue({} as any); - loginAttemptRepo.create.mockResolvedValue({} as any); + securityLogRepo.create.mockResolvedValue({} as never); + loginAttemptRepo.create.mockResolvedValue({} as never); // When & Then await expect( @@ -1684,7 +1684,7 @@ describe("OAuthService", () => { tokenVerifier.verifyGoogleToken.mockResolvedValue(googleProfile); accountRepo.findByProviderAccountId.mockResolvedValue(null); userRepo.findByEmail.mockResolvedValue(lockedUser); - loginAttemptRepo.create.mockResolvedValue({} as any); + loginAttemptRepo.create.mockResolvedValue({} as never); // When & Then await expect( @@ -1712,7 +1712,7 @@ describe("OAuthService", () => { tokenVerifier.verifyGoogleToken.mockResolvedValue(suspendedProfile); accountRepo.findByProviderAccountId.mockResolvedValue(null); userRepo.findByEmail.mockResolvedValue(suspendedUser); - loginAttemptRepo.create.mockResolvedValue({} as any); + loginAttemptRepo.create.mockResolvedValue({} as never); // When & Then await expect( @@ -1746,8 +1746,8 @@ describe("OAuthService", () => { accountRepo.findByProviderAccountId.mockResolvedValue(null); userRepo.findByEmail.mockResolvedValue(null); userRepo.create.mockResolvedValue(mockUser); - accountRepo.createOAuthAccount.mockResolvedValue({} as any); - userRepo.createProfile.mockResolvedValue({} as any); + accountRepo.createOAuthAccount.mockResolvedValue({} as never); + userRepo.createProfile.mockResolvedValue({} as never); todoCategoryRepo.createMany.mockResolvedValue(2); // When @@ -1792,8 +1792,8 @@ describe("OAuthService", () => { accountRepo.findByProviderAccountId.mockResolvedValue(null); userRepo.findByEmail.mockResolvedValue(null); userRepo.create.mockResolvedValue(mockUser); - accountRepo.createOAuthAccount.mockResolvedValue({} as any); - userRepo.createProfile.mockResolvedValue({} as any); + accountRepo.createOAuthAccount.mockResolvedValue({} as never); + userRepo.createProfile.mockResolvedValue({} as never); todoCategoryRepo.createMany.mockResolvedValue(2); // When @@ -1837,8 +1837,8 @@ describe("OAuthService", () => { accountRepo.findByProviderAccountId.mockResolvedValue(null); userRepo.findByEmail.mockResolvedValue(null); userRepo.create.mockResolvedValue(mockUser); - accountRepo.createOAuthAccount.mockResolvedValue({} as any); - userRepo.createProfile.mockResolvedValue({} as any); + accountRepo.createOAuthAccount.mockResolvedValue({} as never); + userRepo.createProfile.mockResolvedValue({} as never); todoCategoryRepo.createMany.mockResolvedValue(2); // When @@ -1882,8 +1882,8 @@ describe("OAuthService", () => { accountRepo.findByProviderAccountId.mockResolvedValue(null); userRepo.findByEmail.mockResolvedValue(null); userRepo.create.mockResolvedValue(mockUser); - accountRepo.createOAuthAccount.mockResolvedValue({} as any); - userRepo.createProfile.mockResolvedValue({} as any); + accountRepo.createOAuthAccount.mockResolvedValue({} as never); + userRepo.createProfile.mockResolvedValue({} as never); todoCategoryRepo.createMany.mockResolvedValue(2); // When From 3ea97d20c7c5debffb31d0df4374c32a8ed8f41e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=B5=E1=86=B7=E1=84=8B=E1=85=AD=E1=86=BC?= =?UTF-8?q?=E1=84=86=E1=85=B5=E1=86=AB?= Date: Thu, 5 Feb 2026 18:41:54 +0900 Subject: [PATCH 3/3] =?UTF-8?q?refactor(api):=20OAuth=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EB=A5=BC=20it.each=20=ED=8C=A8=ED=84=B4=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - oauth.service.spec.ts: Provider별 테스트를 it.each로 통합 - oauth.integration-spec.ts: 기본 카테고리 생성 테스트를 it.each로 통합 - 코드 중복 85줄 감소 (254줄 → 169줄) Gemini 코드 리뷰 피드백 반영 --- .../auth/services/oauth.service.spec.ts | 251 +++++++----------- .../integration/oauth.integration-spec.ts | 172 +++++------- 2 files changed, 169 insertions(+), 254 deletions(-) diff --git a/apps/api/src/modules/auth/services/oauth.service.spec.ts b/apps/api/src/modules/auth/services/oauth.service.spec.ts index 01b71016..ad12ebb1 100644 --- a/apps/api/src/modules/auth/services/oauth.service.spec.ts +++ b/apps/api/src/modules/auth/services/oauth.service.spec.ts @@ -1728,157 +1728,112 @@ describe("OAuthService", () => { describe("기본 카테고리 생성", () => { describe("신규 소셜 로그인 사용자", () => { - it("Apple 로그인 시 기본 카테고리 2개가 생성되어야 한다", async () => { - // Given - 신규 Apple 사용자 - const appleProfile: AppleVerifiedProfile = { - id: "apple-new-user", + const newUserTestCases = [ + { + provider: "Apple", + profile: { + id: "apple-new-user", + email: "apple@example.com", + emailVerified: true, + } as AppleVerifiedProfile, + userId: "new-apple-user", email: "apple@example.com", - emailVerified: true, - }; - const mockUser = UserBuilder.create() - .withId("new-apple-user") - .withEmail("apple@example.com") - .verified() - .build(); - - setupSuccessfulOAuthLogin(mockUser); - tokenVerifier.verifyAppleToken.mockResolvedValue(appleProfile); - accountRepo.findByProviderAccountId.mockResolvedValue(null); - userRepo.findByEmail.mockResolvedValue(null); - userRepo.create.mockResolvedValue(mockUser); - accountRepo.createOAuthAccount.mockResolvedValue({} as never); - userRepo.createProfile.mockResolvedValue({} as never); - todoCategoryRepo.createMany.mockResolvedValue(2); - - // When - await service.handleAppleMobileLogin("valid-id-token"); - - // Then - 기본 카테고리 2개 생성 확인 - expect(todoCategoryRepo.createMany).toHaveBeenCalledWith( - expect.arrayContaining([ - expect.objectContaining({ - userId: "new-apple-user", - name: "중요한 일", - color: "#FFB3B3", - sortOrder: 0, - }), - expect.objectContaining({ - userId: "new-apple-user", - name: "할 일", - color: "#FF6B43", - sortOrder: 1, - }), - ]), - expect.anything(), - ); - }); - - it("Google 로그인 시 기본 카테고리 2개가 생성되어야 한다", async () => { - // Given - 신규 Google 사용자 - const googleProfile: OAuthProfile = { - id: "google-new-user", + verified: true, + setupMock: ( + verifier: Mocked, + profile: AppleVerifiedProfile | OAuthProfile, + ) => + verifier.verifyAppleToken.mockResolvedValue( + profile as AppleVerifiedProfile, + ), + login: (svc: OAuthService) => + svc.handleAppleMobileLogin("valid-id-token"), + }, + { + provider: "Google", + profile: { + id: "google-new-user", + email: "google@example.com", + emailVerified: true, + name: "Google User", + } as OAuthProfile, + userId: "new-google-user", email: "google@example.com", - emailVerified: true, - name: "Google User", - }; - const mockUser = UserBuilder.create() - .withId("new-google-user") - .withEmail("google@example.com") - .verified() - .build(); - - setupSuccessfulOAuthLogin(mockUser); - tokenVerifier.verifyGoogleToken.mockResolvedValue(googleProfile); - accountRepo.findByProviderAccountId.mockResolvedValue(null); - userRepo.findByEmail.mockResolvedValue(null); - userRepo.create.mockResolvedValue(mockUser); - accountRepo.createOAuthAccount.mockResolvedValue({} as never); - userRepo.createProfile.mockResolvedValue({} as never); - todoCategoryRepo.createMany.mockResolvedValue(2); - - // When - await service.handleGoogleMobileLogin("valid-id-token"); - - // Then - 기본 카테고리 2개 생성 확인 - expect(todoCategoryRepo.createMany).toHaveBeenCalledWith( - expect.arrayContaining([ - expect.objectContaining({ - userId: "new-google-user", - name: "중요한 일", - color: "#FFB3B3", - sortOrder: 0, - }), - expect.objectContaining({ - userId: "new-google-user", - name: "할 일", - color: "#FF6B43", - sortOrder: 1, - }), - ]), - expect.anything(), - ); - }); - - it("Kakao 로그인 시 기본 카테고리 2개가 생성되어야 한다", async () => { - // Given - 신규 Kakao 사용자 - const kakaoProfile: OAuthProfile = { - id: "kakao-new-user", + verified: true, + setupMock: ( + verifier: Mocked, + profile: AppleVerifiedProfile | OAuthProfile, + ) => + verifier.verifyGoogleToken.mockResolvedValue( + profile as OAuthProfile, + ), + login: (svc: OAuthService) => + svc.handleGoogleMobileLogin("valid-id-token"), + }, + { + provider: "Kakao", + profile: { + id: "kakao-new-user", + email: "kakao@example.com", + emailVerified: false, + name: "Kakao User", + } as OAuthProfile, + userId: "new-kakao-user", email: "kakao@example.com", - emailVerified: false, - name: "Kakao User", - }; - const mockUser = UserBuilder.create() - .withId("new-kakao-user") - .withEmail("kakao@example.com") - .build(); - - setupSuccessfulOAuthLogin(mockUser); - tokenVerifier.verifyKakaoToken.mockResolvedValue(kakaoProfile); - accountRepo.findByProviderAccountId.mockResolvedValue(null); - userRepo.findByEmail.mockResolvedValue(null); - userRepo.create.mockResolvedValue(mockUser); - accountRepo.createOAuthAccount.mockResolvedValue({} as never); - userRepo.createProfile.mockResolvedValue({} as never); - todoCategoryRepo.createMany.mockResolvedValue(2); - - // When - await service.handleKakaoMobileLogin("valid-access-token"); - - // Then - 기본 카테고리 2개 생성 확인 - expect(todoCategoryRepo.createMany).toHaveBeenCalledWith( - expect.arrayContaining([ - expect.objectContaining({ - userId: "new-kakao-user", - name: "중요한 일", - color: "#FFB3B3", - sortOrder: 0, - }), - expect.objectContaining({ - userId: "new-kakao-user", - name: "할 일", - color: "#FF6B43", - sortOrder: 1, - }), - ]), - expect.anything(), - ); - }); - - it("Naver 로그인 시 기본 카테고리 2개가 생성되어야 한다", async () => { - // Given - 신규 Naver 사용자 - const naverProfile: OAuthProfile = { - id: "naver-new-user", + verified: false, + setupMock: ( + verifier: Mocked, + profile: AppleVerifiedProfile | OAuthProfile, + ) => + verifier.verifyKakaoToken.mockResolvedValue( + profile as OAuthProfile, + ), + login: (svc: OAuthService) => + svc.handleKakaoMobileLogin("valid-access-token"), + }, + { + provider: "Naver", + profile: { + id: "naver-new-user", + email: "naver@example.com", + emailVerified: false, + name: "Naver User", + } as OAuthProfile, + userId: "new-naver-user", email: "naver@example.com", - emailVerified: false, - name: "Naver User", - }; - const mockUser = UserBuilder.create() - .withId("new-naver-user") - .withEmail("naver@example.com") - .build(); + verified: false, + setupMock: ( + verifier: Mocked, + profile: AppleVerifiedProfile | OAuthProfile, + ) => + verifier.verifyNaverToken.mockResolvedValue( + profile as OAuthProfile, + ), + login: (svc: OAuthService) => + svc.handleNaverMobileLogin("valid-access-token"), + }, + ]; + + it.each( + newUserTestCases, + )("$provider 로그인 시 기본 카테고리 2개가 생성되어야 한다", async ({ + profile, + userId, + email, + verified, + setupMock, + login, + }) => { + // Given - 신규 사용자 + const mockUserBuilder = UserBuilder.create() + .withId(userId) + .withEmail(email); + const mockUser = verified + ? mockUserBuilder.verified().build() + : mockUserBuilder.build(); setupSuccessfulOAuthLogin(mockUser); - tokenVerifier.verifyNaverToken.mockResolvedValue(naverProfile); + setupMock(tokenVerifier, profile); accountRepo.findByProviderAccountId.mockResolvedValue(null); userRepo.findByEmail.mockResolvedValue(null); userRepo.create.mockResolvedValue(mockUser); @@ -1887,19 +1842,19 @@ describe("OAuthService", () => { todoCategoryRepo.createMany.mockResolvedValue(2); // When - await service.handleNaverMobileLogin("valid-access-token"); + await login(service); // Then - 기본 카테고리 2개 생성 확인 expect(todoCategoryRepo.createMany).toHaveBeenCalledWith( expect.arrayContaining([ expect.objectContaining({ - userId: "new-naver-user", + userId, name: "중요한 일", color: "#FFB3B3", sortOrder: 0, }), expect.objectContaining({ - userId: "new-naver-user", + userId, name: "할 일", color: "#FF6B43", sortOrder: 1, diff --git a/apps/api/test/integration/oauth.integration-spec.ts b/apps/api/test/integration/oauth.integration-spec.ts index 1b613578..23e8f953 100644 --- a/apps/api/test/integration/oauth.integration-spec.ts +++ b/apps/api/test/integration/oauth.integration-spec.ts @@ -1190,113 +1190,73 @@ describe("OAuth 통합 테스트 (실제 DB)", () => { // =========================================================================== describe("기본 카테고리 생성 (실제 DB)", () => { - it("Apple 로그인 시 기본 카테고리가 DB에 생성되어야 한다", async () => { - // Given: Apple 토큰 설정 - const token = "apple-category-test-token"; - fakeTokenVerifier.setCustomProfile("apple", token, { - id: "apple-category-user", - email: "apple-category@example.com", - emailVerified: true, - }); - - // When: Apple 로그인 - const result = await oauthService.handleAppleMobileLogin(token); - - // Then: DB에서 카테고리 2개 확인 - const categories = await databaseService.todoCategory.findMany({ - where: { userId: result.userId }, - orderBy: { sortOrder: "asc" }, - }); - - expect(categories).toHaveLength(2); - expect(categories[0]).toMatchObject({ - name: "중요한 일", - color: "#FFB3B3", - sortOrder: 0, - }); - expect(categories[1]).toMatchObject({ - name: "할 일", - color: "#FF6B43", - sortOrder: 1, - }); - }); - - it("Google 로그인 시 기본 카테고리가 DB에 생성되어야 한다", async () => { - // Given: Google 토큰 설정 - const token = "google-category-test-token"; - fakeTokenVerifier.setCustomProfile("google", token, { - id: "google-category-user", - email: "google-category@example.com", - emailVerified: true, - name: "Google Category User", - }); - - // When: Google 로그인 - const result = await oauthService.handleGoogleMobileLogin(token); - - // Then: DB에서 카테고리 2개 확인 - const categories = await databaseService.todoCategory.findMany({ - where: { userId: result.userId }, - orderBy: { sortOrder: "asc" }, - }); - - expect(categories).toHaveLength(2); - expect(categories[0]).toMatchObject({ - name: "중요한 일", - color: "#FFB3B3", - sortOrder: 0, - }); - expect(categories[1]).toMatchObject({ - name: "할 일", - color: "#FF6B43", - sortOrder: 1, - }); - }); - - it("Kakao 로그인 시 기본 카테고리가 DB에 생성되어야 한다", async () => { - // Given: Kakao 토큰 설정 - const token = "kakao-category-test-token"; - fakeTokenVerifier.setCustomProfile("kakao", token, { - id: "kakao-category-user", - email: "kakao-category@example.com", - emailVerified: false, - name: "Kakao Category User", - }); - - // When: Kakao 로그인 - const result = await oauthService.handleKakaoMobileLogin(token); - - // Then: DB에서 카테고리 2개 확인 - const categories = await databaseService.todoCategory.findMany({ - where: { userId: result.userId }, - orderBy: { sortOrder: "asc" }, - }); - - expect(categories).toHaveLength(2); - expect(categories[0]).toMatchObject({ - name: "중요한 일", - color: "#FFB3B3", - sortOrder: 0, - }); - expect(categories[1]).toMatchObject({ - name: "할 일", - color: "#FF6B43", - sortOrder: 1, - }); - }); - - it("Naver 로그인 시 기본 카테고리가 DB에 생성되어야 한다", async () => { - // Given: Naver 토큰 설정 - const token = "naver-category-test-token"; - fakeTokenVerifier.setCustomProfile("naver", token, { - id: "naver-category-user", - email: "naver-category@example.com", - emailVerified: false, - name: "Naver Category User", - }); + const categoryTestCases = [ + { + provider: "Apple" as const, + token: "apple-category-test-token", + profile: { + id: "apple-category-user", + email: "apple-category@example.com", + emailVerified: true, + }, + login: (svc: OAuthService, token: string) => + svc.handleAppleMobileLogin(token), + }, + { + provider: "Google" as const, + token: "google-category-test-token", + profile: { + id: "google-category-user", + email: "google-category@example.com", + emailVerified: true, + name: "Google Category User", + }, + login: (svc: OAuthService, token: string) => + svc.handleGoogleMobileLogin(token), + }, + { + provider: "Kakao" as const, + token: "kakao-category-test-token", + profile: { + id: "kakao-category-user", + email: "kakao-category@example.com", + emailVerified: false, + name: "Kakao Category User", + }, + login: (svc: OAuthService, token: string) => + svc.handleKakaoMobileLogin(token), + }, + { + provider: "Naver" as const, + token: "naver-category-test-token", + profile: { + id: "naver-category-user", + email: "naver-category@example.com", + emailVerified: false, + name: "Naver Category User", + }, + login: (svc: OAuthService, token: string) => + svc.handleNaverMobileLogin(token), + }, + ]; + + it.each( + categoryTestCases, + )("$provider 로그인 시 기본 카테고리가 DB에 생성되어야 한다", async ({ + provider, + token, + profile, + login, + }) => { + // Given: 토큰 설정 + fakeTokenVerifier.setCustomProfile( + provider.toLowerCase() as "apple" | "google" | "kakao" | "naver", + token, + profile, + ); - // When: Naver 로그인 - const result = await oauthService.handleNaverMobileLogin(token); + // When: 로그인 + const result = await login(oauthService, token); // Then: DB에서 카테고리 2개 확인 const categories = await databaseService.todoCategory.findMany({