- Update Plant test fixtures to use roomID instead of deprecated location - Add URLDataFetcher protocol to ImageCache for dependency injection - Update ImageCacheTests to use protocol-based mock instead of URLSession subclass - Add missing cancelReminders(for:plantID:) method to MockNotificationService - Add Equatable conformance to ImageCacheError for test assertions Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
651 lines
21 KiB
Swift
651 lines
21 KiB
Swift
//
|
|
// UpdatePlantUseCaseTests.swift
|
|
// PlantGuideTests
|
|
//
|
|
// Unit tests for UpdatePlantUseCase - the use case for updating plant entities
|
|
// in the user's collection.
|
|
//
|
|
|
|
import XCTest
|
|
@testable import PlantGuide
|
|
|
|
// MARK: - Protocol Extension for Testing
|
|
|
|
/// Extension to add exists method to PlantCollectionRepositoryProtocol for testing
|
|
/// This matches the implementation in concrete repository classes
|
|
extension PlantCollectionRepositoryProtocol {
|
|
func exists(id: UUID) async throws -> Bool {
|
|
let plant = try await fetch(id: id)
|
|
return plant != nil
|
|
}
|
|
}
|
|
|
|
// MARK: - Mock Plant Collection Repository
|
|
|
|
/// Mock implementation of PlantCollectionRepositoryProtocol for testing UpdatePlantUseCase
|
|
final class UpdatePlantTestMockRepository: PlantCollectionRepositoryProtocol, @unchecked Sendable {
|
|
|
|
// MARK: - Properties for Testing
|
|
|
|
var plants: [UUID: Plant] = [:]
|
|
var existsCallCount = 0
|
|
var updatePlantCallCount = 0
|
|
var saveCallCount = 0
|
|
var fetchByIdCallCount = 0
|
|
var fetchAllCallCount = 0
|
|
var deleteCallCount = 0
|
|
var searchCallCount = 0
|
|
var filterCallCount = 0
|
|
var getFavoritesCallCount = 0
|
|
var setFavoriteCallCount = 0
|
|
var getStatisticsCallCount = 0
|
|
|
|
var shouldThrowOnExists = false
|
|
var shouldThrowOnUpdate = false
|
|
var shouldThrowOnSave = false
|
|
var shouldThrowOnFetch = false
|
|
var shouldThrowOnDelete = false
|
|
var shouldThrowOnSearch = false
|
|
var shouldThrowOnFilter = false
|
|
var shouldThrowOnGetFavorites = false
|
|
var shouldThrowOnSetFavorite = false
|
|
var shouldThrowOnGetStatistics = false
|
|
|
|
var errorToThrow: Error = NSError(domain: "MockError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Mock repository error"])
|
|
|
|
var lastUpdatedPlant: Plant?
|
|
var lastSavedPlant: Plant?
|
|
var lastSearchQuery: String?
|
|
var lastFilter: PlantFilter?
|
|
|
|
// MARK: - PlantRepositoryProtocol
|
|
|
|
func save(_ plant: Plant) async throws {
|
|
saveCallCount += 1
|
|
lastSavedPlant = plant
|
|
if shouldThrowOnSave {
|
|
throw errorToThrow
|
|
}
|
|
plants[plant.id] = plant
|
|
}
|
|
|
|
func fetch(id: UUID) async throws -> Plant? {
|
|
fetchByIdCallCount += 1
|
|
if shouldThrowOnFetch {
|
|
throw errorToThrow
|
|
}
|
|
return plants[id]
|
|
}
|
|
|
|
func fetchAll() async throws -> [Plant] {
|
|
fetchAllCallCount += 1
|
|
if shouldThrowOnFetch {
|
|
throw errorToThrow
|
|
}
|
|
return Array(plants.values)
|
|
}
|
|
|
|
func delete(id: UUID) async throws {
|
|
deleteCallCount += 1
|
|
if shouldThrowOnDelete {
|
|
throw errorToThrow
|
|
}
|
|
plants.removeValue(forKey: id)
|
|
}
|
|
|
|
// MARK: - PlantCollectionRepositoryProtocol - Additional Methods
|
|
|
|
func exists(id: UUID) async throws -> Bool {
|
|
existsCallCount += 1
|
|
if shouldThrowOnExists {
|
|
throw errorToThrow
|
|
}
|
|
return plants[id] != nil
|
|
}
|
|
|
|
func updatePlant(_ plant: Plant) async throws {
|
|
updatePlantCallCount += 1
|
|
lastUpdatedPlant = plant
|
|
if shouldThrowOnUpdate {
|
|
throw errorToThrow
|
|
}
|
|
plants[plant.id] = plant
|
|
}
|
|
|
|
func searchPlants(query: String) async throws -> [Plant] {
|
|
searchCallCount += 1
|
|
lastSearchQuery = query
|
|
if shouldThrowOnSearch {
|
|
throw errorToThrow
|
|
}
|
|
return plants.values.filter { plant in
|
|
plant.scientificName.lowercased().contains(query.lowercased()) ||
|
|
plant.commonNames.contains { $0.lowercased().contains(query.lowercased()) }
|
|
}
|
|
}
|
|
|
|
func filterPlants(by filter: PlantFilter) async throws -> [Plant] {
|
|
filterCallCount += 1
|
|
lastFilter = filter
|
|
if shouldThrowOnFilter {
|
|
throw errorToThrow
|
|
}
|
|
var result = Array(plants.values)
|
|
|
|
if let isFavorite = filter.isFavorite {
|
|
result = result.filter { $0.isFavorite == isFavorite }
|
|
}
|
|
|
|
if let families = filter.families {
|
|
result = result.filter { families.contains($0.family) }
|
|
}
|
|
|
|
if let source = filter.identificationSource {
|
|
result = result.filter { $0.identificationSource == source }
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func getFavorites() async throws -> [Plant] {
|
|
getFavoritesCallCount += 1
|
|
if shouldThrowOnGetFavorites {
|
|
throw errorToThrow
|
|
}
|
|
return plants.values.filter { $0.isFavorite }
|
|
}
|
|
|
|
func setFavorite(plantID: UUID, isFavorite: Bool) async throws {
|
|
setFavoriteCallCount += 1
|
|
if shouldThrowOnSetFavorite {
|
|
throw errorToThrow
|
|
}
|
|
if var plant = plants[plantID] {
|
|
plant.isFavorite = isFavorite
|
|
plants[plantID] = plant
|
|
}
|
|
}
|
|
|
|
func getCollectionStatistics() async throws -> CollectionStatistics {
|
|
getStatisticsCallCount += 1
|
|
if shouldThrowOnGetStatistics {
|
|
throw errorToThrow
|
|
}
|
|
return CollectionStatistics(
|
|
totalPlants: plants.count,
|
|
favoriteCount: plants.values.filter { $0.isFavorite }.count,
|
|
familyDistribution: [:],
|
|
identificationSourceBreakdown: [:],
|
|
plantsAddedThisMonth: 0,
|
|
upcomingTasksCount: 0,
|
|
overdueTasksCount: 0
|
|
)
|
|
}
|
|
|
|
// MARK: - Helper Methods
|
|
|
|
func reset() {
|
|
plants = [:]
|
|
existsCallCount = 0
|
|
updatePlantCallCount = 0
|
|
saveCallCount = 0
|
|
fetchByIdCallCount = 0
|
|
fetchAllCallCount = 0
|
|
deleteCallCount = 0
|
|
searchCallCount = 0
|
|
filterCallCount = 0
|
|
getFavoritesCallCount = 0
|
|
setFavoriteCallCount = 0
|
|
getStatisticsCallCount = 0
|
|
|
|
shouldThrowOnExists = false
|
|
shouldThrowOnUpdate = false
|
|
shouldThrowOnSave = false
|
|
shouldThrowOnFetch = false
|
|
shouldThrowOnDelete = false
|
|
shouldThrowOnSearch = false
|
|
shouldThrowOnFilter = false
|
|
shouldThrowOnGetFavorites = false
|
|
shouldThrowOnSetFavorite = false
|
|
shouldThrowOnGetStatistics = false
|
|
|
|
lastUpdatedPlant = nil
|
|
lastSavedPlant = nil
|
|
lastSearchQuery = nil
|
|
lastFilter = nil
|
|
}
|
|
|
|
func addPlant(_ plant: Plant) {
|
|
plants[plant.id] = plant
|
|
}
|
|
}
|
|
|
|
// MARK: - UpdatePlantUseCaseTests
|
|
|
|
final class UpdatePlantUseCaseTests: XCTestCase {
|
|
|
|
// MARK: - Properties
|
|
|
|
private var sut: UpdatePlantUseCase!
|
|
private var mockRepository: UpdatePlantTestMockRepository!
|
|
|
|
// MARK: - Test Lifecycle
|
|
|
|
override func setUp() {
|
|
super.setUp()
|
|
mockRepository = UpdatePlantTestMockRepository()
|
|
sut = UpdatePlantUseCase(plantRepository: mockRepository)
|
|
}
|
|
|
|
override func tearDown() {
|
|
sut = nil
|
|
mockRepository = nil
|
|
super.tearDown()
|
|
}
|
|
|
|
// MARK: - Test Helpers
|
|
|
|
private func createTestPlant(
|
|
id: UUID = UUID(),
|
|
scientificName: String = "Monstera deliciosa",
|
|
commonNames: [String] = ["Swiss Cheese Plant"],
|
|
family: String = "Araceae",
|
|
genus: String = "Monstera",
|
|
isFavorite: Bool = false,
|
|
notes: String? = nil,
|
|
customName: String? = nil,
|
|
roomID: UUID? = nil
|
|
) -> Plant {
|
|
Plant(
|
|
id: id,
|
|
scientificName: scientificName,
|
|
commonNames: commonNames,
|
|
family: family,
|
|
genus: genus,
|
|
identificationSource: .onDeviceML,
|
|
notes: notes,
|
|
isFavorite: isFavorite,
|
|
customName: customName,
|
|
roomID: roomID
|
|
)
|
|
}
|
|
|
|
// MARK: - execute() Success Tests
|
|
|
|
func testExecute_WhenPlantExistsAndDataIsValid_SuccessfullyUpdatesPlant() async throws {
|
|
// Given
|
|
let plantID = UUID()
|
|
let originalPlant = createTestPlant(id: plantID, notes: "Original notes")
|
|
mockRepository.addPlant(originalPlant)
|
|
|
|
let roomID = UUID()
|
|
var updatedPlant = originalPlant
|
|
updatedPlant.notes = "Updated notes"
|
|
updatedPlant.customName = "My Monstera"
|
|
updatedPlant.roomID = roomID
|
|
|
|
// When
|
|
let result = try await sut.execute(plant: updatedPlant)
|
|
|
|
// Then
|
|
XCTAssertEqual(result.id, plantID)
|
|
XCTAssertEqual(result.notes, "Updated notes")
|
|
XCTAssertEqual(result.customName, "My Monstera")
|
|
XCTAssertEqual(result.roomID, roomID)
|
|
|
|
XCTAssertEqual(mockRepository.existsCallCount, 1)
|
|
XCTAssertEqual(mockRepository.updatePlantCallCount, 1)
|
|
XCTAssertEqual(mockRepository.lastUpdatedPlant?.id, plantID)
|
|
}
|
|
|
|
func testExecute_WhenUpdatingFavoriteStatus_SuccessfullyUpdates() async throws {
|
|
// Given
|
|
let plantID = UUID()
|
|
let originalPlant = createTestPlant(id: plantID, isFavorite: false)
|
|
mockRepository.addPlant(originalPlant)
|
|
|
|
var updatedPlant = originalPlant
|
|
updatedPlant.isFavorite = true
|
|
|
|
// When
|
|
let result = try await sut.execute(plant: updatedPlant)
|
|
|
|
// Then
|
|
XCTAssertTrue(result.isFavorite)
|
|
XCTAssertEqual(mockRepository.updatePlantCallCount, 1)
|
|
}
|
|
|
|
func testExecute_WhenUpdatingOnlyNotes_SuccessfullyUpdates() async throws {
|
|
// Given
|
|
let plantID = UUID()
|
|
let originalPlant = createTestPlant(id: plantID)
|
|
mockRepository.addPlant(originalPlant)
|
|
|
|
var updatedPlant = originalPlant
|
|
updatedPlant.notes = "This plant needs more water during summer"
|
|
|
|
// When
|
|
let result = try await sut.execute(plant: updatedPlant)
|
|
|
|
// Then
|
|
XCTAssertEqual(result.notes, "This plant needs more water during summer")
|
|
}
|
|
|
|
func testExecute_WhenUpdatingOnlyCustomName_SuccessfullyUpdates() async throws {
|
|
// Given
|
|
let plantID = UUID()
|
|
let originalPlant = createTestPlant(id: plantID)
|
|
mockRepository.addPlant(originalPlant)
|
|
|
|
var updatedPlant = originalPlant
|
|
updatedPlant.customName = "Bob the Plant"
|
|
|
|
// When
|
|
let result = try await sut.execute(plant: updatedPlant)
|
|
|
|
// Then
|
|
XCTAssertEqual(result.customName, "Bob the Plant")
|
|
}
|
|
|
|
func testExecute_WhenUpdatingOnlyRoomID_SuccessfullyUpdates() async throws {
|
|
// Given
|
|
let plantID = UUID()
|
|
let originalPlant = createTestPlant(id: plantID)
|
|
mockRepository.addPlant(originalPlant)
|
|
|
|
let kitchenRoomID = UUID()
|
|
var updatedPlant = originalPlant
|
|
updatedPlant.roomID = kitchenRoomID
|
|
|
|
// When
|
|
let result = try await sut.execute(plant: updatedPlant)
|
|
|
|
// Then
|
|
XCTAssertEqual(result.roomID, kitchenRoomID)
|
|
}
|
|
|
|
func testExecute_PreservesImmutableProperties() async throws {
|
|
// Given
|
|
let plantID = UUID()
|
|
let originalPlant = createTestPlant(
|
|
id: plantID,
|
|
scientificName: "Monstera deliciosa",
|
|
commonNames: ["Swiss Cheese Plant"],
|
|
family: "Araceae",
|
|
genus: "Monstera"
|
|
)
|
|
mockRepository.addPlant(originalPlant)
|
|
|
|
var updatedPlant = originalPlant
|
|
updatedPlant.notes = "New notes"
|
|
|
|
// When
|
|
let result = try await sut.execute(plant: updatedPlant)
|
|
|
|
// Then - Immutable properties should be preserved
|
|
XCTAssertEqual(result.scientificName, "Monstera deliciosa")
|
|
XCTAssertEqual(result.commonNames, ["Swiss Cheese Plant"])
|
|
XCTAssertEqual(result.family, "Araceae")
|
|
XCTAssertEqual(result.genus, "Monstera")
|
|
XCTAssertEqual(result.identificationSource, .onDeviceML)
|
|
}
|
|
|
|
// MARK: - execute() Throws plantNotFound Tests
|
|
|
|
func testExecute_WhenPlantDoesNotExist_ThrowsPlantNotFound() async {
|
|
// Given
|
|
let nonExistentPlantID = UUID()
|
|
let plant = createTestPlant(id: nonExistentPlantID)
|
|
// Don't add plant to repository
|
|
|
|
// When/Then
|
|
do {
|
|
_ = try await sut.execute(plant: plant)
|
|
XCTFail("Expected plantNotFound error to be thrown")
|
|
} catch let error as UpdatePlantError {
|
|
switch error {
|
|
case .plantNotFound(let plantID):
|
|
XCTAssertEqual(plantID, nonExistentPlantID)
|
|
default:
|
|
XCTFail("Expected plantNotFound error, got \(error)")
|
|
}
|
|
} catch {
|
|
XCTFail("Expected UpdatePlantError, got \(error)")
|
|
}
|
|
|
|
XCTAssertEqual(mockRepository.existsCallCount, 1)
|
|
XCTAssertEqual(mockRepository.updatePlantCallCount, 0)
|
|
}
|
|
|
|
func testExecute_WhenPlantWasDeleted_ThrowsPlantNotFound() async {
|
|
// Given
|
|
let plantID = UUID()
|
|
let plant = createTestPlant(id: plantID)
|
|
mockRepository.addPlant(plant)
|
|
|
|
// Simulate plant being deleted before update
|
|
mockRepository.plants.removeValue(forKey: plantID)
|
|
|
|
// When/Then
|
|
do {
|
|
_ = try await sut.execute(plant: plant)
|
|
XCTFail("Expected plantNotFound error to be thrown")
|
|
} catch let error as UpdatePlantError {
|
|
switch error {
|
|
case .plantNotFound(let id):
|
|
XCTAssertEqual(id, plantID)
|
|
default:
|
|
XCTFail("Expected plantNotFound error, got \(error)")
|
|
}
|
|
} catch {
|
|
XCTFail("Expected UpdatePlantError, got \(error)")
|
|
}
|
|
}
|
|
|
|
// MARK: - execute() Throws invalidPlantData Tests
|
|
|
|
func testExecute_WhenScientificNameIsEmpty_ThrowsInvalidPlantData() async {
|
|
// Given
|
|
let plantID = UUID()
|
|
let originalPlant = createTestPlant(id: plantID, scientificName: "Monstera deliciosa")
|
|
mockRepository.addPlant(originalPlant)
|
|
|
|
// Create a plant with empty scientific name (invalid)
|
|
let invalidPlant = Plant(
|
|
id: plantID,
|
|
scientificName: "",
|
|
commonNames: ["Swiss Cheese Plant"],
|
|
family: "Araceae",
|
|
genus: "Monstera",
|
|
identificationSource: .onDeviceML
|
|
)
|
|
|
|
// When/Then
|
|
do {
|
|
_ = try await sut.execute(plant: invalidPlant)
|
|
XCTFail("Expected invalidPlantData error to be thrown")
|
|
} catch let error as UpdatePlantError {
|
|
switch error {
|
|
case .invalidPlantData(let reason):
|
|
XCTAssertTrue(reason.contains("Scientific name"))
|
|
default:
|
|
XCTFail("Expected invalidPlantData error, got \(error)")
|
|
}
|
|
} catch {
|
|
XCTFail("Expected UpdatePlantError, got \(error)")
|
|
}
|
|
|
|
XCTAssertEqual(mockRepository.existsCallCount, 1)
|
|
XCTAssertEqual(mockRepository.updatePlantCallCount, 0)
|
|
}
|
|
|
|
func testExecute_WhenScientificNameIsWhitespaceOnly_ThrowsInvalidPlantData() async {
|
|
// Given
|
|
let plantID = UUID()
|
|
let originalPlant = createTestPlant(id: plantID)
|
|
mockRepository.addPlant(originalPlant)
|
|
|
|
// Note: The current implementation only checks for empty string, not whitespace
|
|
// This test documents the current behavior
|
|
let whitespaceOnlyPlant = Plant(
|
|
id: plantID,
|
|
scientificName: " ",
|
|
commonNames: ["Swiss Cheese Plant"],
|
|
family: "Araceae",
|
|
genus: "Monstera",
|
|
identificationSource: .onDeviceML
|
|
)
|
|
|
|
// When
|
|
// Current implementation does not trim whitespace, so this will succeed
|
|
// If the implementation changes to validate whitespace, this test should be updated
|
|
let result = try? await sut.execute(plant: whitespaceOnlyPlant)
|
|
|
|
// Then
|
|
// Documenting current behavior - whitespace-only scientific names are allowed
|
|
XCTAssertNotNil(result)
|
|
}
|
|
|
|
// MARK: - execute() Throws repositoryUpdateFailed Tests
|
|
|
|
func testExecute_WhenRepositoryUpdateFails_ThrowsRepositoryUpdateFailed() async {
|
|
// Given
|
|
let plantID = UUID()
|
|
let plant = createTestPlant(id: plantID)
|
|
mockRepository.addPlant(plant)
|
|
|
|
let underlyingError = NSError(domain: "CoreData", code: 500, userInfo: [NSLocalizedDescriptionKey: "Database error"])
|
|
mockRepository.shouldThrowOnUpdate = true
|
|
mockRepository.errorToThrow = underlyingError
|
|
|
|
// When/Then
|
|
do {
|
|
_ = try await sut.execute(plant: plant)
|
|
XCTFail("Expected repositoryUpdateFailed error to be thrown")
|
|
} catch let error as UpdatePlantError {
|
|
switch error {
|
|
case .repositoryUpdateFailed(let wrappedError):
|
|
XCTAssertEqual((wrappedError as NSError).domain, "CoreData")
|
|
XCTAssertEqual((wrappedError as NSError).code, 500)
|
|
default:
|
|
XCTFail("Expected repositoryUpdateFailed error, got \(error)")
|
|
}
|
|
} catch {
|
|
XCTFail("Expected UpdatePlantError, got \(error)")
|
|
}
|
|
|
|
XCTAssertEqual(mockRepository.existsCallCount, 1)
|
|
XCTAssertEqual(mockRepository.updatePlantCallCount, 1)
|
|
}
|
|
|
|
func testExecute_WhenRepositoryThrowsNetworkError_ThrowsRepositoryUpdateFailed() async {
|
|
// Given
|
|
let plantID = UUID()
|
|
let plant = createTestPlant(id: plantID)
|
|
mockRepository.addPlant(plant)
|
|
|
|
let networkError = NSError(domain: NSURLErrorDomain, code: NSURLErrorNotConnectedToInternet)
|
|
mockRepository.shouldThrowOnUpdate = true
|
|
mockRepository.errorToThrow = networkError
|
|
|
|
// When/Then
|
|
do {
|
|
_ = try await sut.execute(plant: plant)
|
|
XCTFail("Expected repositoryUpdateFailed error to be thrown")
|
|
} catch let error as UpdatePlantError {
|
|
switch error {
|
|
case .repositoryUpdateFailed(let wrappedError):
|
|
XCTAssertEqual((wrappedError as NSError).domain, NSURLErrorDomain)
|
|
default:
|
|
XCTFail("Expected repositoryUpdateFailed error, got \(error)")
|
|
}
|
|
} catch {
|
|
XCTFail("Expected UpdatePlantError, got \(error)")
|
|
}
|
|
}
|
|
|
|
// MARK: - Error Description Tests
|
|
|
|
func testUpdatePlantError_PlantNotFound_HasCorrectDescription() {
|
|
// Given
|
|
let plantID = UUID()
|
|
let error = UpdatePlantError.plantNotFound(plantID: plantID)
|
|
|
|
// Then
|
|
XCTAssertTrue(error.errorDescription?.contains(plantID.uuidString) ?? false)
|
|
XCTAssertNotNil(error.failureReason)
|
|
XCTAssertNotNil(error.recoverySuggestion)
|
|
}
|
|
|
|
func testUpdatePlantError_InvalidPlantData_HasCorrectDescription() {
|
|
// Given
|
|
let error = UpdatePlantError.invalidPlantData(reason: "Scientific name cannot be empty")
|
|
|
|
// Then
|
|
XCTAssertTrue(error.errorDescription?.contains("Scientific name") ?? false)
|
|
XCTAssertNotNil(error.failureReason)
|
|
XCTAssertNotNil(error.recoverySuggestion)
|
|
}
|
|
|
|
func testUpdatePlantError_RepositoryUpdateFailed_HasCorrectDescription() {
|
|
// Given
|
|
let underlyingError = NSError(domain: "Test", code: 123, userInfo: [NSLocalizedDescriptionKey: "Underlying error"])
|
|
let error = UpdatePlantError.repositoryUpdateFailed(underlyingError)
|
|
|
|
// Then
|
|
XCTAssertTrue(error.errorDescription?.contains("Underlying error") ?? false)
|
|
XCTAssertNotNil(error.failureReason)
|
|
XCTAssertNotNil(error.recoverySuggestion)
|
|
}
|
|
|
|
// MARK: - Protocol Conformance Tests
|
|
|
|
func testUpdatePlantUseCase_ConformsToProtocol() {
|
|
// Then
|
|
XCTAssertTrue(sut is UpdatePlantUseCaseProtocol)
|
|
}
|
|
|
|
// MARK: - Edge Cases
|
|
|
|
func testExecute_WithMultipleConcurrentUpdates_HandlesCorrectly() async throws {
|
|
// Given
|
|
let plantID = UUID()
|
|
let plant = createTestPlant(id: plantID)
|
|
mockRepository.addPlant(plant)
|
|
|
|
// When - Perform multiple concurrent updates
|
|
await withTaskGroup(of: Void.self) { group in
|
|
for i in 0..<10 {
|
|
group.addTask { [sut, mockRepository] in
|
|
var updatedPlant = plant
|
|
updatedPlant.notes = "Update \(i)"
|
|
_ = try? await sut!.execute(plant: updatedPlant)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Then - All updates should complete
|
|
XCTAssertEqual(mockRepository.updatePlantCallCount, 10)
|
|
}
|
|
|
|
func testExecute_WhenExistsCheckThrows_PropagatesError() async {
|
|
// Given
|
|
let plantID = UUID()
|
|
let plant = createTestPlant(id: plantID)
|
|
mockRepository.addPlant(plant)
|
|
mockRepository.shouldThrowOnExists = true
|
|
|
|
// When/Then
|
|
do {
|
|
_ = try await sut.execute(plant: plant)
|
|
XCTFail("Expected error to be thrown")
|
|
} catch {
|
|
// Error should be propagated (wrapped or as-is)
|
|
XCTAssertNotNil(error)
|
|
}
|
|
|
|
XCTAssertEqual(mockRepository.existsCallCount, 1)
|
|
XCTAssertEqual(mockRepository.updatePlantCallCount, 0)
|
|
}
|
|
}
|