Files
PlantGuide/PlantGuideTests/CoreDataCareScheduleStorageTests.swift
treyt 60189a5406 fix: issue #17 - upcoming tasks
Automated fix by Tony CI v3.
Refs #17

Co-Authored-By: Claude <noreply@anthropic.com>
2026-03-09 22:23:56 -05:00

1011 lines
33 KiB
Swift

//
// CoreDataCareScheduleStorageTests.swift
// PlantGuideTests
//
// Unit tests for CoreDataCareScheduleStorage - the Core Data implementation
// of care schedule persistence.
//
import XCTest
import CoreData
@testable import PlantGuide
// MARK: - Mock Core Data Stack
/// Mock implementation of CoreDataStackProtocol for testing
final class MockCoreDataStack: CoreDataStackProtocol, @unchecked Sendable {
private let persistentContainer: NSPersistentContainer
init() {
// Create an in-memory persistent container for testing
let modelURL = Bundle.main.url(forResource: "PlantGuideModel", withExtension: "momd")
let managedObjectModel: NSManagedObjectModel
if let modelURL = modelURL {
managedObjectModel = NSManagedObjectModel(contentsOf: modelURL)!
} else {
// Create a minimal model programmatically for testing if model file not found
managedObjectModel = MockCoreDataStack.createTestModel()
}
persistentContainer = NSPersistentContainer(name: "PlantGuideModel", managedObjectModel: managedObjectModel)
let description = NSPersistentStoreDescription()
description.type = NSInMemoryStoreType
description.shouldAddStoreAsynchronously = false
persistentContainer.persistentStoreDescriptions = [description]
persistentContainer.loadPersistentStores { _, error in
if let error = error {
fatalError("Failed to load in-memory store: \(error)")
}
}
persistentContainer.viewContext.automaticallyMergesChangesFromParent = true
}
func viewContext() -> NSManagedObjectContext {
return persistentContainer.viewContext
}
func newBackgroundContext() -> NSManagedObjectContext {
let context = persistentContainer.newBackgroundContext()
context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
context.automaticallyMergesChangesFromParent = true
return context
}
func performBackgroundTask<T: Sendable>(_ block: @escaping @Sendable (NSManagedObjectContext) throws -> T) async throws -> T {
let context = newBackgroundContext()
return try await withCheckedThrowingContinuation { continuation in
context.perform {
do {
let result = try block(context)
if context.hasChanges {
try context.save()
}
continuation.resume(returning: result)
} catch {
continuation.resume(throwing: error)
}
}
}
}
func save(context: NSManagedObjectContext) throws {
guard context.hasChanges else { return }
try context.save()
}
func reset() throws {
let context = viewContext()
let entityNames = persistentContainer.managedObjectModel.entities.compactMap { $0.name }
for entityName in entityNames {
let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: entityName)
let objects = try context.fetch(fetchRequest)
for object in objects {
context.delete(object)
}
}
try save(context: context)
}
/// Creates a minimal test model programmatically
private static func createTestModel() -> NSManagedObjectModel {
let model = NSManagedObjectModel()
// CareScheduleMO Entity
let scheduleEntity = NSEntityDescription()
scheduleEntity.name = "CareScheduleMO"
scheduleEntity.managedObjectClassName = NSStringFromClass(CareScheduleMO.self)
let scheduleIdAttr = NSAttributeDescription()
scheduleIdAttr.name = "id"
scheduleIdAttr.attributeType = .UUIDAttributeType
let plantIdAttr = NSAttributeDescription()
plantIdAttr.name = "plantID"
plantIdAttr.attributeType = .UUIDAttributeType
let lightReqAttr = NSAttributeDescription()
lightReqAttr.name = "lightRequirement"
lightReqAttr.attributeType = .stringAttributeType
let wateringAttr = NSAttributeDescription()
wateringAttr.name = "wateringSchedule"
wateringAttr.attributeType = .stringAttributeType
let tempMinAttr = NSAttributeDescription()
tempMinAttr.name = "temperatureMin"
tempMinAttr.attributeType = .integer32AttributeType
let tempMaxAttr = NSAttributeDescription()
tempMaxAttr.name = "temperatureMax"
tempMaxAttr.attributeType = .integer32AttributeType
let fertilizerAttr = NSAttributeDescription()
fertilizerAttr.name = "fertilizerSchedule"
fertilizerAttr.attributeType = .stringAttributeType
scheduleEntity.properties = [
scheduleIdAttr, plantIdAttr, lightReqAttr, wateringAttr,
tempMinAttr, tempMaxAttr, fertilizerAttr
]
// CareTaskMO Entity
let taskEntity = NSEntityDescription()
taskEntity.name = "CareTaskMO"
taskEntity.managedObjectClassName = NSStringFromClass(CareTaskMO.self)
let taskIdAttr = NSAttributeDescription()
taskIdAttr.name = "id"
taskIdAttr.attributeType = .UUIDAttributeType
let taskPlantIdAttr = NSAttributeDescription()
taskPlantIdAttr.name = "plantID"
taskPlantIdAttr.attributeType = .UUIDAttributeType
let typeAttr = NSAttributeDescription()
typeAttr.name = "type"
typeAttr.attributeType = .stringAttributeType
let scheduledDateAttr = NSAttributeDescription()
scheduledDateAttr.name = "scheduledDate"
scheduledDateAttr.attributeType = .dateAttributeType
let completedDateAttr = NSAttributeDescription()
completedDateAttr.name = "completedDate"
completedDateAttr.attributeType = .dateAttributeType
completedDateAttr.isOptional = true
let notesAttr = NSAttributeDescription()
notesAttr.name = "notes"
notesAttr.attributeType = .stringAttributeType
taskEntity.properties = [
taskIdAttr, taskPlantIdAttr, typeAttr, scheduledDateAttr, completedDateAttr, notesAttr
]
// Relationships
let tasksRelation = NSRelationshipDescription()
tasksRelation.name = "tasks"
tasksRelation.destinationEntity = taskEntity
tasksRelation.isOptional = true
tasksRelation.deleteRule = .cascadeDeleteRule
let scheduleRelation = NSRelationshipDescription()
scheduleRelation.name = "careSchedule"
scheduleRelation.destinationEntity = scheduleEntity
scheduleRelation.maxCount = 1
scheduleRelation.isOptional = true
tasksRelation.inverseRelationship = scheduleRelation
scheduleRelation.inverseRelationship = tasksRelation
scheduleEntity.properties.append(tasksRelation)
taskEntity.properties.append(scheduleRelation)
model.entities = [scheduleEntity, taskEntity]
return model
}
}
// MARK: - CoreDataCareScheduleStorageTests
final class CoreDataCareScheduleStorageTests: XCTestCase {
// MARK: - Properties
private var sut: CoreDataCareScheduleStorage!
private var mockCoreDataStack: MockCoreDataStack!
// MARK: - Test Lifecycle
override func setUp() {
super.setUp()
mockCoreDataStack = MockCoreDataStack()
sut = CoreDataCareScheduleStorage(coreDataStack: mockCoreDataStack)
}
override func tearDown() {
try? mockCoreDataStack.reset()
sut = nil
mockCoreDataStack = nil
super.tearDown()
}
// MARK: - Test Helpers
private func createTestSchedule(
id: UUID = UUID(),
plantID: UUID = UUID(),
lightRequirement: LightRequirement = .partialShade,
wateringSchedule: String = "Every 3 days",
temperatureRange: ClosedRange<Int> = 60...80,
fertilizerSchedule: String = "Monthly during growing season",
tasks: [CareTask] = []
) -> PlantCareSchedule {
PlantCareSchedule(
id: id,
plantID: plantID,
lightRequirement: lightRequirement,
wateringSchedule: wateringSchedule,
temperatureRange: temperatureRange,
fertilizerSchedule: fertilizerSchedule,
tasks: tasks
)
}
private func createTestTask(
id: UUID = UUID(),
plantID: UUID = UUID(),
type: CareTaskType = .watering,
scheduledDate: Date = Date(),
completedDate: Date? = nil,
notes: String = ""
) -> CareTask {
CareTask(
id: id,
plantID: plantID,
type: type,
scheduledDate: scheduledDate,
completedDate: completedDate,
notes: notes
)
}
// MARK: - save() and fetch() Round-Trip Tests
func testSaveAndFetch_WithValidSchedule_RoundTripsSuccessfully() async throws {
// Given
let plantID = UUID()
let schedule = createTestSchedule(
plantID: plantID,
lightRequirement: .fullSun,
wateringSchedule: "Every 2 days",
temperatureRange: 65...75,
fertilizerSchedule: "Weekly"
)
// When
try await sut.save(schedule)
let fetchedSchedule = try await sut.fetch(for: plantID)
// Then
XCTAssertNotNil(fetchedSchedule)
XCTAssertEqual(fetchedSchedule?.plantID, plantID)
XCTAssertEqual(fetchedSchedule?.lightRequirement, .fullSun)
XCTAssertEqual(fetchedSchedule?.wateringSchedule, "Every 2 days")
XCTAssertEqual(fetchedSchedule?.temperatureRange, 65...75)
XCTAssertEqual(fetchedSchedule?.fertilizerSchedule, "Weekly")
}
func testSaveAndFetch_WithTasks_RoundTripsSuccessfully() async throws {
// Given
let plantID = UUID()
let task1 = createTestTask(plantID: plantID, type: .watering, notes: "Water thoroughly")
let task2 = createTestTask(plantID: plantID, type: .fertilizing, notes: "Use organic fertilizer")
let schedule = createTestSchedule(plantID: plantID, tasks: [task1, task2])
// When
try await sut.save(schedule)
let fetchedSchedule = try await sut.fetch(for: plantID)
// Then
XCTAssertNotNil(fetchedSchedule)
XCTAssertEqual(fetchedSchedule?.tasks.count, 2)
let taskTypes = fetchedSchedule?.tasks.map { $0.type } ?? []
XCTAssertTrue(taskTypes.contains(.watering))
XCTAssertTrue(taskTypes.contains(.fertilizing))
}
func testSave_WhenScheduleAlreadyExists_UpdatesExistingSchedule() async throws {
// Given
let plantID = UUID()
let originalSchedule = createTestSchedule(
plantID: plantID,
wateringSchedule: "Weekly"
)
try await sut.save(originalSchedule)
let updatedSchedule = createTestSchedule(
plantID: plantID,
wateringSchedule: "Every 2 days"
)
// When
try await sut.save(updatedSchedule)
let fetchedSchedule = try await sut.fetch(for: plantID)
// Then
XCTAssertEqual(fetchedSchedule?.wateringSchedule, "Every 2 days")
// Verify only one schedule exists for the plant
let allSchedules = try await sut.fetchAll()
let schedulesForPlant = allSchedules.filter { $0.plantID == plantID }
XCTAssertEqual(schedulesForPlant.count, 1)
}
func testFetch_WhenScheduleDoesNotExist_ReturnsNil() async throws {
// Given
let nonExistentPlantID = UUID()
// When
let fetchedSchedule = try await sut.fetch(for: nonExistentPlantID)
// Then
XCTAssertNil(fetchedSchedule)
}
func testSaveAndFetch_WithAllLightRequirements_RoundTripsCorrectly() async throws {
// Test each light requirement
for lightReq in LightRequirement.allCases {
// Given
let plantID = UUID()
let schedule = createTestSchedule(plantID: plantID, lightRequirement: lightReq)
// When
try await sut.save(schedule)
let fetchedSchedule = try await sut.fetch(for: plantID)
// Then
XCTAssertEqual(fetchedSchedule?.lightRequirement, lightReq, "Failed for light requirement: \(lightReq)")
}
}
// MARK: - fetchAll() Tests
func testFetchAll_WhenEmpty_ReturnsEmptyArray() async throws {
// When
let schedules = try await sut.fetchAll()
// Then
XCTAssertTrue(schedules.isEmpty)
}
func testFetchAll_WithMultipleSchedules_ReturnsAllSchedules() async throws {
// Given
let schedule1 = createTestSchedule(plantID: UUID())
let schedule2 = createTestSchedule(plantID: UUID())
let schedule3 = createTestSchedule(plantID: UUID())
try await sut.save(schedule1)
try await sut.save(schedule2)
try await sut.save(schedule3)
// When
let schedules = try await sut.fetchAll()
// Then
XCTAssertEqual(schedules.count, 3)
}
func testFetchAll_SortsByPlantID() async throws {
// Given
let plantID1 = UUID()
let plantID2 = UUID()
let plantID3 = UUID()
try await sut.save(createTestSchedule(plantID: plantID2))
try await sut.save(createTestSchedule(plantID: plantID1))
try await sut.save(createTestSchedule(plantID: plantID3))
// When
let schedules = try await sut.fetchAll()
// Then
XCTAssertEqual(schedules.count, 3)
// Results should be sorted by plantID (UUID string comparison)
let plantIDs = schedules.map { $0.plantID }
XCTAssertEqual(plantIDs, plantIDs.sorted { $0.uuidString < $1.uuidString })
}
// MARK: - delete() Tests
func testDelete_WhenScheduleExists_RemovesSchedule() async throws {
// Given
let plantID = UUID()
let schedule = createTestSchedule(plantID: plantID)
try await sut.save(schedule)
// Verify schedule exists
let fetchedBefore = try await sut.fetch(for: plantID)
XCTAssertNotNil(fetchedBefore)
// When
try await sut.delete(for: plantID)
// Then
let fetchedAfter = try await sut.fetch(for: plantID)
XCTAssertNil(fetchedAfter)
}
func testDelete_WhenScheduleDoesNotExist_ThrowsScheduleNotFound() async {
// Given
let nonExistentPlantID = UUID()
// When/Then
do {
try await sut.delete(for: nonExistentPlantID)
XCTFail("Expected scheduleNotFound error to be thrown")
} catch let error as CareScheduleStorageError {
switch error {
case .scheduleNotFound(let id):
XCTAssertEqual(id, nonExistentPlantID)
default:
XCTFail("Expected scheduleNotFound error, got \(error)")
}
} catch {
XCTFail("Expected CareScheduleStorageError, got \(error)")
}
}
func testDelete_AlsoDeletesAssociatedTasks() async throws {
// Given
let plantID = UUID()
let task1 = createTestTask(plantID: plantID, type: .watering)
let task2 = createTestTask(plantID: plantID, type: .fertilizing)
let schedule = createTestSchedule(plantID: plantID, tasks: [task1, task2])
try await sut.save(schedule)
// Verify tasks exist
let tasksBefore = try await sut.fetchAllTasks()
XCTAssertEqual(tasksBefore.count, 2)
// When
try await sut.delete(for: plantID)
// Then - Tasks should also be deleted
let tasksAfter = try await sut.fetchAllTasks()
XCTAssertTrue(tasksAfter.isEmpty)
}
// MARK: - updateTask() Tests
func testUpdateTask_WhenTaskExists_UpdatesTask() async throws {
// Given
let plantID = UUID()
let taskID = UUID()
let originalTask = CareTask(
id: taskID,
plantID: plantID,
type: .watering,
scheduledDate: Date(),
completedDate: nil,
notes: "Original notes"
)
let schedule = createTestSchedule(plantID: plantID, tasks: [originalTask])
try await sut.save(schedule)
// When
let updatedTask = CareTask(
id: taskID,
plantID: plantID,
type: .watering,
scheduledDate: Date().addingTimeInterval(86400), // Tomorrow
completedDate: Date(),
notes: "Updated notes"
)
try await sut.updateTask(updatedTask)
// Then
let allTasks = try await sut.fetchAllTasks()
let fetchedTask = allTasks.first { $0.id == taskID }
XCTAssertNotNil(fetchedTask)
XCTAssertEqual(fetchedTask?.notes, "Updated notes")
XCTAssertNotNil(fetchedTask?.completedDate)
}
func testUpdateTask_WhenTaskDoesNotExist_ThrowsTaskNotFound() async {
// Given
let nonExistentTaskID = UUID()
let task = createTestTask(id: nonExistentTaskID)
// When/Then
do {
try await sut.updateTask(task)
XCTFail("Expected taskNotFound error to be thrown")
} catch let error as CareScheduleStorageError {
switch error {
case .taskNotFound(let id):
XCTAssertEqual(id, nonExistentTaskID)
default:
XCTFail("Expected taskNotFound error, got \(error)")
}
} catch {
XCTFail("Expected CareScheduleStorageError, got \(error)")
}
}
func testUpdateTask_UpdatesTaskType() async throws {
// Given
let plantID = UUID()
let taskID = UUID()
let task = CareTask(
id: taskID,
plantID: plantID,
type: .watering,
scheduledDate: Date(),
notes: ""
)
let schedule = createTestSchedule(plantID: plantID, tasks: [task])
try await sut.save(schedule)
// When
let updatedTask = CareTask(
id: taskID,
plantID: plantID,
type: .fertilizing,
scheduledDate: Date(),
notes: ""
)
try await sut.updateTask(updatedTask)
// Then
let allTasks = try await sut.fetchAllTasks()
let fetchedTask = allTasks.first { $0.id == taskID }
XCTAssertEqual(fetchedTask?.type, .fertilizing)
}
// MARK: - fetchUpcomingTasks() Tests
func testFetchUpcomingTasks_ReturnsTasksWithinSpecifiedDays() async throws {
// Given
let plantID = UUID()
let now = Date()
// Task scheduled tomorrow (should be included)
let tomorrowTask = CareTask(
id: UUID(),
plantID: plantID,
type: .watering,
scheduledDate: Calendar.current.date(byAdding: .day, value: 1, to: now)!,
notes: "Tomorrow task"
)
// Task scheduled in 3 days (should be included for 7 days)
let threeDaysTask = CareTask(
id: UUID(),
plantID: plantID,
type: .fertilizing,
scheduledDate: Calendar.current.date(byAdding: .day, value: 3, to: now)!,
notes: "Three days task"
)
// Task scheduled in 10 days (should NOT be included for 7 days)
let tenDaysTask = CareTask(
id: UUID(),
plantID: plantID,
type: .pruning,
scheduledDate: Calendar.current.date(byAdding: .day, value: 10, to: now)!,
notes: "Ten days task"
)
// Task in the past (should NOT be included)
let pastTask = CareTask(
id: UUID(),
plantID: plantID,
type: .repotting,
scheduledDate: Calendar.current.date(byAdding: .day, value: -1, to: now)!,
notes: "Past task"
)
// Completed task (should NOT be included)
let completedTask = CareTask(
id: UUID(),
plantID: plantID,
type: .watering,
scheduledDate: Calendar.current.date(byAdding: .day, value: 2, to: now)!,
completedDate: now,
notes: "Completed task"
)
let schedule = createTestSchedule(
plantID: plantID,
tasks: [tomorrowTask, threeDaysTask, tenDaysTask, pastTask, completedTask]
)
try await sut.save(schedule)
// When
let upcomingTasks = try await sut.fetchUpcomingTasks(days: 7)
// Then
XCTAssertEqual(upcomingTasks.count, 2)
let notes = upcomingTasks.map { $0.notes }
XCTAssertTrue(notes.contains("Tomorrow task"))
XCTAssertTrue(notes.contains("Three days task"))
XCTAssertFalse(notes.contains("Ten days task"))
XCTAssertFalse(notes.contains("Past task"))
XCTAssertFalse(notes.contains("Completed task"))
}
func testFetchUpcomingTasks_ReturnsTasksSortedByScheduledDate() async throws {
// Given
let plantID = UUID()
let now = Date()
let task3Days = createTestTask(
plantID: plantID,
scheduledDate: Calendar.current.date(byAdding: .day, value: 3, to: now)!,
notes: "3 days"
)
let task1Day = createTestTask(
plantID: plantID,
scheduledDate: Calendar.current.date(byAdding: .day, value: 1, to: now)!,
notes: "1 day"
)
let task2Days = createTestTask(
plantID: plantID,
scheduledDate: Calendar.current.date(byAdding: .day, value: 2, to: now)!,
notes: "2 days"
)
let schedule = createTestSchedule(plantID: plantID, tasks: [task3Days, task1Day, task2Days])
try await sut.save(schedule)
// When
let upcomingTasks = try await sut.fetchUpcomingTasks(days: 7)
// Then
XCTAssertEqual(upcomingTasks.count, 3)
XCTAssertEqual(upcomingTasks[0].notes, "1 day")
XCTAssertEqual(upcomingTasks[1].notes, "2 days")
XCTAssertEqual(upcomingTasks[2].notes, "3 days")
}
func testFetchUpcomingTasks_WhenNoTasks_ReturnsEmptyArray() async throws {
// When
let upcomingTasks = try await sut.fetchUpcomingTasks(days: 7)
// Then
XCTAssertTrue(upcomingTasks.isEmpty)
}
func testFetchUpcomingTasks_WithZeroDays_ReturnsNoTasks() async throws {
// Given
let plantID = UUID()
let task = createTestTask(
plantID: plantID,
scheduledDate: Date().addingTimeInterval(3600) // 1 hour from now
)
let schedule = createTestSchedule(plantID: plantID, tasks: [task])
try await sut.save(schedule)
// When
let upcomingTasks = try await sut.fetchUpcomingTasks(days: 0)
// Then
XCTAssertTrue(upcomingTasks.isEmpty)
}
// MARK: - fetchAllTasks() Tests
func testFetchAllTasks_ReturnsAllTasks() async throws {
// Given
let plantID1 = UUID()
let plantID2 = UUID()
let schedule1 = createTestSchedule(
plantID: plantID1,
tasks: [createTestTask(plantID: plantID1), createTestTask(plantID: plantID1)]
)
let schedule2 = createTestSchedule(
plantID: plantID2,
tasks: [createTestTask(plantID: plantID2)]
)
try await sut.save(schedule1)
try await sut.save(schedule2)
// When
let allTasks = try await sut.fetchAllTasks()
// Then
XCTAssertEqual(allTasks.count, 3)
}
// MARK: - exists() Tests
func testExists_WhenScheduleExists_ReturnsTrue() async throws {
// Given
let plantID = UUID()
let schedule = createTestSchedule(plantID: plantID)
try await sut.save(schedule)
// When
let exists = try await sut.exists(for: plantID)
// Then
XCTAssertTrue(exists)
}
func testExists_WhenScheduleDoesNotExist_ReturnsFalse() async throws {
// Given
let nonExistentPlantID = UUID()
// When
let exists = try await sut.exists(for: nonExistentPlantID)
// Then
XCTAssertFalse(exists)
}
// MARK: - markTaskCompleted() Tests
func testMarkTaskCompleted_SetsCompletedDate() async throws {
// Given
let plantID = UUID()
let taskID = UUID()
let task = CareTask(
id: taskID,
plantID: plantID,
type: .watering,
scheduledDate: Date(),
completedDate: nil,
notes: ""
)
let schedule = createTestSchedule(plantID: plantID, tasks: [task])
try await sut.save(schedule)
// When
try await sut.markTaskCompleted(taskID: taskID)
// Then
let allTasks = try await sut.fetchAllTasks()
let completedTask = allTasks.first { $0.id == taskID }
XCTAssertNotNil(completedTask?.completedDate)
}
func testMarkTaskCompleted_WhenTaskDoesNotExist_ThrowsTaskNotFound() async {
// Given
let nonExistentTaskID = UUID()
// When/Then
do {
try await sut.markTaskCompleted(taskID: nonExistentTaskID)
XCTFail("Expected taskNotFound error to be thrown")
} catch let error as CareScheduleStorageError {
switch error {
case .taskNotFound(let id):
XCTAssertEqual(id, nonExistentTaskID)
default:
XCTFail("Expected taskNotFound error, got \(error)")
}
} catch {
XCTFail("Expected CareScheduleStorageError, got \(error)")
}
}
// MARK: - fetchOverdueTasks() Tests
func testFetchOverdueTasks_ReturnsOverdueIncompleteTasks() async throws {
// Given
let plantID = UUID()
let now = Date()
// Overdue task (past date, not completed)
let overdueTask = createTestTask(
plantID: plantID,
type: .watering,
scheduledDate: Calendar.current.date(byAdding: .day, value: -2, to: now)!,
completedDate: nil,
notes: "Overdue"
)
// Past but completed (should NOT be included)
let completedPastTask = createTestTask(
plantID: plantID,
type: .fertilizing,
scheduledDate: Calendar.current.date(byAdding: .day, value: -1, to: now)!,
completedDate: now,
notes: "Completed past"
)
// Future task (should NOT be included)
let futureTask = createTestTask(
plantID: plantID,
type: .pruning,
scheduledDate: Calendar.current.date(byAdding: .day, value: 1, to: now)!,
notes: "Future"
)
let schedule = createTestSchedule(
plantID: plantID,
tasks: [overdueTask, completedPastTask, futureTask]
)
try await sut.save(schedule)
// When
let overdueTasks = try await sut.fetchOverdueTasks()
// Then
XCTAssertEqual(overdueTasks.count, 1)
XCTAssertEqual(overdueTasks.first?.notes, "Overdue")
}
// MARK: - fetchTasks(for:) Tests
func testFetchTasksForPlant_ReturnsOnlyTasksForSpecifiedPlant() async throws {
// Given
let plantID1 = UUID()
let plantID2 = UUID()
let schedule1 = createTestSchedule(
plantID: plantID1,
tasks: [
createTestTask(plantID: plantID1, notes: "Plant 1 Task 1"),
createTestTask(plantID: plantID1, notes: "Plant 1 Task 2")
]
)
let schedule2 = createTestSchedule(
plantID: plantID2,
tasks: [createTestTask(plantID: plantID2, notes: "Plant 2 Task")]
)
try await sut.save(schedule1)
try await sut.save(schedule2)
// When
let tasksForPlant1 = try await sut.fetchTasks(for: plantID1)
// Then
XCTAssertEqual(tasksForPlant1.count, 2)
XCTAssertTrue(tasksForPlant1.allSatisfy { $0.plantID == plantID1 })
}
// MARK: - getTaskStatistics() Tests
func testGetTaskStatistics_ReturnsCorrectCounts() async throws {
// Given
let plantID = UUID()
let now = Date()
let tasks = [
// Upcoming (within 7 days)
createTestTask(
plantID: plantID,
scheduledDate: Calendar.current.date(byAdding: .day, value: 2, to: now)!
),
createTestTask(
plantID: plantID,
scheduledDate: Calendar.current.date(byAdding: .day, value: 5, to: now)!
),
// Overdue
createTestTask(
plantID: plantID,
scheduledDate: Calendar.current.date(byAdding: .day, value: -1, to: now)!
),
createTestTask(
plantID: plantID,
scheduledDate: Calendar.current.date(byAdding: .day, value: -3, to: now)!
),
createTestTask(
plantID: plantID,
scheduledDate: Calendar.current.date(byAdding: .day, value: -5, to: now)!
)
]
let schedule = createTestSchedule(plantID: plantID, tasks: tasks)
try await sut.save(schedule)
// When
let stats = try await sut.getTaskStatistics()
// Then
XCTAssertEqual(stats.upcoming, 2)
XCTAssertEqual(stats.overdue, 3)
}
// MARK: - Error Description Tests
func testCareScheduleStorageError_HasCorrectDescriptions() {
// Test scheduleNotFound
let scheduleNotFoundError = CareScheduleStorageError.scheduleNotFound(UUID())
XCTAssertNotNil(scheduleNotFoundError.errorDescription)
XCTAssertTrue(scheduleNotFoundError.errorDescription?.contains("not found") ?? false)
// Test taskNotFound
let taskNotFoundError = CareScheduleStorageError.taskNotFound(UUID())
XCTAssertNotNil(taskNotFoundError.errorDescription)
XCTAssertTrue(taskNotFoundError.errorDescription?.contains("not found") ?? false)
// Test saveFailed
let saveFailed = CareScheduleStorageError.saveFailed(NSError(domain: "test", code: 1))
XCTAssertNotNil(saveFailed.errorDescription)
// Test fetchFailed
let fetchFailed = CareScheduleStorageError.fetchFailed(NSError(domain: "test", code: 1))
XCTAssertNotNil(fetchFailed.errorDescription)
// Test deleteFailed
let deleteFailed = CareScheduleStorageError.deleteFailed(NSError(domain: "test", code: 1))
XCTAssertNotNil(deleteFailed.errorDescription)
// Test updateFailed
let updateFailed = CareScheduleStorageError.updateFailed(NSError(domain: "test", code: 1))
XCTAssertNotNil(updateFailed.errorDescription)
// Test invalidData
let invalidData = CareScheduleStorageError.invalidData("Test message")
XCTAssertNotNil(invalidData.errorDescription)
XCTAssertTrue(invalidData.errorDescription?.contains("Test message") ?? false)
// Test entityNotFound
let entityNotFound = CareScheduleStorageError.entityNotFound("TestEntity")
XCTAssertNotNil(entityNotFound.errorDescription)
XCTAssertTrue(entityNotFound.errorDescription?.contains("TestEntity") ?? false)
}
// MARK: - Protocol Conformance Tests
func testCoreDataCareScheduleStorage_ConformsToProtocol() {
// Then
XCTAssertTrue(sut is CareScheduleRepositoryProtocol)
}
// MARK: - Edge Cases
func testSaveAndFetch_WithEmptyTasks_WorksCorrectly() async throws {
// Given
let plantID = UUID()
let schedule = createTestSchedule(plantID: plantID, tasks: [])
// When
try await sut.save(schedule)
let fetchedSchedule = try await sut.fetch(for: plantID)
// Then
XCTAssertNotNil(fetchedSchedule)
XCTAssertTrue(fetchedSchedule?.tasks.isEmpty ?? false)
}
func testSaveAndFetch_WithAllTaskTypes_WorksCorrectly() async throws {
// Given
let plantID = UUID()
let tasks = CareTaskType.allCases.map { type in
createTestTask(plantID: plantID, type: type)
}
let schedule = createTestSchedule(plantID: plantID, tasks: tasks)
// When
try await sut.save(schedule)
let fetchedSchedule = try await sut.fetch(for: plantID)
// Then
XCTAssertEqual(fetchedSchedule?.tasks.count, CareTaskType.allCases.count)
for type in CareTaskType.allCases {
XCTAssertTrue(fetchedSchedule?.tasks.contains { $0.type == type } ?? false)
}
}
func testConcurrentSaves_DoNotCorruptData() async throws {
// Given
let iterations = 10
// When
await withTaskGroup(of: Void.self) { group in
for i in 0..<iterations {
group.addTask { [sut] in
let schedule = self.createTestSchedule(
plantID: UUID(),
wateringSchedule: "Schedule \(i)"
)
try? await sut!.save(schedule)
}
}
}
// Then
let allSchedules = try await sut.fetchAll()
XCTAssertEqual(allSchedules.count, iterations)
}
}