Files
Reflect/ReflectTests/DataControllerCRUDTests.swift
Trey t 0442eab1f8 Rebrand entire project from Feels to Reflect
Complete rename across all bundle IDs, App Groups, CloudKit containers,
StoreKit product IDs, data store filenames, URL schemes, logger subsystems,
Swift identifiers, user-facing strings (7 languages), file names, directory
names, Xcode project, schemes, assets, and documentation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 11:47:16 -06:00

367 lines
13 KiB
Swift

//
// DataControllerCRUDTests.swift
// ReflectTests
//
// Tests for DataController create, read, update, and delete operations.
//
import XCTest
import SwiftData
@testable import Reflect
@MainActor
final class DataControllerCreateTests: XCTestCase {
var sut: DataController!
override func setUp() {
super.setUp()
let schema = Schema([MoodEntryModel.self])
let config = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true)
sut = DataController(container: try! ModelContainer(for: schema, configurations: [config]))
}
override func tearDown() {
sut = nil
super.tearDown()
}
// MARK: - Phase 2: Create Tests
func test_add_insertsEntry() {
let date = makeDate(2024, 6, 15)
sut.add(mood: .great, forDate: date, entryType: .listView)
let entry = sut.getEntry(byDate: date)
XCTAssertNotNil(entry, "Entry should exist after add")
XCTAssertEqual(entry?.mood, .great)
XCTAssertEqual(entry?.entryType, EntryType.listView.rawValue)
}
func test_add_replacesExistingForSameDate() {
let date = makeDate(2024, 6, 15)
sut.add(mood: .great, forDate: date, entryType: .listView)
sut.add(mood: .bad, forDate: date, entryType: .widget)
let allEntries = sut.getAllEntries(byDate: date)
XCTAssertEqual(allEntries.count, 1, "Should have exactly 1 entry after 2 adds for same date")
XCTAssertEqual(allEntries.first?.mood, .bad, "Should keep the latest mood")
}
func test_add_setsCorrectWeekday() {
// June 15, 2024 is a Saturday (weekday = 7 in Gregorian)
let date = makeDate(2024, 6, 15)
sut.add(mood: .average, forDate: date, entryType: .listView)
let entry = sut.getEntry(byDate: date)
let expectedWeekday = Calendar.current.component(.weekday, from: date)
XCTAssertEqual(entry?.weekDay, expectedWeekday)
}
func test_add_allMoodValues() {
let moods: [Mood] = [.horrible, .bad, .average, .good, .great]
for (i, mood) in moods.enumerated() {
let date = makeDate(2024, 6, 10 + i)
sut.add(mood: mood, forDate: date, entryType: .listView)
let entry = sut.getEntry(byDate: date)
XCTAssertEqual(entry?.mood, mood, "Mood \(mood) should persist and read back correctly")
}
}
func test_addBatch_insertsMultiple() {
let entries: [(mood: Mood, date: Date, entryType: EntryType)] = [
(.great, makeDate(2024, 6, 10), .listView),
(.good, makeDate(2024, 6, 11), .widget),
(.average, makeDate(2024, 6, 12), .watch),
]
sut.addBatch(entries: entries)
for (mood, date, _) in entries {
let entry = sut.getEntry(byDate: date)
XCTAssertNotNil(entry, "Entry for \(date) should exist")
XCTAssertEqual(entry?.mood, mood)
}
}
func test_addBatch_replacesExisting() {
let date = makeDate(2024, 6, 10)
sut.add(mood: .horrible, forDate: date, entryType: .listView)
let batchEntries: [(mood: Mood, date: Date, entryType: EntryType)] = [
(.great, date, .widget),
]
sut.addBatch(entries: batchEntries)
let allEntries = sut.getAllEntries(byDate: date)
XCTAssertEqual(allEntries.count, 1)
XCTAssertEqual(allEntries.first?.mood, .great)
}
}
// MARK: - Phase 3: Read Tests
@MainActor
final class DataControllerReadTests: XCTestCase {
var sut: DataController!
override func setUp() {
super.setUp()
let schema = Schema([MoodEntryModel.self])
let config = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true)
sut = DataController(container: try! ModelContainer(for: schema, configurations: [config]))
}
override func tearDown() {
sut = nil
super.tearDown()
}
func test_getEntry_emptyDB_returnsNil() {
let entry = sut.getEntry(byDate: Date())
XCTAssertNil(entry)
}
func test_getEntry_boundaryExclusion() {
// Insert entry at exactly midnight of Jan 2 (start of Jan 2)
// Use hour=0 to place it precisely at the boundary
let jan2Midnight = makeDate(2024, 1, 2, hour: 0)
let entry = MoodEntryModel(forDate: jan2Midnight, mood: .great, entryType: .listView)
sut.modelContext.insert(entry)
sut.save()
// Query for Jan 1 should NOT find the Jan 2 entry
// getEntry builds range [Jan 1 00:00, Jan 2 00:00] with <= the midnight entry leaks in
let jan1 = makeDate(2024, 1, 1)
let result = sut.getEntry(byDate: jan1)
XCTAssertNil(result, "Entry at midnight Jan 2 should NOT be returned when querying Jan 1")
}
func test_getData_filtersWeekdays() {
// Add entries for a full week (Mon-Sun)
// Jan 1, 2024 is a Monday (weekday=2)
for i in 0..<7 {
let date = Calendar.current.date(byAdding: .day, value: i, to: makeDate(2024, 1, 1))!
sut.add(mood: .average, forDate: date, entryType: .listView)
}
// Filter to only weekdays 2 (Mon) and 6 (Fri)
let startDate = makeDate(2024, 1, 1)
let endDate = makeDate(2024, 1, 7)
let results = sut.getData(startDate: startDate, endDate: endDate, includedDays: [2, 6])
XCTAssertEqual(results.count, 2, "Should return only Monday and Friday")
}
func test_getData_emptyIncludedDays_returnsAll() {
for i in 0..<7 {
let date = Calendar.current.date(byAdding: .day, value: i, to: makeDate(2024, 1, 1))!
sut.add(mood: .average, forDate: date, entryType: .listView)
}
let startDate = makeDate(2024, 1, 1)
let endDate = makeDate(2024, 1, 7)
let results = sut.getData(startDate: startDate, endDate: endDate, includedDays: [])
XCTAssertEqual(results.count, 7, "Empty includedDays should return all 7 entries")
}
func test_splitIntoYearMonth_groupsCorrectly() {
// Add entries in different months
sut.add(mood: .great, forDate: makeDate(2024, 1, 15), entryType: .listView)
sut.add(mood: .good, forDate: makeDate(2024, 1, 20), entryType: .listView)
sut.add(mood: .bad, forDate: makeDate(2024, 3, 5), entryType: .listView)
let grouped = sut.splitIntoYearMonth(includedDays: [])
XCTAssertEqual(grouped[2024]?[1]?.count, 2, "Jan 2024 should have 2 entries")
XCTAssertEqual(grouped[2024]?[3]?.count, 1, "Mar 2024 should have 1 entry")
XCTAssertNil(grouped[2024]?[2], "Feb 2024 should have no entries")
}
func test_earliestEntry() {
sut.add(mood: .great, forDate: makeDate(2024, 6, 15), entryType: .listView)
sut.add(mood: .bad, forDate: makeDate(2024, 1, 1), entryType: .listView)
sut.add(mood: .good, forDate: makeDate(2024, 3, 10), entryType: .listView)
let earliest = sut.earliestEntry
XCTAssertNotNil(earliest)
XCTAssertTrue(Calendar.current.isDate(earliest!.forDate, inSameDayAs: makeDate(2024, 1, 1)))
}
func test_latestEntry() {
sut.add(mood: .great, forDate: makeDate(2024, 6, 15), entryType: .listView)
sut.add(mood: .bad, forDate: makeDate(2024, 1, 1), entryType: .listView)
sut.add(mood: .good, forDate: makeDate(2024, 3, 10), entryType: .listView)
let latest = sut.latestEntry
XCTAssertNotNil(latest)
XCTAssertTrue(Calendar.current.isDate(latest!.forDate, inSameDayAs: makeDate(2024, 6, 15)))
}
}
// MARK: - Phase 4: Update Tests
@MainActor
final class DataControllerUpdateTests: XCTestCase {
var sut: DataController!
override func setUp() {
super.setUp()
let schema = Schema([MoodEntryModel.self])
let config = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true)
sut = DataController(container: try! ModelContainer(for: schema, configurations: [config]))
}
override func tearDown() {
sut = nil
super.tearDown()
}
func test_update_changesMood() {
let date = makeDate(2024, 6, 15)
sut.add(mood: .bad, forDate: date, entryType: .listView)
let result = sut.update(entryDate: date, withMood: .great)
XCTAssertTrue(result)
let entry = sut.getEntry(byDate: date)
XCTAssertEqual(entry?.mood, .great)
}
func test_update_nonexistent_returnsFalse() {
let result = sut.update(entryDate: makeDate(2024, 6, 15), withMood: .great)
XCTAssertFalse(result)
}
func test_updateNotes_setsNotes() {
let date = makeDate(2024, 6, 15)
sut.add(mood: .good, forDate: date, entryType: .listView)
let result = sut.updateNotes(forDate: date, notes: "Had a great day")
XCTAssertTrue(result)
let entry = sut.getEntry(byDate: date)
XCTAssertEqual(entry?.notes, "Had a great day")
}
func test_updateNotes_nilClearsNotes() {
let date = makeDate(2024, 6, 15)
sut.add(mood: .good, forDate: date, entryType: .listView)
sut.updateNotes(forDate: date, notes: "Some notes")
let result = sut.updateNotes(forDate: date, notes: nil)
XCTAssertTrue(result)
let entry = sut.getEntry(byDate: date)
XCTAssertNil(entry?.notes)
}
func test_updatePhoto_setsPhotoID() {
let date = makeDate(2024, 6, 15)
sut.add(mood: .good, forDate: date, entryType: .listView)
let photoID = UUID()
let result = sut.updatePhoto(forDate: date, photoID: photoID)
XCTAssertTrue(result)
let entry = sut.getEntry(byDate: date)
XCTAssertEqual(entry?.photoID, photoID)
}
func test_update_refreshesTimestamp() {
let date = makeDate(2024, 6, 15)
sut.add(mood: .bad, forDate: date, entryType: .listView)
let originalTimestamp = sut.getEntry(byDate: date)!.timestamp
// Small delay so timestamp would differ
let result = sut.update(entryDate: date, withMood: .great)
XCTAssertTrue(result)
let updatedEntry = sut.getEntry(byDate: date)!
XCTAssertGreaterThanOrEqual(updatedEntry.timestamp, originalTimestamp,
"Timestamp should be updated on mood change so removeDuplicates picks the right entry")
}
}
// MARK: - Phase 5: Delete Tests
@MainActor
final class DataControllerDeleteTests: XCTestCase {
var sut: DataController!
override func setUp() {
super.setUp()
let schema = Schema([MoodEntryModel.self])
let config = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true)
sut = DataController(container: try! ModelContainer(for: schema, configurations: [config]))
}
override func tearDown() {
sut = nil
super.tearDown()
}
func test_clearDB_removesAll() {
sut.add(mood: .great, forDate: makeDate(2024, 6, 10), entryType: .listView)
sut.add(mood: .bad, forDate: makeDate(2024, 6, 11), entryType: .listView)
sut.add(mood: .good, forDate: makeDate(2024, 6, 12), entryType: .listView)
sut.clearDB()
let all = sut.getData(startDate: Date(timeIntervalSince1970: 0), endDate: Date(), includedDays: [])
XCTAssertTrue(all.isEmpty, "clearDB should remove all entries")
}
func test_deleteAllEntries_removesOnlyTargetDate() {
let date1 = makeDate(2024, 6, 10)
let date2 = makeDate(2024, 6, 11)
sut.add(mood: .great, forDate: date1, entryType: .listView)
sut.add(mood: .bad, forDate: date2, entryType: .listView)
sut.deleteAllEntries(forDate: date1)
XCTAssertNil(sut.getEntry(byDate: date1), "Deleted date should be gone")
XCTAssertNotNil(sut.getEntry(byDate: date2), "Other dates should be untouched")
}
func test_removeDuplicates_keepsBestEntry() {
let date = makeDate(2024, 6, 15)
// Manually insert 3 entries for the same date to simulate CloudKit conflict
let entry1 = MoodEntryModel(forDate: date, mood: .missing, entryType: .filledInMissing)
entry1.timestamp = makeDate(2024, 6, 15, hour: 8)
sut.modelContext.insert(entry1)
let entry2 = MoodEntryModel(forDate: date, mood: .great, entryType: .listView)
entry2.timestamp = makeDate(2024, 6, 15, hour: 10)
sut.modelContext.insert(entry2)
let entry3 = MoodEntryModel(forDate: date, mood: .good, entryType: .listView)
entry3.timestamp = makeDate(2024, 6, 15, hour: 9)
sut.modelContext.insert(entry3)
sut.save()
let removed = sut.removeDuplicates()
XCTAssertEqual(removed, 2, "Should remove 2 duplicates")
let remaining = sut.getAllEntries(byDate: date)
XCTAssertEqual(remaining.count, 1, "Should keep exactly 1 entry")
// Should keep non-missing with most recent timestamp (entry2, .great at 10am)
XCTAssertEqual(remaining.first?.mood, .great, "Should keep the best entry (non-missing, most recent)")
}
}
// MARK: - Test Helpers
func makeDate(_ year: Int, _ month: Int, _ day: Int, hour: Int = 12) -> Date {
var components = DateComponents()
components.year = year
components.month = month
components.day = day
components.hour = hour
components.minute = 0
components.second = 0
return Calendar.current.date(from: components)!
}