// // 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 { 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, username: self.username1, password: self.password1) } 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, 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) } @Test func testDeleteDependencies() async throws { let teamRegCol: SyncedCollection = await StoreCenter.main.mainStore.asyncLoadingSynchronizedCollection() let playerRegCol: SyncedCollection = await StoreCenter.main.mainStore.asyncLoadingSynchronizedCollection() let tr1 = TeamRegistration(tournament: "1") let pr11 = PlayerRegistration(teamRegistration: tr1.id, firstName: "a1", lastName: "b1") let pr12 = PlayerRegistration(teamRegistration: tr1.id, firstName: "a1", lastName: "b2") let tr2 = TeamRegistration(tournament: "1") let pr21 = PlayerRegistration(teamRegistration: tr2.id, firstName: "a2", lastName: "b1") let pr22 = PlayerRegistration(teamRegistration: tr2.id, firstName: "a2", lastName: "b2") try await teamRegCol.addOrUpdateAsync(contentOfs: [tr1, tr2]) try await playerRegCol.addOrUpdateAsync(contentOfs: [pr11, pr12, pr21, pr22]) try await teamRegCol.deleteAsync(instance: tr1) #expect(teamRegCol.count == 1) #expect(playerRegCol.count == 2) } /// 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) do { let _ = try self.secondStoreCenter.store(identifier: tournament.id) Issue.record("should go in the catch because the store has been destroyed") } catch { #expect(1 == 1) } // #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(1000)) } // Throw error if timeout is reached throw BoolCheckerError.conditionNotMet(timeout: timeout) } }