- Implement camera capture and plant identification workflow - Add Core Data persistence for plants, care schedules, and cached API data - Create collection view with grid/list layouts and filtering - Build plant detail views with care information display - Integrate Trefle botanical API for plant care data - Add local image storage for captured plant photos - Implement dependency injection container for testability - Include accessibility support throughout the app Bug fixes in this commit: - Fix Trefle API decoding by removing duplicate CodingKeys - Fix LocalCachedImage to load from correct PlantImages directory - Set dateAdded when saving plants for proper collection sorting Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
276 lines
7.8 KiB
Swift
276 lines
7.8 KiB
Swift
//
|
|
// MockPlantCollectionRepository.swift
|
|
// PlantGuideTests
|
|
//
|
|
// Mock implementation of PlantCollectionRepositoryProtocol for unit testing.
|
|
// Provides configurable behavior and call tracking for verification.
|
|
//
|
|
|
|
import Foundation
|
|
@testable import PlantGuide
|
|
|
|
// MARK: - MockPlantCollectionRepository
|
|
|
|
/// Mock implementation of PlantCollectionRepositoryProtocol for testing
|
|
final class MockPlantCollectionRepository: PlantCollectionRepositoryProtocol, @unchecked Sendable {
|
|
|
|
// MARK: - Storage
|
|
|
|
var plants: [UUID: Plant] = [:]
|
|
|
|
// MARK: - Call Tracking
|
|
|
|
var saveCallCount = 0
|
|
var fetchByIdCallCount = 0
|
|
var fetchAllCallCount = 0
|
|
var deleteCallCount = 0
|
|
var existsCallCount = 0
|
|
var updatePlantCallCount = 0
|
|
var searchCallCount = 0
|
|
var filterCallCount = 0
|
|
var getFavoritesCallCount = 0
|
|
var setFavoriteCallCount = 0
|
|
var getStatisticsCallCount = 0
|
|
|
|
// MARK: - Error Configuration
|
|
|
|
var shouldThrowOnSave = false
|
|
var shouldThrowOnFetch = false
|
|
var shouldThrowOnDelete = false
|
|
var shouldThrowOnExists = false
|
|
var shouldThrowOnUpdate = 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"]
|
|
)
|
|
|
|
// MARK: - Captured Values
|
|
|
|
var lastSavedPlant: Plant?
|
|
var lastDeletedPlantID: UUID?
|
|
var lastUpdatedPlant: Plant?
|
|
var lastSearchQuery: String?
|
|
var lastFilter: PlantFilter?
|
|
var lastSetFavoritePlantID: UUID?
|
|
var lastSetFavoriteValue: Bool?
|
|
|
|
// MARK: - Statistics Configuration
|
|
|
|
var statisticsToReturn: CollectionStatistics?
|
|
|
|
// 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
|
|
lastDeletedPlantID = id
|
|
if shouldThrowOnDelete {
|
|
throw errorToThrow
|
|
}
|
|
plants.removeValue(forKey: id)
|
|
}
|
|
|
|
// MARK: - PlantCollectionRepositoryProtocol Extensions
|
|
|
|
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
|
|
}
|
|
let lowercaseQuery = query.lowercased()
|
|
return plants.values.filter { plant in
|
|
plant.scientificName.lowercased().contains(lowercaseQuery) ||
|
|
plant.commonNames.contains { $0.lowercased().contains(lowercaseQuery) } ||
|
|
(plant.notes?.lowercased().contains(lowercaseQuery) ?? false)
|
|
}
|
|
}
|
|
|
|
func filterPlants(by filter: PlantFilter) async throws -> [Plant] {
|
|
filterCallCount += 1
|
|
lastFilter = filter
|
|
if shouldThrowOnFilter {
|
|
throw errorToThrow
|
|
}
|
|
|
|
var result = Array(plants.values)
|
|
|
|
// Apply search query
|
|
if let query = filter.searchQuery, !query.isEmpty {
|
|
let lowercaseQuery = query.lowercased()
|
|
result = result.filter { plant in
|
|
plant.scientificName.lowercased().contains(lowercaseQuery) ||
|
|
plant.commonNames.contains { $0.lowercased().contains(lowercaseQuery) }
|
|
}
|
|
}
|
|
|
|
// Filter by favorites
|
|
if let isFavorite = filter.isFavorite {
|
|
result = result.filter { $0.isFavorite == isFavorite }
|
|
}
|
|
|
|
// Filter by families
|
|
if let families = filter.families, !families.isEmpty {
|
|
result = result.filter { families.contains($0.family) }
|
|
}
|
|
|
|
// Filter by identification source
|
|
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 }
|
|
.sorted { $0.dateIdentified > $1.dateIdentified }
|
|
}
|
|
|
|
func setFavorite(plantID: UUID, isFavorite: Bool) async throws {
|
|
setFavoriteCallCount += 1
|
|
lastSetFavoritePlantID = plantID
|
|
lastSetFavoriteValue = isFavorite
|
|
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
|
|
}
|
|
|
|
if let statistics = statisticsToReturn {
|
|
return statistics
|
|
}
|
|
|
|
// Calculate statistics from current plants
|
|
var familyDistribution: [String: Int] = [:]
|
|
var sourceBreakdown: [IdentificationSource: Int] = [:]
|
|
|
|
for plant in plants.values {
|
|
familyDistribution[plant.family, default: 0] += 1
|
|
sourceBreakdown[plant.identificationSource, default: 0] += 1
|
|
}
|
|
|
|
return CollectionStatistics(
|
|
totalPlants: plants.count,
|
|
favoriteCount: plants.values.filter { $0.isFavorite }.count,
|
|
familyDistribution: familyDistribution,
|
|
identificationSourceBreakdown: sourceBreakdown,
|
|
plantsAddedThisMonth: 0,
|
|
upcomingTasksCount: 0,
|
|
overdueTasksCount: 0
|
|
)
|
|
}
|
|
|
|
// MARK: - Helper Methods
|
|
|
|
/// Resets all state for clean test setup
|
|
func reset() {
|
|
plants = [:]
|
|
|
|
saveCallCount = 0
|
|
fetchByIdCallCount = 0
|
|
fetchAllCallCount = 0
|
|
deleteCallCount = 0
|
|
existsCallCount = 0
|
|
updatePlantCallCount = 0
|
|
searchCallCount = 0
|
|
filterCallCount = 0
|
|
getFavoritesCallCount = 0
|
|
setFavoriteCallCount = 0
|
|
getStatisticsCallCount = 0
|
|
|
|
shouldThrowOnSave = false
|
|
shouldThrowOnFetch = false
|
|
shouldThrowOnDelete = false
|
|
shouldThrowOnExists = false
|
|
shouldThrowOnUpdate = false
|
|
shouldThrowOnSearch = false
|
|
shouldThrowOnFilter = false
|
|
shouldThrowOnGetFavorites = false
|
|
shouldThrowOnSetFavorite = false
|
|
shouldThrowOnGetStatistics = false
|
|
|
|
lastSavedPlant = nil
|
|
lastDeletedPlantID = nil
|
|
lastUpdatedPlant = nil
|
|
lastSearchQuery = nil
|
|
lastFilter = nil
|
|
lastSetFavoritePlantID = nil
|
|
lastSetFavoriteValue = nil
|
|
statisticsToReturn = nil
|
|
}
|
|
|
|
/// Adds a plant directly to storage (bypasses save method)
|
|
func addPlant(_ plant: Plant) {
|
|
plants[plant.id] = plant
|
|
}
|
|
|
|
/// Adds multiple plants directly to storage
|
|
func addPlants(_ plantsToAdd: [Plant]) {
|
|
for plant in plantsToAdd {
|
|
plants[plant.id] = plant
|
|
}
|
|
}
|
|
}
|