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>
367 lines
13 KiB
Swift
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)!
|
|
}
|