remindwall / RWPureSwift / Tests / DaoTests / SchemaTests.swift
Back to remindwall — Current RemindWall
import AppTypes
import Dao
import Dependencies
import DependenciesTestSupport
import Foundation
import GRDB
import SQLiteData
import StructuredQueries
import Testing
@MainActor
@Suite("Schema Tests")
struct SchemaTests {
@Test("Database Seed Sample Data")
func database_config() throws {
withDependencies {
$0.uuid = .incrementing
$0.defaultDatabase = try! $0.appDatabase()
} operation: {
@Dependency(\.defaultDatabase) var defaultDatabase
let _ = try! defaultDatabase.write{ db in
try! db.seedSampleData()
}
}
}
@Test("Insert and query a setting")
func insertAndQuerySetting() async throws {
let fixedDate = Date(timeIntervalSince1970: 1000)
await withDependencies {
$0.uuid = .incrementing
$0.date = .constant(fixedDate)
$0.defaultDatabase = try! $0.appDatabase()
} operation: {
@Dependency(\.defaultDatabase) var defaultDatabase
@Dependency(\.uuid) var uuid
let setting = Setting(
id: Setting.ID(uuid()),
key: "testKey",
value: "testValue",
lastModified: fixedDate
)
try! await defaultDatabase.write { db in
try Setting.insert{setting}.execute(db)
}
let fetched = try! await defaultDatabase.read { db in
try Setting.where { $0.key.eq("testKey") }.fetchOne(db)
}
#expect(fetched != nil)
#expect(fetched?.key == "testKey")
#expect(fetched?.value == "testValue")
}
}
@Test("INSERT OR REPLACE on trackees does not cascade-delete reminders")
func insertOrReplaceDoesNotCascadeDelete() async throws {
await withDependencies {
$0.uuid = .incrementing
$0.defaultDatabase = try! $0.appDatabase()
} operation: {
@Dependency(\.defaultDatabase) var defaultDatabase
@Dependency(\.uuid) var uuid
let trackeeId = Trackee.ID(uuid())
let reminderId = UUID()
// Insert a trackee and a reminder
try! await defaultDatabase.write { db in
try Trackee.insert{Trackee(id: trackeeId, name: "Alice")}.execute(db)
try db.execute(
sql: """
INSERT INTO "reminderTimes" ("id", "weekDay", "hour", "minute", "trackeeId")
VALUES (?, 1, 9, 0, ?)
""",
arguments: [reminderId.uuidString, trackeeId.rawValue.uuidString]
)
}
// Verify reminder exists
let before = try! await defaultDatabase.read { db in
try ReminderTime.where { $0.trackeeId.eq(trackeeId) }.fetchAll(db)
}
#expect(before.count == 1)
// Simulate what SyncEngine does: INSERT OR REPLACE the same trackee
try! await defaultDatabase.write { db in
try db.execute(
sql: """
INSERT OR REPLACE INTO "trackees" ("id", "name")
VALUES (?, ?)
""",
arguments: [trackeeId.rawValue.uuidString, "Alice"]
)
}
// Verify reminder still exists after INSERT OR REPLACE
let after = try! await defaultDatabase.read { db in
try ReminderTime.where { $0.trackeeId.eq(trackeeId) }.fetchAll(db)
}
#expect(after.count == 1, "Reminder was cascade-deleted by INSERT OR REPLACE on trackees")
}
}
@Test("Insert and query a MonitoredStop")
func insertAndQueryMonitoredStop() async throws {
await withDependencies {
$0.uuid = .incrementing
$0.defaultDatabase = try! $0.appDatabase()
} operation: {
@Dependency(\.defaultDatabase) var defaultDatabase
@Dependency(\.uuid) var uuid
let stop = MonitoredStop(
id: MonitoredStop.ID(uuid()),
label: "School bus",
stopId: "1_75403",
routeId: "1_100224",
routeShortName: "12",
sortOrder: 0
)
try! await defaultDatabase.write { db in
try MonitoredStop.insert { stop }.execute(db)
}
let fetched = try! await defaultDatabase.read { db in
try MonitoredStop.where { $0.stopId.eq("1_75403") }.fetchOne(db)
}
#expect(fetched != nil)
#expect(fetched?.label == "School bus")
#expect(fetched?.routeId == "1_100224")
#expect(fetched?.routeShortName == "12")
#expect(fetched?.sortOrder == 0)
}
}
@Test("MonitoredStop ordering by sortOrder")
func monitoredStopOrdering() async throws {
await withDependencies {
$0.uuid = .incrementing
$0.defaultDatabase = try! $0.appDatabase()
} operation: {
@Dependency(\.defaultDatabase) var defaultDatabase
@Dependency(\.uuid) var uuid
try! await defaultDatabase.write { db in
try MonitoredStop.insert {
MonitoredStop(
id: MonitoredStop.ID(uuid()),
label: "Second", stopId: "1_2", routeId: "1_b",
routeShortName: "B", sortOrder: 1
)
MonitoredStop(
id: MonitoredStop.ID(uuid()),
label: "First", stopId: "1_1", routeId: "1_a",
routeShortName: "A", sortOrder: 0
)
}.execute(db)
}
let ordered = try! await defaultDatabase.read { db in
try MonitoredStop.all.order(by: \.sortOrder).fetchAll(db)
}
#expect(ordered.map(\.label) == ["First", "Second"])
}
}
@Test("INSERT OR REPLACE on monitoredStops preserves siblings")
func monitoredStopReplaceDoesNotDropOthers() async throws {
await withDependencies {
$0.uuid = .incrementing
$0.defaultDatabase = try! $0.appDatabase()
} operation: {
@Dependency(\.defaultDatabase) var defaultDatabase
@Dependency(\.uuid) var uuid
let kept = MonitoredStop.ID(uuid())
let replaced = MonitoredStop.ID(uuid())
try! await defaultDatabase.write { db in
try MonitoredStop.insert {
MonitoredStop(
id: kept,
label: "Keep", stopId: "1_k", routeId: "1_a",
routeShortName: "A", sortOrder: 0
)
MonitoredStop(
id: replaced,
label: "Original", stopId: "1_r", routeId: "1_b",
routeShortName: "B", sortOrder: 1
)
}.execute(db)
}
// Simulate what SyncEngine does: INSERT OR REPLACE the same row id.
try! await defaultDatabase.write { db in
try db.execute(
sql: """
INSERT OR REPLACE INTO "monitoredStops"
("id", "label", "stopId", "routeId", "routeShortName", "sortOrder")
VALUES (?, ?, ?, ?, ?, ?)
""",
arguments: [
replaced.rawValue.uuidString,
"Updated", "1_r", "1_b", "B", 1
]
)
}
let all = try! await defaultDatabase.read { db in
try MonitoredStop.all.order(by: \.sortOrder).fetchAll(db)
}
#expect(all.count == 2)
#expect(all.map(\.label) == ["Keep", "Updated"])
}
}
@Test("Trackee defaults to remindersEnabled and can be soft-disabled")
func trackeeRemindersEnabledDefaultAndToggle() async throws {
await withDependencies {
$0.uuid = .incrementing
$0.defaultDatabase = try! $0.appDatabase()
} operation: {
@Dependency(\.defaultDatabase) var defaultDatabase
@Dependency(\.uuid) var uuid
let trackeeId = Trackee.ID(uuid())
// A trackee inserted without naming the flag comes back enabled.
try! await defaultDatabase.write { db in
try Trackee.insert { Trackee(id: trackeeId, name: "Alice") }.execute(db)
}
let inserted = try! await defaultDatabase.read { db in
try Trackee.find(trackeeId).fetchOne(db)
}
#expect(inserted?.remindersEnabled == true)
// Soft-disable persists.
try! await defaultDatabase.write { db in
try Trackee.find(trackeeId)
.update { $0.remindersEnabled = false }
.execute(db)
}
let disabled = try! await defaultDatabase.read { db in
try Trackee.find(trackeeId).fetchOne(db)
}
#expect(disabled?.remindersEnabled == false)
}
}
@Test("Insert, fetch, order and delete a MonitoredRoute")
func monitoredRouteCrud() async throws {
await withDependencies {
$0.uuid = .incrementing
$0.defaultDatabase = try! $0.appDatabase()
} operation: {
@Dependency(\.defaultDatabase) var defaultDatabase
@Dependency(\.uuid) var uuid
let second = MonitoredRoute.ID(uuid())
let first = MonitoredRoute.ID(uuid())
try! await defaultDatabase.write { db in
try MonitoredRoute.insert {
MonitoredRoute(
id: second,
label: "Ferry run",
destinationLatitude: 47.6026,
destinationLongitude: -122.3393,
destinationName: "Colman Dock",
normalMinutes: 25,
sortOrder: 1
)
MonitoredRoute(
id: first,
label: "School run",
destinationLatitude: 47.5423,
destinationLongitude: -122.3866,
destinationName: "Maple Elementary",
normalMinutes: 12,
sortOrder: 0
)
}.execute(db)
}
let ordered = try! await defaultDatabase.read { db in
try MonitoredRoute.all.order(by: \.sortOrder).fetchAll(db)
}
#expect(ordered.map(\.label) == ["School run", "Ferry run"])
#expect(ordered.first?.destinationName == "Maple Elementary")
#expect(ordered.first?.normalMinutes == 12)
#expect(ordered.first?.destinationLatitude == 47.5423)
#expect(ordered.first?.destinationLongitude == -122.3866)
try! await defaultDatabase.write { db in
try MonitoredRoute.find(second).delete().execute(db)
}
let remaining = try! await defaultDatabase.read { db in
try MonitoredRoute.all.fetchAll(db)
}
#expect(remaining.map(\.id) == [first])
}
}
@Test("Per-watch seed inherits the legacy bus window and toggle")
func perWatchSeed() async throws {
await withDependencies {
$0.uuid = .incrementing
$0.defaultDatabase = try! $0.appDatabase()
} operation: {
@Dependency(\.defaultDatabase) var defaultDatabase
@Dependency(\.uuid) var uuid
let seeded = MonitoredStop.ID(uuid())
let alreadySet = MonitoredStop.ID(uuid())
let custom = AlertWindow(
weekdays: [.Saturday], startHour: 8, startMinute: 0, endHour: 10, endMinute: 0
)
try! await defaultDatabase.write { db in
try MonitoredStop.insert {
MonitoredStop(
id: seeded, label: "Legacy", stopId: "1_1", routeId: "1_a",
routeShortName: "A", sortOrder: 0
)
MonitoredStop(
id: alreadySet, label: "Custom", stopId: "1_2", routeId: "1_b",
routeShortName: "B", sortOrder: 1, window: custom
)
}.execute(db)
try Setting.insert {
Setting(
id: Setting.ID(uuid()), key: BUS_WINDOW_SETTING_KEY,
value: BusWindow.default.rawValue,
lastModified: Date(timeIntervalSince1970: 1000)
)
Setting(
id: Setting.ID(uuid()), key: BUS_ALERTS_ENABLED_SETTING_KEY,
value: "false",
lastModified: Date(timeIntervalSince1970: 1000)
)
}.execute(db)
try seedPerWatchSettings(in: db)
}
let stops = try! await defaultDatabase.read { db in
try MonitoredStop.all.order(by: \.sortOrder).fetchAll(db)
}
// NULL window inherits the legacy one; an explicit window is kept.
#expect(stops[0].window == BusWindow.default)
#expect(stops[1].window == custom)
// Legacy alerts-off disables the watches (one-shot, caller-guarded).
#expect(stops.allSatisfy { !$0.enabled })
}
}
@Test("Setting with same key uses last modified for conflict resolution")
func settingLastModifiedConflict() async throws {
await withDependencies {
$0.uuid = .incrementing
$0.defaultDatabase = try! $0.appDatabase()
} operation: {
@Dependency(\.defaultDatabase) var defaultDatabase
@Dependency(\.uuid) var uuid
let earlier = Date(timeIntervalSince1970: 1000)
let later = Date(timeIntervalSince1970: 2000)
let setting1 = Setting(
id: Setting.ID(uuid()),
key: "albumId",
value: "album-old",
lastModified: earlier
)
try! await defaultDatabase.write { db in
try Setting.insert{setting1}.execute(db)
}
// Fetch and verify first insert
let first = try! await defaultDatabase.read { db in
try Setting.where { $0.key.eq("albumId") }.fetchOne(db)
}
#expect(first?.value == "album-old")
// Update with newer timestamp
try! await defaultDatabase.write { db in
try Setting
.where { $0.key.eq("albumId") }
.update {
$0.value = "album-new"
$0.lastModified = later
}
.execute(db)
}
let updated = try! await defaultDatabase.read { db in
try Setting.where { $0.key.eq("albumId") }.fetchOne(db)
}
#expect(updated?.value == "album-new")
#expect(updated?.lastModified == later)
}
}
}