From 5958662655eef2f64f016530b6199d8e72e784ad Mon Sep 17 00:00:00 2001 From: Laurent Date: Thu, 26 Jun 2025 11:26:16 +0200 Subject: [PATCH] merge main + tests --- PadelClubData/Data/GroupStage.swift | 36 +++--- PadelClubData/Data/MatchScheduler.swift | 4 +- PadelClubData/Data/Tournament.swift | 2 +- PadelClubDataTests/SyncDataAccessTests.swift | 114 ++++++++++++++++--- 4 files changed, 122 insertions(+), 34 deletions(-) diff --git a/PadelClubData/Data/GroupStage.swift b/PadelClubData/Data/GroupStage.swift index c364f7b..e50d9cb 100644 --- a/PadelClubData/Data/GroupStage.swift +++ b/PadelClubData/Data/GroupStage.swift @@ -34,7 +34,7 @@ final public class GroupStage: BaseGroupStage, SideStorable { // MARK: - Computed dependencies - public func matches() -> [Match] { + public func _matches() -> [Match] { guard let tournamentStore = self.tournamentStore else { return [] } return tournamentStore.matches.filter { $0.groupStage == self.id }.sorted(by: \.index) // Store.main.filter { $0.groupStage == self.id } @@ -76,15 +76,15 @@ final public class GroupStage: BaseGroupStage, SideStorable { } public func isRunning() -> Bool { // at least a match has started - matches().anySatisfy({ $0.isRunning() }) + _matches().anySatisfy({ $0.isRunning() }) } public func hasStarted() -> Bool { // meaning at least one match is over - matches().filter { $0.hasEnded() }.isEmpty == false + _matches().filter { $0.hasEnded() }.isEmpty == false } public func hasEnded() -> Bool { - let _matches = matches() + let _matches = _matches() if _matches.isEmpty { return false } //guard teams().count == size else { return false } return _matches.anySatisfy { $0.hasEnded() == false } == false @@ -102,7 +102,7 @@ final public class GroupStage: BaseGroupStage, SideStorable { public func removeReturnMatches(onlyLast: Bool = false) { - var returnMatches = matches().filter({ $0.index >= matchCount }) + var returnMatches = _matches().filter({ $0.index >= matchCount }) if onlyLast { let matchPhaseCount = matchPhaseCount - 1 returnMatches = returnMatches.filter({ $0.index >= matchCount * matchPhaseCount }) @@ -115,7 +115,7 @@ final public class GroupStage: BaseGroupStage, SideStorable { } public var matchPhaseCount: Int { - let count = matches().count + let count = _matches().count if matchCount > 0 { return count / matchCount } else { @@ -153,7 +153,7 @@ final public class GroupStage: BaseGroupStage, SideStorable { matches.append(newMatch) } } else { - for match in self.matches() { + for match in self._matches() { match.resetTeamScores(outsideOf: []) teamScores.append(contentsOf: match.createTeamScores()) } @@ -168,7 +168,7 @@ final public class GroupStage: BaseGroupStage, SideStorable { } public func playedMatches() -> [Match] { - let ordered = matches() + let ordered = _matches() let order = _matchOrder() let matchCount = max(1, matchCount) let count = ordered.count / matchCount @@ -256,7 +256,7 @@ final public class GroupStage: BaseGroupStage, SideStorable { matchIndexes.append(index) } } - return matches().filter { matchIndexes.contains($0.index%matchCount) } + return _matches().filter { matchIndexes.contains($0.index%matchCount) } } public func initialStartDate(forTeam team: TeamRegistration) -> Date? { @@ -273,7 +273,7 @@ final public class GroupStage: BaseGroupStage, SideStorable { matchIndexes.append(index) } } - return matches().first(where: { matchIndexes.contains($0.index) }) + return _matches().first(where: { matchIndexes.contains($0.index) }) } public func availableToStart(playedMatches: [Match], in runningMatches: [Match], checkCanPlay: Bool = true) -> [Match] { @@ -321,7 +321,7 @@ final public class GroupStage: BaseGroupStage, SideStorable { } public func isReturnMatchEnabled() -> Bool { - matches().count > matchCount + _matches().count > matchCount } private func _matchOrder() -> [Int] { @@ -359,7 +359,7 @@ final public class GroupStage: BaseGroupStage, SideStorable { public func returnMatchesSuffix(for matchIndex: Int) -> String { if matchCount > 0 { - let count = matches().count + let count = _matches().count if count > matchCount * 2 { return " - vague \((matchIndex / matchCount) + 1)" } @@ -401,7 +401,7 @@ final public class GroupStage: BaseGroupStage, SideStorable { } func _removeMatches() { - self.tournamentStore?.matches.delete(contentOfs: matches()) + self.tournamentStore?.matches.delete(contentOfs: _matches()) } func _numberOfMatchesToBuild() -> Int { @@ -420,7 +420,7 @@ final public class GroupStage: BaseGroupStage, SideStorable { let indexes = [teamPosition, otherTeam].compactMap({ $0.groupStagePosition }).sorted() let combos = Array((0.. 1 { let scoreA = calculateScore(for: teamPosition, matches: matches, groupStagePosition: teamPosition.groupStagePosition!) let scoreB = calculateScore(for: otherTeam, matches: matches, groupStagePosition: otherTeam.groupStagePosition!) @@ -447,7 +447,7 @@ final public class GroupStage: BaseGroupStage, SideStorable { return teamsSorted.first == teamPosition } else { - if let matchIndex = combos.firstIndex(of: indexes), let match = self.matches().first(where: { $0.index == matchIndex }) { + if let matchIndex = combos.firstIndex(of: indexes), let match = self._matches().first(where: { $0.index == matchIndex }) { return teamPosition.id == match.losingTeamId } else { return false @@ -614,7 +614,7 @@ final public class GroupStage: BaseGroupStage, SideStorable { } public func computedStartDate() -> Date? { - return self.matches().sorted(by: \.computedStartDateForSorting).first?.startDate + return self._matches().sorted(by: \.computedStartDateForSorting).first?.startDate } public override func deleteDependencies(store: Store, actionOption: ActionOption) { @@ -627,7 +627,7 @@ final public class GroupStage: BaseGroupStage, SideStorable { func insertOnServer() { self.tournamentStore?.groupStages.writeChangeAndInsertOnServer(instance: self) - for match in self.matches() { + for match in self._matches() { match.insertOnServer() } } @@ -655,7 +655,7 @@ extension GroupStage: Selectable { } public func badgeValue() -> Int? { - return runningMatches(playedMatches: matches()).count + return runningMatches(playedMatches: _matches()).count } public func badgeValueColor() -> Color? { diff --git a/PadelClubData/Data/MatchScheduler.swift b/PadelClubData/Data/MatchScheduler.swift index 33b11e1..8999fa0 100644 --- a/PadelClubData/Data/MatchScheduler.swift +++ b/PadelClubData/Data/MatchScheduler.swift @@ -46,7 +46,7 @@ final public class MatchScheduler: BaseMatchScheduler, SideStorable { groupStages = [specificGroupStage] } - let matches = groupStages.flatMap { $0.matches() } + let matches = groupStages.flatMap { $0._matches() } matches.forEach({ $0.removeCourt() $0.startDate = nil @@ -142,7 +142,7 @@ final public class MatchScheduler: BaseMatchScheduler, SideStorable { let _groupStages = groupStages // Get the maximum count of matches in any group - let maxMatchesCount = _groupStages.map { $0.matches().count }.max() ?? 0 + let maxMatchesCount = _groupStages.map { $0._matches().count }.max() ?? 0 var flattenedMatches = [Match]() if simultaneousStart { // Flatten matches in a round-robin order by cycling through each group diff --git a/PadelClubData/Data/Tournament.swift b/PadelClubData/Data/Tournament.swift index cfc9d2f..9956cd8 100644 --- a/PadelClubData/Data/Tournament.swift +++ b/PadelClubData/Data/Tournament.swift @@ -825,7 +825,7 @@ defer { } public func groupStagesMatches(atStep step: Int = 0) -> [Match] { - return groupStages(atStep: step).flatMap({ $0.matches() }) + return groupStages(atStep: step).flatMap({ $0._matches() }) // return Store.main.filter(isIncluded: { $0.groupStage != nil && groupStageIds.contains($0.groupStage!) }) } diff --git a/PadelClubDataTests/SyncDataAccessTests.swift b/PadelClubDataTests/SyncDataAccessTests.swift index e40e0ae..c33453c 100644 --- a/PadelClubDataTests/SyncDataAccessTests.swift +++ b/PadelClubDataTests/SyncDataAccessTests.swift @@ -607,7 +607,7 @@ struct SyncDataAccessTests { } - @Test func testMatchSharingThenTournamentDelete() async throws { + @Test func testMatchSharingThenRevoking() async throws { guard let userId1 = StoreCenter.main.userId else { throw TestError.notAuthenticated } @@ -618,12 +618,12 @@ struct SyncDataAccessTests { // Setup tournament let tournamentColA: SyncedCollection = await StoreCenter.main.mainStore.asyncLoadingSynchronizedCollection() let eventColA: SyncedCollection = await StoreCenter.main.mainStore.asyncLoadingSynchronizedCollection() + try await tournamentColA.deleteAsync(contentOfs: tournamentColA) let event = Event(creator: userId1) try await eventColA.addOrUpdateAsync(instance: event) - let tournament = Tournament(event: event.id, name: "test_data_access_children") - tournament.relatedUser = userId1 + let tournament = Tournament(event: event.id, name: "testMatchSharingThenTournamentDelete") try await tournamentColA.addOrUpdateAsync(instance: tournament) let tourStoreA = try StoreCenter.main.store(identifier: tournament.id) @@ -644,12 +644,91 @@ struct SyncDataAccessTests { try await playerRegColA.addOrUpdateAsync(contentOfs: [pr11, pr12, pr21, pr22]) let round = Round(tournament: tournament.id) + try await roundColA.addOrUpdateAsync(instance: round) + let match = Match(round: round.id) + try await matchColA.addOrUpdateAsync(instance: match) + let ts1 = TeamScore(match: match.id, team: tr1) let ts2 = TeamScore(match: match.id, team: tr2) + try await teamScoreColA.addOrUpdateAsync(contentOfs: [ts1, ts2]) + try await StoreCenter.main.setAuthorizedUsersAsync(for: match, users: [userId2]) + + let data = try await self.storeCenterB.testSynchronizeOnceAsync() + let syncData = try SyncData(data: data, storeCenter: self.storeCenterB) + + let tournamentColB: SyncedCollection = await self.storeCenterB.mainStore.asyncLoadingSynchronizedCollection() + + #expect(syncData.shared.count == 1) + #expect(tournamentColB.count == 1) + + let tourStoreB = try self.storeCenterB.store(identifier: tournament.id) + let matchColB: SyncedCollection = await tourStoreB.asyncLoadingSynchronizedCollection() + let playerRegColB: SyncedCollection = await tourStoreB.asyncLoadingSynchronizedCollection() + let teamRegColB: SyncedCollection = await tourStoreB.asyncLoadingSynchronizedCollection() + + #expect(matchColB.count == 1) + #expect(playerRegColB.count == 4) + #expect(teamRegColB.count == 2) + + try await StoreCenter.main.setAuthorizedUsersAsync(for: match, users: []) + + let data2 = try await self.storeCenterB.testSynchronizeOnceAsync() + let syncData2 = try SyncData(data: data2, storeCenter: self.storeCenterB) + + #expect(syncData2.revocations.count > 0) + + #expect(matchColB.count == 0) + #expect(playerRegColB.count == 0) + #expect(teamRegColB.count == 0) + + } + + @Test func testMatchSharingThenTournamentDelete() async throws { + guard let userId1 = StoreCenter.main.userId else { + throw TestError.notAuthenticated + } + guard let userId2 = self.storeCenterB.userId else { + throw TestError.notAuthenticated + } + + // Setup tournament + let tournamentColA: SyncedCollection = await StoreCenter.main.mainStore.asyncLoadingSynchronizedCollection() + let eventColA: SyncedCollection = await StoreCenter.main.mainStore.asyncLoadingSynchronizedCollection() + try await tournamentColA.deleteAsync(contentOfs: tournamentColA) + + let event = Event(creator: userId1) + try await eventColA.addOrUpdateAsync(instance: event) + + let tournament = Tournament(event: event.id, name: "testMatchSharingThenTournamentDelete") + try await tournamentColA.addOrUpdateAsync(instance: tournament) + + let tourStoreA = try StoreCenter.main.store(identifier: tournament.id) + let teamRegColA: SyncedCollection = await tourStoreA.asyncLoadingSynchronizedCollection() + let playerRegColA: SyncedCollection = await tourStoreA.asyncLoadingSynchronizedCollection() + let roundColA: SyncedCollection = await tourStoreA.asyncLoadingSynchronizedCollection() + let matchColA: SyncedCollection = await tourStoreA.asyncLoadingSynchronizedCollection() + let teamScoreColA: SyncedCollection = await tourStoreA.asyncLoadingSynchronizedCollection() + + let tr1 = TeamRegistration(tournament: tournament.id) + let pr11 = PlayerRegistration(teamRegistration: tr1.id, firstName: "f1", lastName: "l1") + let pr12 = PlayerRegistration(teamRegistration: tr1.id, firstName: "f2", lastName: "l2") + let tr2 = TeamRegistration(tournament: tournament.id) + let pr21 = PlayerRegistration(teamRegistration: tr2.id, firstName: "f21", lastName: "l21") + let pr22 = PlayerRegistration(teamRegistration: tr2.id, firstName: "f22", lastName: "l22") + + try await teamRegColA.addOrUpdateAsync(contentOfs: [tr1, tr2]) + try await playerRegColA.addOrUpdateAsync(contentOfs: [pr11, pr12, pr21, pr22]) + + let round = Round(tournament: tournament.id) try await roundColA.addOrUpdateAsync(instance: round) + + let match = Match(round: round.id) try await matchColA.addOrUpdateAsync(instance: match) + + let ts1 = TeamScore(match: match.id, team: tr1) + let ts2 = TeamScore(match: match.id, team: tr2) try await teamScoreColA.addOrUpdateAsync(contentOfs: [ts1, ts2]) try await StoreCenter.main.setAuthorizedUsersAsync(for: match, users: [userId2]) @@ -657,35 +736,44 @@ struct SyncDataAccessTests { let data = try await self.storeCenterB.testSynchronizeOnceAsync() let syncData = try SyncData(data: data, storeCenter: self.storeCenterB) - #expect(syncData.shared.count == 1) - let tournamentColB: SyncedCollection = await self.storeCenterB.mainStore.asyncLoadingSynchronizedCollection() + #expect(syncData.shared.count == 1) #expect(tournamentColB.count == 1) let tourStoreB = try self.storeCenterB.store(identifier: tournament.id) let matchColB: SyncedCollection = await tourStoreB.asyncLoadingSynchronizedCollection() let playerRegColB: SyncedCollection = await tourStoreB.asyncLoadingSynchronizedCollection() let teamRegColB: SyncedCollection = await tourStoreB.asyncLoadingSynchronizedCollection() + let teamScoreColB: SyncedCollection = await tourStoreB.asyncLoadingSynchronizedCollection() #expect(matchColB.count == 1) #expect(playerRegColB.count == 4) #expect(teamRegColB.count == 2) + #expect(teamScoreColB.count == 2) - try await tournamentColA.deleteAsync(instance: tournament) - let tournaments: [Tournament] = try await StoreCenter.main.service().get() - #expect(tournaments.count == 0) - - #expect(tournamentColA.count == 0) + try await roundColA.deleteAsync(instance: round) + #expect(roundColA.count == 0) + try await Task.sleep(nanoseconds: 1_000_000_000) // wait for cascading deletes to be finished + let data2 = try await self.storeCenterB.testSynchronizeOnceAsync() let syncData2 = try SyncData(data: data2, storeCenter: self.storeCenterB) - #expect(tournamentColB.count == 0) - + for deletion in syncData2.deletions { + print(">>> deletion type = \(deletion.type), count = \(deletion.items.count)") + } + for revocation in syncData2.revocations { + print(">>> revocation type = \(revocation.type), count = \(revocation.items.count)") + } + #expect(syncData2.deletions.count > 0) + #expect(syncData2.revocations.count > 0) #expect(matchColB.count == 0) + #expect(teamScoreColB.count == 0) + + // the delete of round deletes the match, which should revoke granted objects like player/teams #expect(playerRegColB.count == 0) #expect(teamRegColB.count == 0) @@ -894,7 +982,7 @@ extension GroupStage { matches.append(newMatch) } } else { - for match in self.matches() { + for match in self._matches() { match.resetTeamScores(outsideOf: []) teamScores.append(contentsOf: match.createTeamScores()) }