// // SynchronizationTests.swift // PadelClubDataTests // // Created by Laurent Morvillier on 17/04/2025. // import Testing @testable import PadelClubData @testable import LeStorage enum SyncTestError: Error { case instanceNotFound(id: String) case missingSyncData } struct SynchronizationTests { let username: String = "UserDataTests" let password: String = "MyPass1234--" var secondStoreCenter: StoreCenter init() async throws { let conf = Config.server FileManager.default.deleteDirectoryInDocuments(directoryName: "storage") FileManager.default.deleteDirectoryInDocuments(directoryName: "storage-2") self.secondStoreCenter = StoreCenter(directoryName: "storage-2") self.secondStoreCenter.configureURLs(secureScheme: conf.secure, domain: conf.domain, webSockets: false, useSynchronization: true) self.secondStoreCenter.tokenKeychain = MockKeychainStore(fileName: "storage-2/token.json") self.secondStoreCenter.deviceKeychain = MockKeychainStore(fileName: "storage-2/device.json") try self.secondStoreCenter.deviceKeychain.add(value: UUID().uuidString) self.secondStoreCenter.classProject = "PadelClubData" let token2 = try? self.secondStoreCenter.rawTokenShouldNotBeUsed() if token2 == nil { try await self.login(storeCenter: self.secondStoreCenter) } StoreCenter.main.configureURLs(secureScheme: conf.secure, domain: conf.domain, webSockets: false, useSynchronization: true) StoreCenter.main.tokenKeychain = MockKeychainStore(fileName: "storage/token.json") StoreCenter.main.deviceKeychain = MockKeychainStore(fileName: "storage/device.json") try StoreCenter.main.deviceKeychain.add(value: UUID().uuidString) StoreCenter.main.classProject = "PadelClubData" let token = try? StoreCenter.main.rawTokenShouldNotBeUsed() if token == nil { try await self.login(storeCenter: StoreCenter.main) } } mutating func login(storeCenter: StoreCenter) async throws { let _: CustomUser = try await storeCenter.service().login(username: self.username, password: self.password) } @Test func testDeviceIds() async throws { #expect(StoreCenter.main.deviceId() != self.secondStoreCenter.deviceId()) } @Test func testAuthentication() { #expect(StoreCenter.main.isAuthenticated) #expect(self.secondStoreCenter.isAuthenticated) } @Test func testSynchronization() async throws { guard let userId = StoreCenter.main.userId else { throw TestError.notAuthenticated } // Cleanup let eventCollection1: SyncedCollection = await StoreCenter.main.mainStore.asyncLoadingSynchronizedCollection() try await eventCollection1.loadOnceAsync() #expect(eventCollection1.hasLoaded == true) try await eventCollection1.deleteAsync(contentOfs: Array(eventCollection1)) let eventCollection2: SyncedCollection = await self.secondStoreCenter.mainStore.asyncLoadingSynchronizedCollection() #expect(eventCollection2.hasLoaded == true) eventCollection2.reset() // cleanup sync residues let _ = try await self.secondStoreCenter.testSynchronizeOnceAsync() // Create let event: Event = Event(creator: userId, club: nil, name: "test") try await eventCollection1.addOrUpdateAsync(instance: event) let serverEvents: [Event] = try await StoreCenter.main.service().get() #expect(serverEvents.count == 1) try await eventCollection1.loadDataFromServerIfAllowed(clear: true) #expect(eventCollection1.count == 1) let data = try await self.secondStoreCenter.testSynchronizeOnceAsync() let syncData = try SyncData(data: data, storeCenter: self.secondStoreCenter) #expect(syncData.updates.count == 1) #expect(syncData.deletions.count == 0) #expect(eventCollection2.count == 1) let data2 = try await self.secondStoreCenter.testSynchronizeOnceAsync() let syncData2 = try SyncData(data: data2, storeCenter: self.secondStoreCenter) #expect(syncData2.updates.count == 0) #expect(syncData.deletions.count == 0) #expect(eventCollection2.count == 1) } @Test func testSynchronizationBothWays() async throws { guard let userId = StoreCenter.main.userId else { throw TestError.notAuthenticated } // Setup events collections let eventCollectionA: SyncedCollection = await StoreCenter.main.mainStore.asyncLoadingSynchronizedCollection() try await eventCollectionA.deleteAsync(contentOfs: Array(eventCollectionA)) let eventCollectionB: SyncedCollection = await self.secondStoreCenter.mainStore.asyncLoadingSynchronizedCollection() eventCollectionB.reset() // Setup clubs collections let clubCollectionA: SyncedCollection = await StoreCenter.main.mainStore.asyncLoadingSynchronizedCollection() try await clubCollectionA.deleteAsync(contentOfs: Array(clubCollectionA)) let clubCollectionB: SyncedCollection = await self.secondStoreCenter.mainStore.asyncLoadingSynchronizedCollection() clubCollectionB.reset() // cleanup sync residues let _ = try await self.secondStoreCenter.testSynchronizeOnceAsync() // Create let eventA: Event = Event(creator: userId, club: nil, name: "test-b") try await eventCollectionA.addOrUpdateAsync(instance: eventA) // Retrieve Event let dataB = try await self.secondStoreCenter.testSynchronizeOnceAsync() let syncDataB = try SyncData(data: dataB, storeCenter: self.secondStoreCenter) #expect(syncDataB.updates.count == 1) #expect(eventCollectionB.count == 1) // Create club on 2nd StoreCenter let club = Club(creator: userId, name: "Padel Club", acronym: "PC") try await clubCollectionB.addOrUpdateAsync(instance: club) guard let eventB = eventCollectionB.findById(eventA.id) else { throw SyncTestError.instanceNotFound(id: eventA.id) } eventB.club = club.id try await eventCollectionB.addOrUpdateAsync(instance: eventB) // Synchronize 1st StoreCenter let dataA = try await StoreCenter.main.testSynchronizeOnceAsync() let syncDataA = try SyncData(data: dataA, storeCenter: StoreCenter.main) #expect(eventCollectionA.count == 1) #expect(clubCollectionB.count == 1) #expect(syncDataA.updates.count == 2) guard let clubArray = syncDataA.updates.first(where: { $0.type == Club.self }) else { throw SyncTestError.missingSyncData } #expect(clubArray.items.count == 1) guard let eventArray = syncDataA.updates.first(where: { $0.type == Event.self }) else { throw SyncTestError.missingSyncData } #expect(eventArray.items.count == 1) guard let eventACopy = eventCollectionA.findById(eventA.id) else { throw SyncTestError.instanceNotFound(id: eventA.id) } #expect(eventACopy.club == club.id) } @Test func testSynchronizationDelete() async throws { // Setup tournament let tournament = Tournament() // Setup TeamReg let teamRegColA: SyncedCollection = await StoreCenter.main.mainStore.asyncLoadingSynchronizedCollection() try await teamRegColA.deleteAsync(contentOfs: Array(teamRegColA)) let teamRegColB: SyncedCollection = await self.secondStoreCenter.mainStore.asyncLoadingSynchronizedCollection() teamRegColB.reset() // cleanup sync residues let _ = try await self.secondStoreCenter.testSynchronizeOnceAsync() // Create let trA = TeamRegistration(tournament: tournament.id) try await teamRegColA.addOrUpdateAsync(instance: trA) try await teamRegColA.deleteAsync(instance: trA) #expect(teamRegColA.count == 0) let _ = try await self.secondStoreCenter.testSynchronizeOnceAsync() #expect(teamRegColB.count == 0) } @Test func testSyncConflictResolution() async throws { guard let userId = StoreCenter.main.userId else { throw TestError.notAuthenticated } // Setup events collections let eventCollectionA: SyncedCollection = await StoreCenter.main.mainStore.asyncLoadingSynchronizedCollection(inMemory: true) try await eventCollectionA.loadOnceAsync() try await eventCollectionA.deleteAsync(contentOfs: Array(eventCollectionA)) let eventCollectionB: SyncedCollection = await self.secondStoreCenter.mainStore.asyncLoadingSynchronizedCollection() eventCollectionB.reset() // cleanup sync residues let _ = try await self.secondStoreCenter.testSynchronizeOnceAsync() #expect(eventCollectionA.count == 0) #expect(eventCollectionB.count == 0) // Create let eventA: Event = Event(creator: userId, club: nil, name: "test-b") try await eventCollectionA.addOrUpdateAsync(instance: eventA) #expect(eventCollectionA.count == 1) let serverEvents: [Event] = try await StoreCenter.main.service().get() #expect(serverEvents.count == 1) // Retrieve Event let dataB = try await self.secondStoreCenter.testSynchronizeOnceAsync() let syncDataB = try SyncData(data: dataB, storeCenter: self.secondStoreCenter) #expect(syncDataB.updates.count == 1) #expect(eventCollectionB.count == 1) guard let eventB = eventCollectionB.findById(eventA.id) else { throw SyncTestError.instanceNotFound(id: eventA.id) } eventA.name = "my event is nice" try await eventCollectionA.addOrUpdateAsync(instance: eventA) eventB.name = "my event is better" try await eventCollectionB.addOrUpdateAsync(instance: eventB) try await eventCollectionA.addOrUpdateAsync(instance: eventA) let _ = try await StoreCenter.main.testSynchronizeOnceAsync() let _ = try await self.secondStoreCenter.testSynchronizeOnceAsync() #expect(eventCollectionA.count == 1) #expect(eventCollectionB.count == 1) #expect(eventCollectionA.first?.name == "my event is nice") #expect(eventCollectionB.first?.name == "my event is nice") } /// In this test, the first user: /// - creates one event and 2 clubs /// - shares the event with a second user /// - changes the club on the event /// Here we want to test that the first Club is removed and the second one is received @Test func testRelationshipChange() async throws { guard let userId = StoreCenter.main.userId else { throw TestError.notAuthenticated } // Setup let eventColA: SyncedCollection = await StoreCenter.main.mainStore.asyncLoadingSynchronizedCollection() let clubColA: SyncedCollection = await StoreCenter.main.mainStore.asyncLoadingSynchronizedCollection() let eventColB: SyncedCollection = await self.secondStoreCenter.mainStore.asyncLoadingSynchronizedCollection() let clubColB: SyncedCollection = await self.secondStoreCenter.mainStore.asyncLoadingSynchronizedCollection() if let dataAccessCollection = StoreCenter.main.dataAccessCollection { try await dataAccessCollection.deleteAsync(contentOfs: Array(dataAccessCollection)) } try await eventColA.deleteAsync(contentOfs: Array(eventColA)) try await clubColA.deleteAsync(contentOfs: Array(clubColA)) let _ = try await self.secondStoreCenter.testSynchronizeOnceAsync() #expect(eventColB.count == 0) #expect(clubColB.count == 0) // Create let club1A = Club(creator: userId, name: "Club 1", acronym: "C1") try await clubColA.addOrUpdateAsync(instance: club1A) let club2A = Club(creator: userId, name: "Club 2", acronym: "C2") try await clubColA.addOrUpdateAsync(instance: club2A) let eventA = Event(creator: userId, club: club1A.id, name: "event 1") try await eventColA.addOrUpdateAsync(instance: eventA) // Share with user2 let _ = try await self.secondStoreCenter.testSynchronizeOnceAsync() #expect(eventColB.count == 1) #expect(clubColB.count == 2) // Change the club eventA.club = club2A.id try await eventColA.addOrUpdateAsync(instance: eventA) let _ = try await self.secondStoreCenter.testSynchronizeOnceAsync() #expect(eventColB.first?.club == eventA.club) } // needs to run on a postgreSQL, otherwise fails because of sqlite database locks @Test func testBuildEverything() async throws { // Cleanup let tournamentColA: SyncedCollection = await StoreCenter.main.mainStore.asyncLoadingSynchronizedCollection() let tournamentColB: SyncedCollection = await self.secondStoreCenter.mainStore.asyncLoadingSynchronizedCollection() let tournamentsToDelete: [Tournament] = try await StoreCenter.main.service().get() tournamentColA.delete(contentOfs: tournamentsToDelete) // Setup tournament + build everything let tournament = Tournament() try await tournamentColA.addOrUpdateAsync(instance: tournament) try await tournament.deleteAndBuildEverythingAsync() let tourStore = try StoreCenter.main.store(identifier: tournament.id) let gsColA: SyncedCollection = try tourStore.syncedCollection() let roundColA: SyncedCollection = try tourStore.syncedCollection() let matchesColA: SyncedCollection = try tourStore.syncedCollection() #expect(gsColA.count == 4) #expect(roundColA.count == 15) #expect(matchesColA.count == 56) // Sync with 2nd store try await secondStoreCenter.testSynchronizeOnceAsync() #expect(tournamentColB.count == 1) let tourStoreB = try secondStoreCenter.store(identifier: tournament.id) let gsColB: SyncedCollection = try tourStoreB.syncedCollection() let roundColB: SyncedCollection = try tourStoreB.syncedCollection() let matchesColB: SyncedCollection = try tourStoreB.syncedCollection() #expect(gsColB.count == 4) #expect(roundColB.count == 15) #expect(matchesColB.count == 56) // change setup + build everything tournament.groupStageCount = 2 tournament.teamCount = 20 try await tournamentColA.addOrUpdateAsync(instance: tournament) try await tournament.deleteAndBuildEverythingAsync() #expect(gsColA.count == 2) #expect(roundColA.count == 15) #expect(matchesColA.count == 44) // Sync with 2nd store try await secondStoreCenter.testSynchronizeOnceAsync() #expect(gsColB.count == 2) #expect(roundColB.count == 15) #expect(matchesColB.count == 44) } }