From d94ced5c7e40dd65da9f3ad5d7416135d9b16756 Mon Sep 17 00:00:00 2001 From: Selim Mustafaev Date: Sun, 6 Apr 2025 16:18:28 +0300 Subject: [PATCH] Adding tests for auth screen. Removing more old code. --- AutoCat.xcodeproj/project.pbxproj | 44 ++-- AutoCat/AppDelegate.swift | 50 ---- AutoCat/Controllers/AuthController.swift | 79 ------ .../Controllers/GoogleSignInController.swift | 4 +- .../Location/GlobalEventsController.swift | 4 +- AutoCat/Controllers/MainTabController.swift | 3 +- AutoCat/SceneDelegate.swift | 6 +- .../ServicePropertyWrapper.swift | 24 -- .../Services/ApiService/ApiService.swift | 20 +- .../ApiService/ApiServiceProtocol.swift | 3 + .../LocationService/LocationError.swift | 24 ++ .../StorageService/StorageService.swift | 21 +- AutoCatCore/Utils/Location.swift | 116 --------- .../Storage/StorageServiceTests.swift | 8 +- AutoCatCoreTests/VehicleServiceTests.swift | 39 +++ AutoCatTests/AuthTests.swift | 238 ++++++++++++++++++ 16 files changed, 361 insertions(+), 322 deletions(-) delete mode 100644 AutoCat/Controllers/AuthController.swift delete mode 100644 AutoCatCore/DependencyInjection/ServicePropertyWrapper.swift create mode 100644 AutoCatCore/Services/LocationService/LocationError.swift delete mode 100644 AutoCatCore/Utils/Location.swift create mode 100644 AutoCatTests/AuthTests.swift diff --git a/AutoCat.xcodeproj/project.pbxproj b/AutoCat.xcodeproj/project.pbxproj index b96dffd..6a14f2b 100644 --- a/AutoCat.xcodeproj/project.pbxproj +++ b/AutoCat.xcodeproj/project.pbxproj @@ -12,7 +12,6 @@ 7A06E0B02C7065D8005731AC /* SettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A06E0AF2C7065D8005731AC /* SettingsCoordinator.swift */; }; 7A06E0B32C707E13005731AC /* SettingsServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A06E0B22C707E13005731AC /* SettingsServiceProtocol.swift */; }; 7A06E0B52C707E2B005731AC /* SettingsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A06E0B42C707E2B005731AC /* SettingsService.swift */; }; - 7A1022692C55197D00B84627 /* RealmSwift in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 7ADF23052C25B5BF002624FF /* RealmSwift */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 7A10226C2C551EC500B84627 /* LocationEditScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A10226B2C551EC500B84627 /* LocationEditScreen.swift */; }; 7A10226E2C551EE000B84627 /* LocationEditViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A10226D2C551EE000B84627 /* LocationEditViewModel.swift */; }; 7A1022702C551EFD00B84627 /* LocationEditCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A10226F2C551EFD00B84627 /* LocationEditCoordinator.swift */; }; @@ -27,7 +26,6 @@ 7A11470A23FDE7E600B424AF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7A11470923FDE7E600B424AF /* Assets.xcassets */; }; 7A11470D23FDE7E600B424AF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7A11470B23FDE7E600B424AF /* LaunchScreen.storyboard */; }; 7A11471623FDEB2A00B424AF /* MainSplitController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A11471523FDEB2A00B424AF /* MainSplitController.swift */; }; - 7A11471A23FE839000B424AF /* AuthController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A11471923FE839000B424AF /* AuthController.swift */; }; 7A123C742D9DC40A00781F24 /* RecordScreenOutput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A123C732D9DC40A00781F24 /* RecordScreenOutput.swift */; }; 7A131FD32D37B75500DC7755 /* HistoryScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A131FD22D37B75500DC7755 /* HistoryScreen.swift */; }; 7A131FD52D37B76A00DC7755 /* HistoryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A131FD42D37B76A00DC7755 /* HistoryViewModel.swift */; }; @@ -38,7 +36,6 @@ 7A14416E2C297F7C00E79018 /* Coordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A14416D2C297F7C00E79018 /* Coordinator.swift */; }; 7A17CE4A2A2E820300626A6E /* UIStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A17CE492A2E820300626A6E /* UIStackView.swift */; }; 7A17CE4C2A2E850200626A6E /* UISegmentedControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A17CE4B2A2E850200626A6E /* UISegmentedControl.swift */; }; - 7A1CF80529A41C66007962DA /* RealmSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 7A1CF80429A41C66007962DA /* RealmSwift */; }; 7A1CF81629A42117007962DA /* Realm.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1CF81529A42117007962DA /* Realm.swift */; }; 7A1DC38E2517ED98002E9C99 /* BlockBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1DC38D2517ED98002E9C99 /* BlockBarButtonItem.swift */; }; 7A1E78F62CE900330004B740 /* ReportScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1E78F52CE900330004B740 /* ReportScreen.swift */; }; @@ -102,7 +99,6 @@ 7A6DD90C24335A6D009DE740 /* FlagLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6DD90B24335A6D009DE740 /* FlagLayer.swift */; }; 7A6F096026DBF588003A965D /* VehicleNote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6F095F26DBF588003A965D /* VehicleNote.swift */; }; 7A7097C22C9EC139007CFDCA /* ServiceContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7097C12C9EC139007CFDCA /* ServiceContainer.swift */; }; - 7A7097C62C9EC77A007CFDCA /* ServicePropertyWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7097C52C9EC77A007CFDCA /* ServicePropertyWrapper.swift */; }; 7A7158002C43EA6900852088 /* OwnersScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7157FF2C43EA6900852088 /* OwnersScreen.swift */; }; 7A7158042C43EAA200852088 /* OwnersCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7158032C43EAA200852088 /* OwnersCoordinator.swift */; }; 7A7158072C44085600852088 /* OsagoScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7158062C44085600852088 /* OsagoScreen.swift */; }; @@ -117,6 +113,9 @@ 7A761C08267E8EA20005F28F /* JWT.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A43F9F7246C8A6200BA5B49 /* JWT.swift */; }; 7A761C09267E8EE40005F28F /* Base64FS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A96AE32246C095700297C33 /* Base64FS.swift */; }; 7A761C0B267E8FF90005F28F /* Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A761C0A267E8FF90005F28F /* Error.swift */; }; + 7A7AA2C42DA2A3CB00276D83 /* LocationError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7AA2C32DA2A3CB00276D83 /* LocationError.swift */; }; + 7A7AA2C72DA2A45600276D83 /* RealmSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 7A7AA2C62DA2A45600276D83 /* RealmSwift */; }; + 7A7AA2C82DA2A45600276D83 /* RealmSwift in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 7A7AA2C62DA2A45600276D83 /* RealmSwift */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 7A7DADAC2D99738300F52F6C /* AudioRecordView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7DADAB2D99738300F52F6C /* AudioRecordView.swift */; }; 7A809F392D66755B00CF1B3C /* Error+Canceled.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A809F382D66755B00CF1B3C /* Error+Canceled.swift */; }; 7A8A2209248D10EC0073DFD9 /* ResizeImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A8A2208248D10EC0073DFD9 /* ResizeImage.swift */; }; @@ -185,7 +184,6 @@ 7AC8B2762D6A01C700190706 /* UISearchTextField+Dumb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AC8B2752D6A01C700190706 /* UISearchTextField+Dumb.swift */; }; 7ACBB91E2CB9B155005A5168 /* Mockable in Frameworks */ = {isa = PBXBuildFile; productRef = 7ACBB91D2CB9B155005A5168 /* Mockable */; }; 7ACBB9202CB9B16C005A5168 /* Mockable in Frameworks */ = {isa = PBXBuildFile; productRef = 7ACBB91F2CB9B16C005A5168 /* Mockable */; }; - 7ADF23062C25B5BF002624FF /* RealmSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 7ADF23052C25B5BF002624FF /* RealmSwift */; }; 7ADF6C93250B954900F237B2 /* Navigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ADF6C92250B954900F237B2 /* Navigation.swift */; }; 7ADF6C95250D037700F237B2 /* ShowEventController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ADF6C94250D037700F237B2 /* ShowEventController.swift */; }; 7ADF6C97250F41B000F237B2 /* PNKeyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ADF6C96250F41B000F237B2 /* PNKeyboard.swift */; }; @@ -200,7 +198,6 @@ 7AF231992DA27C1B00AE5EB3 /* ACButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AF231982DA27C1B00AE5EB3 /* ACButtonView.swift */; }; 7AF6D2042677C03B0086EA64 /* AutoCatCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7AF6D1EF2677C03B0086EA64 /* AutoCatCore.framework */; }; 7AF6D2052677C03B0086EA64 /* AutoCatCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 7AF6D1EF2677C03B0086EA64 /* AutoCatCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 7AF6D2122677C12E0086EA64 /* Location.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A000AA124C2EEDE001F5B00 /* Location.swift */; }; 7AF6D2132677C15A0086EA64 /* AudioRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A659B5824A2B1BA0043A0F2 /* AudioRecord.swift */; }; 7AF6D2142677C1680086EA64 /* VehicleEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AAE6AD224CDDF950023860B /* VehicleEvent.swift */; }; 7AF6D2172677C1680086EA64 /* VehicleRegion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB562B9249C9E9B00473D53 /* VehicleRegion.swift */; }; @@ -257,6 +254,7 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( + 7A7AA2C82DA2A45600276D83 /* RealmSwift in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -267,7 +265,6 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - 7A1022692C55197D00B84627 /* RealmSwift in Embed Frameworks */, 7AF6D2052677C03B0086EA64 /* AutoCatCore.framework in Embed Frameworks */, ); name = "Embed Frameworks"; @@ -277,7 +274,6 @@ /* Begin PBXFileReference section */ 6841A913FABBB0AB20DEF4FC /* PagedResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PagedResponse.swift; sourceTree = ""; }; - 7A000AA124C2EEDE001F5B00 /* Location.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Location.swift; sourceTree = ""; }; 7A0420A925619AEC00034941 /* Osago.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Osago.swift; sourceTree = ""; }; 7A0516192414FF0900FC55AC /* DateSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateSection.swift; sourceTree = ""; }; 7A06E0AB2C7065AB005731AC /* SettingsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreen.swift; sourceTree = ""; }; @@ -301,7 +297,6 @@ 7A11470C23FDE7E600B424AF /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 7A11470E23FDE7E600B424AF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 7A11471523FDEB2A00B424AF /* MainSplitController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainSplitController.swift; sourceTree = ""; }; - 7A11471923FE839000B424AF /* AuthController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthController.swift; sourceTree = ""; }; 7A11474323FF06CA00B424AF /* ApiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiService.swift; sourceTree = ""; }; 7A11474623FF2AA500B424AF /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = ""; }; 7A11474823FF2B2D00B424AF /* Response.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Response.swift; sourceTree = ""; }; @@ -393,7 +388,6 @@ 7A6DD90D24337930009DE740 /* PlateNumber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlateNumber.swift; sourceTree = ""; }; 7A6F095F26DBF588003A965D /* VehicleNote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleNote.swift; sourceTree = ""; }; 7A7097C12C9EC139007CFDCA /* ServiceContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceContainer.swift; sourceTree = ""; }; - 7A7097C52C9EC77A007CFDCA /* ServicePropertyWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServicePropertyWrapper.swift; sourceTree = ""; }; 7A7157FF2C43EA6900852088 /* OwnersScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OwnersScreen.swift; sourceTree = ""; }; 7A7158032C43EAA200852088 /* OwnersCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OwnersCoordinator.swift; sourceTree = ""; }; 7A7158062C44085600852088 /* OsagoScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OsagoScreen.swift; sourceTree = ""; }; @@ -403,6 +397,7 @@ 7A7158112C444A6400852088 /* AdsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdsViewModel.swift; sourceTree = ""; }; 7A71EF562D0A26B200943129 /* EventModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventModel.swift; sourceTree = ""; }; 7A761C0A267E8FF90005F28F /* Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Error.swift; sourceTree = ""; }; + 7A7AA2C32DA2A3CB00276D83 /* LocationError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationError.swift; sourceTree = ""; }; 7A7DADAB2D99738300F52F6C /* AudioRecordView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRecordView.swift; sourceTree = ""; }; 7A809F382D66755B00CF1B3C /* Error+Canceled.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Error+Canceled.swift"; sourceTree = ""; }; 7A813DBD2506A57100CC93B9 /* AuthenticationServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AuthenticationServices.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk/System/Library/Frameworks/AuthenticationServices.framework; sourceTree = DEVELOPER_DIR; }; @@ -520,7 +515,6 @@ buildActionMask = 2147483647; files = ( 7AA7BC3525A5DFB80053A5D5 /* ExceptionCatcher in Frameworks */, - 7ADF23062C25B5BF002624FF /* RealmSwift in Frameworks */, 7AA7BC3625A5DFB80053A5D5 /* PKHUD in Frameworks */, 7AC3554A2969652F00889457 /* SwiftEntryKit in Frameworks */, 7ACBB91E2CB9B155005A5168 /* Mockable in Frameworks */, @@ -551,7 +545,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 7A1CF80529A41C66007962DA /* RealmSwift in Frameworks */, + 7A7AA2C72DA2A45600276D83 /* RealmSwift in Frameworks */, 7AABB1F2267E9CC800D7AB32 /* SwiftDate in Frameworks */, 7AF8606E2CB9B86300954D2F /* Mockable in Frameworks */, 7A6C4D9E2C56BCA600982597 /* SwiftLocation in Frameworks */, @@ -655,7 +649,6 @@ isa = PBXGroup; children = ( 7A813DC7250B5C6E00CC93B9 /* Location */, - 7A11471923FE839000B424AF /* AuthController.swift */, 7A96AE2C246B2B7400297C33 /* GoogleSignInController.swift */, 7A11471523FDEB2A00B424AF /* MainSplitController.swift */, 7AC3554B29696A1C00889457 /* MainTabController.swift */, @@ -861,6 +854,7 @@ 7A60D24E2C5A9DA800D13F7B /* LocationServiceProtocol.swift */, 7A60D2502C5A9E4200D13F7B /* GeocoderProtocol.swift */, 7AB0EF802C5CC0FE00291EE6 /* SwiftLocationProtocol.swift */, + 7A7AA2C32DA2A3CB00276D83 /* LocationError.swift */, ); path = LocationService; sourceTree = ""; @@ -929,7 +923,6 @@ isa = PBXGroup; children = ( 7A7097C12C9EC139007CFDCA /* ServiceContainer.swift */, - 7A7097C52C9EC77A007CFDCA /* ServicePropertyWrapper.swift */, ); path = DependencyInjection; sourceTree = ""; @@ -1155,7 +1148,6 @@ children = ( 7A43F9F7246C8A6200BA5B49 /* JWT.swift */, 7A96AE30246B2FE400297C33 /* Constants.swift */, - 7A000AA124C2EEDE001F5B00 /* Location.swift */, 7A64A2232C1A07EA00284124 /* Formatters.swift */, 7ABDA8082D8710F80083C715 /* AutoCancellable.swift */, ); @@ -1245,7 +1237,6 @@ 7A813DC02508C4D900CC93B9 /* ExceptionCatcher */, 7AABDE1C2532F3EB0041AFC6 /* PKHUD */, 7AC355492969652F00889457 /* SwiftEntryKit */, - 7ADF23052C25B5BF002624FF /* RealmSwift */, 7ACBB91D2CB9B155005A5168 /* Mockable */, ); productName = AutoCat; @@ -1318,9 +1309,9 @@ name = AutoCatCore; packageProductDependencies = ( 7AABB1F1267E9CC800D7AB32 /* SwiftDate */, - 7A1CF80429A41C66007962DA /* RealmSwift */, 7A6C4D9D2C56BCA600982597 /* SwiftLocation */, 7AF8606D2CB9B86300954D2F /* Mockable */, + 7A7AA2C62DA2A45600276D83 /* RealmSwift */, ); productName = AutoCatCore; productReference = 7AF6D1EF2677C03B0086EA64 /* AutoCatCore.framework */; @@ -1482,7 +1473,6 @@ 7A2C96122C3B155B00AE46B5 /* NoteAlertModifier.swift in Sources */, 7AE24C5F251F1B4E00758E39 /* Buttons.swift in Sources */, 7AF231972DA1C30000AE5EB3 /* AuthCoordinator.swift in Sources */, - 7A11471A23FE839000B424AF /* AuthController.swift in Sources */, 7A5911F22D63268400EC51BA /* SearchCoordinator.swift in Sources */, 7A7DADAC2D99738300F52F6C /* AudioRecordView.swift in Sources */, 7A1090EC24A4E3E100B4F0B2 /* CellProgressView.swift in Sources */, @@ -1576,7 +1566,7 @@ 7A64A2162C19E4CF00284124 /* VehiclePhotoDto.swift in Sources */, 7A6B65B32CFB0DB500AABA6B /* NullifyDate.swift in Sources */, 7A7097C22C9EC139007CFDCA /* ServiceContainer.swift in Sources */, - 7A7097C62C9EC77A007CFDCA /* ServicePropertyWrapper.swift in Sources */, + 7A7AA2C42DA2A3CB00276D83 /* LocationError.swift in Sources */, 7A54BFD32D43B95E00176D6D /* DbUpdatePolicy.swift in Sources */, 7A5D84BE2C1AE44700C2209B /* VehiclePhoto.swift in Sources */, 7A64A2262C1A32C800284124 /* AudioRecordDto.swift in Sources */, @@ -1587,7 +1577,6 @@ 7A95197B2D80B41600E69883 /* AudioRecordServiceProtocol.swift in Sources */, 7AF6D21C2677C1680086EA64 /* DebugInfo.swift in Sources */, 7ABDA80D2D8721B10083C715 /* Substrings.swift in Sources */, - 7AF6D2122677C12E0086EA64 /* Location.swift in Sources */, 7AF6D2142677C1680086EA64 /* VehicleEvent.swift in Sources */, 7A64A2102C19E1EB00284124 /* VehicleBrandDto.swift in Sources */, 7A599C392C18B22900D47C18 /* FbRefreshTokenModel.swift in Sources */, @@ -2161,16 +2150,16 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - 7A1CF80429A41C66007962DA /* RealmSwift */ = { - isa = XCSwiftPackageProductDependency; - package = 7A1CF7FD29A41C2F007962DA /* XCRemoteSwiftPackageReference "realm-swift" */; - productName = RealmSwift; - }; 7A6C4D9D2C56BCA600982597 /* SwiftLocation */ = { isa = XCSwiftPackageProductDependency; package = 7A6C4D9C2C56BCA600982597 /* XCRemoteSwiftPackageReference "SwiftLocation" */; productName = SwiftLocation; }; + 7A7AA2C62DA2A45600276D83 /* RealmSwift */ = { + isa = XCSwiftPackageProductDependency; + package = 7A1CF7FD29A41C2F007962DA /* XCRemoteSwiftPackageReference "realm-swift" */; + productName = RealmSwift; + }; 7A813DC02508C4D900CC93B9 /* ExceptionCatcher */ = { isa = XCSwiftPackageProductDependency; package = 7A813DBF2508C4D900CC93B9 /* XCRemoteSwiftPackageReference "ExceptionCatcher" */; @@ -2206,11 +2195,6 @@ package = 7ACBB91C2CB9B155005A5168 /* XCRemoteSwiftPackageReference "Mockable" */; productName = Mockable; }; - 7ADF23052C25B5BF002624FF /* RealmSwift */ = { - isa = XCSwiftPackageProductDependency; - package = 7A1CF7FD29A41C2F007962DA /* XCRemoteSwiftPackageReference "realm-swift" */; - productName = RealmSwift; - }; 7AF8606B2CB9B20C00954D2F /* Mockable */ = { isa = XCSwiftPackageProductDependency; package = 7ACBB91C2CB9B155005A5168 /* XCRemoteSwiftPackageReference "Mockable" */; diff --git a/AutoCat/AppDelegate.swift b/AutoCat/AppDelegate.swift index b17ee6b..9a3fa99 100644 --- a/AutoCat/AppDelegate.swift +++ b/AutoCat/AppDelegate.swift @@ -1,5 +1,4 @@ import UIKit -import RealmSwift import PKHUD import AutoCatCore @@ -7,55 +6,6 @@ import AutoCatCore class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - - let config = Realm.Configuration( - schemaVersion: 42, - migrationBlock: { migration, oldSchemaVersion in - if oldSchemaVersion <= 31 { - migration.enumerateObjects(ofType: "Vehicle") { old, new in - if let oldDebugInfo = old?["debugInfo"] as? DynamicObject, let newDebugInfo = new?["debugInfo"] as? DynamicObject { - let addStatus = { (providerName: String) -> Void in - if let provider = oldDebugInfo[providerName] as? DynamicObject, let newProvider = newDebugInfo[providerName] as? DynamicObject { - if provider["error"] != nil { - newProvider["status"] = DebugInfoStatus.error.rawValue - } else { - newProvider["status"] = DebugInfoStatus.success.rawValue - } - } - } - - addStatus("autocod") - addStatus("vin01vin") - addStatus("vin01base") - addStatus("vin01history") - addStatus("nomerogram") - } - } - } - - if oldSchemaVersion <= 34 { - var ids: Set = Set() - migration.enumerateObjects(ofType: "VehicleEvent") { old, new in - guard let oldId = old?["id"] as? String? else { return } - - var newId = oldId ?? UUID().uuidString - if ids.contains(newId) { - newId = UUID().uuidString - } - ids.insert(newId) - - new?["id"] = newId - } - } - - if oldSchemaVersion <= 36 { - migration.enumerateObjects(ofType: "Vehicle") { old, new in - new?["synchronized"] = true - } - } - }) - - Realm.Configuration.defaultConfiguration = config HUD.dimsBackground = true HUD.allowsInteraction = false diff --git a/AutoCat/Controllers/AuthController.swift b/AutoCat/Controllers/AuthController.swift deleted file mode 100644 index 2498704..0000000 --- a/AutoCat/Controllers/AuthController.swift +++ /dev/null @@ -1,79 +0,0 @@ -import UIKit -import RealmSwift -import AuthenticationServices -import PKHUD -import AutoCatCore - -class AuthController: UIViewController { - - @IBOutlet weak var username: UITextField! - @IBOutlet weak var password: UITextField! - @IBOutlet weak var login: UIButton! - @IBOutlet weak var signup: UIButton! - - @Service var settingsService: SettingsServiceProtocol - - override func viewDidLoad() { - super.viewDidLoad() - - // FIX login/password lengt checking -// let authValid = Observable.combineLatest(self.username.rx.text, self.password.rx.text) { name, pass -> Bool in -// guard let name = name, let pass = pass else { return false } -// return name.count >= 4 && pass.count >= 5 -// } -// -// authValid.bind(to: self.login.rx.isEnabled).disposed(by: self.bag) -// authValid.bind(to: self.signup.rx.isEnabled).disposed(by: self.bag) - - if settingsService.user.email.count > 0 { - self.username.text = settingsService.user.email - } - } - - @IBAction func loginTapped(_ sender: UIButton) { - guard let email = self.username.text, let pass = self.password.text else { return } - - Task { - do { - HUD.show(.progress) - let user = try await ApiService.shared.login(email: email, password: pass) - self.goToMainScreen(user: user) - } catch { - HUD.show(error: error) - } - } - } - - @IBAction func signupTapped(_ sender: UIButton) { - guard let email = self.username.text, let pass = self.password.text else { return } - - Task { - do { - HUD.show(.progress) - let user = try await ApiService.shared.signUp(email: email, password: pass) - self.goToMainScreen(user: user) - } catch { - HUD.show(error: error) - } - } - } - - func goToMainScreen(user: AutoCatCore.User) { - guard let realm = try? Realm() else { - HUD.flash(.labeledError(title: nil, subtitle: "Database error")) - return - } - - HUD.hide() - - if user.email != settingsService.user.email { - try? realm.write { - realm.deleteAll() - } - } - - settingsService.user = user - let storyboard = UIStoryboard(name: "Main", bundle: nil) - self.view.window?.rootViewController = storyboard.instantiateViewController(identifier: "MainSplitController") - } -} diff --git a/AutoCat/Controllers/GoogleSignInController.swift b/AutoCat/Controllers/GoogleSignInController.swift index b771dda..d843d1b 100644 --- a/AutoCat/Controllers/GoogleSignInController.swift +++ b/AutoCat/Controllers/GoogleSignInController.swift @@ -18,6 +18,8 @@ class GoogleSignInController: UIViewController, WKNavigationDelegate { private var codeVerifier: String = "" public var completion: (() -> Void)? + + let apiService: ApiServiceProtocol = ServiceContainer.shared.resolve(ApiServiceProtocol.self) override func viewDidLoad() { super.viewDidLoad() @@ -55,7 +57,7 @@ class GoogleSignInController: UIViewController, WKNavigationDelegate { Task { @MainActor in do { let token = try await self.getToken(code: code) - await ApiService.shared.fbVerifyAssertion(provider: "google.com", idToken: token.id_token, accessToken: token.access_token) + await apiService.fbVerifyAssertion(provider: "google.com", idToken: token.id_token, accessToken: token.access_token) self.dismiss(animated: true, completion: self.completion) } catch { HUD.flash(.labeledError(title: nil, subtitle: error.localizedDescription)) diff --git a/AutoCat/Controllers/Location/GlobalEventsController.swift b/AutoCat/Controllers/Location/GlobalEventsController.swift index 641f53e..210b82b 100644 --- a/AutoCat/Controllers/Location/GlobalEventsController.swift +++ b/AutoCat/Controllers/Location/GlobalEventsController.swift @@ -38,6 +38,8 @@ class GlobalEventsController: UIViewController { var map: MKMapView! var filter: Filter! + + let apiService: ApiServiceProtocol = ServiceContainer.shared.resolve(ApiServiceProtocol.self) override func viewDidLoad() { super.viewDidLoad() @@ -68,7 +70,7 @@ class GlobalEventsController: UIViewController { func loadEvents() async { do { HUD.show(.progress) - let events = try await ApiService.shared.events(with: self.filter) + let events = try await apiService.events(with: self.filter) self.title = String.localizedStringWithFormat(NSLocalizedString("events found", comment: ""), events.count) let pins = events.map(EventPin.init(event:)) self.map.removeAnnotations(self.map.annotations) diff --git a/AutoCat/Controllers/MainTabController.swift b/AutoCat/Controllers/MainTabController.swift index a3dca9b..e1688c0 100644 --- a/AutoCat/Controllers/MainTabController.swift +++ b/AutoCat/Controllers/MainTabController.swift @@ -106,7 +106,8 @@ class MainTabController: UITabBarController, UITabBarControllerDelegate { // User probably just saw a vehicle and is about to start entering plate number // Requesting current location ASAP while we still close to initial location - Task { try? await RxLocationManager.requestCurrentLocation() } + let locationService = ServiceContainer.shared.resolve(LocationServiceProtocol.self) + Task { try? await locationService.requestCurrentLocation() } } } diff --git a/AutoCat/SceneDelegate.swift b/AutoCat/SceneDelegate.swift index 5cfb715..ef50614 100644 --- a/AutoCat/SceneDelegate.swift +++ b/AutoCat/SceneDelegate.swift @@ -42,7 +42,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { container.register(SettingsServiceProtocol.self, instance: settingsService) - let apiService = ApiService() + let apiService = ApiService(settingsService: settingsService) container.register(ApiServiceProtocol.self, instance: apiService) let locationService = LocationService( @@ -158,9 +158,11 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { func openReport(with number: String) async { guard let rootController = self.window?.rootViewController else { return } + let apiService: ApiServiceProtocol = ServiceContainer.shared.resolve(ApiServiceProtocol.self) + do { HUD.show(.progress) - let vehicle = try await ApiService.shared.getReport(for: number) + let vehicle = try await apiService.getReport(for: number) Task { let coordinator = ReportCoordinator(controller: rootController, vehicle: vehicle, isPersistent: false) diff --git a/AutoCatCore/DependencyInjection/ServicePropertyWrapper.swift b/AutoCatCore/DependencyInjection/ServicePropertyWrapper.swift deleted file mode 100644 index ca5a175..0000000 --- a/AutoCatCore/DependencyInjection/ServicePropertyWrapper.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// ServicePropertyWrapper.swift -// AutoCatCore -// -// Created by Selim Mustafaev on 21.09.2024. -// Copyright © 2024 Selim Mustafaev. All rights reserved. -// - -@propertyWrapper -@MainActor -public struct Service { - - public var service: Service - - public init() { - - self.service = try! ServiceContainer.shared.resolve(Service.self) - } - - public var wrappedValue: Service { - get { service } - set { service = newValue } - } -} diff --git a/AutoCatCore/Services/ApiService/ApiService.swift b/AutoCatCore/Services/ApiService/ApiService.swift index 144c8b7..2e2c076 100644 --- a/AutoCatCore/Services/ApiService/ApiService.swift +++ b/AutoCatCore/Services/ApiService/ApiService.swift @@ -2,11 +2,11 @@ import Foundation public actor ApiService: ApiServiceProtocol { - public static let shared = ApiService() + var settingsService: SettingsServiceProtocol - @Service var settingsService: SettingsServiceProtocol - - public init() { + public init(settingsService: SettingsServiceProtocol) { + + self.settingsService = settingsService } private let session: URLSession = { @@ -20,7 +20,7 @@ public actor ApiService: ApiServiceProtocol { private func createRequest(api: String, method: String, body: B? = nil, params: [String:P]? = nil) async -> URLRequest? where B: Encodable, P: LosslessStringConvertible { - let baseUrl = await settingsService.backend.baseUrl + let baseUrl = settingsService.backend.baseUrl guard var urlComponents = URLComponents(string: baseUrl + api) else { return nil } @@ -32,7 +32,7 @@ public actor ApiService: ApiServiceProtocol { request.httpMethod = method request.addValue("application/json", forHTTPHeaderField: "Content-Type") request.addValue("application/json", forHTTPHeaderField: "Accept") - await request.addValue("Bearer " + settingsService.user.token, forHTTPHeaderField: "Authorization") + request.addValue("Bearer " + settingsService.user.token, forHTTPHeaderField: "Authorization") if let body = body, method.uppercased() != "GET" { let encoder = JSONEncoder() @@ -109,8 +109,8 @@ public actor ApiService: ApiServiceProtocol { // MARK: - Firebase API public func refreshFbToken() async throws { - guard let token = await settingsService.user.firebaseIdToken, - let refreshToken = await settingsService.user.firebaseRefreshToken, + guard let token = settingsService.user.firebaseIdToken, + let refreshToken = settingsService.user.firebaseRefreshToken, let jwt = JWT(string: token), jwt.expired else { return } @@ -237,7 +237,7 @@ public actor ApiService: ApiServiceProtocol { "forceUpdate": AnyEncodable(force) ] - if let token = await settingsService.user.firebaseIdToken { + if let token = settingsService.user.firebaseIdToken { body["googleIdToken"] = AnyEncodable(token) } @@ -324,7 +324,7 @@ public actor ApiService: ApiServiceProtocol { var body = ["number": number] - if let token = await settingsService.user.firebaseIdToken { + if let token = settingsService.user.firebaseIdToken { body["token"] = token } diff --git a/AutoCatCore/Services/ApiService/ApiServiceProtocol.swift b/AutoCatCore/Services/ApiService/ApiServiceProtocol.swift index 4154f1f..a744589 100644 --- a/AutoCatCore/Services/ApiService/ApiServiceProtocol.swift +++ b/AutoCatCore/Services/ApiService/ApiServiceProtocol.swift @@ -21,6 +21,7 @@ public protocol ApiServiceProtocol: Sendable { func add(event: VehicleEventDto, to number: String) async throws -> VehicleDto func remove(event id: String) async throws -> VehicleDto func edit(event: VehicleEventDto) async throws -> VehicleDto + func events(with filter: Filter) async throws -> [VehicleEventDto] func getBrands() async throws -> [String] func getModels(of brand: String) async throws -> [String] @@ -32,4 +33,6 @@ public protocol ApiServiceProtocol: Sendable { func checkVehicleGb(by number: String) async throws -> VehicleDto func getVehicles(with filter: Filter, pageToken: String?, pageSize: Int) async throws -> PagedResponse + func fbVerifyAssertion(provider: String, idToken: String, accessToken: String?) async + func getReport(for number: String) async throws -> VehicleDto } diff --git a/AutoCatCore/Services/LocationService/LocationError.swift b/AutoCatCore/Services/LocationService/LocationError.swift new file mode 100644 index 0000000..453bed4 --- /dev/null +++ b/AutoCatCore/Services/LocationService/LocationError.swift @@ -0,0 +1,24 @@ +// +// LocationError.swift +// AutoCatCore +// +// Created by Selim Mustafaev on 06.04.2025. +// Copyright © 2025 Selim Mustafaev. All rights reserved. +// + +import Foundation + +public enum LocationError: LocalizedError { + + case generic + case permission + case reverseGeocode + + public var errorDescription: String? { + switch self { + case .generic: "Location error" + case .permission: "Location permission error" + case .reverseGeocode: "Reverse geocode error" + } + } +} diff --git a/AutoCatCore/Services/StorageService/StorageService.swift b/AutoCatCore/Services/StorageService/StorageService.swift index d7c631c..efe3667 100644 --- a/AutoCatCore/Services/StorageService/StorageService.swift +++ b/AutoCatCore/Services/StorageService/StorageService.swift @@ -15,11 +15,22 @@ public actor StorageService: StorageServiceProtocol { var realm: Realm! - public init(settingsService: SettingsServiceProtocol, - config: Realm.Configuration = .defaultConfiguration) async throws { + public init(settingsService: SettingsServiceProtocol, isTest: Bool = false) async throws { + + var realmConfig: Realm.Configuration + + if isTest { + realmConfig = .defaultConfiguration + realmConfig.inMemoryIdentifier = UUID().uuidString + } else { + realmConfig = Realm.Configuration( + schemaVersion: 42, + migrationBlock: { migration, oldSchemaVersion in } + ) + } self.settingsService = settingsService - realm = try await Realm(configuration: config, actor: self) + realm = try await Realm(configuration: realmConfig, actor: self) } public var dbFileURL: URL? { @@ -28,6 +39,10 @@ public actor StorageService: StorageServiceProtocol { } } + public var config: Realm.Configuration { + realm.configuration + } + public func deleteAll() async throws { try await realm.asyncWrite { diff --git a/AutoCatCore/Utils/Location.swift b/AutoCatCore/Utils/Location.swift deleted file mode 100644 index 1b73b20..0000000 --- a/AutoCatCore/Utils/Location.swift +++ /dev/null @@ -1,116 +0,0 @@ -import Foundation -import CoreLocation -import SwiftLocation - -public enum LocationError: LocalizedError { - - case generic - case permission - case reverseGeocode - - public var errorDescription: String? { - switch self { - case .generic: "Location error" - case .permission: "Location permission error" - case .reverseGeocode: "Reverse geocode error" - } - } -} - -@MainActor -public class RxLocationManager { - - private let generalErrors: [CLError.Code] = [.locationUnknown, .denied, .network, .headingFailure, .rangingUnavailable, .rangingFailure] - private let geocodingErrors: [CLError.Code] = [.geocodeCanceled, .geocodeFoundNoResult, .geocodeFoundPartialResult] - - private static let locationManager: Location = { - let manger = CLLocationManager() - manger.desiredAccuracy = kCLLocationAccuracyBest - return Location(locationManager: manger) - }() - - private static var eventTask: Task? - public private(set) static var lastEvent: VehicleEventDto? - - private static func checkPermissions() async throws { - - switch locationManager.authorizationStatus { - case .authorizedWhenInUse, .authorizedAlways: - break - case .notDetermined: - let status = try await locationManager.requestPermission(.always) - if [.authorizedWhenInUse, .authorizedAlways].contains(status) { - return - } else { - throw LocationError.permission - } - case .denied: - throw CLError(.denied) - default: - throw LocationError.permission - } - } - - private static func requestLocation() async throws -> VehicleEventDto { - let locationEvent = try await locationManager.requestLocation(timeout: 20) - - guard let coordinate = locationEvent.location?.coordinate else { - throw LocationError.generic - } - - let settingsService = try ServiceContainer.shared.resolve(SettingsServiceProtocol.self) - - let event = VehicleEventDto(lat: coordinate.latitude, lon: coordinate.longitude, addedBy: settingsService.user.email) - self.lastEvent = event - return event - } - - @discardableResult - public static func requestCurrentLocation() async throws -> VehicleEventDto { - - if let eventTask { - return try await eventTask.value - } else { - try await checkPermissions() - let task = Task { - let location = try await requestLocation() - eventTask = nil - return location - } - eventTask = task - return try await task.value - } - } - - public static func locationRequestInProgress() -> Bool { - return self.eventTask != nil - } - - public static func getAddressForLocation(latitude: Double, longitude: Double) async throws -> String { - - try await withCheckedThrowingContinuation { continuation in - let geocoder = CLGeocoder() - let location = CLLocation(latitude: latitude, longitude: longitude) - geocoder.reverseGeocodeLocation(location) { placemarks, error in - if let error = error { - continuation.resume(throwing: error) - } else if let placemark = placemarks?.first, let name = placemark.name { - continuation.resume(returning: name) - } else { - continuation.resume(throwing: LocationError.reverseGeocode) - } - } - - // TODO: Add cancellation after timeout (20 seconds) - //geocoder.cancelGeocode() - } - } - - public static func resetLastEvent() { - self.lastEvent = nil - } - - public static func getLastEvent() async -> VehicleEventDto? { - lastEvent - } -} diff --git a/AutoCatCoreTests/Storage/StorageServiceTests.swift b/AutoCatCoreTests/Storage/StorageServiceTests.swift index b63fe4c..fa0381e 100644 --- a/AutoCatCoreTests/Storage/StorageServiceTests.swift +++ b/AutoCatCoreTests/Storage/StorageServiceTests.swift @@ -27,12 +27,10 @@ struct StorageServiceTests { init() async throws { - var config = Realm.Configuration.defaultConfiguration - config.inMemoryIdentifier = UUID().uuidString - self.realmConfig = config - settingsServiceMock = MockSettingsServiceProtocol() - self.storageService = try await StorageService(settingsService: settingsServiceMock, config: realmConfig) + self.storageService = try await StorageService(settingsService: settingsServiceMock, isTest: true) + + realmConfig = await storageService.config try addTestVehicle(config: realmConfig) diff --git a/AutoCatCoreTests/VehicleServiceTests.swift b/AutoCatCoreTests/VehicleServiceTests.swift index d8ebdb1..3371cfb 100644 --- a/AutoCatCoreTests/VehicleServiceTests.swift +++ b/AutoCatCoreTests/VehicleServiceTests.swift @@ -421,4 +421,43 @@ struct VehicleServiceTests { #expect(result.errors.count == 0) #expect(result.vehicle.events.count == 0) } + + @Test("Check (from audio record)") + func checkFromAudioRecord() async throws { + + let vehicle: VehicleDto = .normal + let vehicleWithEvent = vehicle.addEvent(.valid) + + given(storageServiceMock) + .loadVehicle(number: .any) + .willReturn(.normal) + + given(apiServiceMock) + .checkVehicle(by: .any, notes: .any, events: .value([.valid]), force: .any) + .willReturn(vehicleWithEvent) + + given(locationServiceMock) + .resetLastEvent() + .willReturn() + + given(storageServiceMock) + .updateVehicle(dto: .any, policy: .any) + .willReturn(true) + + let result = try await vehicleService.checkRecord(number: vehicle.number, event: .valid) + + verify(apiServiceMock) + .checkVehicle(by: .any, notes: .any, events: .value([.valid]), force: .value(false)) + .called(.once) + + verify(storageServiceMock) + .updateVehicle(dto: .value(vehicleWithEvent), policy: .value(.always)) + .called(.once) + + #expect(result.vehicle.number == vehicle.number) + #expect(result.errors.count == 0) + #expect(result.vehicle.events.count == 1) + #expect(result.vehicle.events.first?.latitude == VehicleEventDto.validLatitude) + #expect(result.vehicle.events.first?.longitude == VehicleEventDto.validLongitude) + } } diff --git a/AutoCatTests/AuthTests.swift b/AutoCatTests/AuthTests.swift new file mode 100644 index 0000000..ed7da35 --- /dev/null +++ b/AutoCatTests/AuthTests.swift @@ -0,0 +1,238 @@ +// +// AuthTests.swift +// AutoCatTests +// +// Created by Selim Mustafaev on 06.04.2025. +// Copyright © 2025 Selim Mustafaev. All rights reserved. +// + +import Testing +import Mockable +import AutoCatCore +@testable import AutoCat + +@MainActor +struct AuthTests { + + let apiServiceMock = MockApiServiceProtocol() + let storageServiceMock = MockStorageServiceProtocol() + var settingsServiceMock = MockSettingsServiceProtocol() + + var viewModel: AuthViewModel + + init() { + viewModel = AuthViewModel( + apiService: apiServiceMock, + storageService: storageServiceMock, + settingsService: settingsServiceMock + ) + } + + @Test("Initial load") + func initialLoad() async throws { + + given(settingsServiceMock) + .user.willReturn(User()) + + viewModel.onAppear() + + #expect(viewModel.email == "") + } + + @Test("Initial load (existing user)") + func initialLoadExistingUser() { + + given(settingsServiceMock) + .user.willReturn(User(email: "test@example.com", token: "123")) + + viewModel.onAppear() + + #expect(viewModel.email == "test@example.com") + } + + + + @Test("Login", arguments: [false, true]) + func login(sameUser: Bool) async { + + let existingUser = User(email: "test@example.com") + let newUser = User(email: "test2@example.com") + + given(settingsServiceMock) + .user.willReturn(existingUser) + + given(apiServiceMock) + .login(email: .any, password: .any) + .willReturn(sameUser ? existingUser : newUser) + + given(storageServiceMock) + .deleteAll() + .willReturn() + + await viewModel.login() + + verify(settingsServiceMock) + .user() + .getCalled(.once) + + verify(settingsServiceMock) + .user() + .setCalled(.once) + + verify(apiServiceMock) + .login(email: .any, password: .any) + .called(.once) + + verify(storageServiceMock) + .deleteAll() + .called(sameUser ? .never : .once) + + #expect(viewModel.hud == nil) + } + + @Test("Login (api failed)") + func loginApiFailed() async { + + given(apiServiceMock) + .login(email: .any, password: .any) + .willThrow(TestError.generic) + + await viewModel.login() + + verify(apiServiceMock) + .login(email: .any, password: .any) + .called(.once) + + #expect(viewModel.hud == .error(TestError.generic)) + } + + @Test("Login (DB failed)") + func loginDbFailed() async { + + let existingUser = User(email: "test@example.com") + let newUser = User(email: "test2@example.com") + + given(settingsServiceMock) + .user.willReturn(existingUser) + + given(apiServiceMock) + .login(email: .any, password: .any) + .willReturn(newUser) + + given(storageServiceMock) + .deleteAll() + .willThrow(TestError.generic) + + await viewModel.login() + + verify(settingsServiceMock) + .user() + .getCalled(.once) + + verify(settingsServiceMock) + .user() + .setCalled(.never) + + verify(apiServiceMock) + .login(email: .any, password: .any) + .called(.once) + + verify(storageServiceMock) + .deleteAll() + .called(.once) + + #expect(viewModel.hud == .error(TestError.generic)) + } + + @Test("Signup", arguments: [false, true]) + func signup(sameUser: Bool) async { + + let existingUser = User(email: "test@example.com") + let newUser = User(email: "test2@example.com") + + given(settingsServiceMock) + .user.willReturn(existingUser) + + given(apiServiceMock) + .signUp(email: .any, password: .any) + .willReturn(sameUser ? existingUser : newUser) + + given(storageServiceMock) + .deleteAll() + .willReturn() + + await viewModel.signup() + + verify(settingsServiceMock) + .user() + .getCalled(.once) + + verify(settingsServiceMock) + .user() + .setCalled(.once) + + verify(apiServiceMock) + .signUp(email: .any, password: .any) + .called(.once) + + verify(storageServiceMock) + .deleteAll() + .called(sameUser ? .never : .once) + + #expect(viewModel.hud == nil) + } + + @Test("Signup (api failed)") + func signupApiFailed() async { + + given(apiServiceMock) + .signUp(email: .any, password: .any) + .willThrow(TestError.generic) + + await viewModel.signup() + + verify(apiServiceMock) + .signUp(email: .any, password: .any) + .called(.once) + + #expect(viewModel.hud == .error(TestError.generic)) + } + + @Test("Signup (DB failed)") + func signupDbFailed() async { + + let existingUser = User(email: "test@example.com") + let newUser = User(email: "test2@example.com") + + given(settingsServiceMock) + .user.willReturn(existingUser) + + given(apiServiceMock) + .signUp(email: .any, password: .any) + .willReturn(newUser) + + given(storageServiceMock) + .deleteAll() + .willThrow(TestError.generic) + + await viewModel.signup() + + verify(settingsServiceMock) + .user() + .getCalled(.once) + + verify(settingsServiceMock) + .user() + .setCalled(.never) + + verify(apiServiceMock) + .signUp(email: .any, password: .any) + .called(.once) + + verify(storageServiceMock) + .deleteAll() + .called(.once) + + #expect(viewModel.hud == .error(TestError.generic)) + } +}