From 3139a694adb0902526a3328cc9f63e6a3f00a295 Mon Sep 17 00:00:00 2001 From: Laurent Date: Mon, 12 May 2025 16:48:11 +0200 Subject: [PATCH] Refactor deleteDependencies + tests --- PadelClubData/Data/Club.swift | 15 +- PadelClubData/Data/Court.swift | 2 +- PadelClubData/Data/DateInterval.swift | 2 +- PadelClubData/Data/DrawLog.swift | 2 +- PadelClubData/Data/Event.swift | 27 +- PadelClubData/Data/GroupStage.swift | 46 +--- PadelClubData/Data/Match.swift | 15 +- PadelClubData/Data/Round.swift | 31 ++- PadelClubData/Data/TeamRegistration.swift | 57 ++-- PadelClubData/Data/Tournament.swift | 67 +++-- PadelClubData/Data/TournamentLibrary.swift | 4 +- PadelClubDataTests/DeletionTests.swift | 269 +++++++++++++++++++ PadelClubDataTests/PadelClubDataTests.swift | 1 + PadelClubDataTests/SyncDataAccessTests.swift | 67 ++++- 14 files changed, 458 insertions(+), 147 deletions(-) create mode 100644 PadelClubDataTests/DeletionTests.swift diff --git a/PadelClubData/Data/Club.swift b/PadelClubData/Data/Club.swift index 74bd667..3c6a901 100644 --- a/PadelClubData/Data/Club.swift +++ b/PadelClubData/Data/Club.swift @@ -29,12 +29,15 @@ final public class Club: BaseClub { DataStore.shared.courts.filter { $0.club == self.id }.sorted(by: \.index) } - public override func deleteDependencies(shouldBeSynchronized: Bool) { - let customizedCourts = self.customizedCourts - for customizedCourt in customizedCourts { - customizedCourt.deleteDependencies(shouldBeSynchronized: shouldBeSynchronized) - } - DataStore.shared.courts.deleteDependencies(customizedCourts, shouldBeSynchronized: shouldBeSynchronized) + public override func deleteDependencies(store: Store, shouldBeSynchronized: Bool) { + + store.deleteDependencies(type: Court.self, shouldBeSynchronized: shouldBeSynchronized) { $0.club == self.id } + +// let customizedCourts = self.customizedCourts +// for customizedCourt in customizedCourts { +// customizedCourt.deleteDependencies(store: store, shouldBeSynchronized: shouldBeSynchronized) +// } +// DataStore.shared.courts.deleteDependencies(customizedCourts, shouldBeSynchronized: shouldBeSynchronized) } } diff --git a/PadelClubData/Data/Court.swift b/PadelClubData/Data/Court.swift index a2d6c39..db5f774 100644 --- a/PadelClubData/Data/Court.swift +++ b/PadelClubData/Data/Court.swift @@ -52,7 +52,7 @@ final public class Court: BaseCourt { Store.main.findById(club) } - public override func deleteDependencies(shouldBeSynchronized: Bool) { + public override func deleteDependencies(store: Store, shouldBeSynchronized: Bool) { } } diff --git a/PadelClubData/Data/DateInterval.swift b/PadelClubData/Data/DateInterval.swift index b7cb277..c141a1a 100644 --- a/PadelClubData/Data/DateInterval.swift +++ b/PadelClubData/Data/DateInterval.swift @@ -57,7 +57,7 @@ final public class DateInterval: BaseDateInterval { date <= startDate && date <= endDate && date >= startDate && date >= endDate } - public override func deleteDependencies(shouldBeSynchronized: Bool) { + public override func deleteDependencies(store: Store, shouldBeSynchronized: Bool) { } // enum CodingKeys: String, CodingKey { diff --git a/PadelClubData/Data/DrawLog.swift b/PadelClubData/Data/DrawLog.swift index 4130f73..0671d4b 100644 --- a/PadelClubData/Data/DrawLog.swift +++ b/PadelClubData/Data/DrawLog.swift @@ -74,7 +74,7 @@ final public class DrawLog: BaseDrawLog, SideStorable { return TournamentLibrary.shared.store(tournamentId: self.tournament) } - public override func deleteDependencies(shouldBeSynchronized: Bool) { + public override func deleteDependencies(store: Store, shouldBeSynchronized: Bool) { } } diff --git a/PadelClubData/Data/Event.swift b/PadelClubData/Data/Event.swift index 57db29a..855710a 100644 --- a/PadelClubData/Data/Event.swift +++ b/PadelClubData/Data/Event.swift @@ -25,19 +25,24 @@ final public class Event: BaseEvent { super.init() } - public override func deleteDependencies(shouldBeSynchronized: Bool) { - let tournaments = self.tournaments - for tournament in tournaments { - tournament.deleteDependencies(shouldBeSynchronized: shouldBeSynchronized) - } + public override func deleteDependencies(store: Store, shouldBeSynchronized: Bool) { - DataStore.shared.tournaments.deleteDependencies(tournaments, shouldBeSynchronized: shouldBeSynchronized) + store.deleteDependencies(type: Tournament.self, shouldBeSynchronized: shouldBeSynchronized) { $0.event == self.id && $0.isDeleted == false } + store.deleteDependencies(type: DateInterval.self, shouldBeSynchronized: shouldBeSynchronized) { $0.event == self.id } + - let courtsUnavailabilities = self.courtsUnavailability - for courtsUnavailability in courtsUnavailabilities { - courtsUnavailability.deleteDependencies(shouldBeSynchronized: shouldBeSynchronized) - } - DataStore.shared.dateIntervals.deleteDependencies(courtsUnavailabilities, shouldBeSynchronized: shouldBeSynchronized) +// let tournaments = self.tournaments +// for tournament in tournaments { +// tournament.deleteDependencies(store: store, shouldBeSynchronized: shouldBeSynchronized) +// } +// +// DataStore.shared.tournaments.deleteDependencies(tournaments, shouldBeSynchronized: shouldBeSynchronized) +// +// let courtsUnavailabilities = self.courtsUnavailability +// for courtsUnavailability in courtsUnavailabilities { +// courtsUnavailability.deleteDependencies(store: store, shouldBeSynchronized: shouldBeSynchronized) +// } +// DataStore.shared.dateIntervals.deleteDependencies(courtsUnavailabilities, shouldBeSynchronized: shouldBeSynchronized) } // MARK: - Computed dependencies diff --git a/PadelClubData/Data/GroupStage.swift b/PadelClubData/Data/GroupStage.swift index a067168..7cf6dfc 100644 --- a/PadelClubData/Data/GroupStage.swift +++ b/PadelClubData/Data/GroupStage.swift @@ -599,43 +599,17 @@ final public class GroupStage: BaseGroupStage, SideStorable { return teams(true).firstIndex(of: team) } - public override func deleteDependencies(shouldBeSynchronized: Bool) { - let matches = self._matches() - for match in matches { - match.deleteDependencies(shouldBeSynchronized: shouldBeSynchronized) - } - self.tournamentStore?.matches.deleteDependencies(matches, shouldBeSynchronized: shouldBeSynchronized) - } - -// init(from decoder: Decoder) throws { -// let container = try decoder.container(keyedBy: CodingKeys.self) -// -// id = try container.decode(String.self, forKey: ._id) -// storeId = try container.decode(String.self, forKey: ._storeId) -// lastUpdate = try container.decode(Date.self, forKey: ._lastUpdate) -// tournament = try container.decode(String.self, forKey: ._tournament) -// index = try container.decode(Int.self, forKey: ._index) -// size = try container.decode(Int.self, forKey: ._size) -// format = try container.decodeIfPresent(MatchFormat.self, forKey: ._format) -// startDate = try container.decodeIfPresent(Date.self, forKey: ._startDate) -// name = try container.decodeIfPresent(String.self, forKey: ._name) -// step = try container.decodeIfPresent(Int.self, forKey: ._step) ?? 0 -// } -// -// public func encode(to encoder: Encoder) throws { -// public var container = encoder.container(keyedBy: CodingKeys.self) + public override func deleteDependencies(store: Store, shouldBeSynchronized: Bool) { + + store.deleteDependencies(type: Match.self, shouldBeSynchronized: shouldBeSynchronized) { $0.groupStage == self.id } + // -// try container.encode(id, forKey: ._id) -// try container.encode(storeId, forKey: ._storeId) -// try container.encode(lastUpdate, forKey: ._lastUpdate) -// try container.encode(tournament, forKey: ._tournament) -// try container.encode(index, forKey: ._index) -// try container.encode(size, forKey: ._size) -// try container.encode(format, forKey: ._format) -// try container.encode(startDate, forKey: ._startDate) -// try container.encode(name, forKey: ._name) -// try container.encode(step, forKey: ._step) -// } +// let matches = self._matches() +// for match in matches { +// match.deleteDependencies(store: store, shouldBeSynchronized: shouldBeSynchronized) +// } +// self.tournamentStore?.matches.deleteDependencies(matches, shouldBeSynchronized: shouldBeSynchronized) + } func insertOnServer() { self.tournamentStore?.groupStages.writeChangeAndInsertOnServer(instance: self) diff --git a/PadelClubData/Data/Match.swift b/PadelClubData/Data/Match.swift index 8a37997..fc42482 100644 --- a/PadelClubData/Data/Match.swift +++ b/PadelClubData/Data/Match.swift @@ -70,12 +70,15 @@ final public class Match: BaseMatch, SideStorable { // MARK: - - public override func deleteDependencies(shouldBeSynchronized: Bool) { - let teamScores = self.teamScores - for teamScore in teamScores { - teamScore.deleteDependencies(shouldBeSynchronized: shouldBeSynchronized) - } - self.tournamentStore?.teamScores.deleteDependencies(teamScores, shouldBeSynchronized: shouldBeSynchronized) + public override func deleteDependencies(store: Store, shouldBeSynchronized: Bool) { + + store.deleteDependencies(type: TeamScore.self, shouldBeSynchronized: shouldBeSynchronized) { $0.match == self.id } + +// let teamScores = self.teamScores +// for teamScore in teamScores { +// teamScore.deleteDependencies(store: store, shouldBeSynchronized: shouldBeSynchronized) +// } +// self.tournamentStore?.teamScores.deleteDependencies(teamScores, shouldBeSynchronized: shouldBeSynchronized) } public func indexInRound(in matches: [Match]? = nil) -> Int { diff --git a/PadelClubData/Data/Round.swift b/PadelClubData/Data/Round.swift index 12c0ccf..6c1fd3e 100644 --- a/PadelClubData/Data/Round.swift +++ b/PadelClubData/Data/Round.swift @@ -890,20 +890,25 @@ defer { _cachedLoserRoundsAndChildren = nil } - public override func deleteDependencies(shouldBeSynchronized: Bool) { - let matches = self._matches() - for match in matches { - match.deleteDependencies(shouldBeSynchronized: shouldBeSynchronized) - } - - self.tournamentStore?.matches.deleteDependencies(matches, shouldBeSynchronized: shouldBeSynchronized) - - let loserRounds = self.loserRounds() - for round in loserRounds { - round.deleteDependencies(shouldBeSynchronized: shouldBeSynchronized) - } + public override func deleteDependencies(store: Store, shouldBeSynchronized: Bool) { + + store.deleteDependencies(type: Match.self, shouldBeSynchronized: shouldBeSynchronized) { $0.round == self.id } + store.deleteDependencies(type: Round.self, shouldBeSynchronized: shouldBeSynchronized) { $0.parent == self.id } + - self.tournamentStore?.rounds.deleteDependencies(loserRounds, shouldBeSynchronized: shouldBeSynchronized) +// let matches = self._matches() +// for match in matches { +// match.deleteDependencies(store: store, shouldBeSynchronized: shouldBeSynchronized) +// } +// +// self.tournamentStore?.matches.deleteDependencies(matches, shouldBeSynchronized: shouldBeSynchronized) +// +// let loserRounds = self.loserRounds() +// for round in loserRounds { +// round.deleteDependencies(store: store, shouldBeSynchronized: shouldBeSynchronized) +// } +// +// self.tournamentStore?.rounds.deleteDependencies(loserRounds, shouldBeSynchronized: shouldBeSynchronized) } diff --git a/PadelClubData/Data/TeamRegistration.swift b/PadelClubData/Data/TeamRegistration.swift index 32cfc11..e65b155 100644 --- a/PadelClubData/Data/TeamRegistration.swift +++ b/PadelClubData/Data/TeamRegistration.swift @@ -12,37 +12,6 @@ import SwiftUI @Observable final public class TeamRegistration: BaseTeamRegistration, SideStorable { - // static func resourceName() -> String { "team-registrations" } - // static func tokenExemptedMethods() -> [HTTPMethod] { return [] } - // static func filterByStoreIdentifier() -> Bool { return true } - // static var relationshipNames: [String] = [] - // - // var id: String = Store.randomId() - // var lastUpdate: Date - // var tournament: String - // var groupStage: String? - // var registrationDate: Date? - // var callDate: Date? - // var bracketPosition: Int? - // var groupStagePosition: Int? - // var comment: String? - // var source: String? - // var sourceValue: String? - // var logo: String? - // var name: String? - // - // var walkOut: Bool = false - // var wildCardBracket: Bool = false - // var wildCardGroupStage: Bool = false - // var weight: Int = 0 - // var lockedWeight: Int? - // var confirmationDate: Date? - // var qualified: Bool = false - // var finalRanking: Int? - // var pointsEarned: Int? - // - // var storeId: String? = nil - public init( tournament: String, groupStage: String? = nil, registrationDate: Date? = nil, callDate: Date? = nil, bracketPosition: Int? = nil, groupStagePosition: Int? = nil, @@ -130,18 +99,22 @@ final public class TeamRegistration: BaseTeamRegistration, SideStorable { tournamentStore.teamScores.delete(contentOfs: ts) } - public override func deleteDependencies(shouldBeSynchronized: Bool) { - let unsortedPlayers = unsortedPlayers() - for player in unsortedPlayers { - player.deleteDependencies(shouldBeSynchronized: shouldBeSynchronized) - } - self.tournamentStore?.playerRegistrations.deleteDependencies(unsortedPlayers, shouldBeSynchronized: shouldBeSynchronized) + public override func deleteDependencies(store: Store, shouldBeSynchronized: Bool) { + + store.deleteDependencies(type: TeamScore.self, shouldBeSynchronized: shouldBeSynchronized) { $0.teamRegistration == self.id } + store.deleteDependencies(type: PlayerRegistration.self, shouldBeSynchronized: shouldBeSynchronized) { $0.teamRegistration == self.id } - let teamScores = teamScores() - for teamScore in teamScores { - teamScore.deleteDependencies(shouldBeSynchronized: shouldBeSynchronized) - } - self.tournamentStore?.teamScores.deleteDependencies(teamScores, shouldBeSynchronized: shouldBeSynchronized) + // let unsortedPlayers = unsortedPlayers() +// for player in unsortedPlayers { +// player.deleteDependencies(store: store, shouldBeSynchronized: shouldBeSynchronized) +// } +// self.tournamentStore?.playerRegistrations.deleteDependencies(unsortedPlayers, shouldBeSynchronized: shouldBeSynchronized) +// +// let teamScores = teamScores() +// for teamScore in teamScores { +// teamScore.deleteDependencies(store: store, shouldBeSynchronized: shouldBeSynchronized) +// } +// self.tournamentStore?.teamScores.deleteDependencies(teamScores, shouldBeSynchronized: shouldBeSynchronized) } public func hasArrived(isHere: Bool = false) { diff --git a/PadelClubData/Data/Tournament.swift b/PadelClubData/Data/Tournament.swift index b01e9a5..81856af 100644 --- a/PadelClubData/Data/Tournament.swift +++ b/PadelClubData/Data/Tournament.swift @@ -32,34 +32,47 @@ final public class Tournament: BaseTournament { return TournamentLibrary.shared.store(tournamentId: self.id) } - public override func deleteDependencies(shouldBeSynchronized: Bool) { - guard let store = self.tournamentStore else { return } - - let drawLogs = Array(store.drawLogs) - for drawLog in drawLogs { - drawLog.deleteDependencies(shouldBeSynchronized: shouldBeSynchronized) - } - store.drawLogs.deleteDependencies(drawLogs, shouldBeSynchronized: shouldBeSynchronized) - - let teams = Array(store.teamRegistrations) - for team in teams { - team.deleteDependencies(shouldBeSynchronized: shouldBeSynchronized) - } - store.teamRegistrations.deleteDependencies(teams, shouldBeSynchronized: shouldBeSynchronized) - - let groups = Array(store.groupStages) - for group in groups { - group.deleteDependencies(shouldBeSynchronized: shouldBeSynchronized) - } - store.groupStages.deleteDependencies(groups, shouldBeSynchronized: shouldBeSynchronized) - - let rounds = Array(store.rounds) - for round in rounds { - round.deleteDependencies(shouldBeSynchronized: shouldBeSynchronized) + public override func deleteDependencies(store: Store, shouldBeSynchronized: Bool) { + + do { + let tournamentStore = try store.alternateStore(identifier: self.id) + + tournamentStore.deleteAllDependencies(type: DrawLog.self, shouldBeSynchronized: shouldBeSynchronized) + tournamentStore.deleteAllDependencies(type: TeamRegistration.self, shouldBeSynchronized: shouldBeSynchronized) + tournamentStore.deleteAllDependencies(type: GroupStage.self, shouldBeSynchronized: shouldBeSynchronized) + tournamentStore.deleteAllDependencies(type: Round.self, shouldBeSynchronized: shouldBeSynchronized) + tournamentStore.deleteAllDependencies(type: MatchScheduler.self, shouldBeSynchronized: shouldBeSynchronized) + } catch { + Logger.error(error) } - store.rounds.deleteDependencies(rounds, shouldBeSynchronized: shouldBeSynchronized) - - store.matchSchedulers.deleteDependencies(self._matchSchedulers()) + +// guard let tournamentStore = self.tournamentStore else { return } +// +// let drawLogs = Array(tournamentStore.drawLogs) +// for drawLog in drawLogs { +// drawLog.deleteDependencies(store: tournamentStore, shouldBeSynchronized: shouldBeSynchronized) +// } +// tournamentStore.drawLogs.deleteDependencies(drawLogs, shouldBeSynchronized: shouldBeSynchronized) +// +// let teams = Array(tournamentStore.teamRegistrations) +// for team in teams { +// team.deleteDependencies(store: tournamentStore, shouldBeSynchronized: shouldBeSynchronized) +// } +// tournamentStore.teamRegistrations.deleteDependencies(teams, shouldBeSynchronized: shouldBeSynchronized) +// +// let groups = Array(tournamentStore.groupStages) +// for group in groups { +// group.deleteDependencies(store: tournamentStore, shouldBeSynchronized: shouldBeSynchronized) +// } +// tournamentStore.groupStages.deleteDependencies(groups, shouldBeSynchronized: shouldBeSynchronized) +// +// let rounds = Array(tournamentStore.rounds) +// for round in rounds { +// round.deleteDependencies(store: tournamentStore, shouldBeSynchronized: shouldBeSynchronized) +// } +// tournamentStore.rounds.deleteDependencies(rounds, shouldBeSynchronized: shouldBeSynchronized) +// +// tournamentStore.matchSchedulers.deleteDependencies(self._matchSchedulers()) } diff --git a/PadelClubData/Data/TournamentLibrary.swift b/PadelClubData/Data/TournamentLibrary.swift index 50f2618..207df36 100644 --- a/PadelClubData/Data/TournamentLibrary.swift +++ b/PadelClubData/Data/TournamentLibrary.swift @@ -15,12 +15,12 @@ public class TournamentLibrary { fileprivate var _stores: [String : TournamentStore] = [:] func store(tournamentId: String) -> TournamentStore? { - guard let tournament = DataStore.shared.tournaments.first(where: { $0.id == tournamentId }) else { return nil } + guard let _ = DataStore.shared.tournaments.first(where: { $0.id == tournamentId }) else { return nil } if let store = self._stores[tournamentId] { return store } - let store = StoreCenter.main.store(identifier: tournamentId) + let store = StoreCenter.main.requestStore(identifier: tournamentId) let tournamentStore = TournamentStore(store: store) self._stores[tournamentId] = tournamentStore return tournamentStore diff --git a/PadelClubDataTests/DeletionTests.swift b/PadelClubDataTests/DeletionTests.swift new file mode 100644 index 0000000..3b39b66 --- /dev/null +++ b/PadelClubDataTests/DeletionTests.swift @@ -0,0 +1,269 @@ +// +// DeletionTests.swift +// PadelClubDataTests +// +// Created by Laurent Morvillier on 08/05/2025. +// + +import Testing +@testable import PadelClubData +@testable import LeStorage + +struct DeletionTests { + + let username1: String = "UserDataTests" + let password1: String = "MyPass1234--" + + var secondStoreCenter: StoreCenter + + init() async throws { + + FileManager.default.deleteDirectoryInDocuments(directoryName: "storage") + FileManager.default.deleteDirectoryInDocuments(directoryName: "storage-2") + + self.secondStoreCenter = StoreCenter(directoryName: "storage-2") + self.secondStoreCenter.configureURLs(secureScheme: false, domain: "127.0.0.1:8000", 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, username: self.username1, password: self.password1) + } + + StoreCenter.main.configureURLs(secureScheme: false, domain: "127.0.0.1:8000", 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, username: self.username1, password: self.password1) + } + } + + mutating func login(storeCenter: StoreCenter, username: String, password: String) async throws { + let _: CustomUser = try await storeCenter.service().login(username: username, password: password) + } + + /// This test creates a hierarchy of objects and deletes to see if everything has been properly deleted + @Test func testDeleteEventWithDependencies() async throws { + + guard let user = StoreCenter.main.userId else { + throw TestError.notAuthenticated + } + + let clubColA: SyncedCollection = await StoreCenter.main.mainStore.asyncLoadingSynchronizedCollection() + let eventColA: SyncedCollection = await StoreCenter.main.mainStore.asyncLoadingSynchronizedCollection() + let tournamentColA: SyncedCollection = await StoreCenter.main.mainStore.asyncLoadingSynchronizedCollection() + + let club = Club(creator: user, name: "Club", acronym: "LC") + try await clubColA.addOrUpdateAsync(instance: club) + let event = Event(creator: user, club: club.id, name: "the event") + try await eventColA.addOrUpdateAsync(instance: event) + let tournament = Tournament(event: event.id, name: "P1000") + try await tournamentColA.addOrUpdateAsync(instance: tournament) + + let tournamentStore = StoreCenter.main.requestStore(identifier: tournament.id) + + let groupStageColA: SyncedCollection = await tournamentStore.asyncLoadingSynchronizedCollection() + let roundColA: SyncedCollection = await tournamentStore.asyncLoadingSynchronizedCollection() + let teamRegistrationColA: SyncedCollection = await tournamentStore.asyncLoadingSynchronizedCollection() + let playerRegistrationColA: SyncedCollection = await tournamentStore.asyncLoadingSynchronizedCollection() + let matchColA: SyncedCollection = await tournamentStore.asyncLoadingSynchronizedCollection() + let teamScoreColA: SyncedCollection = await tournamentStore.asyncLoadingSynchronizedCollection() + + let gs1 = GroupStage(tournament: tournament.id) + try await groupStageColA.addOrUpdateAsync(instance: gs1) + + let round1 = Round(tournament: tournament.id, index: 0) + try await roundColA.addOrUpdateAsync(instance: round1) + + let tr1 = TeamRegistration(tournament: tournament.id) + try await teamRegistrationColA.addOrUpdateAsync(instance: tr1) + + let pr1 = PlayerRegistration(teamRegistration: tr1.id, firstName: "A", lastName: "B") + try await playerRegistrationColA.addOrUpdateAsync(instance: pr1) + let pr2 = PlayerRegistration(teamRegistration: tr1.id, firstName: "B", lastName: "C") + try await playerRegistrationColA.addOrUpdateAsync(instance: pr2) + + let match = Match(groupStage: gs1.id, index: 0) + try await matchColA.addOrUpdateAsync(instance: match) + + let ts1 = TeamScore(match: match.id, teamRegistration: tr1.id) + try await teamScoreColA.addOrUpdateAsync(instance: ts1) + + #expect(clubColA.count == 1) + #expect(eventColA.count == 1) + #expect(tournamentColA.count == 1) + #expect(groupStageColA.count == 1) + #expect(roundColA.count == 1) + #expect(matchColA.count == 1) + #expect(teamRegistrationColA.count == 1) + #expect(teamScoreColA.count == 1) + #expect(playerRegistrationColA.count == 2) + + try await eventColA.deleteAsync(instance: event) + + #expect(clubColA.count == 1) + #expect(eventColA.count == 0) + #expect(tournamentColA.count == 0) + #expect(groupStageColA.count == 0) + #expect(roundColA.count == 0) + #expect(matchColA.count == 0) + #expect(teamRegistrationColA.count == 0) + #expect(teamScoreColA.count == 0) + #expect(playerRegistrationColA.count == 0) + + } + + /// This test creates a hierarchy of objects and see if the delete is properly propagated on a second StoreCenter + @Test func testDeleteEventWithDependenciesOnOtherStorage() async throws { + + guard let user = StoreCenter.main.userId else { + throw TestError.notAuthenticated + } + + // Creates collection on StoreCenter.main + let clubColA: SyncedCollection = await StoreCenter.main.mainStore.asyncLoadingSynchronizedCollection() + let eventColA: SyncedCollection = await StoreCenter.main.mainStore.asyncLoadingSynchronizedCollection() + let tournamentColA: SyncedCollection = await StoreCenter.main.mainStore.asyncLoadingSynchronizedCollection() + + let clubColB: SyncedCollection = await self.secondStoreCenter.mainStore.asyncLoadingSynchronizedCollection() + let eventColB: SyncedCollection = await self.secondStoreCenter.mainStore.asyncLoadingSynchronizedCollection() + let tournamentColB: SyncedCollection = await self.secondStoreCenter.mainStore.asyncLoadingSynchronizedCollection() + + // cleanup sync residues + let _ = try await self.secondStoreCenter.testSynchronizeOnceAsync() + + let club = Club(creator: user, name: "Club", acronym: "LC") + try await clubColA.addOrUpdateAsync(instance: club) + let event = Event(creator: user, club: club.id, name: "the event") + try await eventColA.addOrUpdateAsync(instance: event) + let tournament = Tournament(event: event.id, name: "P1000") + try await tournamentColA.addOrUpdateAsync(instance: tournament) + + let tournamentStoreA = StoreCenter.main.requestStore(identifier: tournament.id) + + let groupStageColA: SyncedCollection = await tournamentStoreA.asyncLoadingSynchronizedCollection() + let roundColA: SyncedCollection = await tournamentStoreA.asyncLoadingSynchronizedCollection() + let teamRegistrationColA: SyncedCollection = await tournamentStoreA.asyncLoadingSynchronizedCollection() + let playerRegistrationColA: SyncedCollection = await tournamentStoreA.asyncLoadingSynchronizedCollection() + let matchColA: SyncedCollection = await tournamentStoreA.asyncLoadingSynchronizedCollection() + let teamScoreColA: SyncedCollection = await tournamentStoreA.asyncLoadingSynchronizedCollection() + + let gs1 = GroupStage(tournament: tournament.id) + try await groupStageColA.addOrUpdateAsync(instance: gs1) + + let round1 = Round(tournament: tournament.id, index: 0) + try await roundColA.addOrUpdateAsync(instance: round1) + + let tr1 = TeamRegistration(tournament: tournament.id) + try await teamRegistrationColA.addOrUpdateAsync(instance: tr1) + + let pr1 = PlayerRegistration(teamRegistration: tr1.id, firstName: "A", lastName: "B") + try await playerRegistrationColA.addOrUpdateAsync(instance: pr1) + let pr2 = PlayerRegistration(teamRegistration: tr1.id, firstName: "B", lastName: "C") + try await playerRegistrationColA.addOrUpdateAsync(instance: pr2) + + let match = Match(groupStage: gs1.id, index: 0) + try await matchColA.addOrUpdateAsync(instance: match) + + let ts1 = TeamScore(match: match.id, teamRegistration: tr1.id) + try await teamScoreColA.addOrUpdateAsync(instance: ts1) + + #expect(clubColA.count == 1) + #expect(eventColA.count == 1) + #expect(tournamentColA.count == 1) + #expect(groupStageColA.count == 1) + #expect(roundColA.count == 1) + #expect(matchColA.count == 1) + #expect(teamRegistrationColA.count == 1) + #expect(teamScoreColA.count == 1) + #expect(playerRegistrationColA.count == 2) + + let tournamentStoreB = self.secondStoreCenter.requestStore(identifier: tournament.id) + + let groupStageColB: SyncedCollection = await tournamentStoreB.asyncLoadingSynchronizedCollection() + let roundColB: SyncedCollection = await tournamentStoreB.asyncLoadingSynchronizedCollection() + let teamRegistrationColB: SyncedCollection = await tournamentStoreB.asyncLoadingSynchronizedCollection() + let playerRegistrationColB: SyncedCollection = await tournamentStoreB.asyncLoadingSynchronizedCollection() + let matchColB: SyncedCollection = await tournamentStoreB.asyncLoadingSynchronizedCollection() + let teamScoreColB: SyncedCollection = await tournamentStoreB.asyncLoadingSynchronizedCollection() + + // cleanup sync residues + let _ = try await self.secondStoreCenter.testSynchronizeOnceAsync() +// let syncData = try SyncData(data: data, storeCenter: self.secondStoreCenter) + + #expect(clubColB.count == 1) + #expect(eventColB.count == 1) + #expect(tournamentColB.count == 1) + #expect(groupStageColB.count == 1) + #expect(roundColB.count == 1) + #expect(matchColB.count == 1) + #expect(teamRegistrationColB.count == 1) + #expect(teamScoreColB.count == 1) + #expect(playerRegistrationColB.count == 2) + + try await eventColA.deleteAsync(instance: event) + + let boolChecker = BoolChecker() + try await boolChecker.waitForCondition { + await !StoreCenter.main.hasPendingAPICalls() + } + + // cleanup sync residues + let data = try await self.secondStoreCenter.testSynchronizeOnceAsync() + let syncData = try SyncData(data: data, storeCenter: self.secondStoreCenter) + + #expect(syncData.deletions.count == 8) // 8 different deleted types + + #expect(clubColB.count == 1) + #expect(eventColB.count == 0) + #expect(tournamentColB.count == 0) + #expect(groupStageColB.count == 0) + #expect(roundColB.count == 0) + #expect(matchColB.count == 0) + #expect(teamRegistrationColB.count == 0) + #expect(teamScoreColB.count == 0) + #expect(playerRegistrationColB.count == 0) + + } +} + +enum BoolCheckerError: Error { + case conditionNotMet(timeout: TimeInterval) +} + +actor BoolChecker { + /// Continuously checks a condition every 100ms + /// - Parameters: + /// - checkCondition: A closure that returns a Bool + /// - Throws: BoolCheckerError if condition is not met within 2500ms + /// - Returns: True when the condition becomes true + func waitForCondition( + _ checkCondition: @escaping () async -> Bool + ) async throws { + let timeout: TimeInterval = 30 + let startTime = Date() + + while Date().timeIntervalSince(startTime) < timeout { + // Check if the condition is true + if await checkCondition() { + return + } + + print("sleep...") + + // Wait for 100ms before next check + try? await Task.sleep(for: .milliseconds(100)) + } + + // Throw error if timeout is reached + throw BoolCheckerError.conditionNotMet(timeout: timeout) + } +} diff --git a/PadelClubDataTests/PadelClubDataTests.swift b/PadelClubDataTests/PadelClubDataTests.swift index 1bbd58f..18acb93 100644 --- a/PadelClubDataTests/PadelClubDataTests.swift +++ b/PadelClubDataTests/PadelClubDataTests.swift @@ -12,6 +12,7 @@ import Testing enum TestError: Error { case notAuthenticated case sameDeviceId + case missingEvent } struct PadelClubDataTests { diff --git a/PadelClubDataTests/SyncDataAccessTests.swift b/PadelClubDataTests/SyncDataAccessTests.swift index 08f9a77..c605dbc 100644 --- a/PadelClubDataTests/SyncDataAccessTests.swift +++ b/PadelClubDataTests/SyncDataAccessTests.swift @@ -257,6 +257,71 @@ struct SyncDataAccessTests { #expect(eventColB.first?.club == club2A.id) } - // trouver le moyen de tester les sous-Store et voir si on peut essayer de rendre ça propre sans passer par DataStore ou autre + /// In this test, the first user: + /// - creates one event + /// - shares the event with a second user + /// The second user: + /// - changes the club + /// Here we want to test that the first Club is removed and the second one is received + @Test func testRelationshipChangesByAgent() async throws { + + guard let userId1 = StoreCenter.main.userId else { + throw TestError.notAuthenticated + } + guard let userId2 = self.secondStoreCenter.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 clubA = Club(creator: userId1, name: "Club 1", acronym: "C1") + try await clubColA.addOrUpdateAsync(instance: clubA) + + let eventA = Event(creator: userId1, club: clubA.id, name: "event 1") + try await eventColA.addOrUpdateAsync(instance: eventA) + + // Share with user2 + try await StoreCenter.main.setAuthorizedUsersAsync(for: eventA, users: [userId2]) + let _ = try await self.secondStoreCenter.testSynchronizeOnceAsync() + + guard let eventB = eventColB.first else { + throw TestError.missingEvent + } + #expect(eventA.id == eventB.id) + #expect(clubColB.count == 1) + + // Second user changes the club + let club2B = Club(creator: userId2, name: "Club 2", acronym: "C2") + try await clubColB.addOrUpdateAsync(instance: club2B) + eventB.club = club2B.id + try await eventColB.addOrUpdateAsync(instance: eventB) + + let dataA = try await StoreCenter.main.testSynchronizeOnceAsync() + let syncDataA = try SyncData(data: dataA, storeCenter: StoreCenter.main) + +// #expect(syncDataA.sharedRelationshipSets.count == 1) +// #expect(syncDataA.sharedRelationshipRemovals.count == 1) + + #expect(eventA.club == club2B.id) + #expect(clubColB.count == 1) + + } }