diff --git a/Mark-In-Tests/UseCaseTests/DeleteFolderUseCaseTests.swift b/Mark-In-Tests/UseCaseTests/DeleteFolderUseCaseTests.swift new file mode 100644 index 0000000..1699bb0 --- /dev/null +++ b/Mark-In-Tests/UseCaseTests/DeleteFolderUseCaseTests.swift @@ -0,0 +1,136 @@ +// +// DeleteFolderUseCaseTests.swift +// Mark-In-Tests +// +// Created by 이정동 on 6/6/25. +// + +import Testing + +@testable import Mark_In + +struct DeleteFolderUseCaseTests { + + @Test + func test_includingChildren이_true면_링크들을_삭제한다() async throws { + // Given: 준비 + let userID = "testUser" + let folderID = "testFolderID" + + let stubAuthUserManager = StubAuthUserManager(userID: userID) + let fakeLinkRepo = FakeLinkRepository() + .withTestLinks(userID: userID, folderID: folderID, count: 5) + let fakeFolderRepo = FakeFolderRepository() + .withTestFolder(userID: userID, folderID: folderID) + .withTestFolders(userID: userID, count: 2) + + let sut = DeleteFolderUseCaseImpl( + authUserManager: stubAuthUserManager, + linkRepository: fakeLinkRepo, + folderRepository: fakeFolderRepo + ) + + // When: 실행 + _ = try await sut.execute(folderID: folderID, includingChildren: true) + + // Then: 검증 + let links = fakeLinkRepo.data[userID]! + + #expect(links.filter { $0.folderID == folderID }.isEmpty) + + // TearDown: 해제 + + } + + @Test + func test_includingChildren이_false면_링크들을_기본_폴더로_이동한다() async throws { + // Given: 준비 + let userID = "testUser" + let folderID = "testFolderID" + let defaultFolderID: String? = nil + + let stubAuthUserManager = StubAuthUserManager(userID: userID) + let fakeLinkRepo = FakeLinkRepository() + .withTestLinks(userID: userID, folderID: folderID, count: 5) + let fakeFolderRepo = FakeFolderRepository() + .withTestFolder(userID: userID, folderID: folderID) + .withTestFolder(userID: userID, folderID: defaultFolderID) + + let sut = DeleteFolderUseCaseImpl( + authUserManager: stubAuthUserManager, + linkRepository: fakeLinkRepo, + folderRepository: fakeFolderRepo + ) + + // When: 실행 + _ = try await sut.execute(folderID: folderID, includingChildren: false) + + // Then: 검증 + let links = fakeLinkRepo.data[userID]! + + #expect(links.filter { $0.folderID == defaultFolderID }.count == 5) + + // TearDown: 해제 + + } + + @Test + func test_folderID가_nil이면_폴더는_삭제되지_않는다() async throws { + // Given: 준비 + let userID = "testUser" + let folderID: String? = nil + + let stubAuthUserManager = StubAuthUserManager(userID: userID) + let fakeLinkRepo = FakeLinkRepository() + .withTestLinks(userID: userID, folderID: folderID, count: 5) + let fakeFolderRepo = FakeFolderRepository() + .withTestFolder(userID: userID, folderID: folderID) + + let sut = DeleteFolderUseCaseImpl( + authUserManager: stubAuthUserManager, + linkRepository: fakeLinkRepo, + folderRepository: fakeFolderRepo + ) + + // When: 실행 + _ = try await sut.execute(folderID: folderID, includingChildren: false) + + // Then: 검증 + let folders = fakeFolderRepo.data[userID]! + + #expect(folders.contains { $0.id == folderID }) + + // TearDown: 해제 + + } + + @Test + func test_folderID가_존재하면_폴더를_삭제한다() async throws { + // Given: 준비 + let userID = "testUser" + let folderID = "testFolder" + + let stubAuthUserManager = StubAuthUserManager(userID: userID) + let fakeLinkRepo = FakeLinkRepository() + .withTestLinks(userID: userID, folderID: folderID, count: 5) + let fakeFolderRepo = FakeFolderRepository() + .withTestFolder(userID: userID, folderID: folderID) + + let sut = DeleteFolderUseCaseImpl( + authUserManager: stubAuthUserManager, + linkRepository: fakeLinkRepo, + folderRepository: fakeFolderRepo + ) + + // When: 실행 + _ = try await sut.execute(folderID: folderID, includingChildren: false) + + // Then: 검증 + let folders = fakeFolderRepo.data[userID]! + + #expect(folders.contains { $0.id == folderID } == false) + + // TearDown: 해제 + + } +} diff --git a/Mark-In-Tests/ValidationsTests/DTOFieldKeyMappingTests.swift b/Mark-In-Tests/ValidationsTests/DTOFieldKeyMappingTests.swift new file mode 100644 index 0000000..0c7a8b1 --- /dev/null +++ b/Mark-In-Tests/ValidationsTests/DTOFieldKeyMappingTests.swift @@ -0,0 +1,48 @@ +// +// Mark_In_Tests.swift +// Mark-In-Tests +// +// Created by 이정동 on 6/4/25. +// + +import Testing +@testable import Mark_In + +struct DTOFieldKeyMappingTests { + + @Test + func test_FolderDTO의_CodingKey와_FirestoreFieldKey가_일치해야_한다() async throws { + // Given: 준비 + + // When: 실행 + + // Then: 검증 + #expect(FolderDTO.CodingKeys.id.rawValue == FirestoreFieldKey.Folder.id) + #expect(FolderDTO.CodingKeys.name.rawValue == FirestoreFieldKey.Folder.name) + #expect(FolderDTO.CodingKeys.createdAt.rawValue == FirestoreFieldKey.Folder.createdAt) + + // TearDown: 해제 + + } + + @Test + func test_WebLinkDTO의_CodingKey와_FirestoreFieldKey가_일치해야_한다() async throws { + // Given: 준비 + + // When: 실행 + + // Then: 검증 + #expect(WebLinkDTO.CodingKeys.id.rawValue == FirestoreFieldKey.Link.id) + #expect(WebLinkDTO.CodingKeys.url.rawValue == FirestoreFieldKey.Link.url) + #expect(WebLinkDTO.CodingKeys.title.rawValue == FirestoreFieldKey.Link.title) + #expect(WebLinkDTO.CodingKeys.thumbnailUrl.rawValue == FirestoreFieldKey.Link.thumbnailUrl) + #expect(WebLinkDTO.CodingKeys.faviconUrl.rawValue == FirestoreFieldKey.Link.faviconUrl) + #expect(WebLinkDTO.CodingKeys.isPinned.rawValue == FirestoreFieldKey.Link.isPinned) + #expect(WebLinkDTO.CodingKeys.createdAt.rawValue == FirestoreFieldKey.Link.createdAt) + #expect(WebLinkDTO.CodingKeys.lastAccessedAt.rawValue == FirestoreFieldKey.Link.lastAccessedAt) + #expect(WebLinkDTO.CodingKeys.folderID.rawValue == FirestoreFieldKey.Link.folderID) + + // TearDown: 해제 + + } +} diff --git a/Mark-In.xcodeproj/project.pbxproj b/Mark-In.xcodeproj/project.pbxproj index f074719..a0c4a01 100644 --- a/Mark-In.xcodeproj/project.pbxproj +++ b/Mark-In.xcodeproj/project.pbxproj @@ -62,6 +62,13 @@ remoteGlobalIDString = 57AC54A62DA503DE00BA84BD; remoteInfo = LinkMetadataKitInterface; }; + 57664F6C2DF03A9000CA8D68 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 57BFD8BB2DA4E19600648AD4 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 57BFD8C22DA4E19600648AD4; + remoteInfo = "Mark-In"; + }; 57AC56722DA511E900BA84BD /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 57AC53092DA4FC1800BA84BD /* Util.xcodeproj */; @@ -93,6 +100,7 @@ /* Begin PBXFileReference section */ 57588CE42DD9B4D500DBD9A7 /* AppDI.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = AppDI.xcodeproj; path = AppDI/AppDI.xcodeproj; sourceTree = ""; }; 57606D652DD63B14005EBE3D /* ReducerKit.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = ReducerKit.xcodeproj; path = ReducerKit/ReducerKit.xcodeproj; sourceTree = ""; }; + 57664F682DF03A9000CA8D68 /* Mark-In-Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Mark-In-Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 57AC52EF2DA4FBC900BA84BD /* DesignSystem.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = DesignSystem.xcodeproj; path = DesignSystem/DesignSystem.xcodeproj; sourceTree = ""; }; 57AC53092DA4FC1800BA84BD /* Util.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Util.xcodeproj; path = Util/Util.xcodeproj; sourceTree = ""; }; 57AC53232DA4FC4900BA84BD /* LinkMetadataKit.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = LinkMetadataKit.xcodeproj; path = LinkMetadataKit/LinkMetadataKit.xcodeproj; sourceTree = ""; }; @@ -112,6 +120,11 @@ /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ + 57664F692DF03A9000CA8D68 /* Mark-In-Tests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = "Mark-In-Tests"; + sourceTree = ""; + }; 57BFD8C52DA4E19600648AD4 /* Mark-In */ = { isa = PBXFileSystemSynchronizedRootGroup; exceptions = ( @@ -123,6 +136,13 @@ /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ + 57664F652DF03A9000CA8D68 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 57BFD8C02DA4E19600648AD4 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -217,6 +237,7 @@ 57AC56602DA511AF00BA84BD /* Shared */, 57AC565F2DA511A900BA84BD /* Core */, 57BFD8C52DA4E19600648AD4 /* Mark-In */, + 57664F692DF03A9000CA8D68 /* Mark-In-Tests */, 57AC55422DA507EC00BA84BD /* Frameworks */, 57BFD8C42DA4E19600648AD4 /* Products */, ); @@ -228,6 +249,7 @@ isa = PBXGroup; children = ( 57BFD8C32DA4E19600648AD4 /* Mark-In.app */, + 57664F682DF03A9000CA8D68 /* Mark-In-Tests.xctest */, ); name = Products; sourceTree = ""; @@ -235,6 +257,29 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 57664F672DF03A9000CA8D68 /* Mark-In-Tests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 57664F6E2DF03A9000CA8D68 /* Build configuration list for PBXNativeTarget "Mark-In-Tests" */; + buildPhases = ( + 57664F642DF03A9000CA8D68 /* Sources */, + 57664F652DF03A9000CA8D68 /* Frameworks */, + 57664F662DF03A9000CA8D68 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 57664F6D2DF03A9000CA8D68 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 57664F692DF03A9000CA8D68 /* Mark-In-Tests */, + ); + name = "Mark-In-Tests"; + packageProductDependencies = ( + ); + productName = "Mark-In-Tests"; + productReference = 57664F682DF03A9000CA8D68 /* Mark-In-Tests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; 57BFD8C22DA4E19600648AD4 /* Mark-In */ = { isa = PBXNativeTarget; buildConfigurationList = 57BFD8CF2DA4E19700648AD4 /* Build configuration list for PBXNativeTarget "Mark-In" */; @@ -270,9 +315,13 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1630; + LastSwiftUpdateCheck = 1640; LastUpgradeCheck = 1640; TargetAttributes = { + 57664F672DF03A9000CA8D68 = { + CreatedOnToolsVersion = 16.4; + TestTargetID = 57BFD8C22DA4E19600648AD4; + }; 57BFD8C22DA4E19600648AD4 = { CreatedOnToolsVersion = 16.3; }; @@ -319,6 +368,7 @@ projectRoot = ""; targets = ( 57BFD8C22DA4E19600648AD4 /* Mark-In */, + 57664F672DF03A9000CA8D68 /* Mark-In-Tests */, ); }; /* End PBXProject section */ @@ -369,6 +419,13 @@ /* End PBXReferenceProxy section */ /* Begin PBXResourcesBuildPhase section */ + 57664F662DF03A9000CA8D68 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 57BFD8C12DA4E19600648AD4 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -379,6 +436,13 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 57664F642DF03A9000CA8D68 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 57BFD8BF2DA4E19600648AD4 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -388,7 +452,61 @@ }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + 57664F6D2DF03A9000CA8D68 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 57BFD8C22DA4E19600648AD4 /* Mark-In */; + targetProxy = 57664F6C2DF03A9000CA8D68 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin XCBuildConfiguration section */ + 57664F6F2DF03A9000CA8D68 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 5DFZR8RCQR; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.5; + MACOSX_DEPLOYMENT_TARGET = 15.5; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "kr.co.ios.swift.apple.Mark-In-Tests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = auto; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,7"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Mark-In.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Mark-In"; + XROS_DEPLOYMENT_TARGET = 2.5; + }; + name = Debug; + }; + 57664F702DF03A9000CA8D68 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 5DFZR8RCQR; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.5; + MACOSX_DEPLOYMENT_TARGET = 15.5; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "kr.co.ios.swift.apple.Mark-In-Tests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = auto; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,7"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Mark-In.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Mark-In"; + XROS_DEPLOYMENT_TARGET = 2.5; + }; + name = Release; + }; 57BFD8CD2DA4E19700648AD4 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReferenceAnchor = 57BFD8C52DA4E19600648AD4 /* Mark-In */; @@ -598,6 +716,15 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 57664F6E2DF03A9000CA8D68 /* Build configuration list for PBXNativeTarget "Mark-In-Tests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 57664F6F2DF03A9000CA8D68 /* Debug */, + 57664F702DF03A9000CA8D68 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 57BFD8BE2DA4E19600648AD4 /* Build configuration list for PBXProject "Mark-In" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/Mark-In.xcodeproj/xcshareddata/xcschemes/Mark-In.xcscheme b/Mark-In.xcodeproj/xcshareddata/xcschemes/Mark-In.xcscheme index 2c6fc41..9f3ff0c 100644 --- a/Mark-In.xcodeproj/xcshareddata/xcschemes/Mark-In.xcscheme +++ b/Mark-In.xcodeproj/xcshareddata/xcschemes/Mark-In.xcscheme @@ -29,6 +29,19 @@ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES" shouldAutocreateTestPlan = "YES"> + + + + + + + + + + + + Folder { + let folder = Folder( + id: UUID().uuidString, + name: folder.name, + createdAt: Date() + ) + + data[userID, default: []].append(folder) + return folder + } + + func fetchAll(userID: String) async throws -> [Folder] { + return data[userID] ?? [] + } + + func delete(userID: String, folderID: String) async throws { + data[userID]?.removeAll { $0.id == folderID } + } + + func deleteAll(userID: String) async throws { + data[userID] = [] + } +} + +extension FakeFolderRepository { + func withTestFolders(userID: String, count: Int) -> Self { + let dummyFolders = (0.. Self { + let dummyFolder = Folder.makeTestObject(id: folderID) + data[userID, default: []].append(dummyFolder) + return self + } +} + +private extension Folder { + static func makeTestObject( + id: String? = UUID().uuidString, + name: String = "test-folder", + createdAt: Date = Date() + ) -> Folder { + Folder(id: id, name: name, createdAt: createdAt) + } +} diff --git a/Mark-In/Sources/Data/Tests/FakeLinkRepository.swift b/Mark-In/Sources/Data/Tests/FakeLinkRepository.swift new file mode 100644 index 0000000..1cbfb8a --- /dev/null +++ b/Mark-In/Sources/Data/Tests/FakeLinkRepository.swift @@ -0,0 +1,117 @@ +// +// MockLinkRepository.swift +// Mark-In +// +// Created by 이정동 on 6/6/25. +// + +import Foundation + +final class FakeLinkRepository: LinkRepository { + + var data: [String: [WebLink]] = [:] + + func create(userID: String, link: WriteLink) async throws -> WebLink { + let webLink = WebLink( + id: UUID().uuidString, + url: link.url, + title: link.title, + thumbnailUrl: nil, + faviconUrl: nil, + isPinned: false, + createdAt: Date(), + lastAccessedAt: nil, + folderID: link.folderID + ) + + data[userID, default: []].append(webLink) + + return webLink + } + + func fetchAll(userID: String) async throws -> [WebLink] { + return data[userID] ?? [] + } + + func moveLinkInFolder(userID: String, target linkID: String, to folderID: String?) async throws { + let updated = data[userID]?.map { + $0.id == linkID ? $0.updating(folderID: folderID) : $0 + } + + data[userID] = updated + } + + func moveLinksInFolder(userID: String, fromFolderID: String?, toFolderID: String?) async throws { + let updated = data[userID]?.map { + $0.folderID == fromFolderID ? $0.updating(folderID: toFolderID) : $0 + } + + data[userID] = updated + } + + func delete(userID: String, linkID: String) async throws { + data[userID]?.removeAll { $0.id == linkID } + } + + func deleteAllInFolder(userID: String, folderID: String?) async throws { + data[userID]?.removeAll { $0.folderID == folderID } + } + + func deleteAll(userID: String) async throws { + data[userID] = [] + } +} + +extension FakeLinkRepository { + func withTestLinks(userID: String, folderID: String?, count: Int) -> Self { + let testLinks = (0.. Self { + let testLink = WebLink.makeTestObject(id: linkID, folderID: folderID) + data[userID, default: []].append(testLink) + return self + } +} + +private extension WebLink { + static func makeTestObject( + id: String = UUID().uuidString, + url: String = "https://example.com", + title: String = "Test Link", + thumbnailUrl: String? = nil, + faviconUrl: String? = nil, + isPinned: Bool = false, + createdAt: Date = Date(), + lastAccessedAt: Date? = nil, + folderID: String? = "test-folder", + ) -> WebLink { + WebLink( + id: id, + url: url, + title: title, + thumbnailUrl: thumbnailUrl, + faviconUrl: faviconUrl, + isPinned: isPinned, + createdAt: createdAt, + lastAccessedAt: lastAccessedAt, + folderID: folderID + ) + } + + func updating(folderID: String?) -> WebLink { + WebLink( + id: id, + url: url, + title: title, + thumbnailUrl: thumbnailUrl, + faviconUrl: faviconUrl, + isPinned: isPinned, + createdAt: createdAt, + lastAccessedAt: lastAccessedAt, + folderID: folderID + ) + } +}