mirror of
https://github.com/yattee/yattee.git
synced 2026-11-01 17:37:18 +00:00
221 lines
8.5 KiB
Swift
221 lines
8.5 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("A local record that only repeats the server value leaves it untouched")
|
|
@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)
|
|
}
|
|
}
|
|
}
|