#40: Add basic AuthenticationService and missing UI tests.

* Add MockAuthenticationService and ServerSelectionUITests.
* Add tests covering the Authentication flow.
This commit is contained in:
Doug
2022-07-04 10:00:27 +01:00
committed by GitHub
parent e5376e5b5e
commit 88ce67603e
29 changed files with 641 additions and 203 deletions

View File

@@ -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 */,

View File

@@ -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

View File

@@ -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 ])

View File

@@ -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
}
}

View File

@@ -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)
}
}
}

View File

@@ -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)

View File

@@ -58,6 +58,7 @@ struct LoginServerInfoSection: View {
.padding(.vertical, 2)
}
.buttonStyle(.elementGhost())
.accessibilityIdentifier("editServerButton")
}
}
}

View File

@@ -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))
}
}
}

View File

@@ -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.

View File

@@ -26,7 +26,7 @@ class ServerSelectionViewModel: ServerSelectionViewModelType, ServerSelectionVie
// MARK: Public
var callback: (@MainActor (ServerSelectionViewModelResult) -> Void)?
var callback: (@MainActor (ServerSelectionViewModelAction) -> Void)?
// MARK: - Setup

View File

@@ -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.

View File

@@ -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)
}
}
}

View File

@@ -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>
}

View File

@@ -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)
}
}

View 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
}
}

View File

@@ -19,6 +19,7 @@ enum ClientProxyError: Error {
case failedRetrievingAvatarURL
case failedRetrievingDisplayName
case failedRetrievingSessionVerificationController
case failedLoadingMedia
}
protocol ClientProxyProtocol {

View 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)
}
}

View 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
}

View File

@@ -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()

View File

@@ -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

View File

@@ -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
}
}

View 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.")
}
}

View File

@@ -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)
}

View File

@@ -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)

View 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.")
}
}

View File

@@ -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.")

View File

@@ -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.")
}
}

View File

@@ -0,0 +1 @@
Add AuthenticationService and missing UI tests on the flow.