#40: Add basic AuthenticationService and missing UI tests.
* Add MockAuthenticationService and ServerSelectionUITests. * Add tests covering the Authentication flow.
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 51;
|
||||
objectVersion = 52;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
@@ -73,6 +73,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 +116,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 +135,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 +154,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 +191,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 */; };
|
||||
@@ -214,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 */; };
|
||||
@@ -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,10 +275,11 @@
|
||||
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 */; };
|
||||
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 */; };
|
||||
@@ -309,6 +317,7 @@
|
||||
02A07FF019724B6ACEA73076 /* szl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = szl; path = szl.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
04BBC9E08250EF92ADE89CFD /* sr-Latn */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "sr-Latn"; path = "sr-Latn.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
04E1273CC3BC3E471AF87BE5 /* UserIndicatorQueueTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorQueueTests.swift; sourceTree = "<group>"; };
|
||||
054F469E433864CC6FE6EE8E /* ServerSelectionUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionUITests.swift; sourceTree = "<group>"; };
|
||||
057B747CF045D3C6C30EAB2C /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = fi; path = fi.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
086B997409328F091EBA43CE /* RoomScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenUITests.swift; sourceTree = "<group>"; };
|
||||
08F64963396A6A23538EFCEC /* is */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = is; path = is.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
@@ -392,6 +401,7 @@
|
||||
3DD2D50A7EAA4FC78417730E /* SettingsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsCoordinator.swift; sourceTree = "<group>"; };
|
||||
3DD6E7C1D8B53F47789778CD /* fr-CA */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "fr-CA"; path = "fr-CA.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
3DF1FFC3336EB23374BBBFCC /* UIKitBackgroundTaskService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitBackgroundTaskService.swift; sourceTree = "<group>"; };
|
||||
3F40F48279322E504153AB0D /* MockClientProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockClientProxy.swift; sourceTree = "<group>"; };
|
||||
3F87116470221880017CF522 /* BuildSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildSettings.swift; sourceTree = "<group>"; };
|
||||
3FAA6438B00FDB130F404E31 /* UserIndicatorStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorStore.swift; sourceTree = "<group>"; };
|
||||
3FEE631F3A4AFDC6652DD9DA /* RoomTimelineViewFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineViewFactory.swift; sourceTree = "<group>"; };
|
||||
@@ -443,7 +453,9 @@
|
||||
5B9D5F812E5AD6DC786DBC9B /* NavigationRouterStoreProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationRouterStoreProtocol.swift; sourceTree = "<group>"; };
|
||||
5CB7F9D6FC121204D59E18DF /* Presentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Presentable.swift; sourceTree = "<group>"; };
|
||||
5D26A086A8278D39B5756D6F /* project.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = project.yml; sourceTree = "<group>"; };
|
||||
5D2D0A6F1ABC99D29462FB84 /* AuthenticationCoordinatorUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationCoordinatorUITests.swift; sourceTree = "<group>"; };
|
||||
5D8EA85D4F10D7445BB6368A /* UserIndicatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorTests.swift; sourceTree = "<group>"; };
|
||||
5E75948AA1FE1D1A7809931F /* AuthenticationServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationServiceProtocol.swift; sourceTree = "<group>"; };
|
||||
5F12E996BFBEB43815189ABF /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = uk; path = uk.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
5F4134FEFE4EB55759017408 /* UserSessionProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionProtocol.swift; sourceTree = "<group>"; };
|
||||
5F77E8010D41AA3F5F9A1FCA /* NavigationModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationModule.swift; sourceTree = "<group>"; };
|
||||
@@ -551,6 +563,7 @@
|
||||
A30A1758E2B73EF38E7C42F8 /* ServerSelectionModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionModels.swift; sourceTree = "<group>"; };
|
||||
A436057DBEA1A23CA8CB1FD7 /* UIFont+AttributedStringBuilder.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UIFont+AttributedStringBuilder.h"; sourceTree = "<group>"; };
|
||||
A443FAE2EE820A5790C35C8D /* et */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = et; path = et.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
A4756C5A8C8649AD6C10C615 /* MockUserSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockUserSession.swift; sourceTree = "<group>"; };
|
||||
A64F0DB78E0AC23C91AD89EF /* mk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = mk; path = mk.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
A65F140F9FE5E8D4DAEFF354 /* RoomProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomProxy.swift; sourceTree = "<group>"; };
|
||||
A72232816DCE2B76D48E1367 /* nb-NO */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "nb-NO"; path = "nb-NO.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
@@ -624,6 +637,7 @@
|
||||
CF47564C584F614B7287F3EB /* RootRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootRouter.swift; sourceTree = "<group>"; };
|
||||
CF4B39D52CAE7D21D276ABEE /* ElementNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementNavigationController.swift; sourceTree = "<group>"; };
|
||||
CF847B3C1873B8E81CEE7FAC /* SplashScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashScreenViewModel.swift; sourceTree = "<group>"; };
|
||||
D09A267106B9585D3D0CFC0D /* ClientError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientError.swift; sourceTree = "<group>"; };
|
||||
D0A45283CF1DB96E583BECA6 /* ImageRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageRoomTimelineView.swift; sourceTree = "<group>"; };
|
||||
D1A9CCCF53495CF3D7B19FCE /* MockSessionVerificationControllerProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSessionVerificationControllerProxy.swift; sourceTree = "<group>"; };
|
||||
D29EBCBFEC6FD0941749404D /* NavigationRouterStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationRouterStore.swift; sourceTree = "<group>"; };
|
||||
@@ -634,6 +648,7 @@
|
||||
D6CA5F386C7701C129398945 /* AuthenticationCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationCoordinator.swift; sourceTree = "<group>"; };
|
||||
D6D094C15E8DB424F1C6FC94 /* hr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hr; path = hr.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
D6DC38E64A5ED3FDB201029A /* BugReportService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportService.swift; sourceTree = "<group>"; };
|
||||
DA38899517F08FE2AF34EB45 /* MockAuthenticationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAuthenticationService.swift; sourceTree = "<group>"; };
|
||||
DBD460ED7ED1E03B85DEA25C /* TemplateCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateCoordinator.swift; sourceTree = "<group>"; };
|
||||
DBFEAC3AC691CBB84983E275 /* ElementXTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementXTests.swift; sourceTree = "<group>"; };
|
||||
DCE978A6118C131D7F2A04B3 /* SplashScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashScreenModels.swift; sourceTree = "<group>"; };
|
||||
@@ -670,12 +685,13 @@
|
||||
F0E7BF8F7BB1021F889C6483 /* MockBugReportService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockBugReportService.swift; sourceTree = "<group>"; };
|
||||
F23BA6D4842D53C5AC9B7584 /* nn */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = nn; path = nn.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
F2D58333B377888012740101 /* LoginViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewModel.swift; sourceTree = "<group>"; };
|
||||
F3A1AB5A84D843B6AC8D5F1E /* AuthenticationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationService.swift; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
F6A8C632CEF4600107792899 /* TextRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextRoomTimelineItem.swift; sourceTree = "<group>"; };
|
||||
F73FF1A33198F5FAE9D34B1F /* FormattedBodyText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormattedBodyText.swift; sourceTree = "<group>"; };
|
||||
F77C060C2ACC4CB7336A29E7 /* EmoteRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineItem.swift; sourceTree = "<group>"; };
|
||||
F7B81C8227BBEA95CCE86037 /* MatrixEntitityRegex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatrixEntitityRegex.swift; sourceTree = "<group>"; };
|
||||
F7B81C8227BBEA95CCE86037 /* MatrixEntityRegex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatrixEntityRegex.swift; sourceTree = "<group>"; };
|
||||
F9E785D5137510481733A3E8 /* TextRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextRoomTimelineView.swift; sourceTree = "<group>"; };
|
||||
FA154570F693D93513E584C1 /* RoomMessageFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMessageFactory.swift; sourceTree = "<group>"; };
|
||||
FAB10E673916D2B8D21FD197 /* TemplateModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateModels.swift; sourceTree = "<group>"; };
|
||||
@@ -753,6 +769,7 @@
|
||||
0787F81684E503024BD0C051 /* Services */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
AAFDD509929A0CCF8BCE51EB /* Authentication */,
|
||||
EBBEB5471737E9D116DF4738 /* Background */,
|
||||
0ED3F5C21537519389C07644 /* BugReport */,
|
||||
8039515BAA53B7C3275AC64A /* Client */,
|
||||
@@ -825,6 +842,13 @@
|
||||
path = Resources;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
3180C73BA7B8F5F7447C99B0 /* React */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
);
|
||||
path = React;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
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 = "<group>";
|
||||
@@ -1176,6 +1202,7 @@
|
||||
82D5AD3EAE3A5C1068A44A88 /* Session */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A4756C5A8C8649AD6C10C615 /* MockUserSession.swift */,
|
||||
6E5E9C044BEB7C70B1378E91 /* UserSession.swift */,
|
||||
5F4134FEFE4EB55759017408 /* UserSessionProtocol.swift */,
|
||||
);
|
||||
@@ -1232,10 +1259,12 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
7D0CBC76C80E04345E11F2DB /* Application.swift */,
|
||||
5D2D0A6F1ABC99D29462FB84 /* AuthenticationCoordinatorUITests.swift */,
|
||||
C6FEA87EA3752203065ECE27 /* BugReportUITests.swift */,
|
||||
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 = "<group>";
|
||||
};
|
||||
AAFDD509929A0CCF8BCE51EB /* Authentication */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
F3A1AB5A84D843B6AC8D5F1E /* AuthenticationService.swift */,
|
||||
5E75948AA1FE1D1A7809931F /* AuthenticationServiceProtocol.swift */,
|
||||
DA38899517F08FE2AF34EB45 /* MockAuthenticationService.swift */,
|
||||
);
|
||||
path = Authentication;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
AD5FCF9340D670C526AD17E4 /* UI */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -1404,7 +1443,7 @@
|
||||
CF4B39D52CAE7D21D276ABEE /* ElementNavigationController.swift */,
|
||||
1027BB9A852F445B7623897F /* ElementSettings.swift */,
|
||||
12A626D74BBE9F4A60763B45 /* ImageAnonymizer.swift */,
|
||||
F7B81C8227BBEA95CCE86037 /* MatrixEntitityRegex.swift */,
|
||||
F7B81C8227BBEA95CCE86037 /* MatrixEntityRegex.swift */,
|
||||
44BBB96FAA2F0D53C507396B /* Extensions */,
|
||||
8F9A844EB44B6AD7CA18FD96 /* HTMLParsing */,
|
||||
06501F0E978B2D5C92771DC7 /* Logging */,
|
||||
@@ -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 */,
|
||||
@@ -1954,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 */,
|
||||
@@ -1963,13 +2006,16 @@
|
||||
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 */,
|
||||
4ED453A61AF45EBE18D8BC69 /* NavigationModule.swift in Sources */,
|
||||
22DADD537401E79D66132134 /* NavigationRouter.swift in Sources */,
|
||||
12F70C493FB69F4D7E9A37EA /* NavigationRouterStore.swift in Sources */,
|
||||
@@ -2091,12 +2137,14 @@
|
||||
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 */,
|
||||
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 */,
|
||||
|
||||
@@ -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):
|
||||
@@ -179,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
|
||||
|
||||
|
||||
@@ -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 ])
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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?
|
||||
@@ -35,9 +25,9 @@ class AuthenticationCoordinator: Coordinator {
|
||||
|
||||
weak var delegate: AuthenticationCoordinatorDelegate?
|
||||
|
||||
init(userSessionStore: UserSessionStoreProtocol,
|
||||
init(authenticationService: AuthenticationServiceProtocol,
|
||||
navigationRouter: NavigationRouter) {
|
||||
self.userSessionStore = userSessionStore
|
||||
self.authenticationService = authenticationService
|
||||
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<UserSession, AuthenticationCoordinatorError> {
|
||||
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<Client, Error> = 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<UserSession, AuthenticationCoordinatorError> {
|
||||
switch await userSessionStore.userSession(for: client) {
|
||||
case .success(let clientProxy):
|
||||
return .success(clientProxy)
|
||||
case .failure:
|
||||
return .failure(.failedLoggingIn)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
@@ -135,56 +127,68 @@ 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))
|
||||
private func handleError(_ error: AuthenticationServiceError) {
|
||||
switch error {
|
||||
case .invalidCredentials:
|
||||
loginViewModel.displayError(.alert(ElementL10n.authInvalidLoginParam))
|
||||
case .accountDeactivated:
|
||||
loginViewModel.displayError(.alert(ElementL10n.authInvalidLoginDeactivatedAccount))
|
||||
default:
|
||||
loginViewModel.displayError(.alert(ElementL10n.unknownError))
|
||||
}
|
||||
}
|
||||
|
||||
/// 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))
|
||||
stopLoading()
|
||||
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 username.isMatrixUserID 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 +202,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)
|
||||
|
||||
@@ -58,6 +58,7 @@ struct LoginServerInfoSection: View {
|
||||
.padding(.vertical, 2)
|
||||
}
|
||||
.buttonStyle(.elementGhost())
|
||||
.accessibilityIdentifier("editServerButton")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: AuthenticationServiceError) {
|
||||
switch error {
|
||||
case .invalidServer:
|
||||
serverSelectionViewModel.displayError(.footerMessage(ElementL10n.loginErrorHomeserverNotFound))
|
||||
default:
|
||||
serverSelectionViewModel.displayError(.footerMessage(ElementL10n.unknownError))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -26,7 +26,7 @@ class ServerSelectionViewModel: ServerSelectionViewModelType, ServerSelectionVie
|
||||
|
||||
// MARK: Public
|
||||
|
||||
var callback: (@MainActor (ServerSelectionViewModelResult) -> Void)?
|
||||
var callback: (@MainActor (ServerSelectionViewModelAction) -> Void)?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
//
|
||||
// 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 startLogin(for homeserverAddress: String) async -> Result<Void, AuthenticationServiceError> {
|
||||
homeserver = LoginHomeserver(address: homeserverAddress)
|
||||
return .success(())
|
||||
}
|
||||
|
||||
func login(username: String, password: String) async -> Result<UserSessionProtocol, AuthenticationServiceError> {
|
||||
Benchmark.startTrackingForIdentifier("Login", message: "Started new login")
|
||||
|
||||
// Workaround whilst the SDK requires a full MXID.
|
||||
let username = username.isMatrixUserID ? username : "@\(username):\(homeserver.address)"
|
||||
|
||||
let basePath = userSessionStore.baseDirectoryPath(for: username)
|
||||
let builder = ClientBuilder()
|
||||
.basePath(path: basePath)
|
||||
.username(username: username)
|
||||
|
||||
let loginTask: Task<Client, Error> = Task.detached {
|
||||
let client = try builder.build()
|
||||
try client.login(username: username, password: password)
|
||||
return client
|
||||
}
|
||||
|
||||
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) }
|
||||
|
||||
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<UserSessionProtocol, AuthenticationServiceError> {
|
||||
switch await userSessionStore.userSession(for: client) {
|
||||
case .success(let clientProxy):
|
||||
return .success(clientProxy)
|
||||
case .failure:
|
||||
return .failure(.failedLoggingIn)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
//
|
||||
// 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 }
|
||||
|
||||
/// Sets up the service for login on the specified homeserver address.
|
||||
func startLogin(for homeserverAddress: String) async -> Result<Void, AuthenticationServiceError>
|
||||
/// Performs a password login using the current homeserver.
|
||||
func login(username: String, password: String) async -> Result<UserSessionProtocol, AuthenticationServiceError>
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
//
|
||||
// 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 startLogin(for homeserverAddress: String) async -> Result<Void, AuthenticationServiceError> {
|
||||
// Map the address to the mock homeservers
|
||||
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(())
|
||||
} else {
|
||||
// Otherwise fail with an invalid server.
|
||||
return .failure(.invalidServer)
|
||||
}
|
||||
}
|
||||
|
||||
func login(username: String, password: String) async -> Result<UserSessionProtocol, AuthenticationServiceError> {
|
||||
// Login only succeeds if the username and password match the valid credentials property
|
||||
guard username == validCredentials.username, password == validCredentials.password else {
|
||||
return .failure(.invalidCredentials)
|
||||
}
|
||||
|
||||
let userSession = MockUserSession(clientProxy: MockClientProxy(userIdentifier: username),
|
||||
mediaProvider: MockMediaProvider())
|
||||
return .success(userSession)
|
||||
}
|
||||
}
|
||||
30
ElementX/Sources/Services/Client/ClientError.swift
Normal file
30
ElementX/Sources/Services/Client/ClientError.swift
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ enum ClientProxyError: Error {
|
||||
case failedRetrievingAvatarURL
|
||||
case failedRetrievingDisplayName
|
||||
case failedRetrievingSessionVerificationController
|
||||
case failedLoadingMedia
|
||||
}
|
||||
|
||||
protocol ClientProxyProtocol {
|
||||
|
||||
39
ElementX/Sources/Services/Client/MockClientProxy.swift
Normal file
39
ElementX/Sources/Services/Client/MockClientProxy.swift
Normal file
@@ -0,0 +1,39 @@
|
||||
//
|
||||
// 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<ClientProxyCallback, Never>()
|
||||
|
||||
let userIdentifier: String
|
||||
|
||||
let rooms = [RoomProxy]()
|
||||
|
||||
func loadUserDisplayName() async -> Result<String, ClientProxyError> {
|
||||
.failure(.failedRetrievingDisplayName)
|
||||
}
|
||||
|
||||
func loadUserAvatarURLString() async -> Result<String, ClientProxyError> {
|
||||
.failure(.failedRetrievingAvatarURL)
|
||||
}
|
||||
|
||||
func mediaSourceForURLString(_ urlString: String) -> MatrixRustSDK.MediaSource {
|
||||
MatrixRustSDK.mediaSourceFromUrl(url: urlString)
|
||||
}
|
||||
|
||||
func loadMediaContentForSource(_ source: MatrixRustSDK.MediaSource) throws -> Data {
|
||||
throw ClientProxyError.failedLoadingMedia
|
||||
}
|
||||
|
||||
func sessionVerificationControllerProxy() async -> Result<SessionVerificationControllerProxyProtocol, ClientProxyError> {
|
||||
.failure(.failedRetrievingSessionVerificationController)
|
||||
}
|
||||
}
|
||||
17
ElementX/Sources/Services/Session/MockUserSession.swift
Normal file
17
ElementX/Sources/Services/Session/MockUserSession.swift
Normal file
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// MockUserSession.swift
|
||||
// ElementX
|
||||
//
|
||||
// Created by Doug on 29/06/2022.
|
||||
// Copyright © 2022 Element. All rights reserved.
|
||||
//
|
||||
|
||||
import Combine
|
||||
|
||||
struct MockUserSession: UserSessionProtocol {
|
||||
let callbacks = PassthroughSubject<UserSessionCallback, Never>()
|
||||
let sessionVerificationController: SessionVerificationControllerProxyProtocol? = nil
|
||||
|
||||
let clientProxy: ClientProxyProtocol
|
||||
let mediaProvider: MediaProviderProtocol
|
||||
}
|
||||
@@ -98,8 +98,6 @@ class UserSessionStore: UserSessionStoreProtocol {
|
||||
}
|
||||
|
||||
private func setupProxyForClient(_ client: Client) async -> Result<ClientProxyProtocol, UserSessionStoreError> {
|
||||
Benchmark.endTrackingForIdentifier("Login", message: "Finished login")
|
||||
|
||||
do {
|
||||
let accessToken = try client.restoreToken()
|
||||
let userId = try client.userId()
|
||||
|
||||
@@ -10,8 +10,9 @@ import Foundation
|
||||
|
||||
enum UITestScreenIdentifier: String {
|
||||
case login
|
||||
case loginOIDC
|
||||
case loginUnsupported
|
||||
case serverSelection
|
||||
case serverSelectionNonModal
|
||||
case authenticationFlow
|
||||
case simpleRegular
|
||||
case simpleUpgrade
|
||||
case settings
|
||||
|
||||
@@ -11,28 +11,37 @@ 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
|
||||
|
||||
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() {
|
||||
@@ -40,34 +49,34 @@ 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 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))
|
||||
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 .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 router = NavigationRouter(navigationController: ElementNavigationController())
|
||||
return SettingsCoordinator(parameters: .init(navigationRouter: router,
|
||||
return SettingsCoordinator(parameters: .init(navigationRouter: navigationRouter,
|
||||
bugReportService: MockBugReportService()))
|
||||
case .bugReport:
|
||||
return BugReportCoordinator(parameters: .init(bugReportService: MockBugReportService(),
|
||||
@@ -95,7 +104,8 @@ class MockScreen: Identifiable {
|
||||
}
|
||||
}()
|
||||
|
||||
init(id: UITestScreenIdentifier) {
|
||||
init(id: UITestScreenIdentifier, navigationRouter: NavigationRouter) {
|
||||
self.id = id
|
||||
self.navigationRouter = navigationRouter
|
||||
}
|
||||
}
|
||||
|
||||
76
UITests/Sources/AuthenticationCoordinatorUITests.swift
Normal file
76
UITests/Sources/AuthenticationCoordinatorUITests.swift
Normal file
@@ -0,0 +1,76 @@
|
||||
//
|
||||
// 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() {
|
||||
// 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()
|
||||
|
||||
// Then the login form should be updated for OIDC.
|
||||
XCTAssertTrue(app.buttons["oidcButton"].waitForExistence(timeout: 1), "The OIDC button should be shown after selecting a homeserver with OIDC.")
|
||||
}
|
||||
}
|
||||
@@ -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,35 +38,49 @@ 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"
|
||||
validateOIDCButtonIsShown(for: state)
|
||||
validateServerDescriptionIsHidden(for: state)
|
||||
validateLoginFormIsHidden(for: state)
|
||||
validateOIDCButtonIsShown(for: state)
|
||||
validateUnsupportedServerTextIsHidden(for: state)
|
||||
}
|
||||
|
||||
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"
|
||||
validateUnsupportedServerTextIsShown(for: state)
|
||||
validateServerDescriptionIsHidden(for: state)
|
||||
validateLoginFormIsHidden(for: state)
|
||||
validateOIDCButtonIsHidden(for: state)
|
||||
validateUnsupportedServerTextIsShown(for: state)
|
||||
}
|
||||
|
||||
/// Checks that the server description label is shown.
|
||||
@@ -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.
|
||||
@@ -123,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)
|
||||
}
|
||||
|
||||
@@ -136,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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
100
UITests/Sources/ServerSelectionUITests.swift
Normal file
100
UITests/Sources/ServerSelectionUITests.swift
Normal file
@@ -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.")
|
||||
}
|
||||
}
|
||||
@@ -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.")
|
||||
|
||||
@@ -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.")
|
||||
}
|
||||
}
|
||||
|
||||
1
changelog.d/pr-126.change
Normal file
1
changelog.d/pr-126.change
Normal file
@@ -0,0 +1 @@
|
||||
Add AuthenticationService and missing UI tests on the flow.
|
||||
Reference in New Issue
Block a user