From d27703b75c6005626849880bb490d4fbddaae484 Mon Sep 17 00:00:00 2001 From: GeonH0 Date: Sun, 29 Sep 2024 21:13:21 +0900 Subject: [PATCH] =?UTF-8?q?Feat:=20Login=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../HomeCafeRecipes.xcodeproj/project.pbxproj | 12 ++ .../Domain/Entities/User.swift | 16 ++ .../LoginUseCaseTests.swift | 167 ++++++++++++++++++ 3 files changed, 195 insertions(+) create mode 100644 HomeCafeRecipes/HomeCafeRecipesTests/LoginUseCaseTests.swift diff --git a/HomeCafeRecipes/HomeCafeRecipes.xcodeproj/project.pbxproj b/HomeCafeRecipes/HomeCafeRecipes.xcodeproj/project.pbxproj index 07e99bf..fc06db7 100644 --- a/HomeCafeRecipes/HomeCafeRecipes.xcodeproj/project.pbxproj +++ b/HomeCafeRecipes/HomeCafeRecipes.xcodeproj/project.pbxproj @@ -88,6 +88,7 @@ 1D7641492C831295002AC68F /* LoginService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D7641482C831295002AC68F /* LoginService.swift */; }; 1D8474562C6C917900323001 /* SearchFeedListUseCaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D8474552C6C917900323001 /* SearchFeedListUseCaseTests.swift */; }; 1D8474592C6CCF6900323001 /* TestUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D8474582C6CCF6900323001 /* TestUtils.swift */; }; + 1D93B9992CA26EB80094277F /* LoginUseCaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D93B9982CA26EB80094277F /* LoginUseCaseTests.swift */; }; 1D95A0A62C37C79500F09077 /* RecipeDetailError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D95A0A52C37C79500F09077 /* RecipeDetailError.swift */; }; 1D96FDAA2C7F55E600EFC657 /* LoginInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D96FDA92C7F55E600EFC657 /* LoginInteractor.swift */; }; 1DBB55062C8418490009E033 /* LoginRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DBB55052C8418490009E033 /* LoginRouter.swift */; }; @@ -98,6 +99,10 @@ 1DBC63672C47D23000DA00C2 /* AddRecipeError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DBC63652C47D23000DA00C2 /* AddRecipeError.swift */; }; 1DBD90B92C91BDAC00184F67 /* SignUpService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DBD90B82C91BDAC00184F67 /* SignUpService.swift */; }; 1DBD90BB2C91DE1600184F67 /* EmptyResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DBD90BA2C91DE1600184F67 /* EmptyResponse.swift */; }; + 1DC373A72CA810BB00B2E831 /* LoginRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D7641462C831192002AC68F /* LoginRepository.swift */; }; + 1DC373A82CA8169F00B2E831 /* LoginUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D7641442C81BE90002AC68F /* LoginUseCase.swift */; }; + 1DC373A92CA81AD400B2E831 /* LoginError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DD4F7092C80C947003E9D9D /* LoginError.swift */; }; + 1DC373AA2CA81AD900B2E831 /* LoginService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D7641482C831295002AC68F /* LoginService.swift */; }; 1DC7CC322C283C0200796889 /* RecipeUploadImgaeCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DC7CC312C283C0200796889 /* RecipeUploadImgaeCell.swift */; }; 1DC7CC342C294F9200796889 /* SelectImageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DC7CC332C294F9200796889 /* SelectImageCell.swift */; }; 1DD4F70A2C80C947003E9D9D /* LoginError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DD4F7092C80C947003E9D9D /* LoginError.swift */; }; @@ -206,6 +211,7 @@ 1D7641482C831295002AC68F /* LoginService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginService.swift; sourceTree = ""; }; 1D8474552C6C917900323001 /* SearchFeedListUseCaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchFeedListUseCaseTests.swift; sourceTree = ""; }; 1D8474582C6CCF6900323001 /* TestUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestUtils.swift; sourceTree = ""; }; + 1D93B9982CA26EB80094277F /* LoginUseCaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginUseCaseTests.swift; sourceTree = ""; }; 1D95A0A52C37C79500F09077 /* RecipeDetailError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeDetailError.swift; sourceTree = ""; }; 1D96FDA92C7F55E600EFC657 /* LoginInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginInteractor.swift; sourceTree = ""; }; 1DBB55052C8418490009E033 /* LoginRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginRouter.swift; sourceTree = ""; }; @@ -388,6 +394,7 @@ 1D5AEE802C61099900BBD5F0 /* RecipeListInteractorTests.swift */, 1D5AEF292C64730A00BBD5F0 /* FetchFeedListUseCaseTests.swift */, 1D8474552C6C917900323001 /* SearchFeedListUseCaseTests.swift */, + 1D93B9982CA26EB80094277F /* LoginUseCaseTests.swift */, ); path = HomeCafeRecipesTests; sourceTree = ""; @@ -837,6 +844,7 @@ 1DF0D1992C7B92C500E2C94C /* DateFormatter+Extensions.swift in Sources */, 1D5AEF382C64794A00BBD5F0 /* CGSize+addButton.swift in Sources */, 1D6958D82C3D5A80008604B3 /* RecipeDeatilInteractorTests.swift in Sources */, + 1DC373A82CA8169F00B2E831 /* LoginUseCase.swift in Sources */, 1D5AEE812C61099900BBD5F0 /* RecipeListInteractorTests.swift in Sources */, 1D60CC402C3EB76600D08FA3 /* APIConfig.swift in Sources */, 1D5AEF3A2C64795900BBD5F0 /* SearchFeedListRepository.swift in Sources */, @@ -852,9 +860,11 @@ 1D5AEF372C64791E00BBD5F0 /* RecipeUploadResponseDTO.swift in Sources */, 1D6958D92C3D5AF7008604B3 /* RecipeDetailInteractor.swift in Sources */, 1D166D0E2C4AD54E00A50963 /* AddRecipeViewModel.swift in Sources */, + 1DC373A92CA81AD400B2E831 /* LoginError.swift in Sources */, 1D39729C2C45905700495014 /* MultipartFormDataRequest.swift in Sources */, 1D5AEF332C64790200BBD5F0 /* RecipeUploadDTO.swift in Sources */, 1DF0D1A22C7B92F800E2C94C /* User.swift in Sources */, + 1DC373A72CA810BB00B2E831 /* LoginRepository.swift in Sources */, 1D2C16FD2BE532B800C04508 /* HomeCafeRecipesTests.swift in Sources */, 1D6958E42C3D5EA6008604B3 /* NetworkResponseDTO.swift in Sources */, 1D5AEF302C6478D800BBD5F0 /* RecipeFetchService.swift in Sources */, @@ -862,9 +872,11 @@ 1D5AEF322C6478FE00BBD5F0 /* RecipePostService.swift in Sources */, 1D6958DB2C3D5C91008604B3 /* Recipe.swift in Sources */, 1D6958E02C3D5E3D008604B3 /* RecipeDetailError.swift in Sources */, + 1DC373AA2CA81AD900B2E831 /* LoginService.swift in Sources */, 1D6958DA2C3D5BA4008604B3 /* FetchRecipeDetailUseCase.swift in Sources */, 1D39729E2C46C57A00495014 /* FetchRecipeDetailUseCaseTests.swift in Sources */, 1D6958E22C3D5E99008604B3 /* RecipeImageDTO.swift in Sources */, + 1D93B9992CA26EB80094277F /* LoginUseCaseTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/HomeCafeRecipes/HomeCafeRecipes/Domain/Entities/User.swift b/HomeCafeRecipes/HomeCafeRecipes/Domain/Entities/User.swift index f4f03ad..7aaa9c9 100644 --- a/HomeCafeRecipes/HomeCafeRecipes/Domain/Entities/User.swift +++ b/HomeCafeRecipes/HomeCafeRecipes/Domain/Entities/User.swift @@ -13,3 +13,19 @@ struct User { let nickname: String let createdAt: Date } + +extension User { + static func dummyUser( + id: Int = 1, + profileImage: String = "", + nickname: String = "testID", + createAt: Date = Date() + ) -> User { + return User( + id: id, + profileImage: profileImage, + nickname: nickname, + createdAt: createAt + ) + } +} diff --git a/HomeCafeRecipes/HomeCafeRecipesTests/LoginUseCaseTests.swift b/HomeCafeRecipes/HomeCafeRecipesTests/LoginUseCaseTests.swift new file mode 100644 index 0000000..3884302 --- /dev/null +++ b/HomeCafeRecipes/HomeCafeRecipesTests/LoginUseCaseTests.swift @@ -0,0 +1,167 @@ +// +// LoginUseCaseTests.swift +// HomeCafeRecipesTests +// +// Created by 김건호 on 9/24/24. +// + +import XCTest + +import RxSwift + +@testable +import HomeCafeRecipes + +final class LoginUseCaseTests: XCTestCase { + var loginRepository: LoginRepositoryMock! + var disposeBag: DisposeBag! + + final class LoginRepositoryMock: LoginRepository { + var loginCallCount: Int = 0 + var loginStub: Single = .just(User.dummyUser()) + func login( + userID: String, + password: String + ) -> Single { + loginCallCount += 1 + return loginStub + } + } + + func createUsecase() -> LoginUseCase { + let usecase = LoginUseCaseImpl(repository: loginRepository) + return usecase + } + + func assertLoginError(_ actual: Error, expectedError: LoginError, file: StaticString = #file, line: UInt = #line) { + if let actualError = actual as? LoginError { + switch (actualError, expectedError) { + case (.IDIsEmpty, .IDIsEmpty), + (.passwordIsEmpty, .passwordIsEmpty): + return + case (.genericError(let actual), .genericError(let expected)): + XCTAssertEqual(actual.localizedDescription, expected.localizedDescription, file: file, line: line) + default: + XCTFail("Expected \(expectedError) but got \(actualError)", file: file, line: line) + } + } else { + XCTFail("Expected \(expectedError) but got \(actual)", file: file, line: line) + } + } + + override func setUpWithError() throws { + loginRepository = .init() + disposeBag = .init() + } +} + +extension LoginUseCaseTests { + func test_execute를_호출하면_loginRepository의_login을_호출합니다(){ + let usecase = createUsecase() + + usecase.execute( + userID: "testID", + password: "testPassword" + ) + .subscribe() + .disposed(by: disposeBag) + + XCTAssertEqual(loginRepository.loginCallCount, 1) + } + + func test_excute를_호출할때_ID가_비워있으면_IDIsEmptyError을_return합니다() { + let usecase = createUsecase() + + usecase.execute( + userID: "", + password: "testPassword" + ) + .subscribe { result in + switch result { + case .success(let loginResult): + switch loginResult { + case .failure(let error): + self.assertLoginError(error, expectedError: .IDIsEmpty) + case .success: + XCTFail("Expected failure but got success") + } + case .failure: + XCTFail("Expected success but got failure") + } + } + .disposed(by: disposeBag) + } + + func test_excute를_호출할때_password가_비워있으면_passwordIsEmpty을_return합니다() { + let usecase = createUsecase() + + usecase.execute( + userID: "testID", + password: "" + ) + .subscribe { result in + switch result { + case .success(let loginResult): + switch loginResult { + case .failure(let error): + self.assertLoginError(error, expectedError: .passwordIsEmpty) + case .success: + XCTFail("Expected failure but got success") + } + case .failure: + XCTFail("Expected success but got failure") + } + } + .disposed(by: disposeBag) + } + + func test_excute를_호출할때_성공할경우_성공한User의_정보를_return합니다() { + let usecase = createUsecase() + + usecase.execute( + userID: "testId", + password: "testPassword" + ).subscribe(onSuccess: { loginResult in + switch loginResult { + case .success(let user): + print(user) + XCTAssertEqual(user.nickname, "testID") + case .failure: + XCTFail("Expected success but got failure") + } + }, onFailure: { error in + XCTFail("Expected success but got failure with error: \(error.localizedDescription)") + }) + .disposed(by: disposeBag) + } + + func test_excute를_호출할때_잘못된자격증명으로_실패를_return합니다() { + loginRepository.loginStub = .error(NSError( + domain: "TestErrorDomain", + code: -1, + userInfo: [NSLocalizedDescriptionKey: "Invalid credentials"] + )) + let usecase = createUsecase() + + usecase.execute( + userID: "wrongID@test.com", + password: "wrongPassword" + ).subscribe(onSuccess: { loginResult in + switch loginResult { + case .success: + XCTFail("Expected failure but got success") + case .failure(let error): + self.assertLoginError(error, expectedError: .genericError( + NSError( + domain: "TestErrorDomain", + code: -1, + userInfo: [NSLocalizedDescriptionKey: "Invalid credentials"] + ) + )) + } + }, onFailure: { error in + XCTFail("Expected success but got failure with error: \(error.localizedDescription)") + }) + .disposed(by: disposeBag) + } +}