You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
PadelClubData/PadelClubDataTests/SynchronizationTests.swift

371 lines
15 KiB

//
// 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<Event> = await StoreCenter.main.mainStore.asyncLoadingSynchronizedCollection()
try await eventCollection1.loadOnceAsync()
#expect(eventCollection1.hasLoaded == true)
try await eventCollection1.deleteAsync(contentOfs: Array(eventCollection1))
let eventCollection2: SyncedCollection<Event> = 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<Event> = await StoreCenter.main.mainStore.asyncLoadingSynchronizedCollection()
try await eventCollectionA.deleteAsync(contentOfs: Array(eventCollectionA))
let eventCollectionB: SyncedCollection<Event> = await self.secondStoreCenter.mainStore.asyncLoadingSynchronizedCollection()
eventCollectionB.reset()
// Setup clubs collections
let clubCollectionA: SyncedCollection<Club> = await StoreCenter.main.mainStore.asyncLoadingSynchronizedCollection()
try await clubCollectionA.deleteAsync(contentOfs: Array(clubCollectionA))
let clubCollectionB: SyncedCollection<Club> = 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<TeamRegistration> = await StoreCenter.main.mainStore.asyncLoadingSynchronizedCollection()
try await teamRegColA.deleteAsync(contentOfs: Array(teamRegColA))
let teamRegColB: SyncedCollection<TeamRegistration> = 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<Event> = await StoreCenter.main.mainStore.asyncLoadingSynchronizedCollection(inMemory: true)
try await eventCollectionA.loadOnceAsync()
try await eventCollectionA.deleteAsync(contentOfs: Array(eventCollectionA))
let eventCollectionB: SyncedCollection<Event> = 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<Event> = await StoreCenter.main.mainStore.asyncLoadingSynchronizedCollection()
let clubColA: SyncedCollection<Club> = await StoreCenter.main.mainStore.asyncLoadingSynchronizedCollection()
let eventColB: SyncedCollection<Event> = await self.secondStoreCenter.mainStore.asyncLoadingSynchronizedCollection()
let clubColB: SyncedCollection<Club> = 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<Tournament> = await StoreCenter.main.mainStore.asyncLoadingSynchronizedCollection()
let tournamentColB: SyncedCollection<Tournament> = 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<GroupStage> = try tourStore.syncedCollection()
let roundColA: SyncedCollection<Round> = try tourStore.syncedCollection()
let matchesColA: SyncedCollection<Match> = 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<GroupStage> = try tourStoreB.syncedCollection()
let roundColB: SyncedCollection<Round> = try tourStoreB.syncedCollection()
let matchesColB: SyncedCollection<Match> = 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)
}
}