diff --git a/PadelClubData/Subscriptions/Guard.swift b/PadelClubData/Subscriptions/Guard.swift index 2164667..bb9ed87 100644 --- a/PadelClubData/Subscriptions/Guard.swift +++ b/PadelClubData/Subscriptions/Guard.swift @@ -266,7 +266,26 @@ import Combine // return units.reduce(0) { $0 + $1.purchasedQuantity } } - public func paymentForNewTournament() -> TournamentPayment? { + struct CanCreateResponse: Decodable { var canCreate: Bool } + + public func paymentForNewTournament() async -> TournamentPayment? { + + if let payment = self.localPaymentForNewTournament() { + return payment + } else if let services = try? StoreCenter.main.service() { + do { + let response: CanCreateResponse = try await services.run(path: "is_granted_unlimited_access/", method: .get, requiresToken: true) + if response.canCreate { + return .unlimited + } + } catch { + Logger.error(error) + } + } + return nil + } + + public func localPaymentForNewTournament() -> TournamentPayment? { switch self.currentPlan { case .monthlyUnlimited: diff --git a/PadelClubDataTests/PadelClubDataTests.swift b/PadelClubDataTests/PadelClubDataTests.swift new file mode 100644 index 0000000..18acb93 --- /dev/null +++ b/PadelClubDataTests/PadelClubDataTests.swift @@ -0,0 +1,94 @@ +// +// PadelClubDataTests.swift +// PadelClubDataTests +// +// Created by Laurent Morvillier on 15/04/2025. +// + +import Testing +@testable import PadelClubData +@testable import LeStorage + +enum TestError: Error { + case notAuthenticated + case sameDeviceId + case missingEvent +} + +struct PadelClubDataTests { + + let username: String = "UserDataTests" + let password: String = "MyPass1234--" + + init() async throws { + StoreCenter.main.configureURLs(secureScheme: false, domain: "127.0.0.1:8000") + StoreCenter.main.tokenKeychain = MockKeychainStore(fileName: "token.json") + try await self.login() + } + + mutating func login() async throws { + let _: CustomUser = try await StoreCenter.main.service().login(username: self.username, password: self.password) + } + + @Test func testAuthentication() { + #expect(StoreCenter.main.isAuthenticated) + } + + @Test func createTournament() async throws { + + guard let userId = StoreCenter.main.userId else { + throw TestError.notAuthenticated + } + + // Cleanup + let events = DataStore.shared.events + try await DataStore.shared.events.deleteAsync(contentOfs: Array(events)) + + try await DataStore.shared.events.loadDataFromServerIfAllowed(clear: true) + #expect(DataStore.shared.events.count == 0) + + try await DataStore.shared.tournaments.loadDataFromServerIfAllowed(clear: true) + #expect(DataStore.shared.tournaments.count == 0) + + // Create + let event: Event = Event(creator: userId, club: nil, name: "test") + try await DataStore.shared.events.addOrUpdateAsync(instance: event) + + let tournament: Tournament = Tournament.fake() + tournament.event = event.id + try await DataStore.shared.tournaments.addOrUpdateAsync(instance: tournament) + + // Test server content + try await DataStore.shared.events.loadDataFromServerIfAllowed(clear: true) + #expect(DataStore.shared.events.count == 1) + + try await DataStore.shared.tournaments.loadDataFromServerIfAllowed(clear: true) + #expect(DataStore.shared.tournaments.count == 1) + + } + + @Test func dualStoreCenter() async throws { + + let secondStoreServer = StoreCenter() + secondStoreServer.configureURLs(secureScheme: false, domain: "127.0.0.1:8000") + secondStoreServer.tokenKeychain = MockKeychainStore(fileName: "token.json") + + let _: CustomUser = try await secondStoreServer.service().login(username: self.username, password: self.password) + + #expect(StoreCenter.main.isAuthenticated) + #expect(secondStoreServer.isAuthenticated) + + } + + @Test func testWebsocketSynchronization() async throws { + + let secondStoreServer = StoreCenter() + secondStoreServer.configureURLs(secureScheme: false, domain: "127.0.0.1:8000") + secondStoreServer.tokenKeychain = MockKeychainStore(fileName: "token.json") + + let events = DataStore.shared.events + try await DataStore.shared.events.deleteAsync(contentOfs: Array(events)) + + } + +} diff --git a/PadelClubDataTests/SyncDataAccessTests.swift b/PadelClubDataTests/SyncDataAccessTests.swift new file mode 100644 index 0000000..c605dbc --- /dev/null +++ b/PadelClubDataTests/SyncDataAccessTests.swift @@ -0,0 +1,327 @@ +// +// DataAccessSyncTests.swift +// PadelClubDataTests +// +// Created by Laurent Morvillier on 02/05/2025. +// + +import Testing +@testable import PadelClubData +@testable import LeStorage + +struct SyncDataAccessTests { + + let username1: String = "UserDataTests" + let password1: String = "MyPass1234--" + + let username2: String = "seconduser" + let password2: 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.username2, password: self.password2) + } + + 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) + } + + @Test func testSetup() async throws { + #expect(StoreCenter.main.isAuthenticated) + #expect(self.secondStoreCenter.isAuthenticated) + + guard let userId1 = StoreCenter.main.userId else { + throw TestError.notAuthenticated + } + guard let userId2 = self.secondStoreCenter.userId else { + throw TestError.notAuthenticated + } + #expect(userId1 != userId2) + } + + /// In this test, the first user: + /// - creates an event and a tournament + /// - shares the tournament with a second user + /// - remove the sharing with the second user + /// We test that the data is properly received and removed upon the sharing actions + @Test func testTournamentSharing() 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 tournamentColA: SyncedCollection = await StoreCenter.main.mainStore.asyncLoadingSynchronizedCollection() + let eventColB: SyncedCollection = await self.secondStoreCenter.mainStore.asyncLoadingSynchronizedCollection() + let tournamentColB: 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 tournamentColA.deleteAsync(contentOfs: Array(tournamentColA)) + + let _ = try await self.secondStoreCenter.testSynchronizeOnceAsync() + + #expect(eventColB.count == 0) + #expect(tournamentColB.count == 0) + + // Create + let eventA = Event(creator: userId1) + try await eventColA.addOrUpdateAsync(instance: eventA) + let tournamentA = Tournament(event: eventA.id, name: "P100") + try await tournamentColA.addOrUpdateAsync(instance: tournamentA) + + // Share with user2 + try await StoreCenter.main.setAuthorizedUsersAsync(for: tournamentA, users: [userId2]) + + var dataB = try await self.secondStoreCenter.testSynchronizeOnceAsync() + var syncDataB = try SyncData(data: dataB, storeCenter: self.secondStoreCenter) + #expect(syncDataB.grants.count == 2) + + #expect(eventColB.count == 1) + #expect(tournamentColB.count == 1) + + // Remove sharing from user2 + try await StoreCenter.main.setAuthorizedUsersAsync(for: tournamentA, users: []) + + dataB = try await self.secondStoreCenter.testSynchronizeOnceAsync() + syncDataB = try SyncData(data: dataB, storeCenter: self.secondStoreCenter) + #expect(syncDataB.revocations.count == 1) + #expect(syncDataB.revocationParents.count == 1) + + #expect(eventColB.count == 0) + #expect(tournamentColB.count == 0) + + let dataAccesses: [DataAccess] = try await StoreCenter.main.service().get() + #expect(dataAccesses.count == 0) + } + + /// In this test, the first user: + /// - creates a club and 2 events in that club + /// - shares both events with a second user + /// - removes the sharing of 1 event + /// Here we want to test that the Club instance remains even if one event is removed from the second user + @Test func testSharedRelationship() 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 A", acronym: "CA") + try await clubColA.addOrUpdateAsync(instance: clubA) + + let event1A = Event(creator: userId1, club: clubA.id, name: "event 1") + let event2A = Event(creator: userId1, club: clubA.id, name: "event 2") + try await eventColA.addOrUpdateAsync(contentOfs: [event1A, event2A]) + + // Share with user2 + try await StoreCenter.main.setAuthorizedUsersAsync(for: event1A, users: [userId2]) + try await StoreCenter.main.setAuthorizedUsersAsync(for: event2A, users: [userId2]) + + var dataB = try await self.secondStoreCenter.testSynchronizeOnceAsync() + var syncDataB = try SyncData(data: dataB, storeCenter: self.secondStoreCenter) + + #expect(syncDataB.grants.count == 2) + + let clubGrants = syncDataB.grants.first { $0.type == Club.self } + let eventGrants = syncDataB.grants.first { $0.type == Event.self } + #expect(clubGrants?.items.count == 1) + #expect(eventGrants?.items.count == 2) + + #expect(eventColB.count == 2) + #expect(clubColB.count == 1) + + // Remove sharing from user2 + try await StoreCenter.main.setAuthorizedUsersAsync(for: event1A, users: []) + + dataB = try await self.secondStoreCenter.testSynchronizeOnceAsync() + syncDataB = try SyncData(data: dataB, storeCenter: self.secondStoreCenter) + #expect(syncDataB.revocations.count == 1) + #expect(syncDataB.revocationParents.count == 1) + + #expect(eventColB.count == 1) + #expect(clubColB.count == 1) // club remains because used in event2A + } + + /// 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 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 club1A = Club(creator: userId1, name: "Club 1", acronym: "C1") + try await clubColA.addOrUpdateAsync(instance: club1A) + let club2A = Club(creator: userId1, name: "Club 2", acronym: "C2") + try await clubColA.addOrUpdateAsync(instance: club2A) + + let eventA = Event(creator: userId1, club: club1A.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() + #expect(eventColB.count == 1) + #expect(clubColB.count == 1) + + // Change the club + eventA.club = club2A.id + try await eventColA.addOrUpdateAsync(instance: eventA) + let dataB = try await self.secondStoreCenter.testSynchronizeOnceAsync() + + let syncDataB = try SyncData(data: dataB, storeCenter: self.secondStoreCenter) + + #expect(syncDataB.sharedRelationshipSets.count == 1) + #expect(syncDataB.sharedRelationshipRemovals.count == 1) + + #expect(eventColB.first?.club == club2A.id) + } + + /// 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) + + } + +}