From ef2b65152f7a394723d5ca487f27b4fab98338ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=92=E1=85=A9=E1=86=BC=E1=84=89=E1=85=A5=E1=86=A8?= =?UTF-8?q?=E1=84=92=E1=85=A7=E1=86=AB?= Date: Thu, 29 Jan 2026 17:11:39 +0900 Subject: [PATCH] =?UTF-8?q?Test(member):=20Member=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20UseCase=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - FetchMemberUseCaseTests: 멤버 조회 테스트 (5개 TC) - DeleteTravelMemberUseCaseTests: 멤버 삭제 테스트 (5개 TC) - DelegateOwnerUseCaseTests: 관리자 권한 위임 테스트 (6개 TC) - JoinTravelUseCaseTests: 여행 참여 테스트 (7개 TC) - LeaveTravelUseCaseTests: 여행 탈퇴 테스트 (6개 TC) - TestTags: .member 태그 추가 - MemberTestPlan.md 테스트 계획 문서 추가 --- Domain/Tests/Reports/MemberTestPlan.md | 179 ++++++++++++++++++ Domain/Tests/TestTags.swift | 1 + .../Member/DelegateOwnerUseCaseTests.swift | 132 +++++++++++++ .../DeleteTravelMemberUseCaseTests.swift | 112 +++++++++++ .../Member/FetchMemberUseCaseTests.swift | 102 ++++++++++ .../Member/JoinTravelUseCaseTests.swift | 161 ++++++++++++++++ .../Member/LeaveTravelUseCaseTests.swift | 110 +++++++++++ 7 files changed, 797 insertions(+) create mode 100644 Domain/Tests/Reports/MemberTestPlan.md create mode 100644 Domain/Tests/UseCase/Member/DelegateOwnerUseCaseTests.swift create mode 100644 Domain/Tests/UseCase/Member/DeleteTravelMemberUseCaseTests.swift create mode 100644 Domain/Tests/UseCase/Member/FetchMemberUseCaseTests.swift create mode 100644 Domain/Tests/UseCase/Member/JoinTravelUseCaseTests.swift create mode 100644 Domain/Tests/UseCase/Member/LeaveTravelUseCaseTests.swift diff --git a/Domain/Tests/Reports/MemberTestPlan.md b/Domain/Tests/Reports/MemberTestPlan.md new file mode 100644 index 0000000..18d637b --- /dev/null +++ b/Domain/Tests/Reports/MemberTestPlan.md @@ -0,0 +1,179 @@ +# Member Domain Test Plan + +## 개요 +Member 도메인의 5개 UseCase에 대한 테스트 계획입니다. + +| 항목 | 내용 | +|------|------| +| 작성자 | 홍석현 | +| 작성일 | 2026-01-29 | +| 대상 모듈 | Domain/UseCase/Travel/Member | +| 테스트 프레임워크 | Swift Testing | + +--- + +## UseCase 목록 + +| # | UseCase | Input | Output | 의존성 | +|---|---------|-------|--------|--------| +| 1 | FetchMemberUseCase | travelId: String | MyTravelMember | TravelMemberRepositoryProtocol | +| 2 | DeleteTravelMemberUseCase | travelId: String, memberId: String | Void | TravelMemberRepositoryProtocol | +| 3 | DelegateOwnerUseCase | travelId: String, newOwnerId: String | Travel | TravelMemberRepositoryProtocol | +| 4 | JoinTravelUseCase | inviteCode: String | Travel | TravelMemberRepositoryProtocol | +| 5 | LeaveTravelUseCase | travelId: String | Void | TravelMemberRepositoryProtocol | + +--- + +## Entity 구조 + +### TravelMember +```swift +struct TravelMember { + let id: String + let name: String + let role: MemberRole + let email: String? + let avatarUrl: String? +} +``` + +### MyTravelMember +```swift +struct MyTravelMember { + let myInfo: TravelMember + let memberInfo: [TravelMember] +} +``` + +### MemberRole +```swift +enum MemberRole: String { + case owner = "owner" + case member = "member" +} +``` + +--- + +## Test Cases + +### 1. FetchMemberUseCase Tests + +| TC ID | 우선순위 | 테스트 설명 | 입력 | 예상 결과 | +|-------|----------|-------------|------|-----------| +| TC-MEMBER-001 | P0 | 유효한 travelId로 멤버 조회 성공 | travelId: "MOCK-1" | myInfo 존재, memberInfo 배열 반환 | +| TC-MEMBER-002 | P0 | 존재하지 않는 travelId로 조회 시 에러 | travelId: "INVALID" | throws Error (404) | +| TC-MEMBER-003 | P1 | myInfo가 owner 역할인지 확인 | travelId: "MOCK-1" | myInfo.role == .owner | +| TC-MEMBER-004 | P1 | 빈 문자열 travelId로 조회 시 에러 | travelId: "" | throws Error | + +### 2. DeleteTravelMemberUseCase Tests + +| TC ID | 우선순위 | 테스트 설명 | 입력 | 예상 결과 | +|-------|----------|-------------|------|-----------| +| TC-MEMBER-010 | P0 | 유효한 memberId로 멤버 삭제 성공 | travelId: "MOCK-1", memberId: "MOCKmember-1" | 성공 (no throw) | +| TC-MEMBER-011 | P0 | 존재하지 않는 travelId로 삭제 시 에러 | travelId: "INVALID", memberId: "any" | throws Error (404) | +| TC-MEMBER-012 | P1 | 존재하지 않는 memberId로 삭제 시 | travelId: "MOCK-1", memberId: "INVALID" | 성공 (멤버가 없어도 에러 없음) | +| TC-MEMBER-013 | P1 | 빈 문자열 파라미터로 삭제 시 | travelId: "", memberId: "" | 동작 확인 | + +### 3. DelegateOwnerUseCase Tests + +| TC ID | 우선순위 | 테스트 설명 | 입력 | 예상 결과 | +|-------|----------|-------------|------|-----------| +| TC-MEMBER-020 | P0 | 유효한 newOwnerId로 권한 위임 성공 | travelId: "MOCK-1", newOwnerId: "MOCKmember-1" | Travel 반환, ownerName 변경됨 | +| TC-MEMBER-021 | P0 | 존재하지 않는 travelId로 위임 시 에러 | travelId: "INVALID", newOwnerId: "any" | throws Error (404) | +| TC-MEMBER-022 | P1 | 존재하지 않는 newOwnerId로 위임 시 | travelId: "MOCK-1", newOwnerId: "INVALID" | Travel 반환 (기존 ownerName 유지) | +| TC-MEMBER-023 | P1 | 권한 위임 후 반환된 Travel 정보 검증 | travelId: "MOCK-1", newOwnerId: "MOCKmember-1" | Travel.id, title 등 유지 | + +### 4. JoinTravelUseCase Tests + +| TC ID | 우선순위 | 테스트 설명 | 입력 | 예상 결과 | +|-------|----------|-------------|------|-----------| +| TC-MEMBER-030 | P0 | 유효한 초대코드로 여행 참여 성공 | inviteCode: "INV-0001" | Travel 반환, 멤버 추가됨 | +| TC-MEMBER-031 | P0 | 잘못된 초대코드로 참여 시 에러 | inviteCode: "INVALID" | throws Error (404) | +| TC-MEMBER-032 | P1 | 참여 후 멤버 수 증가 확인 | inviteCode: "INV-0002" | members.count 증가 | +| TC-MEMBER-033 | P1 | 빈 초대코드로 참여 시 에러 | inviteCode: "" | throws Error | +| TC-MEMBER-034 | P2 | 다양한 형식의 초대코드 테스트 | 여러 inviteCode | 형식에 따른 결과 | + +### 5. LeaveTravelUseCase Tests + +| TC ID | 우선순위 | 테스트 설명 | 입력 | 예상 결과 | +|-------|----------|-------------|------|-----------| +| TC-MEMBER-040 | P0 | 유효한 travelId로 여행 탈퇴 성공 | travelId: "MOCK-1" | 성공 (no throw) | +| TC-MEMBER-041 | P0 | 존재하지 않는 travelId로 탈퇴 시 에러 | travelId: "INVALID" | throws Error (404) | +| TC-MEMBER-042 | P1 | 빈 문자열 travelId로 탈퇴 시 | travelId: "" | 동작 확인 | +| TC-MEMBER-043 | P1 | 여러 번 탈퇴 시도 | travelId: "MOCK-1" (2회) | 2회 모두 성공 | + +--- + +## 테스트 파일 구조 + +``` +Domain/Tests/ +├── Reports/ +│ └── MemberTestPlan.md +├── TestTags.swift (기존 - member 태그 추가 필요) +└── UseCase/ + └── Member/ + ├── FetchMemberUseCaseTests.swift + ├── DeleteTravelMemberUseCaseTests.swift + ├── DelegateOwnerUseCaseTests.swift + ├── JoinTravelUseCaseTests.swift + └── LeaveTravelUseCaseTests.swift +``` + +--- + +## Mock 구현 현황 + +### MockTravelMemberRepository +- **상태**: 완전 구현됨 +- **초기 데이터**: 25개의 Travel (MOCK-1 ~ MOCK-25) +- **각 Travel의 멤버**: 1명 (MOCKmember-{i}) +- **초대코드 형식**: INV-000{i} + +### Mock 동작 +| 메서드 | 동작 | +|--------|------| +| fetchMember | travelId로 조회, 없으면 404 에러 | +| deleteMember | travelId로 Travel 찾아 memberId 제거 | +| delegateOwner | newOwnerId를 찾아 ownerName 변경 | +| joinTravel | inviteCode로 Travel 찾아 새 멤버 추가 | +| leaveTravel | travelId로 Travel 찾아 본인 제거 | + +--- + +## 테스트 실행 + +```bash +# 전체 Member 테스트 실행 +swift test --filter "Member" + +# 특정 UseCase 테스트 실행 +swift test --filter "FetchMemberUseCaseTests" + +# 태그 기반 실행 +swift test --filter "member" +``` + +--- + +## 테스트 커버리지 목표 + +| UseCase | 목표 커버리지 | 예상 TC 수 | +|---------|--------------|-----------| +| FetchMemberUseCase | 100% | 4 | +| DeleteTravelMemberUseCase | 100% | 4 | +| DelegateOwnerUseCase | 100% | 4 | +| JoinTravelUseCase | 100% | 5 | +| LeaveTravelUseCase | 100% | 4 | +| **총계** | **100%** | **21** | + +--- + +## 주의사항 + +1. **Swift Testing 사용**: XCTest 대신 Swift Testing 프레임워크 사용 +2. **태그 사용**: `.tags(.useCase, .member)` 태그 적용 +3. **의존성 주입**: `withDependencies`로 Mock 주입 +4. **원본 코드 수정 금지**: 테스트 코드만 작성 +5. **비동기 테스트**: `async throws` 패턴 사용 diff --git a/Domain/Tests/TestTags.swift b/Domain/Tests/TestTags.swift index ce185ba..0c3792a 100644 --- a/Domain/Tests/TestTags.swift +++ b/Domain/Tests/TestTags.swift @@ -17,4 +17,5 @@ extension Tag { @Tag static var settlement: Self @Tag static var auth: Self @Tag static var model: Self + @Tag static var member: Self } diff --git a/Domain/Tests/UseCase/Member/DelegateOwnerUseCaseTests.swift b/Domain/Tests/UseCase/Member/DelegateOwnerUseCaseTests.swift new file mode 100644 index 0000000..33f8cff --- /dev/null +++ b/Domain/Tests/UseCase/Member/DelegateOwnerUseCaseTests.swift @@ -0,0 +1,132 @@ +// +// DelegateOwnerUseCaseTests.swift +// DomainTests +// +// Created by 홍석현 on 1/29/26. +// Generated by Test Automation System +// + +import Testing +import Foundation +@testable import Domain + +@Suite("관리자 권한 위임 UseCase 테스트", .tags(.useCase, .member)) +struct DelegateOwnerUseCaseTests { + + // MARK: - Happy Path Tests + + @Test("TC-MEMBER-020: 유효한 newOwnerId로 권한 위임 성공") + func delegateOwner_withValidIds_shouldReturnUpdatedTravel() async throws { + // given + let travelId = "MOCK-1" + let newOwnerId = "MOCKmember-1" + let useCase = DelegateOwnerUseCase() + + // when + let result = try await useCase.execute(travelId: travelId, newOwnerId: newOwnerId) + + // then + #expect(result.id == travelId) + #expect(result.ownerName == "친구1") // Mock에서 MOCKmember-{i}의 name은 "친구1" + } + + @Test("TC-MEMBER-023: 권한 위임 후 반환된 Travel 정보 검증") + func delegateOwner_shouldPreserveTravelInfo() async throws { + // given + let travelId = "MOCK-1" + let newOwnerId = "MOCKmember-1" + let useCase = DelegateOwnerUseCase() + + // when + let result = try await useCase.execute(travelId: travelId, newOwnerId: newOwnerId) + + // then - 기본 정보가 유지되어야 함 + #expect(result.id == travelId) + #expect(result.title == "여행 1") + #expect(result.status == .active) + #expect(result.inviteCode == "INV-0001") + } + + // MARK: - Error Cases + + @Test("TC-MEMBER-021: 존재하지 않는 travelId로 위임 시 에러") + func delegateOwner_withInvalidTravelId_shouldThrowError() async throws { + // given + let travelId = "INVALID-TRAVEL-ID" + let newOwnerId = "any-member-id" + let useCase = DelegateOwnerUseCase() + + // when & then + await #expect(throws: Error.self) { + _ = try await useCase.execute(travelId: travelId, newOwnerId: newOwnerId) + } + } + + @Test("TC-MEMBER-024: 빈 문자열 travelId로 위임 시 에러") + func delegateOwner_withEmptyTravelId_shouldThrowError() async throws { + // given + let travelId = "" + let newOwnerId = "any-member-id" + let useCase = DelegateOwnerUseCase() + + // when & then + await #expect(throws: Error.self) { + _ = try await useCase.execute(travelId: travelId, newOwnerId: newOwnerId) + } + } + + // MARK: - Edge Cases + + @Test("TC-MEMBER-022: 존재하지 않는 newOwnerId로 위임 시 기존 ownerName 유지") + func delegateOwner_withInvalidNewOwnerId_shouldKeepOriginalOwnerName() async throws { + /* + ┌─────────────────────────────────────────────────────┐ + │ Mock 동작 설명 │ + ├─────────────────────────────────────────────────────┤ + │ - MockTravelMemberRepository는 newOwnerId를 │ + │ members에서 찾지 못하면 기존 ownerName을 유지함 │ + │ - 이는 실제 서버 동작과 다를 수 있음 │ + └─────────────────────────────────────────────────────┘ + */ + + // given + let travelId = "MOCK-2" + let newOwnerId = "INVALID-MEMBER-ID" + let useCase = DelegateOwnerUseCase() + + // when + let result = try await useCase.execute(travelId: travelId, newOwnerId: newOwnerId) + + // then - 존재하지 않는 멤버이므로 기존 ownerName 유지 + #expect(result.ownerName == "김민희") + } + + @Test("TC-MEMBER-025: 다양한 travelId로 권한 위임", arguments: ["MOCK-3", "MOCK-4", "MOCK-5"]) + func delegateOwner_withVariousTravelIds_shouldSucceed(travelId: String) async throws { + // given + let index = travelId.replacingOccurrences(of: "MOCK-", with: "") + let newOwnerId = "MOCKmember-\(index)" + let useCase = DelegateOwnerUseCase() + + // when + let result = try await useCase.execute(travelId: travelId, newOwnerId: newOwnerId) + + // then + #expect(result.id == travelId) + #expect(result.ownerName == "친구1") + } + + @Test("TC-MEMBER-026: 권한 위임 후 members 배열 유지 확인") + func delegateOwner_shouldPreserveMembers() async throws { + // given + let travelId = "MOCK-6" + let newOwnerId = "MOCKmember-6" + let useCase = DelegateOwnerUseCase() + + // when + let result = try await useCase.execute(travelId: travelId, newOwnerId: newOwnerId) + + // then - members 배열이 유지되어야 함 + #expect(result.members.isEmpty == false) + } +} diff --git a/Domain/Tests/UseCase/Member/DeleteTravelMemberUseCaseTests.swift b/Domain/Tests/UseCase/Member/DeleteTravelMemberUseCaseTests.swift new file mode 100644 index 0000000..e0314f6 --- /dev/null +++ b/Domain/Tests/UseCase/Member/DeleteTravelMemberUseCaseTests.swift @@ -0,0 +1,112 @@ +// +// DeleteTravelMemberUseCaseTests.swift +// DomainTests +// +// Created by 홍석현 on 1/29/26. +// Generated by Test Automation System +// + +import Testing +import Foundation +@testable import Domain + +@Suite("멤버 삭제 UseCase 테스트", .tags(.useCase, .member)) +struct DeleteTravelMemberUseCaseTests { + + // MARK: - Happy Path Tests + + @Test("TC-MEMBER-010: 유효한 memberId로 멤버 삭제 성공") + func deleteMember_withValidIds_shouldSucceed() async throws { + // given + let travelId = "MOCK-1" + let memberId = "MOCKmember-1" + let useCase = DeleteTravelMemberUseCase() + + // when & then - 에러가 발생하지 않으면 성공 + try await useCase.execute(travelId: travelId, memberId: memberId) + } + + @Test("TC-MEMBER-012: 존재하지 않는 memberId로 삭제 시 성공") + func deleteMember_withInvalidMemberId_shouldSucceed() async throws { + /* + ┌─────────────────────────────────────────────────────┐ + │ Mock 동작 설명 │ + ├─────────────────────────────────────────────────────┤ + │ - MockTravelMemberRepository는 존재하지 않는 │ + │ memberId로 삭제해도 에러를 발생시키지 않음 │ + │ - filter를 사용하여 해당 멤버만 제외하므로 │ + │ 없는 멤버를 삭제해도 정상 동작함 │ + └─────────────────────────────────────────────────────┘ + */ + + // given + let travelId = "MOCK-1" + let memberId = "INVALID-MEMBER-ID" + let useCase = DeleteTravelMemberUseCase() + + // when & then - 존재하지 않는 memberId도 에러 없이 처리됨 + try await useCase.execute(travelId: travelId, memberId: memberId) + } + + // MARK: - Error Cases + + @Test("TC-MEMBER-011: 존재하지 않는 travelId로 삭제 시 에러") + func deleteMember_withInvalidTravelId_shouldThrowError() async throws { + // given + let travelId = "INVALID-TRAVEL-ID" + let memberId = "any-member-id" + let useCase = DeleteTravelMemberUseCase() + + // when & then + await #expect(throws: Error.self) { + try await useCase.execute(travelId: travelId, memberId: memberId) + } + } + + @Test("TC-MEMBER-013: 빈 문자열 travelId로 삭제 시 에러") + func deleteMember_withEmptyTravelId_shouldThrowError() async throws { + // given + let travelId = "" + let memberId = "any-member-id" + let useCase = DeleteTravelMemberUseCase() + + // when & then + await #expect(throws: Error.self) { + try await useCase.execute(travelId: travelId, memberId: memberId) + } + } + + // MARK: - Edge Cases + + @Test("TC-MEMBER-014: 다양한 travelId로 멤버 삭제", arguments: ["MOCK-2", "MOCK-3", "MOCK-4"]) + func deleteMember_withVariousTravelIds_shouldSucceed(travelId: String) async throws { + // given + let memberId = "MOCKmember-\(travelId.replacingOccurrences(of: "MOCK-", with: ""))" + let useCase = DeleteTravelMemberUseCase() + + // when & then + try await useCase.execute(travelId: travelId, memberId: memberId) + } + + @Test("TC-MEMBER-015: 동일 멤버 연속 삭제 시도") + func deleteMember_sameMemberTwice_shouldSucceedBothTimes() async throws { + /* + ┌─────────────────────────────────────────────────────┐ + │ 동작 설명 │ + ├─────────────────────────────────────────────────────┤ + │ - 첫 번째 삭제: 멤버가 실제로 제거됨 │ + │ - 두 번째 삭제: 이미 없는 멤버이므로 아무 동작 없음 │ + │ - 두 번 모두 에러 없이 완료됨 │ + └─────────────────────────────────────────────────────┘ + */ + + // given + let travelId = "MOCK-5" + let memberId = "MOCKmember-5" + let useCase = DeleteTravelMemberUseCase() + + // when & then - 두 번 삭제해도 에러 없음 + try await useCase.execute(travelId: travelId, memberId: memberId) + try await useCase.execute(travelId: travelId, memberId: memberId) + } +} diff --git a/Domain/Tests/UseCase/Member/FetchMemberUseCaseTests.swift b/Domain/Tests/UseCase/Member/FetchMemberUseCaseTests.swift new file mode 100644 index 0000000..ddb11f8 --- /dev/null +++ b/Domain/Tests/UseCase/Member/FetchMemberUseCaseTests.swift @@ -0,0 +1,102 @@ +// +// FetchMemberUseCaseTests.swift +// DomainTests +// +// Created by 홍석현 on 1/29/26. +// Generated by Test Automation System +// + +import Testing +import Foundation +@testable import Domain + +@Suite("멤버 조회 UseCase 테스트", .tags(.useCase, .member)) +struct FetchMemberUseCaseTests { + + // MARK: - Happy Path Tests + + @Test("TC-MEMBER-001: 유효한 travelId로 멤버 조회 성공") + func fetchMember_withValidTravelId_shouldReturnMyTravelMember() async throws { + // given + let travelId = "MOCK-1" + let useCase = FetchMemberUseCase() + + // when + let result = try await useCase.execute(travelId: travelId) + + // then + #expect(result.myInfo.id.isEmpty == false) + #expect(result.myInfo.name.isEmpty == false) + } + + @Test("TC-MEMBER-003: myInfo가 owner 역할인지 확인") + func fetchMember_shouldReturnOwnerAsMyInfo() async throws { + // given + let travelId = "MOCK-1" + let useCase = FetchMemberUseCase() + + // when + let result = try await useCase.execute(travelId: travelId) + + // then + #expect(result.myInfo.role == .owner) + } + + @Test("TC-MEMBER-005: memberInfo 배열 유효성 확인") + func fetchMember_shouldReturnValidMemberInfo() async throws { + // given + let travelId = "MOCK-1" + let useCase = FetchMemberUseCase() + + // when + let result = try await useCase.execute(travelId: travelId) + + // then + // memberInfo는 빈 배열일 수 있지만, 요소가 있다면 유효해야 함 + for member in result.memberInfo { + #expect(member.id.isEmpty == false) + #expect(member.name.isEmpty == false) + } + } + + // MARK: - Error Cases + + @Test("TC-MEMBER-002: 존재하지 않는 travelId로 조회 시 에러") + func fetchMember_withInvalidTravelId_shouldThrowError() async throws { + // given + let travelId = "INVALID-TRAVEL-ID" + let useCase = FetchMemberUseCase() + + // when & then + await #expect(throws: Error.self) { + _ = try await useCase.execute(travelId: travelId) + } + } + + @Test("TC-MEMBER-004: 빈 문자열 travelId로 조회 시 에러") + func fetchMember_withEmptyTravelId_shouldThrowError() async throws { + // given + let travelId = "" + let useCase = FetchMemberUseCase() + + // when & then + await #expect(throws: Error.self) { + _ = try await useCase.execute(travelId: travelId) + } + } + + // MARK: - Edge Cases + + @Test("TC-MEMBER-006: 다양한 travelId로 조회", arguments: ["MOCK-1", "MOCK-5", "MOCK-10", "MOCK-25"]) + func fetchMember_withVariousTravelIds_shouldSucceed(travelId: String) async throws { + // given + let useCase = FetchMemberUseCase() + + // when + let result = try await useCase.execute(travelId: travelId) + + // then + #expect(result.myInfo.id.isEmpty == false) + #expect(result.myInfo.name.isEmpty == false) + } +} diff --git a/Domain/Tests/UseCase/Member/JoinTravelUseCaseTests.swift b/Domain/Tests/UseCase/Member/JoinTravelUseCaseTests.swift new file mode 100644 index 0000000..6ace875 --- /dev/null +++ b/Domain/Tests/UseCase/Member/JoinTravelUseCaseTests.swift @@ -0,0 +1,161 @@ +// +// JoinTravelUseCaseTests.swift +// DomainTests +// +// Created by 홍석현 on 1/29/26. +// Generated by Test Automation System +// + +import Testing +import Foundation +@testable import Domain + +@Suite("여행 참여 UseCase 테스트", .tags(.useCase, .member)) +struct JoinTravelUseCaseTests { + + // MARK: - Happy Path Tests + + @Test("TC-MEMBER-030: 유효한 초대코드로 여행 참여 성공") + func joinTravel_withValidInviteCode_shouldReturnTravel() async throws { + // given + let inviteCode = "INV-0001" + let useCase = JoinTravelUseCase() + + // when + let result = try await useCase.execute(inviteCode: inviteCode) + + // then + #expect(result.id == "MOCK-1") + #expect(result.inviteCode == inviteCode) + } + + @Test("TC-MEMBER-032: 참여 후 멤버 수 증가 확인") + func joinTravel_shouldIncreaseMemberCount() async throws { + /* + ┌─────────────────────────────────────────────────────┐ + │ Mock 동작 설명 │ + ├─────────────────────────────────────────────────────┤ + │ - MockTravelMemberRepository는 joinTravel 호출 시 │ + │ 새로운 멤버를 members 배열에 추가함 │ + │ - 기존 멤버 1명 + 새 멤버 1명 = 최소 2명 │ + └─────────────────────────────────────────────────────┘ + */ + + // given + let inviteCode = "INV-0002" + let useCase = JoinTravelUseCase() + + // when + let result = try await useCase.execute(inviteCode: inviteCode) + + // then - 멤버가 최소 1명 이상 존재해야 함 + #expect(result.members.isEmpty == false) + } + + @Test("TC-MEMBER-035: 참여 후 반환된 Travel 정보 검증") + func joinTravel_shouldReturnValidTravelInfo() async throws { + // given + let inviteCode = "INV-0003" + let useCase = JoinTravelUseCase() + + // when + let result = try await useCase.execute(inviteCode: inviteCode) + + // then + #expect(result.id == "MOCK-3") + #expect(result.title == "여행 3") + #expect(result.status == .active) + #expect(result.ownerName.isEmpty == false) + } + + // MARK: - Error Cases + + @Test("TC-MEMBER-031: 잘못된 초대코드로 참여 시 에러") + func joinTravel_withInvalidInviteCode_shouldThrowError() async throws { + // given + let inviteCode = "INVALID-CODE" + let useCase = JoinTravelUseCase() + + // when & then + await #expect(throws: Error.self) { + _ = try await useCase.execute(inviteCode: inviteCode) + } + } + + @Test("TC-MEMBER-033: 빈 초대코드로 참여 시 에러") + func joinTravel_withEmptyInviteCode_shouldThrowError() async throws { + // given + let inviteCode = "" + let useCase = JoinTravelUseCase() + + // when & then + await #expect(throws: Error.self) { + _ = try await useCase.execute(inviteCode: inviteCode) + } + } + + // MARK: - Edge Cases + + @Test("TC-MEMBER-034: 다양한 형식의 초대코드 테스트", arguments: ["INV-0004", "INV-0005", "INV-0009"]) + func joinTravel_withVariousInviteCodes_shouldSucceed(inviteCode: String) async throws { + /* + ┌─────────────────────────────────────────────────────┐ + │ Mock 동작 설명 │ + ├─────────────────────────────────────────────────────┤ + │ - Mock의 초대코드 형식: "INV-000{i}" (i: 1~25) │ + │ - 예: INV-0001, INV-0002, ..., INV-00025 │ + │ - 10 이상의 경우 INV-00010 형식이 아닌 INV-00010 형식 │ + └─────────────────────────────────────────────────────┘ + */ + + // given + let useCase = JoinTravelUseCase() + + // when + let result = try await useCase.execute(inviteCode: inviteCode) + + // then + #expect(result.inviteCode == inviteCode) + #expect(result.id.isEmpty == false) + } + + @Test("TC-MEMBER-036: 동일 초대코드로 여러 번 참여") + func joinTravel_sameCodeMultipleTimes_shouldAddMultipleMembers() async throws { + /* + ┌─────────────────────────────────────────────────────┐ + │ Mock 동작 설명 │ + ├─────────────────────────────────────────────────────┤ + │ - 동일한 초대코드로 여러 번 참여하면 │ + │ 매번 새로운 멤버가 추가됨 │ + │ - 실제 서버에서는 중복 참여 방지 로직이 있을 수 있음 │ + └─────────────────────────────────────────────────────┘ + */ + + // given + let inviteCode = "INV-0006" + let useCase = JoinTravelUseCase() + + // when - 두 번 참여 + let firstResult = try await useCase.execute(inviteCode: inviteCode) + let secondResult = try await useCase.execute(inviteCode: inviteCode) + + // then - 멤버 수가 증가해야 함 + #expect(secondResult.members.count > firstResult.members.count) + } + + @Test("TC-MEMBER-037: 새로 추가된 멤버의 역할 확인") + func joinTravel_newMember_shouldHaveMemberRole() async throws { + // given + let inviteCode = "INV-0007" + let useCase = JoinTravelUseCase() + + // when + let result = try await useCase.execute(inviteCode: inviteCode) + + // then - 새로 추가된 멤버는 .member 역할이어야 함 + let newMembers = result.members.filter { $0.id.hasPrefix("MOCK_JOIN_") } + for member in newMembers { + #expect(member.role == .member) + } + } +} diff --git a/Domain/Tests/UseCase/Member/LeaveTravelUseCaseTests.swift b/Domain/Tests/UseCase/Member/LeaveTravelUseCaseTests.swift new file mode 100644 index 0000000..5cf9dcf --- /dev/null +++ b/Domain/Tests/UseCase/Member/LeaveTravelUseCaseTests.swift @@ -0,0 +1,110 @@ +// +// LeaveTravelUseCaseTests.swift +// DomainTests +// +// Created by 홍석현 on 1/29/26. +// Generated by Test Automation System +// + +import Testing +import Foundation +@testable import Domain + +@Suite("여행 탈퇴 UseCase 테스트", .tags(.useCase, .member)) +struct LeaveTravelUseCaseTests { + + // MARK: - Happy Path Tests + + @Test("TC-MEMBER-040: 유효한 travelId로 여행 탈퇴 성공") + func leaveTravel_withValidTravelId_shouldSucceed() async throws { + // given + let travelId = "MOCK-1" + let useCase = LeaveTravelUseCase() + + // when & then - 에러가 발생하지 않으면 성공 + try await useCase.execute(travelId: travelId) + } + + @Test("TC-MEMBER-044: 다양한 travelId로 탈퇴", arguments: ["MOCK-8", "MOCK-9", "MOCK-10"]) + func leaveTravel_withVariousTravelIds_shouldSucceed(travelId: String) async throws { + // given + let useCase = LeaveTravelUseCase() + + // when & then + try await useCase.execute(travelId: travelId) + } + + // MARK: - Error Cases + + @Test("TC-MEMBER-041: 존재하지 않는 travelId로 탈퇴 시 에러") + func leaveTravel_withInvalidTravelId_shouldThrowError() async throws { + // given + let travelId = "INVALID-TRAVEL-ID" + let useCase = LeaveTravelUseCase() + + // when & then + await #expect(throws: Error.self) { + try await useCase.execute(travelId: travelId) + } + } + + @Test("TC-MEMBER-042: 빈 문자열 travelId로 탈퇴 시 에러") + func leaveTravel_withEmptyTravelId_shouldThrowError() async throws { + // given + let travelId = "" + let useCase = LeaveTravelUseCase() + + // when & then + await #expect(throws: Error.self) { + try await useCase.execute(travelId: travelId) + } + } + + // MARK: - Edge Cases + + @Test("TC-MEMBER-043: 여러 번 탈퇴 시도") + func leaveTravel_multipleTimes_shouldSucceedBothTimes() async throws { + /* + ┌─────────────────────────────────────────────────────┐ + │ Mock 동작 설명 │ + ├─────────────────────────────────────────────────────┤ + │ - MockTravelMemberRepository는 leaveTravel 호출 시 │ + │ "MOCK_LEAVE" ID를 가진 멤버를 제거함 │ + │ - 이미 탈퇴한 상태에서 다시 탈퇴해도 에러 없음 │ + │ - 실제 서버에서는 이미 탈퇴한 사용자 처리가 다를 수 있음 │ + └─────────────────────────────────────────────────────┘ + */ + + // given + let travelId = "MOCK-11" + let useCase = LeaveTravelUseCase() + + // when & then - 두 번 탈퇴해도 에러 없음 + try await useCase.execute(travelId: travelId) + try await useCase.execute(travelId: travelId) + } + + @Test("TC-MEMBER-045: 연속적인 여러 여행 탈퇴") + func leaveTravel_multipleTrips_shouldSucceed() async throws { + // given + let travelIds = ["MOCK-12", "MOCK-13", "MOCK-14"] + let useCase = LeaveTravelUseCase() + + // when & then + for travelId in travelIds { + try await useCase.execute(travelId: travelId) + } + } + + @Test("TC-MEMBER-046: 특수 문자가 포함된 travelId로 탈퇴 시 에러") + func leaveTravel_withSpecialCharacters_shouldThrowError() async throws { + // given + let travelId = "MOCK@#$%" + let useCase = LeaveTravelUseCase() + + // when & then - Mock에서 찾을 수 없으므로 에러 발생 + await #expect(throws: Error.self) { + try await useCase.execute(travelId: travelId) + } + } +}