From 4ef554634a034dd978dee86e6b5e3bea3283e3eb Mon Sep 17 00:00:00 2001 From: Doug Date: Thu, 30 Jun 2022 17:21:25 +0100 Subject: [PATCH 1/6] Add MockAuthenticationService and ServerSelectionUITests. --- ElementX.xcodeproj/project.pbxproj | 50 ++++++++- ElementX/Sources/AppCoordinator.swift | 18 +--- .../Sources/AppCoordinatorStateMachine.swift | 10 +- .../AuthenticationCoordinator.swift | 80 +++----------- .../LoginScreen/LoginCoordinator.swift | 89 ++++++++-------- .../ServerSelectionCoordinator.swift | 43 +++++--- .../ServerSelectionModels.swift | 2 +- .../ServerSelectionViewModel.swift | 2 +- .../ServerSelectionViewModelProtocol.swift | 2 +- .../AuthenticationService.swift | 86 +++++++++++++++ .../AuthenticationServiceProtocol.swift | 28 +++++ .../MockAuthenticationService.swift | 49 +++++++++ .../Sources/Services/Client/ClientError.swift | 30 ++++++ .../Services/Client/ClientProxyProtocol.swift | 1 + .../Services/Client/MockClientProxy.swift | 34 ++++++ .../Services/Session/MockUserSession.swift | 14 +++ .../MockUserSessionStore.swift | 30 ++++++ ElementX/Sources/UITestScreenIdentifier.swift | 4 +- ElementX/Sources/UITestsAppCoordinator.swift | 26 ++--- UITests/Sources/LoginScreenUITests.swift | 23 +++- UITests/Sources/RoomScreenUITests.swift | 8 +- UITests/Sources/ServerSelectionUITests.swift | 100 ++++++++++++++++++ UITests/Sources/SplashScreenUITests.swift | 5 +- UnitTests/Sources/LoginViewModelTests.swift | 3 - 24 files changed, 555 insertions(+), 182 deletions(-) create mode 100644 ElementX/Sources/Services/Authentication/AuthenticationService.swift create mode 100644 ElementX/Sources/Services/Authentication/AuthenticationServiceProtocol.swift create mode 100644 ElementX/Sources/Services/Authentication/MockAuthenticationService.swift create mode 100644 ElementX/Sources/Services/Client/ClientError.swift create mode 100644 ElementX/Sources/Services/Client/MockClientProxy.swift create mode 100644 ElementX/Sources/Services/Session/MockUserSession.swift create mode 100644 ElementX/Sources/Services/UserSessionStore/MockUserSessionStore.swift create mode 100644 UITests/Sources/ServerSelectionUITests.swift diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 8fc874f76e..dd021b1dc5 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -49,6 +49,7 @@ 1999ECC6777752A2616775CF /* MemberDetailsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A152791A2F56BD193BFE986 /* MemberDetailsProvider.swift */; }; 1A70A2199394B5EC660934A5 /* MatrixRustSDK in Frameworks */ = {isa = PBXBuildFile; productRef = A678E40E917620059695F067 /* MatrixRustSDK */; }; 1AE4AEA0FA8DEF52671832E0 /* RoomTimelineItemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED1D792EB82506A19A72C8DE /* RoomTimelineItemProtocol.swift */; }; + 1B9A74CD48DDC3A0AF027EFF /* MockUserSessionStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2349F5E576CEE032770CC6F9 /* MockUserSessionStore.swift */; }; 1E59B77A0B2CE83DCC1B203C /* LoginViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A05707BF550D770168A406DB /* LoginViewModelTests.swift */; }; 1F3232BD368DF430AB433907 /* DesignKit in Frameworks */ = {isa = PBXBuildFile; productRef = A5A56C4F47C368EBE5C5E870 /* DesignKit */; }; 206F0DBAB6AF042CA1FF2C0D /* SettingsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D487C1185D658F8B15B8F55 /* SettingsViewModelTests.swift */; }; @@ -73,6 +74,7 @@ 2FE4EEF780553B25A446BBFB /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = EFFA5FD06AAAC4AF544B594E /* AppDelegate.swift */; }; 30122AB3484AC6C3A7F6A717 /* ActivityIndicatorView.xib in Resources */ = {isa = PBXBuildFile; fileRef = B64F3A3D0DF86ED5A241AB05 /* ActivityIndicatorView.xib */; }; 3097A0A867D2B19CE32DAE58 /* UIKitBackgroundTaskService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DF1FFC3336EB23374BBBFCC /* UIKitBackgroundTaskService.swift */; }; + 32FC143630CE22A9E403370B /* MockAuthenticationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA38899517F08FE2AF34EB45 /* MockAuthenticationService.swift */; }; 33B4E59D408AE6E02323EE41 /* NoticeRoomMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBDA364DFFC3AC71C4771251 /* NoticeRoomMessage.swift */; }; 344AF4CBB6D8786214878642 /* NavigationRouterStoreProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B9D5F812E5AD6DC786DBC9B /* NavigationRouterStoreProtocol.swift */; }; 34966D4C1C2C6D37FE3F7F50 /* SettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DD2D50A7EAA4FC78417730E /* SettingsCoordinator.swift */; }; @@ -115,6 +117,7 @@ 5375902175B2FEA2949D7D74 /* LoginScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDDDDD9FE1A699D23A5E096 /* LoginScreen.swift */; }; 53B9C2240C2F5533246EE230 /* RectangleToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6235E1CE00A6D989D7DB6D47 /* RectangleToastView.swift */; }; 541374590CA7E8318BD480FD /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 187853A7E643995EE49FAD43 /* Localizable.stringsdict */; }; + 56DACDD379A86A1F5DEFE7BE /* AuthenticationServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E75948AA1FE1D1A7809931F /* AuthenticationServiceProtocol.swift */; }; 56F0A22972A3BB519DA2261C /* HomeScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24F5530B2212862FA4BEFF2D /* HomeScreenViewModelProtocol.swift */; }; 59C41313AED7566C3AC51163 /* RoomSummary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29A953B6C0C431DBF4DD00B4 /* RoomSummary.swift */; }; 5B2C4C17888FC095ED6880B2 /* SplashViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 48971F1FFD7FC5C466889FC7 /* SplashViewController.xib */; }; @@ -133,6 +136,7 @@ 684BDE198AE5AA1392288A73 /* SplashScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32CE6D4FF64C9A3C18619224 /* SplashScreen.swift */; }; 68AC3C84E2B438036B174E30 /* EmoteRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 471EB7D96AFEA8D787659686 /* EmoteRoomTimelineView.swift */; }; 69BCBB4FB2DC3D61A28D3FD8 /* TimelineStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DC2C9E0E15C79BBDA80F0A2 /* TimelineStyle.swift */; }; + 6A0E7551E0D1793245F34CDD /* ClientError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09A267106B9585D3D0CFC0D /* ClientError.swift */; }; 6A367F3D7A437A79B7D9A31C /* FullscreenLoadingViewPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4112D04077F6709C5CA0A13E /* FullscreenLoadingViewPresenter.swift */; }; 6AC1DC1EAD9F7568360DA1BA /* ServerSelectionModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = A30A1758E2B73EF38E7C42F8 /* ServerSelectionModels.swift */; }; 6D046D653DA28ADF1E6E59A4 /* BackgroundTaskServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE73D571D4F9C36DD45255A /* BackgroundTaskServiceProtocol.swift */; }; @@ -151,6 +155,7 @@ 7756C4E90CABE6F14F7920A0 /* BugReportUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6FEA87EA3752203065ECE27 /* BugReportUITests.swift */; }; 77D7DAA41AAB36800C1F2E2D /* RoomTimelineProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 095AED4CF56DFF3EB7BB84C8 /* RoomTimelineProviderProtocol.swift */; }; 77E192BA943B90F9F310CA23 /* WeakDictionaryKeyReference.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FFCC48E7F701B6C24484593 /* WeakDictionaryKeyReference.swift */; }; + 77FACC29F98FE2E65BBB6A5F /* ServerSelectionUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 054F469E433864CC6FE6EE8E /* ServerSelectionUITests.swift */; }; 78B71D53C1FC55FB7A9B75F0 /* RoomTimelineController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24B0C97D2F560BCB72BE73B1 /* RoomTimelineController.swift */; }; 7963F98CDFDEAC75E072BD81 /* TextRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6A8C632CEF4600107792899 /* TextRoomTimelineItem.swift */; }; 79A6E08ADE6E7C460A8A17A5 /* UserSessionStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C37FB986891D90BEAA93EAE /* UserSessionStore.swift */; }; @@ -187,6 +192,7 @@ 964B9D2EC38C488C360CE0C9 /* HomeScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = B902EA6CD3296B0E10EE432B /* HomeScreen.swift */; }; 9738F894DB1BD383BE05767A /* ElementSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1027BB9A852F445B7623897F /* ElementSettings.swift */; }; 978BB24F2A5D31EE59EEC249 /* UserSessionProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F4134FEFE4EB55759017408 /* UserSessionProtocol.swift */; }; + 9847B056C1A216C314D21E68 /* AuthenticationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3A1AB5A84D843B6AC8D5F1E /* AuthenticationService.swift */; }; 989029A28C9E2F828AD6658A /* AppIcon.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 16DC8C5B2991724903F1FA6A /* AppIcon.pdf */; }; 992F5E750F5030C4BA2D0D03 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 01C4C7DB37597D7D8379511A /* Assets.xcassets */; }; 99ED42B8F8D6BFB1DBCF4C45 /* DTCoreText in Frameworks */ = {isa = PBXBuildFile; productRef = 36B7FC232711031AA2B0D188 /* DTCoreText */; }; @@ -253,6 +259,7 @@ D5EA4C6C80579279770D5804 /* ImageRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A45283CF1DB96E583BECA6 /* ImageRoomTimelineView.swift */; }; D6417E5A799C3C7F14F9EC0A /* SessionVerificationViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3069ADED46D063202FE7698 /* SessionVerificationViewModelProtocol.swift */; }; D826154612415D2A3BB6EBF3 /* ListTableViewAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E854E7CF531DAC5CBEBDC75 /* ListTableViewAdapter.swift */; }; + D8359F67AF3A83516E9083C1 /* MockUserSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4756C5A8C8649AD6C10C615 /* MockUserSession.swift */; }; D8CFF02C2730EE5BC4F17ABF /* ElementToggleStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0960A7F5C1B0B6679BDF26F9 /* ElementToggleStyle.swift */; }; D94F664677C380A3CAB8D7F6 /* ActivityIndicatorPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68706A66BBA04268F7747A2F /* ActivityIndicatorPresenter.swift */; }; DCB781BD227CA958809AFADF /* Coordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95CC95CD75B688E946438165 /* Coordinator.swift */; }; @@ -268,6 +275,7 @@ EA31DD9043B91ECB8E45A9A6 /* ScreenshotDetectorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F03C9D319676F3C0DC6B0203 /* ScreenshotDetectorTests.swift */; }; EA65360A0EC026DD83AC0CF5 /* AuthenticationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6CA5F386C7701C129398945 /* AuthenticationCoordinator.swift */; }; EBD6C79705B3DDB2F7E5F554 /* UserSessionStoreProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1B52D0ABBA7091A991CAFE /* UserSessionStoreProtocol.swift */; }; + EE4F5601356228FF72FC56B6 /* MockClientProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F40F48279322E504153AB0D /* MockClientProxy.swift */; }; EE8491AD81F47DF3C192497B /* DecorationTimelineItemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 184CF8C196BE143AE226628D /* DecorationTimelineItemProtocol.swift */; }; EEC40663922856C65D1E0DF5 /* KeychainControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB9C37196A4C79F24CE80C6 /* KeychainControllerTests.swift */; }; EF99A92701E401C4CD5ADC50 /* SplashScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCE978A6118C131D7F2A04B3 /* SplashScreenModels.swift */; }; @@ -309,6 +317,7 @@ 02A07FF019724B6ACEA73076 /* szl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = szl; path = szl.lproj/Localizable.strings; sourceTree = ""; }; 04BBC9E08250EF92ADE89CFD /* sr-Latn */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "sr-Latn"; path = "sr-Latn.lproj/Localizable.strings"; sourceTree = ""; }; 04E1273CC3BC3E471AF87BE5 /* UserIndicatorQueueTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorQueueTests.swift; sourceTree = ""; }; + 054F469E433864CC6FE6EE8E /* ServerSelectionUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionUITests.swift; sourceTree = ""; }; 057B747CF045D3C6C30EAB2C /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = fi; path = fi.lproj/Localizable.stringsdict; sourceTree = ""; }; 086B997409328F091EBA43CE /* RoomScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenUITests.swift; sourceTree = ""; }; 08F64963396A6A23538EFCEC /* is */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = is; path = is.lproj/Localizable.stringsdict; sourceTree = ""; }; @@ -355,6 +364,7 @@ 21BA866267F84BF4350B0CB7 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "pt-BR"; path = "pt-BR.lproj/Localizable.stringsdict"; sourceTree = ""; }; 22B384D54464FA39C6C7F6E7 /* ca */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ca; path = ca.lproj/Localizable.stringsdict; sourceTree = ""; }; 233D5F7E5E9F49ABF3413291 /* hr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = hr; path = hr.lproj/Localizable.stringsdict; sourceTree = ""; }; + 2349F5E576CEE032770CC6F9 /* MockUserSessionStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockUserSessionStore.swift; sourceTree = ""; }; 24A534A4619D8FEFB6439FCC /* SplashScreenPageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashScreenPageView.swift; sourceTree = ""; }; 24B0C97D2F560BCB72BE73B1 /* RoomTimelineController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineController.swift; sourceTree = ""; }; 24F5530B2212862FA4BEFF2D /* HomeScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenViewModelProtocol.swift; sourceTree = ""; }; @@ -392,6 +402,7 @@ 3DD2D50A7EAA4FC78417730E /* SettingsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsCoordinator.swift; sourceTree = ""; }; 3DD6E7C1D8B53F47789778CD /* fr-CA */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "fr-CA"; path = "fr-CA.lproj/Localizable.strings"; sourceTree = ""; }; 3DF1FFC3336EB23374BBBFCC /* UIKitBackgroundTaskService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitBackgroundTaskService.swift; sourceTree = ""; }; + 3F40F48279322E504153AB0D /* MockClientProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockClientProxy.swift; sourceTree = ""; }; 3F87116470221880017CF522 /* BuildSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildSettings.swift; sourceTree = ""; }; 3FAA6438B00FDB130F404E31 /* UserIndicatorStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorStore.swift; sourceTree = ""; }; 3FEE631F3A4AFDC6652DD9DA /* RoomTimelineViewFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineViewFactory.swift; sourceTree = ""; }; @@ -444,6 +455,7 @@ 5CB7F9D6FC121204D59E18DF /* Presentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Presentable.swift; sourceTree = ""; }; 5D26A086A8278D39B5756D6F /* project.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = project.yml; sourceTree = ""; }; 5D8EA85D4F10D7445BB6368A /* UserIndicatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorTests.swift; sourceTree = ""; }; + 5E75948AA1FE1D1A7809931F /* AuthenticationServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationServiceProtocol.swift; sourceTree = ""; }; 5F12E996BFBEB43815189ABF /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = uk; path = uk.lproj/Localizable.stringsdict; sourceTree = ""; }; 5F4134FEFE4EB55759017408 /* UserSessionProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionProtocol.swift; sourceTree = ""; }; 5F77E8010D41AA3F5F9A1FCA /* NavigationModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationModule.swift; sourceTree = ""; }; @@ -551,6 +563,7 @@ A30A1758E2B73EF38E7C42F8 /* ServerSelectionModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionModels.swift; sourceTree = ""; }; A436057DBEA1A23CA8CB1FD7 /* UIFont+AttributedStringBuilder.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UIFont+AttributedStringBuilder.h"; sourceTree = ""; }; A443FAE2EE820A5790C35C8D /* et */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = et; path = et.lproj/Localizable.strings; sourceTree = ""; }; + A4756C5A8C8649AD6C10C615 /* MockUserSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockUserSession.swift; sourceTree = ""; }; A64F0DB78E0AC23C91AD89EF /* mk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = mk; path = mk.lproj/Localizable.strings; sourceTree = ""; }; A65F140F9FE5E8D4DAEFF354 /* RoomProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomProxy.swift; sourceTree = ""; }; A72232816DCE2B76D48E1367 /* nb-NO */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "nb-NO"; path = "nb-NO.lproj/Localizable.strings"; sourceTree = ""; }; @@ -624,6 +637,7 @@ CF47564C584F614B7287F3EB /* RootRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootRouter.swift; sourceTree = ""; }; CF4B39D52CAE7D21D276ABEE /* ElementNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementNavigationController.swift; sourceTree = ""; }; CF847B3C1873B8E81CEE7FAC /* SplashScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashScreenViewModel.swift; sourceTree = ""; }; + D09A267106B9585D3D0CFC0D /* ClientError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientError.swift; sourceTree = ""; }; D0A45283CF1DB96E583BECA6 /* ImageRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageRoomTimelineView.swift; sourceTree = ""; }; D1A9CCCF53495CF3D7B19FCE /* MockSessionVerificationControllerProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSessionVerificationControllerProxy.swift; sourceTree = ""; }; D29EBCBFEC6FD0941749404D /* NavigationRouterStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationRouterStore.swift; sourceTree = ""; }; @@ -634,6 +648,7 @@ D6CA5F386C7701C129398945 /* AuthenticationCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationCoordinator.swift; sourceTree = ""; }; D6D094C15E8DB424F1C6FC94 /* hr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hr; path = hr.lproj/Localizable.strings; sourceTree = ""; }; D6DC38E64A5ED3FDB201029A /* BugReportService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportService.swift; sourceTree = ""; }; + DA38899517F08FE2AF34EB45 /* MockAuthenticationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAuthenticationService.swift; sourceTree = ""; }; DBD460ED7ED1E03B85DEA25C /* TemplateCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateCoordinator.swift; sourceTree = ""; }; DBFEAC3AC691CBB84983E275 /* ElementXTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementXTests.swift; sourceTree = ""; }; DCE978A6118C131D7F2A04B3 /* SplashScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashScreenModels.swift; sourceTree = ""; }; @@ -670,6 +685,7 @@ F0E7BF8F7BB1021F889C6483 /* MockBugReportService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockBugReportService.swift; sourceTree = ""; }; F23BA6D4842D53C5AC9B7584 /* nn */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = nn; path = nn.lproj/Localizable.stringsdict; sourceTree = ""; }; F2D58333B377888012740101 /* LoginViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewModel.swift; sourceTree = ""; }; + F3A1AB5A84D843B6AC8D5F1E /* AuthenticationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationService.swift; sourceTree = ""; }; F506C6ADB1E1DA6638078E11 /* UITests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = UITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; F5C4AF6E3885730CD560311C /* ScreenshotDetector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenshotDetector.swift; sourceTree = ""; }; F6A8C632CEF4600107792899 /* TextRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextRoomTimelineItem.swift; sourceTree = ""; }; @@ -753,6 +769,7 @@ 0787F81684E503024BD0C051 /* Services */ = { isa = PBXGroup; children = ( + AAFDD509929A0CCF8BCE51EB /* Authentication */, EBBEB5471737E9D116DF4738 /* Background */, 0ED3F5C21537519389C07644 /* BugReport */, 8039515BAA53B7C3275AC64A /* Client */, @@ -825,6 +842,13 @@ path = Resources; sourceTree = ""; }; + 3180C73BA7B8F5F7447C99B0 /* React */ = { + isa = PBXGroup; + children = ( + ); + path = React; + sourceTree = ""; + }; 328DD5DA1281F758B72006C7 /* Views */ = { isa = PBXGroup; children = ( @@ -1159,8 +1183,10 @@ 8039515BAA53B7C3275AC64A /* Client */ = { isa = PBXGroup; children = ( + D09A267106B9585D3D0CFC0D /* ClientError.swift */, 18F2958E6D247AE2516BEEE8 /* ClientProxy.swift */, 6033779EB37259F27F938937 /* ClientProxyProtocol.swift */, + 3F40F48279322E504153AB0D /* MockClientProxy.swift */, ); path = Client; sourceTree = ""; @@ -1176,6 +1202,7 @@ 82D5AD3EAE3A5C1068A44A88 /* Session */ = { isa = PBXGroup; children = ( + A4756C5A8C8649AD6C10C615 /* MockUserSession.swift */, 6E5E9C044BEB7C70B1378E91 /* UserSession.swift */, 5F4134FEFE4EB55759017408 /* UserSessionProtocol.swift */, ); @@ -1200,6 +1227,7 @@ children = ( 3A4427F9E0571B4E6E048A2B /* KeychainController.swift */, 0CD51F9FDC91C231906D76C8 /* KeychainControllerProtocol.swift */, + 2349F5E576CEE032770CC6F9 /* MockUserSessionStore.swift */, 8C37FB986891D90BEAA93EAE /* UserSessionStore.swift */, BF1B52D0ABBA7091A991CAFE /* UserSessionStoreProtocol.swift */, ); @@ -1236,6 +1264,7 @@ 4D6E4C37E9F0E53D3DF951AC /* HomeScreenUITests.swift */, 1DB34B0C74CD242FED9DD069 /* LoginScreenUITests.swift */, 086B997409328F091EBA43CE /* RoomScreenUITests.swift */, + 054F469E433864CC6FE6EE8E /* ServerSelectionUITests.swift */, 6D4777F0142E330A75C46FE4 /* SessionVerificationUITests.swift */, E3E29F98CF0E960689A410E3 /* SettingsUITests.swift */, 325A2B3278875554DDEB8A9B /* SplashScreenUITests.swift */, @@ -1339,6 +1368,16 @@ path = UnitTests; sourceTree = ""; }; + AAFDD509929A0CCF8BCE51EB /* Authentication */ = { + isa = PBXGroup; + children = ( + F3A1AB5A84D843B6AC8D5F1E /* AuthenticationService.swift */, + 5E75948AA1FE1D1A7809931F /* AuthenticationServiceProtocol.swift */, + DA38899517F08FE2AF34EB45 /* MockAuthenticationService.swift */, + ); + path = Authentication; + sourceTree = ""; + }; AD5FCF9340D670C526AD17E4 /* UI */ = { isa = PBXGroup; children = ( @@ -1454,6 +1493,7 @@ E74CD7681375AD2EAA34D66B /* Authentication */, 4009BE2E791C16AC6EE39A7E /* BugReport */, B53CA9BECD3F97805E1432D0 /* HomeScreen */, + 3180C73BA7B8F5F7447C99B0 /* React */, 679E9837ECA8D6776079D16E /* RoomScreen */, D958761758AA1110476DE6A3 /* SessionVerification */, 70B74A432C241E56A7ACE610 /* Settings */, @@ -1664,7 +1704,7 @@ }; }; buildConfigurationList = 7AE41FCCF9D1352E2770D1F9 /* Build configuration list for PBXProject "ElementX" */; - compatibilityVersion = "Xcode 10.0"; + compatibilityVersion = "Xcode 11.0"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( @@ -1897,6 +1937,8 @@ A6DEC1ADEC8FEEC206A0FA37 /* AttributedStringBuilderProtocol.swift in Sources */, EA65360A0EC026DD83AC0CF5 /* AuthenticationCoordinator.swift in Sources */, B037C365CF8A58A0D149A2DB /* AuthenticationIconImage.swift in Sources */, + 9847B056C1A216C314D21E68 /* AuthenticationService.swift in Sources */, + 56DACDD379A86A1F5DEFE7BE /* AuthenticationServiceProtocol.swift in Sources */, E0A4DCA633D174EB43AD599F /* BackgroundTaskProtocol.swift in Sources */, 6D046D653DA28ADF1E6E59A4 /* BackgroundTaskServiceProtocol.swift in Sources */, CB326BAB54E9B68658909E36 /* Benchmark.swift in Sources */, @@ -1911,6 +1953,7 @@ 187E18F21EF4DA244E436E58 /* BugReportViewModelProtocol.swift in Sources */, 05776B005C57E92582F0CF08 /* BuildSettings.swift in Sources */, E1DF24D085572A55C9758A2D /* Bundle.swift in Sources */, + 6A0E7551E0D1793245F34CDD /* ClientError.swift in Sources */, 1950A80CD198BED283DFC2CE /* ClientProxy.swift in Sources */, 24BDDD09A90B8BFE3793F3AA /* ClientProxyProtocol.swift in Sources */, DCB781BD227CA958809AFADF /* Coordinator.swift in Sources */, @@ -1963,13 +2006,17 @@ A5EC21A071F58FC1229C20D0 /* MemberDetailsProviderProtocol.swift in Sources */, 24906A1E82D0046655958536 /* MessageComposer.swift in Sources */, 072BA9DBA932374CCA300125 /* MessageComposerTextField.swift in Sources */, + 32FC143630CE22A9E403370B /* MockAuthenticationService.swift in Sources */, 28410F3DE89C2C44E4F75C92 /* MockBugReportService.swift in Sources */, + EE4F5601356228FF72FC56B6 /* MockClientProxy.swift in Sources */, 67E391A2E00709FB41903B36 /* MockMediaProvider.swift in Sources */, 51DB67C5B5BC68B0A6FF54D4 /* MockRoomProxy.swift in Sources */, 29AEE68A604940180AB9EBFF /* MockRoomSummary.swift in Sources */, E81EEC1675F2371D12A880A3 /* MockRoomTimelineController.swift in Sources */, 9BE7A9CF6C593251D734B461 /* MockServerSelectionScreenState.swift in Sources */, D034A195A3494E38BF060485 /* MockSessionVerificationControllerProxy.swift in Sources */, + D8359F67AF3A83516E9083C1 /* MockUserSession.swift in Sources */, + 1B9A74CD48DDC3A0AF027EFF /* MockUserSessionStore.swift in Sources */, 4ED453A61AF45EBE18D8BC69 /* NavigationModule.swift in Sources */, 22DADD537401E79D66132134 /* NavigationRouter.swift in Sources */, 12F70C493FB69F4D7E9A37EA /* NavigationRouterStore.swift in Sources */, @@ -2097,6 +2144,7 @@ 9DC5FB22B8F86C3B51E907C1 /* HomeScreenUITests.swift in Sources */, 5C8AFBF168A41E20835F3B86 /* LoginScreenUITests.swift in Sources */, 2F1CF90A3460C153154427F0 /* RoomScreenUITests.swift in Sources */, + 77FACC29F98FE2E65BBB6A5F /* ServerSelectionUITests.swift in Sources */, 05EC896A4B9AF4A56670C0BB /* SessionVerificationUITests.swift in Sources */, 490E606044B18985055FF690 /* SettingsUITests.swift in Sources */, A00DFC1DD3567B1EDC9F8D16 /* SplashScreenUITests.swift in Sources */, diff --git a/ElementX/Sources/AppCoordinator.swift b/ElementX/Sources/AppCoordinator.swift index d1129849cb..5524b1afa7 100644 --- a/ElementX/Sources/AppCoordinator.swift +++ b/ElementX/Sources/AppCoordinator.swift @@ -89,23 +89,15 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator { // MARK: - AuthenticationCoordinatorDelegate - func authenticationCoordinatorDidStartLoading(_ authenticationCoordinator: AuthenticationCoordinator) { - stateMachine.processEvent(.attemptedSignIn) - } - func authenticationCoordinator(_ authenticationCoordinator: AuthenticationCoordinator, didLoginWithSession userSession: UserSessionProtocol) { self.userSession = userSession remove(childCoordinator: authenticationCoordinator) stateMachine.processEvent(.succeededSigningIn) } - func authenticationCoordinator(_ authenticationCoordinator: AuthenticationCoordinator, didFailWithError error: AuthenticationCoordinatorError) { - stateMachine.processEvent(.failedSigningIn) - } - // MARK: - Private - // swiftlint:disable cyclomatic_complexity function_body_length + // swiftlint:disable cyclomatic_complexity private func setupStateMachine() { stateMachine.addTransitionHandler { [weak self] context in guard let self = self else { return } @@ -113,13 +105,7 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator { switch (context.fromState, context.event, context.toState) { case (.initial, .startWithAuthentication, .signedOut): self.startAuthentication() - case (.signedOut, .attemptedSignIn, .signingIn): - self.showLoadingIndicator() - case (.signingIn, .failedSigningIn, .signedOut): - self.hideLoadingIndicator() - self.showLoginErrorToast() - case (.signingIn, .succeededSigningIn, .homeScreen): - self.hideLoadingIndicator() + case (.signedOut, .succeededSigningIn, .homeScreen): self.presentHomeScreen() case (.initial, .startWithExistingSession, .restoringSession): diff --git a/ElementX/Sources/AppCoordinatorStateMachine.swift b/ElementX/Sources/AppCoordinatorStateMachine.swift index 81024c9552..837421e973 100644 --- a/ElementX/Sources/AppCoordinatorStateMachine.swift +++ b/ElementX/Sources/AppCoordinatorStateMachine.swift @@ -16,8 +16,6 @@ class AppCoordinatorStateMachine { case initial /// Showing the login screen case signedOut - /// Processing sign in request - case signingIn /// Opening an existing session. case restoringSession /// Showing the home screen @@ -41,12 +39,8 @@ class AppCoordinatorStateMachine { enum Event: EventType { /// Start the `AppCoordinator` by showing authentication. case startWithAuthentication - /// A sign in request has been started - case attemptedSignIn /// Signing in succeeded case succeededSigningIn - /// Signing in failed - case failedSigningIn /// Start the `AppCoordinator` by restoring an existing account. case startWithExistingSession @@ -84,9 +78,7 @@ class AppCoordinatorStateMachine { init() { stateMachine = StateMachine(state: .initial) { machine in machine.addRoutes(event: .startWithAuthentication, transitions: [ .initial => .signedOut ]) - machine.addRoutes(event: .attemptedSignIn, transitions: [ .signedOut => .signingIn ]) - machine.addRoutes(event: .succeededSigningIn, transitions: [ .signingIn => .homeScreen ]) - machine.addRoutes(event: .failedSigningIn, transitions: [ .signingIn => .signedOut ]) + machine.addRoutes(event: .succeededSigningIn, transitions: [ .signedOut => .homeScreen ]) machine.addRoutes(event: .startWithExistingSession, transitions: [ .initial => .restoringSession ]) machine.addRoutes(event: .succeededRestoringSession, transitions: [ .restoringSession => .homeScreen ]) diff --git a/ElementX/Sources/Screens/Authentication/AuthenticationCoordinator.swift b/ElementX/Sources/Screens/Authentication/AuthenticationCoordinator.swift index 1dc581f195..65c275f416 100644 --- a/ElementX/Sources/Screens/Authentication/AuthenticationCoordinator.swift +++ b/ElementX/Sources/Screens/Authentication/AuthenticationCoordinator.swift @@ -6,28 +6,18 @@ // Copyright © 2022 Element. All rights reserved. // -import Foundation +import UIKit import MatrixRustSDK -enum AuthenticationCoordinatorError: Error { - case failedLoggingIn -} - @MainActor protocol AuthenticationCoordinatorDelegate: AnyObject { - - func authenticationCoordinatorDidStartLoading(_ authenticationCoordinator: AuthenticationCoordinator) - func authenticationCoordinator(_ authenticationCoordinator: AuthenticationCoordinator, didLoginWithSession userSession: UserSessionProtocol) - - func authenticationCoordinator(_ authenticationCoordinator: AuthenticationCoordinator, - didFailWithError error: AuthenticationCoordinatorError) } -class AuthenticationCoordinator: Coordinator { +class AuthenticationCoordinator: Coordinator, Presentable { - private let userSessionStore: UserSessionStoreProtocol + private let authenticationService: AuthenticationServiceProtocol private let navigationRouter: NavigationRouter private(set) var clientProxy: ClientProxyProtocol? @@ -37,7 +27,7 @@ class AuthenticationCoordinator: Coordinator { init(userSessionStore: UserSessionStoreProtocol, navigationRouter: NavigationRouter) { - self.userSessionStore = userSessionStore + self.authenticationService = AuthenticationService(userSessionStore: userSessionStore) self.navigationRouter = navigationRouter } @@ -45,6 +35,10 @@ class AuthenticationCoordinator: Coordinator { showSplashScreen() } + func toPresentable() -> UIViewController { + navigationRouter.toPresentable() + } + // MARK: - Private private func showSplashScreen() { @@ -67,28 +61,18 @@ class AuthenticationCoordinator: Coordinator { } private func showLoginScreen() { - let homeserver = LoginHomeserver(address: BuildSettings.defaultHomeserverURLString) - let parameters = LoginCoordinatorParameters(navigationRouter: navigationRouter, homeserver: homeserver) + let parameters = LoginCoordinatorParameters(authenticationService: authenticationService, + navigationRouter: navigationRouter) let coordinator = LoginCoordinator(parameters: parameters) coordinator.callback = { [weak self, weak coordinator] action in - guard let self = self, let coordinator = coordinator else { - return - } + guard let self = self, let coordinator = coordinator else { return } switch action { - case .login(let username, let password): - Task { - switch await self.login(username: username, password: password) { - case .success(let userSession): - self.delegate?.authenticationCoordinator(self, didLoginWithSession: userSession) - self.remove(childCoordinator: coordinator) - self.navigationRouter.dismissModule() - case .failure(let error): - self.delegate?.authenticationCoordinator(self, didFailWithError: error) - MXLog.error("Failed logging in user with error: \(error)") - } - } + case .signedIn(let userSession): + self.delegate?.authenticationCoordinator(self, didLoginWithSession: userSession) + self.remove(childCoordinator: coordinator) + self.navigationRouter.dismissModule() case .continueWithOIDC: break } @@ -101,38 +85,4 @@ class AuthenticationCoordinator: Coordinator { self?.remove(childCoordinator: coordinator) } } - - private func login(username: String, password: String) async -> Result { - Benchmark.startTrackingForIdentifier("Login", message: "Started new login") - - delegate?.authenticationCoordinatorDidStartLoading(self) - - let basePath = userSessionStore.baseDirectoryPath(for: username) - let builder = ClientBuilder() - .basePath(path: basePath) - .username(username: username) - - let loginTask: Task = Task.detached { - let client = try builder.build() - try client.login(username: username, password: password) - return client - } - - switch await loginTask.result { - case .success(let client): - return await userSession(for: client) - case .failure(let error): - MXLog.error("Failed logging in with error: \(error)") - return .failure(.failedLoggingIn) - } - } - - private func userSession(for client: Client) async -> Result { - switch await userSessionStore.userSession(for: client) { - case .success(let clientProxy): - return .success(clientProxy) - case .failure: - return .failure(.failedLoggingIn) - } - } } diff --git a/ElementX/Sources/Screens/Authentication/LoginScreen/LoginCoordinator.swift b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginCoordinator.swift index af31c60051..e96e93d07c 100644 --- a/ElementX/Sources/Screens/Authentication/LoginScreen/LoginCoordinator.swift +++ b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginCoordinator.swift @@ -18,26 +18,17 @@ import SwiftUI import MatrixRustSDK struct LoginCoordinatorParameters { + /// The service used to authenticate the user. + let authenticationService: AuthenticationServiceProtocol + /// The navigation router used to present the server selection screen. let navigationRouter: NavigationRouterType - /// The homeserver to be shown initially. - let homeserver: LoginHomeserver } -enum LoginCoordinatorAction: CustomStringConvertible { - /// Login with the associated username and password. - case login(username: String, password: String) +enum LoginCoordinatorAction { + /// Login was successful. + case signedIn(UserSessionProtocol) /// Continue using OIDC. case continueWithOIDC - - /// A string representation of the action, ignoring any associated values that could leak PII. - var description: String { - switch self { - case .login: - return "login" - case .continueWithOIDC: - return "continueWithOIDC" - } - } } final class LoginCoordinator: Coordinator, Presentable { @@ -56,6 +47,7 @@ final class LoginCoordinator: Coordinator, Presentable { } } + private var authenticationService: AuthenticationServiceProtocol { parameters.authenticationService } private var navigationRouter: NavigationRouterType { parameters.navigationRouter } private var indicatorPresenter: UserIndicatorTypePresenterProtocol private var activityIndicator: UserIndicator? @@ -71,7 +63,7 @@ final class LoginCoordinator: Coordinator, Presentable { init(parameters: LoginCoordinatorParameters) { self.parameters = parameters - let viewModel = LoginViewModel(homeserver: parameters.homeserver) + let viewModel = LoginViewModel(homeserver: parameters.authenticationService.homeserver) loginViewModel = viewModel let view = LoginScreen(context: viewModel.context) @@ -136,55 +128,66 @@ final class LoginCoordinator: Coordinator, Presentable { /// Processes an error to either update the flow or display it to the user. private func handleError(_ error: Error) { - loginViewModel.displayError(.alert(error.localizedDescription)) + switch error { + case AuthenticationServiceError.invalidCredentials: + loginViewModel.displayError(.alert(ElementL10n.authInvalidLoginParam)) + case AuthenticationServiceError.accountDeactivated: + loginViewModel.displayError(.alert(ElementL10n.authInvalidLoginDeactivatedAccount)) + default: + loginViewModel.displayError(.alert(error.localizedDescription)) + } } /// Requests the authentication coordinator to log in using the specified credentials. private func login(username: String, password: String) { - var username = loginViewModel.context.username + startLoading(isInteractionBlocking: true) - if !isMXID(username: username) { - let homeserver = loginViewModel.context.viewState.homeserver - username = "@\(username):\(homeserver.address)" + Task { + switch await authenticationService.login(username: username, password: password) { + case .success(let userSession): + callback?(.signedIn(userSession)) + case .failure(let error): + stopLoading() + handleError(error) + } } - - callback?(.login(username: username, password: password)) } /// Parses the specified username and looks up the homeserver when a Matrix ID is entered. private func parseUsername(_ username: String) { - guard isMXID(username: username) else { return } + guard authenticationService.usernameIsMatrixID(username) else { return } - let domain = String(username.split(separator: ":")[1]) + let homeserverDomain = String(username.split(separator: ":")[1]) - let homeserver = LoginHomeserver(address: domain) - updateViewModel(homeserver: homeserver) - indicateSuccess() - } - - /// Checks whether the specified username is a Matrix ID or not. - private func isMXID(username: String) -> Bool { - let range = NSRange(location: 0, length: username.count) + startLoading(isInteractionBlocking: false) - let detector = try? NSRegularExpression(pattern: MatrixEntityRegex.userId.rawValue, options: .caseInsensitive) - return detector?.numberOfMatches(in: username, range: range) ?? 0 > 0 + Task { + switch await authenticationService.startLogin(for: homeserverDomain) { + case .success: + updateViewModel() + stopLoading() + case .failure(let error): + stopLoading() + handleError(error) + } + } } /// Updates the view model with a different homeserver. - private func updateViewModel(homeserver: LoginHomeserver) { - loginViewModel.update(homeserver: homeserver) + private func updateViewModel() { + loginViewModel.update(homeserver: authenticationService.homeserver) indicateSuccess() } /// Presents the server selection screen as a modal. private func presentServerSelectionScreen() { MXLog.debug("[LoginCoordinator] presentServerSelectionScreen") - let parameters = ServerSelectionCoordinatorParameters(homeserver: loginViewModel.context.viewState.homeserver, + let parameters = ServerSelectionCoordinatorParameters(authenticationService: authenticationService, hasModalPresentation: true) let coordinator = ServerSelectionCoordinator(parameters: parameters) - coordinator.callback = { [weak self, weak coordinator] result in + coordinator.callback = { [weak self, weak coordinator] action in guard let self = self, let coordinator = coordinator else { return } - self.serverSelectionCoordinator(coordinator, didCompleteWith: result) + self.serverSelectionCoordinator(coordinator, didCompleteWith: action) } coordinator.start() @@ -198,10 +201,10 @@ final class LoginCoordinator: Coordinator, Presentable { /// Handles the result from the server selection modal, dismissing it after updating the view. private func serverSelectionCoordinator(_ coordinator: ServerSelectionCoordinator, - didCompleteWith result: ServerSelectionCoordinatorResult) { + didCompleteWith action: ServerSelectionCoordinatorAction) { navigationRouter.dismissModule(animated: true) { [weak self] in - if case let .selected(homeserver) = result { - self?.updateViewModel(homeserver: homeserver) + if action == .updated { + self?.updateViewModel() } self?.remove(childCoordinator: coordinator) diff --git a/ElementX/Sources/Screens/Authentication/ServerSelection/ServerSelectionCoordinator.swift b/ElementX/Sources/Screens/Authentication/ServerSelection/ServerSelectionCoordinator.swift index 16083b1754..1fb157cd0e 100644 --- a/ElementX/Sources/Screens/Authentication/ServerSelection/ServerSelectionCoordinator.swift +++ b/ElementX/Sources/Screens/Authentication/ServerSelection/ServerSelectionCoordinator.swift @@ -17,14 +17,14 @@ import SwiftUI struct ServerSelectionCoordinatorParameters { - /// The homeserver to be shown initially. - let homeserver: LoginHomeserver + /// The service used to authenticate the user. + let authenticationService: AuthenticationServiceProtocol /// Whether the screen is presented modally or within a navigation stack. let hasModalPresentation: Bool } -enum ServerSelectionCoordinatorResult { - case selected(LoginHomeserver) +enum ServerSelectionCoordinatorAction { + case updated case dismiss } @@ -38,6 +38,7 @@ final class ServerSelectionCoordinator: Coordinator, Presentable { private let serverSelectionHostingController: UIViewController private var serverSelectionViewModel: ServerSelectionViewModelProtocol + private var authenticationService: AuthenticationServiceProtocol { parameters.authenticationService } private var indicatorPresenter: UserIndicatorTypePresenterProtocol private var loadingIndicator: UserIndicator? @@ -45,14 +46,14 @@ final class ServerSelectionCoordinator: Coordinator, Presentable { // Must be used only internally var childCoordinators: [Coordinator] = [] - var callback: (@MainActor (ServerSelectionCoordinatorResult) -> Void)? + var callback: (@MainActor (ServerSelectionCoordinatorAction) -> Void)? // MARK: - Setup init(parameters: ServerSelectionCoordinatorParameters) { self.parameters = parameters - let viewModel = ServerSelectionViewModel(homeserverAddress: parameters.homeserver.address, + let viewModel = ServerSelectionViewModel(homeserverAddress: parameters.authenticationService.homeserver.address, hasModalPresentation: parameters.hasModalPresentation) let view = ServerSelectionScreen(context: viewModel.context) serverSelectionViewModel = viewModel @@ -66,11 +67,11 @@ final class ServerSelectionCoordinator: Coordinator, Presentable { func start() { MXLog.debug("[ServerSelectionCoordinator] did start.") - serverSelectionViewModel.callback = { [weak self] result in + serverSelectionViewModel.callback = { [weak self] action in guard let self = self else { return } - MXLog.debug("[ServerSelectionCoordinator] ServerSelectionViewModel did complete with result: \(result).") + MXLog.debug("[ServerSelectionCoordinator] ServerSelectionViewModel did callback with action: \(action).") - switch result { + switch action { case .confirm(let homeserverAddress): self.useHomeserver(homeserverAddress) case .dismiss: @@ -102,9 +103,25 @@ final class ServerSelectionCoordinator: Coordinator, Presentable { private func useHomeserver(_ homeserverAddress: String) { startLoading() - let homeserverAddress = LoginHomeserver.sanitized(homeserverAddress) - - stopLoading() - callback?(.selected(LoginHomeserver(address: homeserverAddress))) + Task { + switch await authenticationService.startLogin(for: homeserverAddress) { + case .success: + callback?(.updated) + stopLoading() + case .failure(let error): + stopLoading() + handleError(error) + } + } + } + + /// Processes an error to either update the flow or display it to the user. + private func handleError(_ error: Error) { + switch error { + case AuthenticationServiceError.invalidServer: + serverSelectionViewModel.displayError(.footerMessage(ElementL10n.loginErrorHomeserverNotFound)) + default: + serverSelectionViewModel.displayError(.footerMessage(ElementL10n.unknownError)) + } } } diff --git a/ElementX/Sources/Screens/Authentication/ServerSelection/ServerSelectionModels.swift b/ElementX/Sources/Screens/Authentication/ServerSelection/ServerSelectionModels.swift index e5e5792ef6..93c6265c64 100644 --- a/ElementX/Sources/Screens/Authentication/ServerSelection/ServerSelectionModels.swift +++ b/ElementX/Sources/Screens/Authentication/ServerSelection/ServerSelectionModels.swift @@ -18,7 +18,7 @@ import Foundation // MARK: View model -enum ServerSelectionViewModelResult { +enum ServerSelectionViewModelAction { /// The user would like to use the homeserver at the given address. case confirm(homeserverAddress: String) /// Dismiss the view without using the entered address. diff --git a/ElementX/Sources/Screens/Authentication/ServerSelection/ServerSelectionViewModel.swift b/ElementX/Sources/Screens/Authentication/ServerSelection/ServerSelectionViewModel.swift index 6bae4d54f5..d20d8da055 100644 --- a/ElementX/Sources/Screens/Authentication/ServerSelection/ServerSelectionViewModel.swift +++ b/ElementX/Sources/Screens/Authentication/ServerSelection/ServerSelectionViewModel.swift @@ -26,7 +26,7 @@ class ServerSelectionViewModel: ServerSelectionViewModelType, ServerSelectionVie // MARK: Public - var callback: (@MainActor (ServerSelectionViewModelResult) -> Void)? + var callback: (@MainActor (ServerSelectionViewModelAction) -> Void)? // MARK: - Setup diff --git a/ElementX/Sources/Screens/Authentication/ServerSelection/ServerSelectionViewModelProtocol.swift b/ElementX/Sources/Screens/Authentication/ServerSelection/ServerSelectionViewModelProtocol.swift index e6094708b0..1235abe1f2 100644 --- a/ElementX/Sources/Screens/Authentication/ServerSelection/ServerSelectionViewModelProtocol.swift +++ b/ElementX/Sources/Screens/Authentication/ServerSelection/ServerSelectionViewModelProtocol.swift @@ -19,7 +19,7 @@ import Foundation @MainActor protocol ServerSelectionViewModelProtocol { - var callback: (@MainActor (ServerSelectionViewModelResult) -> Void)? { get set } + var callback: (@MainActor (ServerSelectionViewModelAction) -> Void)? { get set } var context: ServerSelectionViewModelType.Context { get } /// Displays an error to the user. diff --git a/ElementX/Sources/Services/Authentication/AuthenticationService.swift b/ElementX/Sources/Services/Authentication/AuthenticationService.swift new file mode 100644 index 0000000000..126038b3cb --- /dev/null +++ b/ElementX/Sources/Services/Authentication/AuthenticationService.swift @@ -0,0 +1,86 @@ +// +// AuthenticationService.swift +// ElementX +// +// Created by Doug on 29/06/2022. +// Copyright © 2022 Element. All rights reserved. +// + +import Foundation +import MatrixRustSDK + +class AuthenticationService: AuthenticationServiceProtocol { + + // MARK: - Properties + + // MARK: Private + + private(set) var homeserver: LoginHomeserver = LoginHomeserver(address: BuildSettings.defaultHomeserverURLString) + private let userSessionStore: UserSessionStoreProtocol + + // MARK: - Setup + + init(userSessionStore: UserSessionStoreProtocol) { + self.userSessionStore = userSessionStore + } + + // MARK: - Public + + func usernameIsMatrixID(_ username: String) -> Bool { + let range = NSRange(location: 0, length: username.count) + + let detector = try? NSRegularExpression(pattern: MatrixEntityRegex.userId.rawValue, options: .caseInsensitive) + return detector?.numberOfMatches(in: username, range: range) ?? 0 == 1 + } + + func startLogin(for homeserverAddress: String) async -> Result { + homeserver = LoginHomeserver(address: homeserverAddress) + return .success(()) + } + + func login(username: String, password: String) async -> Result { + Benchmark.startTrackingForIdentifier("Login", message: "Started new login") + + // Workaround whilst the SDK requires a full MXID. + let username = usernameIsMatrixID(username) ? username : "@\(username):\(homeserver.address)" + + let basePath = userSessionStore.baseDirectoryPath(for: username) + let builder = ClientBuilder() + .basePath(path: basePath) + .username(username: username) + + let loginTask: Task = Task.detached { + let client = try builder.build() + try client.login(username: username, password: password) + return client + } + + switch await loginTask.result { + case .success(let client): + return await userSession(for: client) + case .failure(let error): + MXLog.error("Failed logging in with error: \(error)") + guard let error = error as? ClientError else { return .failure(.failedLoggingIn) } + + switch error.code { + case .forbidden: + return .failure(.invalidCredentials) + case .userDeactivated: + return .failure(.accountDeactivated) + default: + return .failure(.failedLoggingIn) + } + } + } + + // MARK: - Private + + private func userSession(for client: Client) async -> Result { + switch await userSessionStore.userSession(for: client) { + case .success(let clientProxy): + return .success(clientProxy) + case .failure: + return .failure(.failedLoggingIn) + } + } +} diff --git a/ElementX/Sources/Services/Authentication/AuthenticationServiceProtocol.swift b/ElementX/Sources/Services/Authentication/AuthenticationServiceProtocol.swift new file mode 100644 index 0000000000..68a41c668d --- /dev/null +++ b/ElementX/Sources/Services/Authentication/AuthenticationServiceProtocol.swift @@ -0,0 +1,28 @@ +// +// AuthenticationServiceProtocol.swift +// ElementX +// +// Created by Doug on 29/06/2022. +// Copyright © 2022 Element. All rights reserved. +// + +import Foundation + +enum AuthenticationServiceError: Error { + case invalidServer + case invalidCredentials + case accountDeactivated + case failedLoggingIn +} + +@MainActor +protocol AuthenticationServiceProtocol { + var homeserver: LoginHomeserver { get } + + /// Checks whether the specified username is a Matrix ID or not. + func usernameIsMatrixID(_ username: String) -> Bool + /// Sets up the service for login on the specified homeserver address. + func startLogin(for homeserverAddress: String) async -> Result + /// Performs a password login using the current homeserver. + func login(username: String, password: String) async -> Result +} diff --git a/ElementX/Sources/Services/Authentication/MockAuthenticationService.swift b/ElementX/Sources/Services/Authentication/MockAuthenticationService.swift new file mode 100644 index 0000000000..9ed19dd839 --- /dev/null +++ b/ElementX/Sources/Services/Authentication/MockAuthenticationService.swift @@ -0,0 +1,49 @@ +// +// MockAuthenticationService.swift +// ElementX +// +// Created by Doug on 29/06/2022. +// Copyright © 2022 Element. All rights reserved. +// + +import Foundation + +class MockAuthenticationService: AuthenticationServiceProtocol { + let validCredentials = (username: "alice", password: "12345678") + private(set) var homeserver: LoginHomeserver = .mockMatrixDotOrg + + func usernameIsMatrixID(_ username: String) -> Bool { + let range = NSRange(location: 0, length: username.count) + + let detector = try? NSRegularExpression(pattern: MatrixEntityRegex.userId.rawValue, options: .caseInsensitive) + return detector?.numberOfMatches(in: username, range: range) ?? 0 == 1 + } + + func startLogin(for homeserverAddress: String) async -> Result { + if LoginHomeserver.mockMatrixDotOrg.address.contains(homeserverAddress) { + homeserver = .mockMatrixDotOrg + return .success(()) + } else if LoginHomeserver.mockOIDC.address.contains(homeserverAddress) { + homeserver = .mockOIDC + return .success(()) + } else if LoginHomeserver.mockBasicServer.address.contains(homeserverAddress) { + homeserver = .mockBasicServer + return .success(()) + } else if LoginHomeserver.mockUnsupported.address.contains(homeserverAddress) { + homeserver = .mockUnsupported + return .success(()) + } + + return .failure(.invalidServer) + } + + func login(username: String, password: String) async -> Result { + guard username == validCredentials.username, password == validCredentials.password else { + return .failure(.failedLoggingIn) + } + + let userSession = MockUserSession(clientProxy: MockClientProxy(userIdentifier: username), + mediaProvider: MockMediaProvider()) + return .success(userSession) + } +} diff --git a/ElementX/Sources/Services/Client/ClientError.swift b/ElementX/Sources/Services/Client/ClientError.swift new file mode 100644 index 0000000000..63a30ed98f --- /dev/null +++ b/ElementX/Sources/Services/Client/ClientError.swift @@ -0,0 +1,30 @@ +// +// ClientError.swift +// ElementX +// +// Created by Doug on 30/06/2022. +// Copyright © 2022 Element. All rights reserved. +// + +import Foundation +import MatrixRustSDK + +enum MatrixErrorCode: String, CaseIterable { + case unknown = "M_UNKNOWN" + case userDeactivated = "M_USER_DEACTIVATED" + case forbidden = "M_FORBIDDEN" +} + +extension ClientError { + var code: MatrixErrorCode { + guard case let .Generic(message) = self else { return .unknown } + + for code in MatrixErrorCode.allCases { + if message.contains(code.rawValue) { + return code + } + } + + return .unknown + } +} diff --git a/ElementX/Sources/Services/Client/ClientProxyProtocol.swift b/ElementX/Sources/Services/Client/ClientProxyProtocol.swift index 9a2221873c..17b543c55e 100644 --- a/ElementX/Sources/Services/Client/ClientProxyProtocol.swift +++ b/ElementX/Sources/Services/Client/ClientProxyProtocol.swift @@ -19,6 +19,7 @@ enum ClientProxyError: Error { case failedRetrievingAvatarURL case failedRetrievingDisplayName case failedRetrievingSessionVerificationController + case failedLoadingMedia } protocol ClientProxyProtocol { diff --git a/ElementX/Sources/Services/Client/MockClientProxy.swift b/ElementX/Sources/Services/Client/MockClientProxy.swift new file mode 100644 index 0000000000..7b49384c6e --- /dev/null +++ b/ElementX/Sources/Services/Client/MockClientProxy.swift @@ -0,0 +1,34 @@ +// +// MockClientProxy.swift +// ElementX +// +// Created by Doug on 29/06/2022. +// Copyright © 2022 Element. All rights reserved. +// + +import Combine +import MatrixRustSDK + +struct MockClientProxy: ClientProxyProtocol { + let callbacks = PassthroughSubject() + + let userIdentifier: String + + let rooms = [RoomProxy]() + + func loadUserDisplayName() async -> Result { + .failure(.failedRetrievingDisplayName) + } + + func loadUserAvatarURLString() async -> Result { + .failure(.failedRetrievingAvatarURL) + } + + func mediaSourceForURLString(_ urlString: String) -> MatrixRustSDK.MediaSource { + MatrixRustSDK.mediaSourceFromUrl(url: urlString) + } + + func loadMediaContentForSource(_ source: MatrixRustSDK.MediaSource) throws -> Data { + throw ClientProxyError.failedLoadingMedia + } +} diff --git a/ElementX/Sources/Services/Session/MockUserSession.swift b/ElementX/Sources/Services/Session/MockUserSession.swift new file mode 100644 index 0000000000..b2e6357402 --- /dev/null +++ b/ElementX/Sources/Services/Session/MockUserSession.swift @@ -0,0 +1,14 @@ +// +// MockUserSession.swift +// ElementX +// +// Created by Doug on 29/06/2022. +// Copyright © 2022 Element. All rights reserved. +// + +import Foundation + +struct MockUserSession: UserSessionProtocol { + let clientProxy: ClientProxyProtocol + let mediaProvider: MediaProviderProtocol +} diff --git a/ElementX/Sources/Services/UserSessionStore/MockUserSessionStore.swift b/ElementX/Sources/Services/UserSessionStore/MockUserSessionStore.swift new file mode 100644 index 0000000000..e5a55d9a1d --- /dev/null +++ b/ElementX/Sources/Services/UserSessionStore/MockUserSessionStore.swift @@ -0,0 +1,30 @@ +// +// MockUserSessionStore.swift +// ElementX +// +// Created by Doug on 30/06/2022. +// Copyright © 2022 Element. All rights reserved. +// + +import Foundation +import MatrixRustSDK + +struct MockUserSessionStore: UserSessionStoreProtocol { + var hasSessions: Bool { false } + + func restoreUserSession() async -> Result { + return .failure(.failedRestoringLogin) + } + + func userSession(for client: Client) async -> Result { + return .failure(.failedSettingUpSession) + } + + func logout(userSession: UserSessionProtocol) { } + + func baseDirectoryPath(for username: String) -> String { + FileManager.default.temporaryDirectory.path + } + + +} diff --git a/ElementX/Sources/UITestScreenIdentifier.swift b/ElementX/Sources/UITestScreenIdentifier.swift index 1ec203e33e..33b8e2a6c0 100644 --- a/ElementX/Sources/UITestScreenIdentifier.swift +++ b/ElementX/Sources/UITestScreenIdentifier.swift @@ -10,8 +10,8 @@ import Foundation enum UITestScreenIdentifier: String { case login - case loginOIDC - case loginUnsupported + case serverSelection + case serverSelectionNonModal case simpleRegular case simpleUpgrade case settings diff --git a/ElementX/Sources/UITestsAppCoordinator.swift b/ElementX/Sources/UITestsAppCoordinator.swift index b74b37bad2..4e2cf90893 100644 --- a/ElementX/Sources/UITestsAppCoordinator.swift +++ b/ElementX/Sources/UITestsAppCoordinator.swift @@ -21,6 +21,8 @@ class UITestsAppCoordinator: Coordinator { window.rootViewController = mainNavigationController window.tintColor = .element.accent + UIView.setAnimationsEnabled(false) + let screens = mockScreens() let rootView = UITestsRootView(mockScreens: screens) { id in guard let screen = screens.first(where: { $0.id == id }) else { @@ -50,24 +52,22 @@ class MockScreen: Identifiable { lazy var coordinator: Coordinator & Presentable = { switch id { case .login: - let router = NavigationRouter(navigationController: ElementNavigationController()) - return LoginCoordinator(parameters: .init(navigationRouter: router, - homeserver: .mockMatrixDotOrg)) - case .loginOIDC: - let router = NavigationRouter(navigationController: ElementNavigationController()) - return LoginCoordinator(parameters: .init(navigationRouter: router, - homeserver: .mockOIDC)) - case .loginUnsupported: - let router = NavigationRouter(navigationController: ElementNavigationController()) - return LoginCoordinator(parameters: .init(navigationRouter: router, - homeserver: .mockUnsupported)) + let navigationRouter = NavigationRouter(navigationController: ElementNavigationController()) + return LoginCoordinator(parameters: .init(authenticationService: MockAuthenticationService(), + navigationRouter: navigationRouter)) + case .serverSelection: + return ServerSelectionCoordinator(parameters: .init(authenticationService: MockAuthenticationService(), + hasModalPresentation: true)) + case .serverSelectionNonModal: + return ServerSelectionCoordinator(parameters: .init(authenticationService: MockAuthenticationService(), + hasModalPresentation: false)) case .simpleRegular: return TemplateCoordinator(parameters: .init(promptType: .regular)) case .simpleUpgrade: return TemplateCoordinator(parameters: .init(promptType: .upgrade)) case .settings: - let router = NavigationRouter(navigationController: ElementNavigationController()) - return SettingsCoordinator(parameters: .init(navigationRouter: router, + let navigationRouter = NavigationRouter(navigationController: ElementNavigationController()) + return SettingsCoordinator(parameters: .init(navigationRouter: navigationRouter, bugReportService: MockBugReportService())) case .bugReport: return BugReportCoordinator(parameters: .init(bugReportService: MockBugReportService(), diff --git a/UITests/Sources/LoginScreenUITests.swift b/UITests/Sources/LoginScreenUITests.swift index a6d503ec44..276b6612ef 100644 --- a/UITests/Sources/LoginScreenUITests.swift +++ b/UITests/Sources/LoginScreenUITests.swift @@ -27,6 +27,7 @@ class LoginScreenUITests: XCTestCase { } func testMatrixDotOrg() { + // Given the initial login screen which defaults to matrix.org. app = Application.launch() app.goToScreenWithIdentifier(.login) @@ -37,19 +38,27 @@ class LoginScreenUITests: XCTestCase { validateNextButtonIsDisabled(for: state) validateUnsupportedServerTextIsHidden(for: state) + // When typing in a username and password. app.textFields.element.tap() - app.typeText("@test:server.com") + app.typeText("@test:matrix.org") app.secureTextFields.element.tap() app.typeText("12345678") + // Then the form should be ready to submit. validateNextButtonIsEnabled(for: "matrix.org with credentials entered") } func testOIDC() { + // Given the initial login screen. app = Application.launch() - app.goToScreenWithIdentifier(.loginOIDC) + app.goToScreenWithIdentifier(.login) + + // When entering a username on a homeserver that only supports OIDC. + app.textFields.element.tap() + app.typeText("@test:company.com\n") + // Then the screen should be configured for OIDC. let state = "an OIDC only server" validateServerDescriptionIsHidden(for: state) validateLoginFormIsHidden(for: state) @@ -58,9 +67,15 @@ class LoginScreenUITests: XCTestCase { } func testUnsupported() { + // Given the initial login screen. app = Application.launch() - app.goToScreenWithIdentifier(.loginUnsupported) + app.goToScreenWithIdentifier(.login) + + // When entering a username on a homeserver with an unsupported flow. + app.textFields.element.tap() + app.typeText("@test:server.net\n") + // Then the screen should not allow login to continue. let state = "an unsupported server" validateServerDescriptionIsHidden(for: state) validateLoginFormIsHidden(for: state) @@ -78,7 +93,7 @@ class LoginScreenUITests: XCTestCase { /// Checks that the server description label is hidden. func validateServerDescriptionIsHidden(for state: String) { let descriptionLabel = app.staticTexts["serverDescriptionText"] - XCTAssertFalse(descriptionLabel.exists, "The server description should be shown for \(state).") + XCTAssertFalse(descriptionLabel.exists, "The server description should be hidden for \(state).") } /// Checks that the username and password text fields are shown along with the next button. diff --git a/UITests/Sources/RoomScreenUITests.swift b/UITests/Sources/RoomScreenUITests.swift index de53380ef1..faa75961e5 100644 --- a/UITests/Sources/RoomScreenUITests.swift +++ b/UITests/Sources/RoomScreenUITests.swift @@ -20,23 +20,19 @@ import ElementX @MainActor class RoomScreenUITests: XCTestCase { - func testPlainNoAvatar() async throws { + func testPlainNoAvatar() { let app = Application.launch() app.goToScreenWithIdentifier(.roomPlainNoAvatar) - try await Task.sleep(nanoseconds: 400_000_000) - XCTAssert(app.staticTexts["roomNameLabel"].exists) XCTAssert(app.staticTexts["roomAvatarPlaceholderImage"].exists) XCTAssertFalse(app.images["encryptionBadgeIcon"].exists) } - func testEncryptedWithAvatar() async throws { + func testEncryptedWithAvatar() { let app = Application.launch() app.goToScreenWithIdentifier(.roomEncryptedWithAvatar) - try await Task.sleep(nanoseconds: 400_000_000) - XCTAssert(app.staticTexts["roomNameLabel"].exists) XCTAssert(app.images["roomAvatarImage"].exists) XCTAssert(app.images["encryptionBadgeIcon"].exists) diff --git a/UITests/Sources/ServerSelectionUITests.swift b/UITests/Sources/ServerSelectionUITests.swift new file mode 100644 index 0000000000..148454254c --- /dev/null +++ b/UITests/Sources/ServerSelectionUITests.swift @@ -0,0 +1,100 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +import ElementX + +@MainActor +class ServerSelectionUITests: XCTestCase { + + let textFieldIdentifier = "addressTextField" + + func testNormalState() async { + // Given the initial server selection screen as a modal. + let app = Application.launch() + app.goToScreenWithIdentifier(.serverSelection) + + // Then it should be configured for matrix.org and with a cancel button + let serverTextField = app.textFields.element + XCTAssertEqual(serverTextField.value as? String, "matrix.org", "The server shown should be matrix.org with the https scheme hidden.") + + let confirmButton = app.buttons["confirmButton"] + XCTAssertEqual(confirmButton.label, ElementL10n.actionConfirm, "The confirm button should say Confirm when in modal presentation.") + XCTAssertTrue(confirmButton.exists, "The confirm button should always be shown.") + XCTAssertTrue(confirmButton.isEnabled, "The confirm button should be enabled when there is an address.") + + let textFieldFooter = app.staticTexts[textFieldIdentifier] + XCTAssertTrue(textFieldFooter.exists) + XCTAssertEqual(textFieldFooter.label, ElementL10n.serverSelectionServerFooter) + + let dismissButton = app.buttons["dismissButton"] + XCTAssertTrue(dismissButton.exists, "The dismiss button should be shown during modal presentation.") + } + + func testEmptyAddress() async { + // Given the initial server selection screen as a modal. + let app = Application.launch() + app.goToScreenWithIdentifier(.serverSelection) + + // When clearing the server address text field. + app.textFields.element.tap() + app.textFields.element.buttons.element.tap() + + // Then the screen should not allow the user to continue. + let serverTextField = app.textFields.element + XCTAssertEqual(serverTextField.value as? String, ElementL10n.serverSelectionServerUrl, "The text field should show placeholder text in this state.") + + let confirmButton = app.buttons["confirmButton"] + XCTAssertTrue(confirmButton.exists, "The confirm button should always be shown.") + XCTAssertFalse(confirmButton.isEnabled, "The confirm button should be disabled when the address is empty.") + } + + func testInvalidAddress() { + // Given the initial server selection screen as a modal. + let app = Application.launch() + app.goToScreenWithIdentifier(.serverSelection) + + // When typing in an invalid homeserver + app.textFields.element.tap() + app.textFields.element.buttons.element.tap() + app.typeText("thisisbad\n") // The tests only accept an address from LoginHomeserver.mockXYZ + + // Then an error should be shown and the confirmation button disabled. + let serverTextField = app.textFields.element + XCTAssertEqual(serverTextField.value as? String, "thisisbad", "The text field should show the entered server.") + + let confirmButton = app.buttons["confirmButton"] + XCTAssertTrue(confirmButton.exists, "The confirm button should always be shown.") + XCTAssertFalse(confirmButton.isEnabled, "The confirm button should be disabled when there is an error.") + + let textFieldFooter = app.staticTexts[textFieldIdentifier] + XCTAssertTrue(textFieldFooter.exists) + XCTAssertEqual(textFieldFooter.label, ElementL10n.loginErrorHomeserverNotFound) + } + + func testNonModalPresentation() { + // Given the initial server selection screen pushed onto the stack. + let app = Application.launch() + app.goToScreenWithIdentifier(.serverSelectionNonModal) + + // Then the screen should be tweaked slightly to reflect the change of navigation. + let dismissButton = app.buttons["dismissButton"] + XCTAssertFalse(dismissButton.exists, "The dismiss button should be hidden when not in modal presentation.") + + let confirmButton = app.buttons["confirmButton"] + XCTAssertEqual(confirmButton.label, ElementL10n.actionNext, "The confirm button should say Next when not in modal presentation.") + } +} diff --git a/UITests/Sources/SplashScreenUITests.swift b/UITests/Sources/SplashScreenUITests.swift index 8eac79ffc5..5ed3437311 100644 --- a/UITests/Sources/SplashScreenUITests.swift +++ b/UITests/Sources/SplashScreenUITests.swift @@ -27,7 +27,7 @@ class SplashScreenUITests: XCTestCase { XCTAssertEqual(getStartedButton.label, ElementL10n.loginSplashSubmit) } - func testSwipingBetweenPages() async throws { + func testSwipingBetweenPages() { let app = Application.launch() app.goToScreenWithIdentifier(.splash) @@ -42,7 +42,6 @@ class SplashScreenUITests: XCTestCase { // When swiping to the next screen. page1TitleText.swipeLeft() - try await Task.sleep(nanoseconds: 200_000_000) // Wait for the animation. // Then the second screen should be shown. XCTAssertFalse(page1TitleText.isHittable, "The title from the first page of the carousel should be offscreen.") @@ -50,7 +49,6 @@ class SplashScreenUITests: XCTestCase { // When swiping back to the previous screen. page2TitleText.swipeRight() - try await Task.sleep(nanoseconds: 200_000_000) // Wait for the animation. // Then the first screen should be shown again. XCTAssertTrue(page1TitleText.isHittable, "The title from the first page of the carousel should be onscreen.") @@ -58,7 +56,6 @@ class SplashScreenUITests: XCTestCase { // When swiping back to the previous screen. page1TitleText.swipeRight() - try await Task.sleep(nanoseconds: 200_000_000) // Wait for the animation. // Then the screen shouldn't change and the hidden screen should be ignored. XCTAssertTrue(page1TitleText.isHittable, "The title from the first page of the carousel should be still be onscreen.") diff --git a/UnitTests/Sources/LoginViewModelTests.swift b/UnitTests/Sources/LoginViewModelTests.swift index 903d7edc73..24ae86a497 100644 --- a/UnitTests/Sources/LoginViewModelTests.swift +++ b/UnitTests/Sources/LoginViewModelTests.swift @@ -136,14 +136,11 @@ class LoginViewModelTests: XCTestCase { // Given the coordinator and view model results that contain passwords. let password = "supersecretpassword" let viewModelAction: LoginViewModelAction = .login(username: "Alice", password: password) - let coordinatorAction: LoginCoordinatorAction = .login(username: "Alice", password: password) // When creating a string representation of those results (e.g. for logging). let viewModelActionString = "\(viewModelAction)" - let coordinatorActionString = "\(coordinatorAction)" // Then the password should not be included in that string. XCTAssertFalse("\(viewModelActionString)".contains(password), "The password must not be included in any strings.") - XCTAssertFalse("\(coordinatorActionString)".contains(password), "The password must not be included in any strings.") } } From 8183eb2fd060b6cdf72af36e15f06075d22289d0 Mon Sep 17 00:00:00 2001 From: Doug Date: Thu, 30 Jun 2022 18:00:03 +0100 Subject: [PATCH 2/6] Add tests covering the Authentication flow. --- ElementX.xcodeproj/project.pbxproj | 8 +- ElementX/Sources/AppCoordinator.swift | 3 +- .../AuthenticationCoordinator.swift | 4 +- .../View/LoginServerInfoSection.swift | 1 + .../MockAuthenticationService.swift | 2 +- .../MockUserSessionStore.swift | 30 ------- ElementX/Sources/UITestScreenIdentifier.swift | 1 + ElementX/Sources/UITestsAppCoordinator.swift | 28 ++++--- .../AuthenticationCoordinatorUITests.swift | 79 +++++++++++++++++++ 9 files changed, 109 insertions(+), 47 deletions(-) delete mode 100644 ElementX/Sources/Services/UserSessionStore/MockUserSessionStore.swift create mode 100644 UITests/Sources/AuthenticationCoordinatorUITests.swift diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index dd021b1dc5..ad2aad7b30 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -49,7 +49,6 @@ 1999ECC6777752A2616775CF /* MemberDetailsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A152791A2F56BD193BFE986 /* MemberDetailsProvider.swift */; }; 1A70A2199394B5EC660934A5 /* MatrixRustSDK in Frameworks */ = {isa = PBXBuildFile; productRef = A678E40E917620059695F067 /* MatrixRustSDK */; }; 1AE4AEA0FA8DEF52671832E0 /* RoomTimelineItemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED1D792EB82506A19A72C8DE /* RoomTimelineItemProtocol.swift */; }; - 1B9A74CD48DDC3A0AF027EFF /* MockUserSessionStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2349F5E576CEE032770CC6F9 /* MockUserSessionStore.swift */; }; 1E59B77A0B2CE83DCC1B203C /* LoginViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A05707BF550D770168A406DB /* LoginViewModelTests.swift */; }; 1F3232BD368DF430AB433907 /* DesignKit in Frameworks */ = {isa = PBXBuildFile; productRef = A5A56C4F47C368EBE5C5E870 /* DesignKit */; }; 206F0DBAB6AF042CA1FF2C0D /* SettingsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D487C1185D658F8B15B8F55 /* SettingsViewModelTests.swift */; }; @@ -220,6 +219,7 @@ A8EC7C9D886244DAE9433E37 /* SessionVerificationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4C18FAAD59AE7F1462D817E /* SessionVerificationViewModel.swift */; }; AB34401E4E1CAD5D2EC3072B /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9760103CF316DF68698BCFE6 /* LaunchScreen.storyboard */; }; ABF3FAB234AD3565B214309B /* TimelineSenderAvatarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BC588051E6572A1AF51D738 /* TimelineSenderAvatarView.swift */; }; + ACF094CF3BF02DBFA6DFDE60 /* AuthenticationCoordinatorUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D2D0A6F1ABC99D29462FB84 /* AuthenticationCoordinatorUITests.swift */; }; B037C365CF8A58A0D149A2DB /* AuthenticationIconImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97755C01C3971474EFAD5367 /* AuthenticationIconImage.swift */; }; B245583C63F8F90357B87FAE /* SwiftState in Frameworks */ = {isa = PBXBuildFile; productRef = 3853B78FB8531B83936C5DA6 /* SwiftState */; }; B3357B00F1AA930E54F76609 /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47EBB5D698CE9A25BB553A2D /* Strings.swift */; }; @@ -364,7 +364,6 @@ 21BA866267F84BF4350B0CB7 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "pt-BR"; path = "pt-BR.lproj/Localizable.stringsdict"; sourceTree = ""; }; 22B384D54464FA39C6C7F6E7 /* ca */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ca; path = ca.lproj/Localizable.stringsdict; sourceTree = ""; }; 233D5F7E5E9F49ABF3413291 /* hr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = hr; path = hr.lproj/Localizable.stringsdict; sourceTree = ""; }; - 2349F5E576CEE032770CC6F9 /* MockUserSessionStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockUserSessionStore.swift; sourceTree = ""; }; 24A534A4619D8FEFB6439FCC /* SplashScreenPageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashScreenPageView.swift; sourceTree = ""; }; 24B0C97D2F560BCB72BE73B1 /* RoomTimelineController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineController.swift; sourceTree = ""; }; 24F5530B2212862FA4BEFF2D /* HomeScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenViewModelProtocol.swift; sourceTree = ""; }; @@ -454,6 +453,7 @@ 5B9D5F812E5AD6DC786DBC9B /* NavigationRouterStoreProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationRouterStoreProtocol.swift; sourceTree = ""; }; 5CB7F9D6FC121204D59E18DF /* Presentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Presentable.swift; sourceTree = ""; }; 5D26A086A8278D39B5756D6F /* project.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = project.yml; sourceTree = ""; }; + 5D2D0A6F1ABC99D29462FB84 /* AuthenticationCoordinatorUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationCoordinatorUITests.swift; sourceTree = ""; }; 5D8EA85D4F10D7445BB6368A /* UserIndicatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorTests.swift; sourceTree = ""; }; 5E75948AA1FE1D1A7809931F /* AuthenticationServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationServiceProtocol.swift; sourceTree = ""; }; 5F12E996BFBEB43815189ABF /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = uk; path = uk.lproj/Localizable.stringsdict; sourceTree = ""; }; @@ -1227,7 +1227,6 @@ children = ( 3A4427F9E0571B4E6E048A2B /* KeychainController.swift */, 0CD51F9FDC91C231906D76C8 /* KeychainControllerProtocol.swift */, - 2349F5E576CEE032770CC6F9 /* MockUserSessionStore.swift */, 8C37FB986891D90BEAA93EAE /* UserSessionStore.swift */, BF1B52D0ABBA7091A991CAFE /* UserSessionStoreProtocol.swift */, ); @@ -1260,6 +1259,7 @@ isa = PBXGroup; children = ( 7D0CBC76C80E04345E11F2DB /* Application.swift */, + 5D2D0A6F1ABC99D29462FB84 /* AuthenticationCoordinatorUITests.swift */, C6FEA87EA3752203065ECE27 /* BugReportUITests.swift */, 4D6E4C37E9F0E53D3DF951AC /* HomeScreenUITests.swift */, 1DB34B0C74CD242FED9DD069 /* LoginScreenUITests.swift */, @@ -2016,7 +2016,6 @@ 9BE7A9CF6C593251D734B461 /* MockServerSelectionScreenState.swift in Sources */, D034A195A3494E38BF060485 /* MockSessionVerificationControllerProxy.swift in Sources */, D8359F67AF3A83516E9083C1 /* MockUserSession.swift in Sources */, - 1B9A74CD48DDC3A0AF027EFF /* MockUserSessionStore.swift in Sources */, 4ED453A61AF45EBE18D8BC69 /* NavigationModule.swift in Sources */, 22DADD537401E79D66132134 /* NavigationRouter.swift in Sources */, 12F70C493FB69F4D7E9A37EA /* NavigationRouterStore.swift in Sources */, @@ -2138,6 +2137,7 @@ buildActionMask = 2147483647; files = ( 7405B4824D45BA7C3D943E76 /* Application.swift in Sources */, + ACF094CF3BF02DBFA6DFDE60 /* AuthenticationCoordinatorUITests.swift in Sources */, 7756C4E90CABE6F14F7920A0 /* BugReportUITests.swift in Sources */, 499A26EB06C97E48C27A2DB9 /* BuildSettings.swift in Sources */, 94D0F36A87E596A93C0C178A /* Bundle.swift in Sources */, diff --git a/ElementX/Sources/AppCoordinator.swift b/ElementX/Sources/AppCoordinator.swift index 5524b1afa7..0e3ce5da61 100644 --- a/ElementX/Sources/AppCoordinator.swift +++ b/ElementX/Sources/AppCoordinator.swift @@ -165,7 +165,8 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator { } private func startAuthentication() { - let coordinator = AuthenticationCoordinator(userSessionStore: userSessionStore, + let authenticationService = AuthenticationService(userSessionStore: userSessionStore) + let coordinator = AuthenticationCoordinator(authenticationService: authenticationService, navigationRouter: navigationRouter) coordinator.delegate = self diff --git a/ElementX/Sources/Screens/Authentication/AuthenticationCoordinator.swift b/ElementX/Sources/Screens/Authentication/AuthenticationCoordinator.swift index 65c275f416..28481c12c9 100644 --- a/ElementX/Sources/Screens/Authentication/AuthenticationCoordinator.swift +++ b/ElementX/Sources/Screens/Authentication/AuthenticationCoordinator.swift @@ -25,9 +25,9 @@ class AuthenticationCoordinator: Coordinator, Presentable { weak var delegate: AuthenticationCoordinatorDelegate? - init(userSessionStore: UserSessionStoreProtocol, + init(authenticationService: AuthenticationServiceProtocol, navigationRouter: NavigationRouter) { - self.authenticationService = AuthenticationService(userSessionStore: userSessionStore) + self.authenticationService = authenticationService self.navigationRouter = navigationRouter } diff --git a/ElementX/Sources/Screens/Authentication/LoginScreen/View/LoginServerInfoSection.swift b/ElementX/Sources/Screens/Authentication/LoginScreen/View/LoginServerInfoSection.swift index 5a3cfe529d..4fa5ca1568 100644 --- a/ElementX/Sources/Screens/Authentication/LoginScreen/View/LoginServerInfoSection.swift +++ b/ElementX/Sources/Screens/Authentication/LoginScreen/View/LoginServerInfoSection.swift @@ -58,6 +58,7 @@ struct LoginServerInfoSection: View { .padding(.vertical, 2) } .buttonStyle(.elementGhost()) + .accessibilityIdentifier("editServerButton") } } } diff --git a/ElementX/Sources/Services/Authentication/MockAuthenticationService.swift b/ElementX/Sources/Services/Authentication/MockAuthenticationService.swift index 9ed19dd839..67839a73ba 100644 --- a/ElementX/Sources/Services/Authentication/MockAuthenticationService.swift +++ b/ElementX/Sources/Services/Authentication/MockAuthenticationService.swift @@ -39,7 +39,7 @@ class MockAuthenticationService: AuthenticationServiceProtocol { func login(username: String, password: String) async -> Result { guard username == validCredentials.username, password == validCredentials.password else { - return .failure(.failedLoggingIn) + return .failure(.invalidCredentials) } let userSession = MockUserSession(clientProxy: MockClientProxy(userIdentifier: username), diff --git a/ElementX/Sources/Services/UserSessionStore/MockUserSessionStore.swift b/ElementX/Sources/Services/UserSessionStore/MockUserSessionStore.swift deleted file mode 100644 index e5a55d9a1d..0000000000 --- a/ElementX/Sources/Services/UserSessionStore/MockUserSessionStore.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// MockUserSessionStore.swift -// ElementX -// -// Created by Doug on 30/06/2022. -// Copyright © 2022 Element. All rights reserved. -// - -import Foundation -import MatrixRustSDK - -struct MockUserSessionStore: UserSessionStoreProtocol { - var hasSessions: Bool { false } - - func restoreUserSession() async -> Result { - return .failure(.failedRestoringLogin) - } - - func userSession(for client: Client) async -> Result { - return .failure(.failedSettingUpSession) - } - - func logout(userSession: UserSessionProtocol) { } - - func baseDirectoryPath(for username: String) -> String { - FileManager.default.temporaryDirectory.path - } - - -} diff --git a/ElementX/Sources/UITestScreenIdentifier.swift b/ElementX/Sources/UITestScreenIdentifier.swift index 33b8e2a6c0..e00e0e0be5 100644 --- a/ElementX/Sources/UITestScreenIdentifier.swift +++ b/ElementX/Sources/UITestScreenIdentifier.swift @@ -12,6 +12,7 @@ enum UITestScreenIdentifier: String { case login case serverSelection case serverSelectionNonModal + case authenticationFlow case simpleRegular case simpleUpgrade case settings diff --git a/ElementX/Sources/UITestsAppCoordinator.swift b/ElementX/Sources/UITestsAppCoordinator.swift index 4e2cf90893..6892eb7593 100644 --- a/ElementX/Sources/UITestsAppCoordinator.swift +++ b/ElementX/Sources/UITestsAppCoordinator.swift @@ -11,12 +11,16 @@ import SwiftUI class UITestsAppCoordinator: Coordinator { private let window: UIWindow - private let mainNavigationController: UINavigationController + private let mainNavigationController: ElementNavigationController + private let navigationRouter: NavigationRouter + private var hostingController: UIViewController? var childCoordinators: [Coordinator] = [] init() { - mainNavigationController = UINavigationController() + mainNavigationController = ElementNavigationController() + navigationRouter = NavigationRouter(navigationController: mainNavigationController) + window = UIWindow(frame: UIScreen.main.bounds) window.rootViewController = mainNavigationController window.tintColor = .element.accent @@ -24,17 +28,20 @@ class UITestsAppCoordinator: Coordinator { UIView.setAnimationsEnabled(false) let screens = mockScreens() + let rootView = UITestsRootView(mockScreens: screens) { id in guard let screen = screens.first(where: { $0.id == id }) else { fatalError() } screen.coordinator.start() - - self.mainNavigationController.pushViewController(screen.coordinator.toPresentable(), animated: true) + self.navigationRouter.setRootModule(screen.coordinator) } - mainNavigationController.setViewControllers([UIHostingController(rootView: rootView)], animated: false) + let hostingController = UIHostingController(rootView: rootView) + self.hostingController = hostingController + + mainNavigationController.setViewControllers([hostingController], animated: false) } func start() { @@ -42,17 +49,17 @@ class UITestsAppCoordinator: Coordinator { } private func mockScreens() -> [MockScreen] { - UITestScreenIdentifier.allCases.map { MockScreen(id: $0) } + UITestScreenIdentifier.allCases.map { MockScreen(id: $0, navigationRouter: navigationRouter) } } } @MainActor class MockScreen: Identifiable { let id: UITestScreenIdentifier + let navigationRouter: NavigationRouter lazy var coordinator: Coordinator & Presentable = { switch id { case .login: - let navigationRouter = NavigationRouter(navigationController: ElementNavigationController()) return LoginCoordinator(parameters: .init(authenticationService: MockAuthenticationService(), navigationRouter: navigationRouter)) case .serverSelection: @@ -61,12 +68,14 @@ class MockScreen: Identifiable { case .serverSelectionNonModal: return ServerSelectionCoordinator(parameters: .init(authenticationService: MockAuthenticationService(), hasModalPresentation: false)) + case .authenticationFlow: + return AuthenticationCoordinator(authenticationService: MockAuthenticationService(), + navigationRouter: navigationRouter) case .simpleRegular: return TemplateCoordinator(parameters: .init(promptType: .regular)) case .simpleUpgrade: return TemplateCoordinator(parameters: .init(promptType: .upgrade)) case .settings: - let navigationRouter = NavigationRouter(navigationController: ElementNavigationController()) return SettingsCoordinator(parameters: .init(navigationRouter: navigationRouter, bugReportService: MockBugReportService())) case .bugReport: @@ -95,7 +104,8 @@ class MockScreen: Identifiable { } }() - init(id: UITestScreenIdentifier) { + init(id: UITestScreenIdentifier, navigationRouter: NavigationRouter) { self.id = id + self.navigationRouter = navigationRouter } } diff --git a/UITests/Sources/AuthenticationCoordinatorUITests.swift b/UITests/Sources/AuthenticationCoordinatorUITests.swift new file mode 100644 index 0000000000..de144c6639 --- /dev/null +++ b/UITests/Sources/AuthenticationCoordinatorUITests.swift @@ -0,0 +1,79 @@ +// +// AuthenticationCoordinatorUITests.swift +// UITests +// +// Created by Doug on 30/06/2022. +// Copyright © 2022 Element. All rights reserved. +// + +import XCTest + +@testable import ElementX + +@MainActor +class AuthenticationCoordinatorUITests: XCTestCase { + func testLoginWithPassword() { + // Given the authentication flow. + let app = Application.launch() + app.goToScreenWithIdentifier(.authenticationFlow) + + // Splash Screen: Tap get started button + app.buttons["getStartedButton"].tap() + + // Login Screen: Enter valid credentials + app.textFields["usernameTextField"].tap() + app.typeText("alice\n") + app.secureTextFields["passwordTextField"].tap() + app.typeText("12345678") + + // Login Screen: Tap next + app.buttons["nextButton"].tap() + + // Then login should succeed. + XCTAssertFalse(app.alerts.element.exists, "No alert should be shown when logging in with valid credentials.") + } + + func testLoginWithIncorrectPassword() { + // Given the authentication flow. + let app = Application.launch() + app.goToScreenWithIdentifier(.authenticationFlow) + + // Splash Screen: Tap get started button + app.buttons["getStartedButton"].tap() + + // Login Screen: Enter invalid credentials + app.textFields["usernameTextField"].tap() + app.typeText("alice\n") + app.typeText("87654321\n") + + // Then login should fail. + XCTAssertTrue(app.alerts.element.exists, "An error alert should be shown when attempting login with invalid credentials.") + } + + func testSelectingOIDCServer() async { + // Given the authentication flow. + let app = Application.launch() + app.goToScreenWithIdentifier(.authenticationFlow) + + // Splash Screen: Tap get started button + app.buttons["getStartedButton"].tap() + + // Login Screen: Tap edit server button. + XCTAssertFalse(app.buttons["oidcButton"].exists, "The OIDC button shouldn't be shown before entering a supported homeserver.") + app.buttons["editServerButton"].tap() + + // Server Selection: Clear the default and enter OIDC server. + app.textFields["addressTextField"].tap() + app.textFields["addressTextField"].buttons.element.tap() + app.typeText("company.com") + + // Dismiss server screen. + app.buttons["confirmButton"].tap() + + await Task.yield() + + // Then the login form should be updated for OIDC. + XCTAssertTrue(app.buttons["oidcButton"].exists, "The OIDC button should be shown after selecting a homeserver with OIDC.") + } +} + From f9c5f5cac0ec1fa6d7e886db33f3fd0566f10fe8 Mon Sep 17 00:00:00 2001 From: Doug Date: Thu, 30 Jun 2022 18:24:56 +0100 Subject: [PATCH 3/6] Wait for existence when updating the login screen. --- UITests/Sources/AuthenticationCoordinatorUITests.swift | 7 ++----- UITests/Sources/LoginScreenUITests.swift | 8 ++++---- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/UITests/Sources/AuthenticationCoordinatorUITests.swift b/UITests/Sources/AuthenticationCoordinatorUITests.swift index de144c6639..93a027cc56 100644 --- a/UITests/Sources/AuthenticationCoordinatorUITests.swift +++ b/UITests/Sources/AuthenticationCoordinatorUITests.swift @@ -50,7 +50,7 @@ class AuthenticationCoordinatorUITests: XCTestCase { XCTAssertTrue(app.alerts.element.exists, "An error alert should be shown when attempting login with invalid credentials.") } - func testSelectingOIDCServer() async { + func testSelectingOIDCServer() { // Given the authentication flow. let app = Application.launch() app.goToScreenWithIdentifier(.authenticationFlow) @@ -70,10 +70,7 @@ class AuthenticationCoordinatorUITests: XCTestCase { // Dismiss server screen. app.buttons["confirmButton"].tap() - await Task.yield() - // Then the login form should be updated for OIDC. - XCTAssertTrue(app.buttons["oidcButton"].exists, "The OIDC button should be shown after selecting a homeserver with OIDC.") + XCTAssertTrue(app.buttons["oidcButton"].waitForExistence(timeout: 1), "The OIDC button should be shown after selecting a homeserver with OIDC.") } } - diff --git a/UITests/Sources/LoginScreenUITests.swift b/UITests/Sources/LoginScreenUITests.swift index 276b6612ef..0a6a1f7774 100644 --- a/UITests/Sources/LoginScreenUITests.swift +++ b/UITests/Sources/LoginScreenUITests.swift @@ -60,9 +60,9 @@ class LoginScreenUITests: XCTestCase { // Then the screen should be configured for OIDC. let state = "an OIDC only server" + validateOIDCButtonIsShown(for: state) validateServerDescriptionIsHidden(for: state) validateLoginFormIsHidden(for: state) - validateOIDCButtonIsShown(for: state) validateUnsupportedServerTextIsHidden(for: state) } @@ -77,10 +77,10 @@ class LoginScreenUITests: XCTestCase { // Then the screen should not allow login to continue. let state = "an unsupported server" + validateUnsupportedServerTextIsShown(for: state) validateServerDescriptionIsHidden(for: state) validateLoginFormIsHidden(for: state) validateOIDCButtonIsHidden(for: state) - validateUnsupportedServerTextIsShown(for: state) } /// Checks that the server description label is shown. @@ -138,7 +138,7 @@ class LoginScreenUITests: XCTestCase { /// Checks that the OIDC button is shown on the screen. func validateOIDCButtonIsShown(for state: String) { let oidcButton = app.buttons["oidcButton"] - XCTAssertTrue(oidcButton.exists, "The OIDC button should be shown for \(state).") + XCTAssertTrue(oidcButton.waitForExistence(timeout: 1), "The OIDC button should be shown for \(state).") XCTAssertEqual(oidcButton.label, ElementL10n.loginContinue) } @@ -151,7 +151,7 @@ class LoginScreenUITests: XCTestCase { /// Checks that the unsupported homeserver text is shown on the screen. func validateUnsupportedServerTextIsShown(for state: String) { let unsupportedText = app.staticTexts["unsupportedServerText"] - XCTAssertTrue(unsupportedText.exists, "The unsupported homeserver text should be shown for \(state).") + XCTAssertTrue(unsupportedText.waitForExistence(timeout: 1), "The unsupported homeserver text should be shown for \(state).") XCTAssertEqual(unsupportedText.label, ElementL10n.autodiscoverWellKnownError) } From e48980a6717c212b6d9938a46dbd5d45a9b3e84f Mon Sep 17 00:00:00 2001 From: Doug Date: Fri, 1 Jul 2022 10:44:43 +0100 Subject: [PATCH 4/6] Self review for PR. --- .../Authentication/LoginScreen/LoginCoordinator.swift | 9 +++++---- .../ServerSelection/ServerSelectionCoordinator.swift | 4 ++-- .../Services/Authentication/AuthenticationService.swift | 3 +++ .../Authentication/MockAuthenticationService.swift | 3 +++ .../Services/UserSessionStore/UserSessionStore.swift | 2 -- changelog.d/pr-126.change | 1 + 6 files changed, 14 insertions(+), 8 deletions(-) create mode 100644 changelog.d/pr-126.change diff --git a/ElementX/Sources/Screens/Authentication/LoginScreen/LoginCoordinator.swift b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginCoordinator.swift index e96e93d07c..3f02d40184 100644 --- a/ElementX/Sources/Screens/Authentication/LoginScreen/LoginCoordinator.swift +++ b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginCoordinator.swift @@ -127,14 +127,14 @@ final class LoginCoordinator: Coordinator, Presentable { } /// Processes an error to either update the flow or display it to the user. - private func handleError(_ error: Error) { + private func handleError(_ error: AuthenticationServiceError) { switch error { - case AuthenticationServiceError.invalidCredentials: + case .invalidCredentials: loginViewModel.displayError(.alert(ElementL10n.authInvalidLoginParam)) - case AuthenticationServiceError.accountDeactivated: + case .accountDeactivated: loginViewModel.displayError(.alert(ElementL10n.authInvalidLoginDeactivatedAccount)) default: - loginViewModel.displayError(.alert(error.localizedDescription)) + loginViewModel.displayError(.alert(ElementL10n.unknownError)) } } @@ -146,6 +146,7 @@ final class LoginCoordinator: Coordinator, Presentable { switch await authenticationService.login(username: username, password: password) { case .success(let userSession): callback?(.signedIn(userSession)) + stopLoading() case .failure(let error): stopLoading() handleError(error) diff --git a/ElementX/Sources/Screens/Authentication/ServerSelection/ServerSelectionCoordinator.swift b/ElementX/Sources/Screens/Authentication/ServerSelection/ServerSelectionCoordinator.swift index 1fb157cd0e..f891a4021e 100644 --- a/ElementX/Sources/Screens/Authentication/ServerSelection/ServerSelectionCoordinator.swift +++ b/ElementX/Sources/Screens/Authentication/ServerSelection/ServerSelectionCoordinator.swift @@ -116,9 +116,9 @@ final class ServerSelectionCoordinator: Coordinator, Presentable { } /// Processes an error to either update the flow or display it to the user. - private func handleError(_ error: Error) { + private func handleError(_ error: AuthenticationServiceError) { switch error { - case AuthenticationServiceError.invalidServer: + case .invalidServer: serverSelectionViewModel.displayError(.footerMessage(ElementL10n.loginErrorHomeserverNotFound)) default: serverSelectionViewModel.displayError(.footerMessage(ElementL10n.unknownError)) diff --git a/ElementX/Sources/Services/Authentication/AuthenticationService.swift b/ElementX/Sources/Services/Authentication/AuthenticationService.swift index 126038b3cb..abcd4497ba 100644 --- a/ElementX/Sources/Services/Authentication/AuthenticationService.swift +++ b/ElementX/Sources/Services/Authentication/AuthenticationService.swift @@ -57,8 +57,11 @@ class AuthenticationService: AuthenticationServiceProtocol { switch await loginTask.result { case .success(let client): + Benchmark.endTrackingForIdentifier("Login", message: "Finished login") return await userSession(for: client) case .failure(let error): + Benchmark.endTrackingForIdentifier("Login", message: "Login failed") + MXLog.error("Failed logging in with error: \(error)") guard let error = error as? ClientError else { return .failure(.failedLoggingIn) } diff --git a/ElementX/Sources/Services/Authentication/MockAuthenticationService.swift b/ElementX/Sources/Services/Authentication/MockAuthenticationService.swift index 67839a73ba..bac0ff901c 100644 --- a/ElementX/Sources/Services/Authentication/MockAuthenticationService.swift +++ b/ElementX/Sources/Services/Authentication/MockAuthenticationService.swift @@ -20,6 +20,7 @@ class MockAuthenticationService: AuthenticationServiceProtocol { } func startLogin(for homeserverAddress: String) async -> Result { + // Map the address to the mock homeservers if LoginHomeserver.mockMatrixDotOrg.address.contains(homeserverAddress) { homeserver = .mockMatrixDotOrg return .success(()) @@ -34,10 +35,12 @@ class MockAuthenticationService: AuthenticationServiceProtocol { return .success(()) } + // Otherwise fail with an invalid server. return .failure(.invalidServer) } func login(username: String, password: String) async -> Result { + // Login only succeeds if the username and password match the valid credentials property guard username == validCredentials.username, password == validCredentials.password else { return .failure(.invalidCredentials) } diff --git a/ElementX/Sources/Services/UserSessionStore/UserSessionStore.swift b/ElementX/Sources/Services/UserSessionStore/UserSessionStore.swift index ad4354615d..b3f1f3e1fc 100644 --- a/ElementX/Sources/Services/UserSessionStore/UserSessionStore.swift +++ b/ElementX/Sources/Services/UserSessionStore/UserSessionStore.swift @@ -98,8 +98,6 @@ class UserSessionStore: UserSessionStoreProtocol { } private func setupProxyForClient(_ client: Client) async -> Result { - Benchmark.endTrackingForIdentifier("Login", message: "Finished login") - do { let accessToken = try client.restoreToken() let userId = try client.userId() diff --git a/changelog.d/pr-126.change b/changelog.d/pr-126.change new file mode 100644 index 0000000000..c362ea0573 --- /dev/null +++ b/changelog.d/pr-126.change @@ -0,0 +1 @@ +Add AuthenticationService and missing UI tests on the flow. From 5173a2904f32742ff4c84348f09ca5a967dee8bc Mon Sep 17 00:00:00 2001 From: Doug Date: Fri, 1 Jul 2022 18:57:01 +0100 Subject: [PATCH 5/6] Rebase on develop. --- ElementX/Sources/Services/Client/MockClientProxy.swift | 5 +++++ ElementX/Sources/Services/Session/MockUserSession.swift | 5 ++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/ElementX/Sources/Services/Client/MockClientProxy.swift b/ElementX/Sources/Services/Client/MockClientProxy.swift index 7b49384c6e..96b12abfa6 100644 --- a/ElementX/Sources/Services/Client/MockClientProxy.swift +++ b/ElementX/Sources/Services/Client/MockClientProxy.swift @@ -10,6 +10,7 @@ import Combine import MatrixRustSDK struct MockClientProxy: ClientProxyProtocol { + let callbacks = PassthroughSubject() let userIdentifier: String @@ -31,4 +32,8 @@ struct MockClientProxy: ClientProxyProtocol { func loadMediaContentForSource(_ source: MatrixRustSDK.MediaSource) throws -> Data { throw ClientProxyError.failedLoadingMedia } + + func sessionVerificationControllerProxy() async -> Result { + .failure(.failedRetrievingSessionVerificationController) + } } diff --git a/ElementX/Sources/Services/Session/MockUserSession.swift b/ElementX/Sources/Services/Session/MockUserSession.swift index b2e6357402..87aba767e2 100644 --- a/ElementX/Sources/Services/Session/MockUserSession.swift +++ b/ElementX/Sources/Services/Session/MockUserSession.swift @@ -6,9 +6,12 @@ // Copyright © 2022 Element. All rights reserved. // -import Foundation +import Combine struct MockUserSession: UserSessionProtocol { + let callbacks = PassthroughSubject() + let sessionVerificationController: SessionVerificationControllerProxyProtocol? = nil + let clientProxy: ClientProxyProtocol let mediaProvider: MediaProviderProtocol } From e8852bcda4bcc9df2e0bda58a789f68f5fee4868 Mon Sep 17 00:00:00 2001 From: Doug Date: Mon, 4 Jul 2022 09:37:17 +0100 Subject: [PATCH 6/6] PR remarks. --- ElementX.xcodeproj/project.pbxproj | 10 +++++----- ElementX/Sources/Other/Extensions/String.swift | 8 ++++++++ ...xEntitityRegex.swift => MatrixEntityRegex.swift} | 0 .../LoginScreen/LoginCoordinator.swift | 2 +- .../Authentication/AuthenticationService.swift | 9 +-------- .../AuthenticationServiceProtocol.swift | 2 -- .../Authentication/MockAuthenticationService.swift | 13 +++---------- 7 files changed, 18 insertions(+), 26 deletions(-) rename ElementX/Sources/Other/{MatrixEntitityRegex.swift => MatrixEntityRegex.swift} (100%) diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index ad2aad7b30..d9efd3d803 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 52; objects = { /* Begin PBXBuildFile section */ @@ -279,7 +279,7 @@ EE8491AD81F47DF3C192497B /* DecorationTimelineItemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 184CF8C196BE143AE226628D /* DecorationTimelineItemProtocol.swift */; }; EEC40663922856C65D1E0DF5 /* KeychainControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB9C37196A4C79F24CE80C6 /* KeychainControllerTests.swift */; }; EF99A92701E401C4CD5ADC50 /* SplashScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCE978A6118C131D7F2A04B3 /* SplashScreenModels.swift */; }; - F03E16ED043C62FED5A07AE0 /* MatrixEntitityRegex.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7B81C8227BBEA95CCE86037 /* MatrixEntitityRegex.swift */; }; + F03E16ED043C62FED5A07AE0 /* MatrixEntityRegex.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7B81C8227BBEA95CCE86037 /* MatrixEntityRegex.swift */; }; F040ABFEB0A2B142D948BA12 /* Untranslated.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = F75DF9500D69A3AAF8339E69 /* Untranslated.stringsdict */; }; F2DD8661B5C0BA2BB526FA6C /* KeychainControllerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD51F9FDC91C231906D76C8 /* KeychainControllerProtocol.swift */; }; F4C3FEDB1B3A05376A1723A3 /* KeychainController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A4427F9E0571B4E6E048A2B /* KeychainController.swift */; }; @@ -691,7 +691,7 @@ F6A8C632CEF4600107792899 /* TextRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextRoomTimelineItem.swift; sourceTree = ""; }; F73FF1A33198F5FAE9D34B1F /* FormattedBodyText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormattedBodyText.swift; sourceTree = ""; }; F77C060C2ACC4CB7336A29E7 /* EmoteRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineItem.swift; sourceTree = ""; }; - F7B81C8227BBEA95CCE86037 /* MatrixEntitityRegex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatrixEntitityRegex.swift; sourceTree = ""; }; + F7B81C8227BBEA95CCE86037 /* MatrixEntityRegex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatrixEntityRegex.swift; sourceTree = ""; }; F9E785D5137510481733A3E8 /* TextRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextRoomTimelineView.swift; sourceTree = ""; }; FA154570F693D93513E584C1 /* RoomMessageFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMessageFactory.swift; sourceTree = ""; }; FAB10E673916D2B8D21FD197 /* TemplateModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateModels.swift; sourceTree = ""; }; @@ -1443,7 +1443,7 @@ CF4B39D52CAE7D21D276ABEE /* ElementNavigationController.swift */, 1027BB9A852F445B7623897F /* ElementSettings.swift */, 12A626D74BBE9F4A60763B45 /* ImageAnonymizer.swift */, - F7B81C8227BBEA95CCE86037 /* MatrixEntitityRegex.swift */, + F7B81C8227BBEA95CCE86037 /* MatrixEntityRegex.swift */, 44BBB96FAA2F0D53C507396B /* Extensions */, 8F9A844EB44B6AD7CA18FD96 /* HTMLParsing */, 06501F0E978B2D5C92771DC7 /* Logging */, @@ -1997,7 +1997,7 @@ 2F30EFEB7BD39242D1AD96F3 /* LoginViewModelProtocol.swift in Sources */, B94368839BDB69172E28E245 /* MXLog.swift in Sources */, BCC3EDB7AD0902797CB4BBC2 /* MXLogger.m in Sources */, - F03E16ED043C62FED5A07AE0 /* MatrixEntitityRegex.swift in Sources */, + F03E16ED043C62FED5A07AE0 /* MatrixEntityRegex.swift in Sources */, EA1E7949533E19C6D862680A /* MediaProvider.swift in Sources */, 7002C55A4C917F3715765127 /* MediaProviderProtocol.swift in Sources */, 62BBF5BE7B905222F0477FF2 /* MediaSource.swift in Sources */, diff --git a/ElementX/Sources/Other/Extensions/String.swift b/ElementX/Sources/Other/Extensions/String.swift index f08aeb54a1..81f2a10875 100644 --- a/ElementX/Sources/Other/Extensions/String.swift +++ b/ElementX/Sources/Other/Extensions/String.swift @@ -31,4 +31,12 @@ extension String { return string } + + /// Whether or not the string is a Matrix user ID. + var isMatrixUserID: Bool { + let range = NSRange(location: 0, length: count) + + let detector = try? NSRegularExpression(pattern: MatrixEntityRegex.userId.rawValue, options: .caseInsensitive) + return detector?.numberOfMatches(in: self, range: range) ?? 0 == 1 + } } diff --git a/ElementX/Sources/Other/MatrixEntitityRegex.swift b/ElementX/Sources/Other/MatrixEntityRegex.swift similarity index 100% rename from ElementX/Sources/Other/MatrixEntitityRegex.swift rename to ElementX/Sources/Other/MatrixEntityRegex.swift diff --git a/ElementX/Sources/Screens/Authentication/LoginScreen/LoginCoordinator.swift b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginCoordinator.swift index 3f02d40184..4f2a636011 100644 --- a/ElementX/Sources/Screens/Authentication/LoginScreen/LoginCoordinator.swift +++ b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginCoordinator.swift @@ -156,7 +156,7 @@ final class LoginCoordinator: Coordinator, Presentable { /// Parses the specified username and looks up the homeserver when a Matrix ID is entered. private func parseUsername(_ username: String) { - guard authenticationService.usernameIsMatrixID(username) else { return } + guard username.isMatrixUserID else { return } let homeserverDomain = String(username.split(separator: ":")[1]) diff --git a/ElementX/Sources/Services/Authentication/AuthenticationService.swift b/ElementX/Sources/Services/Authentication/AuthenticationService.swift index abcd4497ba..cd72570384 100644 --- a/ElementX/Sources/Services/Authentication/AuthenticationService.swift +++ b/ElementX/Sources/Services/Authentication/AuthenticationService.swift @@ -26,13 +26,6 @@ class AuthenticationService: AuthenticationServiceProtocol { // MARK: - Public - func usernameIsMatrixID(_ username: String) -> Bool { - let range = NSRange(location: 0, length: username.count) - - let detector = try? NSRegularExpression(pattern: MatrixEntityRegex.userId.rawValue, options: .caseInsensitive) - return detector?.numberOfMatches(in: username, range: range) ?? 0 == 1 - } - func startLogin(for homeserverAddress: String) async -> Result { homeserver = LoginHomeserver(address: homeserverAddress) return .success(()) @@ -42,7 +35,7 @@ class AuthenticationService: AuthenticationServiceProtocol { Benchmark.startTrackingForIdentifier("Login", message: "Started new login") // Workaround whilst the SDK requires a full MXID. - let username = usernameIsMatrixID(username) ? username : "@\(username):\(homeserver.address)" + let username = username.isMatrixUserID ? username : "@\(username):\(homeserver.address)" let basePath = userSessionStore.baseDirectoryPath(for: username) let builder = ClientBuilder() diff --git a/ElementX/Sources/Services/Authentication/AuthenticationServiceProtocol.swift b/ElementX/Sources/Services/Authentication/AuthenticationServiceProtocol.swift index 68a41c668d..8bbed51471 100644 --- a/ElementX/Sources/Services/Authentication/AuthenticationServiceProtocol.swift +++ b/ElementX/Sources/Services/Authentication/AuthenticationServiceProtocol.swift @@ -19,8 +19,6 @@ enum AuthenticationServiceError: Error { protocol AuthenticationServiceProtocol { var homeserver: LoginHomeserver { get } - /// Checks whether the specified username is a Matrix ID or not. - func usernameIsMatrixID(_ username: String) -> Bool /// Sets up the service for login on the specified homeserver address. func startLogin(for homeserverAddress: String) async -> Result /// Performs a password login using the current homeserver. diff --git a/ElementX/Sources/Services/Authentication/MockAuthenticationService.swift b/ElementX/Sources/Services/Authentication/MockAuthenticationService.swift index bac0ff901c..1d2bab7b9e 100644 --- a/ElementX/Sources/Services/Authentication/MockAuthenticationService.swift +++ b/ElementX/Sources/Services/Authentication/MockAuthenticationService.swift @@ -12,13 +12,6 @@ class MockAuthenticationService: AuthenticationServiceProtocol { let validCredentials = (username: "alice", password: "12345678") private(set) var homeserver: LoginHomeserver = .mockMatrixDotOrg - func usernameIsMatrixID(_ username: String) -> Bool { - let range = NSRange(location: 0, length: username.count) - - let detector = try? NSRegularExpression(pattern: MatrixEntityRegex.userId.rawValue, options: .caseInsensitive) - return detector?.numberOfMatches(in: username, range: range) ?? 0 == 1 - } - func startLogin(for homeserverAddress: String) async -> Result { // Map the address to the mock homeservers if LoginHomeserver.mockMatrixDotOrg.address.contains(homeserverAddress) { @@ -33,10 +26,10 @@ class MockAuthenticationService: AuthenticationServiceProtocol { } else if LoginHomeserver.mockUnsupported.address.contains(homeserverAddress) { homeserver = .mockUnsupported return .success(()) + } else { + // Otherwise fail with an invalid server. + return .failure(.invalidServer) } - - // Otherwise fail with an invalid server. - return .failure(.invalidServer) } func login(username: String, password: String) async -> Result {