mirror of
https://github.com/yattee/yattee.git
synced 2026-10-23 13:12:53 +00:00
The tie test claimed the local record only repeated the server value, but it fixtures opposing toggles stamped at the same instant, so name it for the rule it proves. Note beside the strategy why that tie needs no deterministic tiebreaker: no writer stamps these settings with a fixed epoch date, unlike accounts and data contexts.
221 lines
8.4 KiB
Swift
221 lines
8.4 KiB
Swift
//
|
|
// CloudKitSyncTests.swift
|
|
// YatteeTests
|
|
//
|
|
// Tests for CloudKit record mapping and conflict resolution.
|
|
//
|
|
|
|
import CloudKit
|
|
import Foundation
|
|
import Testing
|
|
@testable import Yattee
|
|
|
|
@Suite("CloudKit Sync")
|
|
struct CloudKitSyncTests {
|
|
|
|
@MainActor
|
|
private static func makeMapper() -> CloudKitRecordMapper {
|
|
CloudKitRecordMapper(zone: CKRecordZone(zoneName: RecordType.zoneName))
|
|
}
|
|
|
|
@MainActor
|
|
private static func makeEntry(
|
|
videoID: String = "video1",
|
|
watchedSeconds: TimeInterval = 0,
|
|
duration: TimeInterval = 0,
|
|
isLive: Bool = false,
|
|
updatedAt: Date = Date(timeIntervalSince1970: 1_000)
|
|
) -> WatchEntry {
|
|
let entry = WatchEntry(
|
|
videoID: videoID,
|
|
sourceRawValue: "global",
|
|
title: "Video",
|
|
authorName: "Channel",
|
|
authorID: "ch1",
|
|
duration: duration,
|
|
watchedSeconds: watchedSeconds,
|
|
isLive: isLive
|
|
)
|
|
entry.updatedAt = updatedAt
|
|
return entry
|
|
}
|
|
|
|
@MainActor
|
|
private static func makeNotificationSettings(
|
|
channelID: String = "channel1",
|
|
notificationsEnabled: Bool = false,
|
|
updatedAt: Date = Date(timeIntervalSince1970: 1_000)
|
|
) -> ChannelNotificationSettings {
|
|
let settings = ChannelNotificationSettings(
|
|
channelID: channelID,
|
|
notificationsEnabled: notificationsEnabled,
|
|
sourceRawValue: "global",
|
|
globalProvider: "youtube"
|
|
)
|
|
settings.updatedAt = updatedAt
|
|
return settings
|
|
}
|
|
|
|
// MARK: - Record Mapper
|
|
|
|
@Suite("Record Mapper")
|
|
struct RecordMapperTests {
|
|
@Test("isLive survives the record round-trip")
|
|
@MainActor
|
|
func liveFlagRoundTrip() throws {
|
|
let mapper = CloudKitSyncTests.makeMapper()
|
|
let record = mapper.toCKRecord(watchEntry: CloudKitSyncTests.makeEntry(isLive: true))
|
|
|
|
#expect(record["isLive"] as? Int64 == 1)
|
|
|
|
let decoded = try mapper.toWatchEntry(from: record)
|
|
#expect(decoded.isLive)
|
|
}
|
|
|
|
@Test("Records from older clients without isLive read as not live")
|
|
@MainActor
|
|
func missingLiveFlagReadsFalse() throws {
|
|
let mapper = CloudKitSyncTests.makeMapper()
|
|
let record = mapper.toCKRecord(watchEntry: CloudKitSyncTests.makeEntry(isLive: true))
|
|
// Simulate a record written before the field existed
|
|
record["isLive"] = nil
|
|
|
|
let decoded = try mapper.toWatchEntry(from: record)
|
|
#expect(!decoded.isLive)
|
|
}
|
|
}
|
|
|
|
// MARK: - Conflict Resolution
|
|
|
|
@Suite("Watch Entry Conflicts")
|
|
struct WatchEntryConflictTests {
|
|
@Test("Newer local live watch keeps its isLive flag over an old server record")
|
|
@MainActor
|
|
func localLiveWinsOverStaleServer() async throws {
|
|
let mapper = CloudKitSyncTests.makeMapper()
|
|
let local = mapper.toCKRecord(watchEntry: CloudKitSyncTests.makeEntry(
|
|
isLive: true,
|
|
updatedAt: Date(timeIntervalSince1970: 2_000)
|
|
))
|
|
let server = mapper.toCKRecord(watchEntry: CloudKitSyncTests.makeEntry(
|
|
watchedSeconds: 1_234,
|
|
duration: 3_600,
|
|
updatedAt: Date(timeIntervalSince1970: 1_000)
|
|
))
|
|
// Old clients never wrote the field at all
|
|
server["isLive"] = nil
|
|
|
|
let resolver = CloudKitConflictResolver()
|
|
let resolved = await resolver.resolveWatchEntryConflict(local: local, server: server)
|
|
|
|
#expect(resolved["isLive"] as? Int64 == 1)
|
|
#expect(resolved["watchedSeconds"] as? Double == 0)
|
|
}
|
|
|
|
@Test("Newer local VOD heal clears a stale live flag on the server")
|
|
@MainActor
|
|
func localVODHealClearsServerLiveFlag() async throws {
|
|
let mapper = CloudKitSyncTests.makeMapper()
|
|
let local = mapper.toCKRecord(watchEntry: CloudKitSyncTests.makeEntry(
|
|
watchedSeconds: 300,
|
|
duration: 1_200,
|
|
updatedAt: Date(timeIntervalSince1970: 2_000)
|
|
))
|
|
let server = mapper.toCKRecord(watchEntry: CloudKitSyncTests.makeEntry(
|
|
isLive: true,
|
|
updatedAt: Date(timeIntervalSince1970: 1_000)
|
|
))
|
|
|
|
let resolver = CloudKitConflictResolver()
|
|
let resolved = await resolver.resolveWatchEntryConflict(local: local, server: server)
|
|
|
|
#expect(resolved["isLive"] as? Int64 == 0)
|
|
#expect(resolved["watchedSeconds"] as? Double == 300)
|
|
}
|
|
|
|
@Test("Newer server record keeps its isLive flag")
|
|
@MainActor
|
|
func newerServerLiveFlagWins() async throws {
|
|
let mapper = CloudKitSyncTests.makeMapper()
|
|
let local = mapper.toCKRecord(watchEntry: CloudKitSyncTests.makeEntry(
|
|
watchedSeconds: 500,
|
|
duration: 3_600,
|
|
updatedAt: Date(timeIntervalSince1970: 1_000)
|
|
))
|
|
let server = mapper.toCKRecord(watchEntry: CloudKitSyncTests.makeEntry(
|
|
isLive: true,
|
|
updatedAt: Date(timeIntervalSince1970: 2_000)
|
|
))
|
|
|
|
let resolver = CloudKitConflictResolver()
|
|
let resolved = await resolver.resolveWatchEntryConflict(local: local, server: server)
|
|
|
|
#expect(resolved["isLive"] as? Int64 == 1)
|
|
#expect(resolved["watchedSeconds"] as? Double == 0)
|
|
}
|
|
}
|
|
|
|
@Suite("Channel Notification Settings Conflicts")
|
|
struct ChannelNotificationSettingsConflictTests {
|
|
@Test("Newer local toggle wins over the older server value")
|
|
@MainActor
|
|
func newerLocalToggleWins() async throws {
|
|
let mapper = CloudKitSyncTests.makeMapper()
|
|
let local = mapper.toCKRecord(channelNotificationSettings: CloudKitSyncTests.makeNotificationSettings(
|
|
notificationsEnabled: false,
|
|
updatedAt: Date(timeIntervalSince1970: 2_000)
|
|
))
|
|
let server = mapper.toCKRecord(channelNotificationSettings: CloudKitSyncTests.makeNotificationSettings(
|
|
notificationsEnabled: true,
|
|
updatedAt: Date(timeIntervalSince1970: 1_000)
|
|
))
|
|
|
|
let resolver = CloudKitConflictResolver()
|
|
let resolved = await resolver.resolveChannelNotificationSettingsConflict(local: local, server: server)
|
|
|
|
#expect(resolved["notificationsEnabled"] as? Int64 == 0)
|
|
#expect(resolved["updatedAt"] as? Date == Date(timeIntervalSince1970: 2_000))
|
|
}
|
|
|
|
@Test("Newer server toggle keeps its value")
|
|
@MainActor
|
|
func newerServerToggleWins() async throws {
|
|
let mapper = CloudKitSyncTests.makeMapper()
|
|
let local = mapper.toCKRecord(channelNotificationSettings: CloudKitSyncTests.makeNotificationSettings(
|
|
notificationsEnabled: false,
|
|
updatedAt: Date(timeIntervalSince1970: 1_000)
|
|
))
|
|
let server = mapper.toCKRecord(channelNotificationSettings: CloudKitSyncTests.makeNotificationSettings(
|
|
notificationsEnabled: true,
|
|
updatedAt: Date(timeIntervalSince1970: 2_000)
|
|
))
|
|
|
|
let resolver = CloudKitConflictResolver()
|
|
let resolved = await resolver.resolveChannelNotificationSettingsConflict(local: local, server: server)
|
|
|
|
#expect(resolved["notificationsEnabled"] as? Int64 == 1)
|
|
#expect(resolved["updatedAt"] as? Date == Date(timeIntervalSince1970: 2_000))
|
|
}
|
|
|
|
@Test("Equal timestamps keep the server value")
|
|
@MainActor
|
|
func equalTimestampsKeepServerValue() async throws {
|
|
let mapper = CloudKitSyncTests.makeMapper()
|
|
let stamp = Date(timeIntervalSince1970: 1_000)
|
|
let local = mapper.toCKRecord(channelNotificationSettings: CloudKitSyncTests.makeNotificationSettings(
|
|
notificationsEnabled: false,
|
|
updatedAt: stamp
|
|
))
|
|
let server = mapper.toCKRecord(channelNotificationSettings: CloudKitSyncTests.makeNotificationSettings(
|
|
notificationsEnabled: true,
|
|
updatedAt: stamp
|
|
))
|
|
|
|
let resolver = CloudKitConflictResolver()
|
|
let resolved = await resolver.resolveChannelNotificationSettingsConflict(local: local, server: server)
|
|
|
|
#expect(resolved["notificationsEnabled"] as? Int64 == 1)
|
|
}
|
|
}
|
|
}
|