From 8f39f6b0589316b3b02fcf85412c7d3b3af15a64 Mon Sep 17 00:00:00 2001 From: Alessandro Boron Date: Mon, 2 Dec 2024 11:11:07 +0100 Subject: [PATCH] Malicious site protection - extract special error page navigation logic (#3624) Task/Issue URL: https://app.asana.com/0/1206329551987282/1208836682251611/f Tech Design URL: https://app.asana.com/0/1206329551987282/1207273224076495/f **Description**: This PR extracts the navigation logic for special error pages in its own class. This PR focuses on creating the the entites used to encapsulate the logic for SSL and Malicious site protection --- DuckDuckGo.xcodeproj/project.pbxproj | 128 ++++++++- .../MaliciousSiteProtectionManager.swift | 56 ++++ .../Model/SpecialErrorModel.swift | 31 +++ .../SpecialErrorPageActionHandler.swift | 36 +++ .../SpecialErrorPageContextHandling.swift | 40 +++ .../SpecialErrorPageNavigationDelegate.swift | 26 ++ .../WebViewNavigationHandling.swift | 69 +++++ ...rPageNavigationHandler+MaliciousSite.swift | 83 ++++++ ...pecialErrorPageNavigationHandler+SSL.swift | 101 +++++++ .../SpecialErrorPageNavigationHandler.swift | 165 ++++++++++++ DuckDuckGo/TabViewController.swift | 92 ++----- DuckDuckGoTests/MockTabDelegate.swift | 1 - .../SSLErrorPageNavigationHandlerTests.swift | 198 ++++++++++++++ ...ageNavigationHandlerIntegrationTests.swift | 253 ++++++++++++++++++ ...ecialErrorPageNavigationHandlerTests.swift | 181 +++++++++++++ .../SpecialErrorPageUserScriptTests.swift | 19 +- ...ciousSiteProtectionNavigationHandler.swift | 34 +++ .../TestDoubles/DummyWKNavigation.swift | 23 ++ .../MockSSLErrorPageNavigationHandler.swift | 65 +++++ ...ckSpecialErrorPageNavigationDelegate.swift | 29 ++ .../TestDoubles/MockSpecialErrorWebView.swift | 63 +++++ 21 files changed, 1616 insertions(+), 77 deletions(-) create mode 100644 DuckDuckGo/MaliciousSiteProtection/MaliciousSiteProtectionManager.swift create mode 100644 DuckDuckGo/SpecialErrorPage/Model/SpecialErrorModel.swift create mode 100644 DuckDuckGo/SpecialErrorPage/SpecialErrorPageInterfaces/SpecialErrorPageActionHandler.swift create mode 100644 DuckDuckGo/SpecialErrorPage/SpecialErrorPageInterfaces/SpecialErrorPageContextHandling.swift create mode 100644 DuckDuckGo/SpecialErrorPage/SpecialErrorPageInterfaces/SpecialErrorPageNavigationDelegate.swift create mode 100644 DuckDuckGo/SpecialErrorPage/SpecialErrorPageInterfaces/WebViewNavigationHandling.swift create mode 100644 DuckDuckGo/SpecialErrorPage/SpecialErrorPageNavigationHandler+MaliciousSite.swift create mode 100644 DuckDuckGo/SpecialErrorPage/SpecialErrorPageNavigationHandler+SSL.swift create mode 100644 DuckDuckGo/SpecialErrorPage/SpecialErrorPageNavigationHandler.swift create mode 100644 DuckDuckGoTests/SpecialErrorPage/SSLErrorPageNavigationHandlerTests.swift create mode 100644 DuckDuckGoTests/SpecialErrorPage/SpecialErrorPageNavigationHandlerIntegrationTests.swift create mode 100644 DuckDuckGoTests/SpecialErrorPage/SpecialErrorPageNavigationHandlerTests.swift rename DuckDuckGoTests/{ => SpecialErrorPage}/SpecialErrorPageUserScriptTests.swift (63%) create mode 100644 DuckDuckGoTests/SpecialErrorPage/TestDoubles/DummyMaliciousSiteProtectionNavigationHandler.swift create mode 100644 DuckDuckGoTests/SpecialErrorPage/TestDoubles/DummyWKNavigation.swift create mode 100644 DuckDuckGoTests/SpecialErrorPage/TestDoubles/MockSSLErrorPageNavigationHandler.swift create mode 100644 DuckDuckGoTests/SpecialErrorPage/TestDoubles/MockSpecialErrorPageNavigationDelegate.swift create mode 100644 DuckDuckGoTests/SpecialErrorPage/TestDoubles/MockSpecialErrorWebView.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 86e91dc461..dfc65d37f4 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -755,6 +755,27 @@ 9F23B8032C2BCD0000950875 /* DaxDialogStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F23B8022C2BCD0000950875 /* DaxDialogStyles.swift */; }; 9F23B8062C2BE22700950875 /* OnboardingIntroViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F23B8052C2BE22700950875 /* OnboardingIntroViewModelTests.swift */; }; 9F23B8092C2BE9B700950875 /* MockURLOpener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F23B8082C2BE9B700950875 /* MockURLOpener.swift */; }; + 9F254AAB2CF47DD50063B308 /* MaliciousSiteProtectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F254AAA2CF47DD50063B308 /* MaliciousSiteProtectionManager.swift */; }; + 9F254AAD2CF480170063B308 /* SpecialErrorPageNavigationHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F254AAC2CF480170063B308 /* SpecialErrorPageNavigationHandler.swift */; }; + 9F254AB12CF48A6B0063B308 /* SpecialErrorPageNavigationHandler+SSL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F254AB02CF48A6B0063B308 /* SpecialErrorPageNavigationHandler+SSL.swift */; }; + 9F254AB32CF4A1F10063B308 /* SpecialErrorPageNavigationHandler+MaliciousSite.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F254AB22CF4A1E10063B308 /* SpecialErrorPageNavigationHandler+MaliciousSite.swift */; }; + 9F254ACB2CF5CDC60063B308 /* SpecialErrorPageNavigationHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F254ACA2CF5CDC60063B308 /* SpecialErrorPageNavigationHandlerTests.swift */; }; + 9F254ACE2CF5D3540063B308 /* SpecialErrorPageNavigationHandlerIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F254ACD2CF5D3540063B308 /* SpecialErrorPageNavigationHandlerIntegrationTests.swift */; }; + 9F254AD22CF5D3A80063B308 /* MockSpecialErrorWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F254AD12CF5D3A20063B308 /* MockSpecialErrorWebView.swift */; }; + 9F254AD32CF5D3A80063B308 /* MockSpecialErrorWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F254AD12CF5D3A20063B308 /* MockSpecialErrorWebView.swift */; }; + 9F254AD52CF5E5B10063B308 /* DummyMaliciousSiteProtectionNavigationHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F254AD42CF5E5B10063B308 /* DummyMaliciousSiteProtectionNavigationHandler.swift */; }; + 9F254AD62CF5E5B10063B308 /* DummyMaliciousSiteProtectionNavigationHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F254AD42CF5E5B10063B308 /* DummyMaliciousSiteProtectionNavigationHandler.swift */; }; + 9F254AD82CF605310063B308 /* MockSSLErrorPageNavigationHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F254AD72CF605310063B308 /* MockSSLErrorPageNavigationHandler.swift */; }; + 9F254AD92CF605310063B308 /* MockSSLErrorPageNavigationHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F254AD72CF605310063B308 /* MockSSLErrorPageNavigationHandler.swift */; }; + 9F254ADB2CF6120E0063B308 /* MockSpecialErrorPageNavigationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F254ADA2CF6120E0063B308 /* MockSpecialErrorPageNavigationDelegate.swift */; }; + 9F254ADC2CF6120E0063B308 /* MockSpecialErrorPageNavigationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F254ADA2CF6120E0063B308 /* MockSpecialErrorPageNavigationDelegate.swift */; }; + 9F254ADE2CF636CF0063B308 /* DummyWKNavigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F254ADD2CF636CF0063B308 /* DummyWKNavigation.swift */; }; + 9F254ADF2CF636CF0063B308 /* DummyWKNavigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F254ADD2CF636CF0063B308 /* DummyWKNavigation.swift */; }; + 9F254AFF2CF9FA1B0063B308 /* WebViewNavigationHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F254AFE2CF9FA1B0063B308 /* WebViewNavigationHandling.swift */; }; + 9F254B012CF9FA8D0063B308 /* SpecialErrorPageActionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F254B002CF9FA8D0063B308 /* SpecialErrorPageActionHandler.swift */; }; + 9F254B032CF9FB2E0063B308 /* SpecialErrorPageNavigationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F254B022CF9FB2E0063B308 /* SpecialErrorPageNavigationDelegate.swift */; }; + 9F254B052CF9FB890063B308 /* SpecialErrorPageContextHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F254B042CF9FB890063B308 /* SpecialErrorPageContextHandling.swift */; }; + 9F254B082CF9FC270063B308 /* SpecialErrorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F254B072CF9FC270063B308 /* SpecialErrorModel.swift */; }; 9F46BEF82CD8D7490092E0EF /* OnboardingView+AddToDockContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F46BEF72CD8D7490092E0EF /* OnboardingView+AddToDockContent.swift */; }; 9F4CC5152C47AD08006A96EB /* ContextualOnboardingPresenterMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F4CC5142C47AD08006A96EB /* ContextualOnboardingPresenterMock.swift */; }; 9F4CC5172C48B8D4006A96EB /* TabViewControllerDaxDialogTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F4CC5162C48B8D4006A96EB /* TabViewControllerDaxDialogTests.swift */; }; @@ -989,6 +1010,7 @@ CBAD0F082CFE27E2006267B8 /* AppStateTransitions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAD0F072CFE27D5006267B8 /* AppStateTransitions.swift */; }; CBC83E3429B631780008E19C /* Configuration in Frameworks */ = {isa = PBXBuildFile; productRef = CBC83E3329B631780008E19C /* Configuration */; }; CBC88EE12C7F834300F0F8C5 /* SpecialErrorPageUserScriptTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBC88EE02C7F834300F0F8C5 /* SpecialErrorPageUserScriptTests.swift */; }; + CBC88EE32C7F8B1700F0F8C5 /* SSLErrorPageNavigationHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBC88EE22C7F8B1700F0F8C5 /* SSLErrorPageNavigationHandlerTests.swift */; }; CBC88EE52C8097B500F0F8C5 /* URLCredentialCreator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBC88EE42C8097B500F0F8C5 /* URLCredentialCreator.swift */; }; CBCCF96828885DEE006F4A71 /* AppPrivacyConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02C4BC3127C3F9B600C40026 /* AppPrivacyConfigurationTests.swift */; }; CBD4F13C279EBF4A00B20FD7 /* HomeMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBD4F13B279EBF4A00B20FD7 /* HomeMessage.swift */; }; @@ -2597,6 +2619,22 @@ 9F23B8022C2BCD0000950875 /* DaxDialogStyles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DaxDialogStyles.swift; sourceTree = ""; }; 9F23B8052C2BE22700950875 /* OnboardingIntroViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingIntroViewModelTests.swift; sourceTree = ""; }; 9F23B8082C2BE9B700950875 /* MockURLOpener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockURLOpener.swift; sourceTree = ""; }; + 9F254AAA2CF47DD50063B308 /* MaliciousSiteProtectionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaliciousSiteProtectionManager.swift; sourceTree = ""; }; + 9F254AAC2CF480170063B308 /* SpecialErrorPageNavigationHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpecialErrorPageNavigationHandler.swift; sourceTree = ""; }; + 9F254AB02CF48A6B0063B308 /* SpecialErrorPageNavigationHandler+SSL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SpecialErrorPageNavigationHandler+SSL.swift"; sourceTree = ""; }; + 9F254AB22CF4A1E10063B308 /* SpecialErrorPageNavigationHandler+MaliciousSite.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SpecialErrorPageNavigationHandler+MaliciousSite.swift"; sourceTree = ""; }; + 9F254ACA2CF5CDC60063B308 /* SpecialErrorPageNavigationHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpecialErrorPageNavigationHandlerTests.swift; sourceTree = ""; }; + 9F254ACD2CF5D3540063B308 /* SpecialErrorPageNavigationHandlerIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpecialErrorPageNavigationHandlerIntegrationTests.swift; sourceTree = ""; }; + 9F254AD12CF5D3A20063B308 /* MockSpecialErrorWebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSpecialErrorWebView.swift; sourceTree = ""; }; + 9F254AD42CF5E5B10063B308 /* DummyMaliciousSiteProtectionNavigationHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DummyMaliciousSiteProtectionNavigationHandler.swift; sourceTree = ""; }; + 9F254AD72CF605310063B308 /* MockSSLErrorPageNavigationHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSSLErrorPageNavigationHandler.swift; sourceTree = ""; }; + 9F254ADA2CF6120E0063B308 /* MockSpecialErrorPageNavigationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSpecialErrorPageNavigationDelegate.swift; sourceTree = ""; }; + 9F254ADD2CF636CF0063B308 /* DummyWKNavigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DummyWKNavigation.swift; sourceTree = ""; }; + 9F254AFE2CF9FA1B0063B308 /* WebViewNavigationHandling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewNavigationHandling.swift; sourceTree = ""; }; + 9F254B002CF9FA8D0063B308 /* SpecialErrorPageActionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpecialErrorPageActionHandler.swift; sourceTree = ""; }; + 9F254B022CF9FB2E0063B308 /* SpecialErrorPageNavigationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpecialErrorPageNavigationDelegate.swift; sourceTree = ""; }; + 9F254B042CF9FB890063B308 /* SpecialErrorPageContextHandling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpecialErrorPageContextHandling.swift; sourceTree = ""; }; + 9F254B072CF9FC270063B308 /* SpecialErrorModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpecialErrorModel.swift; sourceTree = ""; }; 9F46BEF72CD8D7490092E0EF /* OnboardingView+AddToDockContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OnboardingView+AddToDockContent.swift"; sourceTree = ""; }; 9F4CC5142C47AD08006A96EB /* ContextualOnboardingPresenterMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextualOnboardingPresenterMock.swift; sourceTree = ""; }; 9F4CC5162C48B8D4006A96EB /* TabViewControllerDaxDialogTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabViewControllerDaxDialogTests.swift; sourceTree = ""; }; @@ -2837,6 +2875,7 @@ CBB6B2542AF6D543006B777C /* lt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = lt; path = lt.lproj/InfoPlist.strings; sourceTree = ""; }; CBC7AB542AF6D583008CB798 /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ro; path = ro.lproj/InfoPlist.strings; sourceTree = ""; }; CBC88EE02C7F834300F0F8C5 /* SpecialErrorPageUserScriptTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpecialErrorPageUserScriptTests.swift; sourceTree = ""; }; + CBC88EE22C7F8B1700F0F8C5 /* SSLErrorPageNavigationHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSLErrorPageNavigationHandlerTests.swift; sourceTree = ""; }; CBC88EE42C8097B500F0F8C5 /* URLCredentialCreator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLCredentialCreator.swift; sourceTree = ""; }; CBC8DC252AF6D4CD00BA681A /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/InfoPlist.strings; sourceTree = ""; }; CBD4F13B279EBF4A00B20FD7 /* HomeMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeMessage.swift; sourceTree = ""; }; @@ -4383,6 +4422,7 @@ 98B001B1251EABB40090EC07 /* InfoPlist.strings */, 85DFEDEB24C7CC7600973FE7 /* iPad */, F1C5ECFA1E37B15B00C599A4 /* Main */, + 9F254AA92CF47CD30063B308 /* MaliciousSiteProtection */, EECD94B22A28B8580085C66E /* NetworkProtection */, F13B4BF31F18C73A00814661 /* NewTabPage */, 85AE668C20971FCA0014CF04 /* Notifications */, @@ -4394,6 +4434,7 @@ C1B7B51D28941F160098FD6A /* RemoteMessaging */, F1AB2B401E3F75A000868554 /* Settings */, 0A6CC0EE23904D5400E4F627 /* Settings.bundle */, + 9F254AA62CF4777F0063B308 /* SpecialErrorPage */, D664C7922B289AA000CBFA76 /* Subscription */, 85F98F8C296F0ED100742F4A /* Sync */, F13B4BF41F18C74500814661 /* Tabs */, @@ -4968,6 +5009,69 @@ name = Onboarding; sourceTree = ""; }; + 9F254AA62CF4777F0063B308 /* SpecialErrorPage */ = { + isa = PBXGroup; + children = ( + 9F254B062CF9FC130063B308 /* Model */, + 9F254AFD2CF9F9EC0063B308 /* SpecialErrorPageInterfaces */, + 9F254AAC2CF480170063B308 /* SpecialErrorPageNavigationHandler.swift */, + 9F254AB02CF48A6B0063B308 /* SpecialErrorPageNavigationHandler+SSL.swift */, + 9F254AB22CF4A1E10063B308 /* SpecialErrorPageNavigationHandler+MaliciousSite.swift */, + ); + path = SpecialErrorPage; + sourceTree = ""; + }; + 9F254AA92CF47CD30063B308 /* MaliciousSiteProtection */ = { + isa = PBXGroup; + children = ( + 9F254AAA2CF47DD50063B308 /* MaliciousSiteProtectionManager.swift */, + ); + path = MaliciousSiteProtection; + sourceTree = ""; + }; + 9F254AC92CF5CA860063B308 /* SpecialErrorPage */ = { + isa = PBXGroup; + children = ( + 9F254AD02CF5D38E0063B308 /* TestDoubles */, + CBC88EE02C7F834300F0F8C5 /* SpecialErrorPageUserScriptTests.swift */, + CBC88EE22C7F8B1700F0F8C5 /* SSLErrorPageNavigationHandlerTests.swift */, + 9F254ACA2CF5CDC60063B308 /* SpecialErrorPageNavigationHandlerTests.swift */, + 9F254ACD2CF5D3540063B308 /* SpecialErrorPageNavigationHandlerIntegrationTests.swift */, + ); + path = SpecialErrorPage; + sourceTree = ""; + }; + 9F254AD02CF5D38E0063B308 /* TestDoubles */ = { + isa = PBXGroup; + children = ( + 9F254AD12CF5D3A20063B308 /* MockSpecialErrorWebView.swift */, + 9F254AD42CF5E5B10063B308 /* DummyMaliciousSiteProtectionNavigationHandler.swift */, + 9F254AD72CF605310063B308 /* MockSSLErrorPageNavigationHandler.swift */, + 9F254ADA2CF6120E0063B308 /* MockSpecialErrorPageNavigationDelegate.swift */, + 9F254ADD2CF636CF0063B308 /* DummyWKNavigation.swift */, + ); + path = TestDoubles; + sourceTree = ""; + }; + 9F254AFD2CF9F9EC0063B308 /* SpecialErrorPageInterfaces */ = { + isa = PBXGroup; + children = ( + 9F254AFE2CF9FA1B0063B308 /* WebViewNavigationHandling.swift */, + 9F254B002CF9FA8D0063B308 /* SpecialErrorPageActionHandler.swift */, + 9F254B022CF9FB2E0063B308 /* SpecialErrorPageNavigationDelegate.swift */, + 9F254B042CF9FB890063B308 /* SpecialErrorPageContextHandling.swift */, + ); + path = SpecialErrorPageInterfaces; + sourceTree = ""; + }; + 9F254B062CF9FC130063B308 /* Model */ = { + isa = PBXGroup; + children = ( + 9F254B072CF9FC270063B308 /* SpecialErrorModel.swift */, + ); + path = Model; + sourceTree = ""; + }; 9F4CC5252C4E22F9006A96EB /* ContextualOnboarding */ = { isa = PBXGroup; children = ( @@ -5979,6 +6083,7 @@ F1D477C71F2139210031ED49 /* OmniBar */, 9F23B8042C2BE20500950875 /* Onboarding */, 98EA2C3F218BB5140023E1DC /* Settings */, + 9F254AC92CF5CA860063B308 /* SpecialErrorPage */, F1BDDBFC2C340D9C00459306 /* Subscription */, 569437222BDD402600C0881B /* Sync */, F13B4BF71F18C9E800814661 /* Tabs */, @@ -6092,7 +6197,6 @@ F13B4BF81F18CA0600814661 /* TabsModelTests.swift */, F189AED61F18F6DE001EBAE1 /* TabTests.swift */, D625AAEA2BBEEFC900BC189A /* TabURLInterceptorTests.swift */, - CBC88EE02C7F834300F0F8C5 /* SpecialErrorPageUserScriptTests.swift */, ); name = Tabs; sourceTree = ""; @@ -7597,6 +7701,7 @@ files = ( EE4FB1862A28CE7200E5CBA7 /* NetworkProtectionStatusView.swift in Sources */, C17B59592A03AAD30055F2D1 /* PasswordGenerationPromptViewModel.swift in Sources */, + 9F254B082CF9FC270063B308 /* SpecialErrorModel.swift in Sources */, BDE91CDE2C62B90F0005CB74 /* UnifiedFeedbackRootView.swift in Sources */, D65625A12C232F5E006EF297 /* SettingsDuckPlayerView.swift in Sources */, D6FEB8B52B74994000C3615F /* HeadlessWebViewCoordinator.swift in Sources */, @@ -7683,10 +7788,13 @@ B652DF12287C336E00C12A9C /* ContentBlockingUpdating.swift in Sources */, C1E42C7B2C5CD8AE00509204 /* AutofillCredentialsDebugViewController.swift in Sources */, 314C92BA27C3E7CB0042EC96 /* QuickLookContainerViewController.swift in Sources */, + 9F254B052CF9FB890063B308 /* SpecialErrorPageContextHandling.swift in Sources */, 855D914D2063EF6A00C4B448 /* TabSwitcherTransition.swift in Sources */, + 9F254AB32CF4A1F10063B308 /* SpecialErrorPageNavigationHandler+MaliciousSite.swift in Sources */, CB258D1229A4F24900DEBA24 /* ConfigurationManager.swift in Sources */, 8546A54A2A672959003929BF /* MainViewController+Email.swift in Sources */, F4F6DFB226E6AEC100ED7E12 /* AddOrEditBookmarkViewController.swift in Sources */, + 9F254B012CF9FA8D0063B308 /* SpecialErrorPageActionHandler.swift in Sources */, EE458D0D2AB1DA4600FC651A /* EventMapping+NetworkProtectionError.swift in Sources */, F44D279F27F331BB0037F371 /* AutofillLoginPromptViewController.swift in Sources */, C1BF0BA529B63D7200482B73 /* AutofillLoginPromptHelper.swift in Sources */, @@ -7733,6 +7841,7 @@ 6FB2A67A2C2C5BAE004D20C8 /* FavoritePlaceholderItemView.swift in Sources */, 6FBF0F8B2BD7C0A900136CF0 /* AllProtectedCell.swift in Sources */, 9F4CC5242C4A4F0D006A96EB /* SwiftUITestUtilities.swift in Sources */, + 9F254AFF2CF9FA1B0063B308 /* WebViewNavigationHandling.swift in Sources */, 6FDC64032C92F4D600DB71B3 /* NewTabPageSettingsPersistentStore.swift in Sources */, 1E4F4A5A297193DE00625985 /* MainViewController+CookiesManaged.swift in Sources */, C12324C32C4697C900FBB26B /* AutofillBreakageReportTableViewCell.swift in Sources */, @@ -7848,6 +7957,7 @@ 316AA45A2CF8E31F00A2ED28 /* AIChatSettings.swift in Sources */, F17922E01E71BB59006E3D97 /* AutocompleteViewControllerDelegate.swift in Sources */, BDE91CDC2C62AA3A0005CB74 /* DefaultMetadataCollector.swift in Sources */, + 9F254AAD2CF480170063B308 /* SpecialErrorPageNavigationHandler.swift in Sources */, D664C7C82B289AA200CBFA76 /* SubscriptionFlowView.swift in Sources */, EE458D142ABB652900FC651A /* NetworkProtectionDebugUtilities.swift in Sources */, 9F9A922E2C86A56B001D036D /* OnboardingManager.swift in Sources */, @@ -7918,6 +8028,7 @@ 850365F323DE087800D0F787 /* UIImageViewExtension.swift in Sources */, 56D060262C359D2E003BAEB5 /* ContextualOnboardingDialogs.swift in Sources */, 9F8E0F382CCFAA8A001EA7C5 /* AddToDockPromoView.swift in Sources */, + 9F254B032CF9FB2E0063B308 /* SpecialErrorPageNavigationDelegate.swift in Sources */, 373608922ABB430D00629E7F /* FavoritesDisplayMode+UserDefaults.swift in Sources */, C160544129D6044D00B715A1 /* AutofillInterfaceUsernameTruncator.swift in Sources */, 31C70B5528045E3500FB6AD1 /* SecureVaultReporter.swift in Sources */, @@ -8059,6 +8170,7 @@ 1EEF387D285B1A1100383393 /* TrackerImageCache.swift in Sources */, 3151F0EC27357FEE00226F58 /* VoiceSearchFeedbackViewModel.swift in Sources */, 1E53508F2C7C9A1F00818DAA /* DefaultSubscriptionManager+AccountManagerKeychainAccessDelegate.swift in Sources */, + 9F254AB12CF48A6B0063B308 /* SpecialErrorPageNavigationHandler+SSL.swift in Sources */, 1DDF402D2BA09482006850D9 /* SettingsMainSettingsView.swift in Sources */, 85010502292FB1000033978F /* FireproofFaviconUpdater.swift in Sources */, F1C4A70E1E57725800A6CA1B /* OmniBar.swift in Sources */, @@ -8100,6 +8212,7 @@ 6FE127402C204D9B00EB5724 /* ShortcutsView.swift in Sources */, 85F98F92296F32BD00742F4A /* SyncSettingsViewController.swift in Sources */, 84E341961E2F7EFB00BDBA6F /* AppDelegate.swift in Sources */, + 9F254AAB2CF47DD50063B308 /* MaliciousSiteProtectionManager.swift in Sources */, 310D091D2799F57200DC0060 /* Download.swift in Sources */, BDE91CDA2C62A70B0005CB74 /* UnifiedMetadataCollector.swift in Sources */, C13F3F6C2B7F88470083BE40 /* AuthConfirmationPromptViewModel.swift in Sources */, @@ -8219,9 +8332,11 @@ 8528AE84212FF9A100D0BD74 /* AppRatingPromptStorageTests.swift in Sources */, 569437312BE3F64400C0881B /* SyncErrorHandlerSyncPausedAlertsTests.swift in Sources */, 9F16230B2CA0F0190093C4FC /* DebouncerTests.swift in Sources */, + 9F254ADC2CF6120E0063B308 /* MockSpecialErrorPageNavigationDelegate.swift in Sources */, 1CB7B82323CEA28300AA24EA /* DateExtensionTests.swift in Sources */, 31C138A427A3352600FFD4B2 /* DownloadTests.swift in Sources */, 853A717820F645FB00FE60BC /* PixelTests.swift in Sources */, + 9F254AD92CF605310063B308 /* MockSSLErrorPageNavigationHandler.swift in Sources */, 6FD0C4212C5BF774000561C9 /* NewTabPageViewModelTests.swift in Sources */, 984D036124AF49B80066CFB8 /* TabPreviewsSourceTests.swift in Sources */, 85D2187024BF24DB004373D2 /* FaviconRequestModifierTests.swift in Sources */, @@ -8262,7 +8377,9 @@ CBDD5DE129A6741300832877 /* MockBundle.swift in Sources */, C158AC7B297AB5DC0008723A /* MockSecureVault.swift in Sources */, 569437342BE4E41500C0881B /* SyncErrorHandlerSyncErrorsAlertsTests.swift in Sources */, + CBC88EE32C7F8B1700F0F8C5 /* SSLErrorPageNavigationHandlerTests.swift in Sources */, 85C11E4120904BBE00BFFEB4 /* VariantManagerTests.swift in Sources */, + 9F254AD52CF5E5B10063B308 /* DummyMaliciousSiteProtectionNavigationHandler.swift in Sources */, F1134ECE1F40EA9C00B73467 /* AtbParserTests.swift in Sources */, F189AEE41F18FDAF001EBAE1 /* LinkTests.swift in Sources */, 6F7FB8E12C660B3E00867DA7 /* NewTabPageFavoritesModelTests.swift in Sources */, @@ -8291,6 +8408,7 @@ 310E79BD2949CAA5007C49E8 /* FireButtonReferenceTests.swift in Sources */, 4B62C4BA25B930DD008912C6 /* AppConfigurationFetchTests.swift in Sources */, 31C7D71C27515A6300A95D0A /* MockVoiceSearchHelper.swift in Sources */, + 9F254ACE2CF5D3540063B308 /* SpecialErrorPageNavigationHandlerIntegrationTests.swift in Sources */, 8519526C2CE256BC00578553 /* AutocompleteSuggestionsDataSourceTests.swift in Sources */, 6F395BBB2CD2C87D00B92FC3 /* BoolFileMarkerTests.swift in Sources */, 8598F67B2405EB8D00FBC70C /* KeyboardSettingsTests.swift in Sources */, @@ -8298,6 +8416,7 @@ 85D2187224BF24F2004373D2 /* NotFoundCachingDownloaderTests.swift in Sources */, C1935A242C89CC6D001AD72D /* AutofillHeaderViewFactoryTests.swift in Sources */, C111B26927F579EF006558B1 /* BookmarkOrFolderTests.swift in Sources */, + 9F254ADF2CF636CF0063B308 /* DummyWKNavigation.swift in Sources */, 6F7BACD42CEE084B00F561D8 /* OmniBarEqualityCheckTests.swift in Sources */, 6F7FB8E72C66197E00867DA7 /* NewTabPageSectionsSettingsModelTests.swift in Sources */, 851CD674244D7E6000331B98 /* UserDefaultsExtension.swift in Sources */, @@ -8378,6 +8497,7 @@ 85D2187424BF25CD004373D2 /* FaviconsTests.swift in Sources */, 9F6933212C5B9A5B00CD6A5D /* OnboardingHostingControllerMock.swift in Sources */, 9F6933192C59BB0300CD6A5D /* OnboardingPixelReporterMock.swift in Sources */, + 9F254ACB2CF5CDC60063B308 /* SpecialErrorPageNavigationHandlerTests.swift in Sources */, CBC88EE12C7F834300F0F8C5 /* SpecialErrorPageUserScriptTests.swift in Sources */, 56D0602D2C383FD2003BAEB5 /* OnboardingSuggestedSearchesProviderTests.swift in Sources */, 31E77B272D038BBC006F1C9F /* OmnibarAccessoryHandlerTests.swift in Sources */, @@ -8402,6 +8522,7 @@ F1134ED61F40F29F00B73467 /* StatisticsUserDefaultsTests.swift in Sources */, 98629D342C21BE37001E6031 /* BookmarksStateValidationTests.swift in Sources */, C1FFBD462C761BE20073622B /* SyncPromoManagerTests.swift in Sources */, + 9F254AD32CF5D3A80063B308 /* MockSpecialErrorWebView.swift in Sources */, F1BDDBFF2C340D9C00459306 /* SubscriptionPagesUseSubscriptionFeatureTests.swift in Sources */, 98EA2C3C218B9AAD0023E1DC /* ThemeManagerTests.swift in Sources */, 569437292BDD487600C0881B /* SyncCredentialsAdapterTests.swift in Sources */, @@ -8472,8 +8593,13 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 9F254AD62CF5E5B10063B308 /* DummyMaliciousSiteProtectionNavigationHandler.swift in Sources */, 85F21DB0210F5E32002631A6 /* AtbIntegrationTests.swift in Sources */, + 9F254AD22CF5D3A80063B308 /* MockSpecialErrorWebView.swift in Sources */, + 9F254ADE2CF636CF0063B308 /* DummyWKNavigation.swift in Sources */, 8551912724746EDC0010FDD0 /* SnapshotHelper.swift in Sources */, + 9F254ADB2CF6120E0063B308 /* MockSpecialErrorPageNavigationDelegate.swift in Sources */, + 9F254AD82CF605310063B308 /* MockSSLErrorPageNavigationHandler.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/DuckDuckGo/MaliciousSiteProtection/MaliciousSiteProtectionManager.swift b/DuckDuckGo/MaliciousSiteProtection/MaliciousSiteProtectionManager.swift new file mode 100644 index 0000000000..5175a16879 --- /dev/null +++ b/DuckDuckGo/MaliciousSiteProtection/MaliciousSiteProtectionManager.swift @@ -0,0 +1,56 @@ +// +// MaliciousSiteProtectionManager.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// 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 Foundation + +final class MaliciousSiteProtectionManager: MaliciousSiteDetecting { + + func evaluate(_ url: URL) async -> ThreatKind? { + try? await Task.sleep(interval: 0.3) + return .none + } + +} + +// MARK: - To Remove + +// These entities are copied from BSK and they will be used to mock the library +import SpecialErrorPages + +protocol MaliciousSiteDetecting { + func evaluate(_ url: URL) async -> ThreatKind? +} + +public enum ThreatKind: String, CaseIterable, CustomStringConvertible { + public var description: String { rawValue } + + case phishing + case malware +} + +public extension ThreatKind { + + var errorPageType: SpecialErrorKind { + switch self { + case .malware: .phishing // WIP in BSK + case .phishing: .phishing + } + } + +} diff --git a/DuckDuckGo/SpecialErrorPage/Model/SpecialErrorModel.swift b/DuckDuckGo/SpecialErrorPage/Model/SpecialErrorModel.swift new file mode 100644 index 0000000000..cd4b3b3411 --- /dev/null +++ b/DuckDuckGo/SpecialErrorPage/Model/SpecialErrorModel.swift @@ -0,0 +1,31 @@ +// +// SpecialErrorModel.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// 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 Foundation +import SpecialErrorPages + +struct SpecialErrorModel: Equatable { + let url: URL + let errorData: SpecialErrorData +} + +struct SSLSpecialError { + let type: SSLErrorType + let error: SpecialErrorModel +} diff --git a/DuckDuckGo/SpecialErrorPage/SpecialErrorPageInterfaces/SpecialErrorPageActionHandler.swift b/DuckDuckGo/SpecialErrorPage/SpecialErrorPageInterfaces/SpecialErrorPageActionHandler.swift new file mode 100644 index 0000000000..2d12540cde --- /dev/null +++ b/DuckDuckGo/SpecialErrorPage/SpecialErrorPageInterfaces/SpecialErrorPageActionHandler.swift @@ -0,0 +1,36 @@ +// +// SpecialErrorPageActionHandler.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// 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 Foundation + +/// A type that defines actions for handling special error pages. +/// +/// This protocol is intended to be adopted by types that need to manage user interactions +/// with special error pages, such as navigating to a site, leaving a site, or presenting +/// advanced information related to the error. +protocol SpecialErrorPageActionHandler { + /// Handles the action of navigating to the site associated with the error page + func visitSite() + + /// Handles the action of leaving the site associated with the error page + func leaveSite() + + /// Handles the action of requesting more detailed information about the error + func advancedInfoPresented() +} diff --git a/DuckDuckGo/SpecialErrorPage/SpecialErrorPageInterfaces/SpecialErrorPageContextHandling.swift b/DuckDuckGo/SpecialErrorPage/SpecialErrorPageInterfaces/SpecialErrorPageContextHandling.swift new file mode 100644 index 0000000000..0c3ce79206 --- /dev/null +++ b/DuckDuckGo/SpecialErrorPage/SpecialErrorPageInterfaces/SpecialErrorPageContextHandling.swift @@ -0,0 +1,40 @@ +// +// SpecialErrorPageContextHandling.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// 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 Foundation +import WebKit +import SpecialErrorPages + +/// A type that defines the base functionality for handling navigation related to special error pages. +protocol SpecialErrorPageContextHandling: AnyObject { + /// The delegate that handles navigation actions for special error pages. + var delegate: SpecialErrorPageNavigationDelegate? { get set } + + /// A Boolean value indicating whether the special error page is currently visible. + var isSpecialErrorPageVisible: Bool { get } + + /// The URL that failed to load, if any. + var failedURL: URL? { get } + + /// Attaches a web view to the special error page handling. + func attachWebView(_ webView: WKWebView) + + /// Sets the user script for the special error page. + func setUserScript(_ userScript: SpecialErrorPageUserScript?) +} diff --git a/DuckDuckGo/SpecialErrorPage/SpecialErrorPageInterfaces/SpecialErrorPageNavigationDelegate.swift b/DuckDuckGo/SpecialErrorPage/SpecialErrorPageInterfaces/SpecialErrorPageNavigationDelegate.swift new file mode 100644 index 0000000000..509a16c7a2 --- /dev/null +++ b/DuckDuckGo/SpecialErrorPage/SpecialErrorPageInterfaces/SpecialErrorPageNavigationDelegate.swift @@ -0,0 +1,26 @@ +// +// SpecialErrorPageNavigationDelegate.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// 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 Foundation + +/// A delegate for handling navigation actions related to special error pages. +protocol SpecialErrorPageNavigationDelegate: AnyObject { + /// Asks the delegate to close the special error page tab when the web view can't navigate back. + func closeSpecialErrorPageTab() +} diff --git a/DuckDuckGo/SpecialErrorPage/SpecialErrorPageInterfaces/WebViewNavigationHandling.swift b/DuckDuckGo/SpecialErrorPage/SpecialErrorPageInterfaces/WebViewNavigationHandling.swift new file mode 100644 index 0000000000..31cb18a9bf --- /dev/null +++ b/DuckDuckGo/SpecialErrorPage/SpecialErrorPageInterfaces/WebViewNavigationHandling.swift @@ -0,0 +1,69 @@ +// +// WebViewNavigationHandling.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// 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 Foundation +import WebKit + +// MARK: - WebViewNavigation + +/// For testing purposes. +protocol WebViewNavigation {} + +// Used in tests. WKNavigation() crashes on deinit when initialising it manually. +// As workaround we used to Swizzle the implementation of deinit in tests. +// The problem with that approach is that when running different test suites it is possible that unrelated tests re-set the original implementation of deinit while other tests are running. +// This cause the app to crash as the original implementation is executed. +// Defining a protocol for WKNavigation and using mocks such as DummyWKNavigation in tests resolves the problem. +extension WKNavigation: WebViewNavigation {} + +// MARK: - WebViewNavigationHandling + +/// A protocol that defines methods for handling navigation events of `WKWebView`. +protocol WebViewNavigationHandling: AnyObject { + /// Decides whether to cancel navigation to prevent opening a site and show a special error page based on the specified action information. + /// + /// - Parameters: + /// - navigationAction: Details about the action that triggered the navigation request. + /// - webView: The web view from which the navigation request began. + /// - Returns: A Boolean value that indicates whether the navigation action was handled. + func handleSpecialErrorNavigation(navigationAction: WKNavigationAction, webView: WKWebView) async -> Bool + + /// Handles authentication challenges received by the web view. + /// + /// - Parameters: + /// - webView: The web view that receives the authentication challenge. + /// - challenge: The authentication challenge. + /// - completionHandler: A completion handler block to execute with the response. + func handleWebView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) + + /// Handles failures during provisional navigation. + /// + /// - Parameters: + /// - webView: The `WKWebView` instance that failed the navigation. + /// - navigation: The navigation object for the operation. + /// - error: The error that occurred. + func handleWebView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WebViewNavigation, withError error: NSError) + + /// Handles the successful completion of a navigation in the web view. + /// + /// - Parameters: + /// - webView: The web view that loaded the content. + /// - navigation: The navigation object that finished. + func handleWebView(_ webView: WKWebView, didFinish navigation: WebViewNavigation) +} diff --git a/DuckDuckGo/SpecialErrorPage/SpecialErrorPageNavigationHandler+MaliciousSite.swift b/DuckDuckGo/SpecialErrorPage/SpecialErrorPageNavigationHandler+MaliciousSite.swift new file mode 100644 index 0000000000..e49ea0e215 --- /dev/null +++ b/DuckDuckGo/SpecialErrorPage/SpecialErrorPageNavigationHandler+MaliciousSite.swift @@ -0,0 +1,83 @@ +// +// SpecialErrorPageNavigationHandler+MaliciousSite.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// 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 Foundation +import BrowserServicesKit +import Core +import SpecialErrorPages +import WebKit + +enum MaliciousSiteProtectionNavigationResult: Equatable { + case navigationHandled(SpecialErrorModel) + case navigationNotHandled +} + +protocol MaliciousSiteProtectionNavigationHandling: AnyObject { + /// Decides whether to cancel navigation to prevent opening the YouTube app from the web view. + /// + /// - Parameters: + /// - navigationAction: The navigation action to evaluate. + /// - webView: The web view where navigation is occurring. + /// - Returns: `true` if the navigation should be canceled, `false` otherwise. + func handleMaliciousSiteProtectionNavigation(for navigationAction: WKNavigationAction, webView: WKWebView) async -> MaliciousSiteProtectionNavigationResult +} + +final class MaliciousSiteProtectionNavigationHandler { + private let maliciousSiteProtectionManager: MaliciousSiteDetecting + private let storageCache: StorageCache + + init( + maliciousSiteProtectionManager: MaliciousSiteDetecting = MaliciousSiteProtectionManager(), + storageCache: StorageCache = AppDependencyProvider.shared.storageCache + ) { + self.maliciousSiteProtectionManager = maliciousSiteProtectionManager + self.storageCache = storageCache + } +} + +// MARK: - MaliciousSiteProtectionNavigationHandling + +extension MaliciousSiteProtectionNavigationHandler: MaliciousSiteProtectionNavigationHandling { + + @MainActor + func handleMaliciousSiteProtectionNavigation(for navigationAction: WKNavigationAction, webView: WKWebView) async -> MaliciousSiteProtectionNavigationResult { + // Implement logic to use `maliciousSiteProtectionManager.evaluate(url)` + // Return navigationNotHandled for the time being + return .navigationNotHandled + } + +} + +// MARK: - SpecialErrorPageActionHandler + +extension MaliciousSiteProtectionNavigationHandler: SpecialErrorPageActionHandler { + + func visitSite() { + // Fire Pixel + } + + func leaveSite() { + // Fire Pixel + } + + func advancedInfoPresented() { + // Fire Pixel + } + +} diff --git a/DuckDuckGo/SpecialErrorPage/SpecialErrorPageNavigationHandler+SSL.swift b/DuckDuckGo/SpecialErrorPage/SpecialErrorPageNavigationHandler+SSL.swift new file mode 100644 index 0000000000..10d5b71f3a --- /dev/null +++ b/DuckDuckGo/SpecialErrorPage/SpecialErrorPageNavigationHandler+SSL.swift @@ -0,0 +1,101 @@ +// +// SpecialErrorPageNavigationHandler+SSL.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// 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 Foundation +import Common +import BrowserServicesKit +import SpecialErrorPages +import Core + +protocol SSLSpecialErrorPageNavigationHandling { + func handleServerTrustChallenge(_ challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) + func makeNewRequestURLAndSpecialErrorDataIfEnabled(error: NSError) -> SSLSpecialError? + func errorPageVisited(errorType: SSLErrorType) +} + +final class SSLErrorPageNavigationHandler { + private var shouldBypassSSLError = false + + private let urlCredentialCreator: URLCredentialCreating + private let storageCache: StorageCache + private let featureFlagger: FeatureFlagger + + init( + urlCredentialCreator: URLCredentialCreating = URLCredentialCreator(), + storageCache: StorageCache = AppDependencyProvider.shared.storageCache, + featureFlagger: FeatureFlagger = AppDependencyProvider.shared.featureFlagger + ) { + self.urlCredentialCreator = urlCredentialCreator + self.storageCache = storageCache + self.featureFlagger = featureFlagger + } +} + +// MARK: - SSLSpecialErrorPageNavigationHandling + +extension SSLErrorPageNavigationHandler: SSLSpecialErrorPageNavigationHandling { + + func handleServerTrustChallenge(_ challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { + guard shouldBypassSSLError, + let credential = urlCredentialCreator.urlCredentialFrom(trust: challenge.protectionSpace.serverTrust) else { + completionHandler(.performDefaultHandling, nil) + return + } + shouldBypassSSLError = false + completionHandler(.useCredential, credential) + } + + func makeNewRequestURLAndSpecialErrorDataIfEnabled(error: NSError) -> SSLSpecialError? { + guard featureFlagger.isFeatureOn(.sslCertificatesBypass), + error.isServerCertificateUntrusted, + let errorType = error.sslErrorType, + let failedURL = error.failedUrl, + let host = failedURL.host + else { + return nil + } + + let errorData = SpecialErrorData.ssl(type: errorType, domain: host, eTldPlus1: storageCache.tld.eTLDplus1(host)) + + return SSLSpecialError(type: errorType, error: SpecialErrorModel(url: failedURL, errorData: errorData)) + } + + func errorPageVisited(errorType: SSLErrorType) { + Pixel.fire(pixel: .certificateWarningDisplayed(errorType.pixelParameter)) + } + +} + +// MARK: - SSLErrorPageNavigationHandler + +extension SSLErrorPageNavigationHandler: SpecialErrorPageActionHandler { + + func leaveSite() { + Pixel.fire(pixel: .certificateWarningLeaveClicked) + } + + func visitSite() { + shouldBypassSSLError = true + } + + func advancedInfoPresented() { + Pixel.fire(pixel: .certificateWarningAdvancedClicked) + } + +} diff --git a/DuckDuckGo/SpecialErrorPage/SpecialErrorPageNavigationHandler.swift b/DuckDuckGo/SpecialErrorPage/SpecialErrorPageNavigationHandler.swift new file mode 100644 index 0000000000..57422c2610 --- /dev/null +++ b/DuckDuckGo/SpecialErrorPage/SpecialErrorPageNavigationHandler.swift @@ -0,0 +1,165 @@ +// +// SpecialErrorPageNavigationHandler.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// 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 Foundation +import WebKit +import SpecialErrorPages +import Core + +typealias SpecialErrorPageManaging = SpecialErrorPageContextHandling & WebViewNavigationHandling & SpecialErrorPageUserScriptDelegate + +final class SpecialErrorPageNavigationHandler: SpecialErrorPageContextHandling { + private var webView: WKWebView? + private(set) var errorData: SpecialErrorData? + private var errorPageType: SpecialErrorKind? + private(set) var isSpecialErrorPageVisible = false + private(set) var failedURL: URL? + private weak var userScript: SpecialErrorPageUserScript? + weak var delegate: SpecialErrorPageNavigationDelegate? + + private let sslErrorPageNavigationHandler: SSLSpecialErrorPageNavigationHandling & SpecialErrorPageActionHandler + private let maliciousSiteProtectionNavigationHandler: MaliciousSiteProtectionNavigationHandling & SpecialErrorPageActionHandler + + init( + sslErrorPageNavigationHandler: SSLSpecialErrorPageNavigationHandling & SpecialErrorPageActionHandler = SSLErrorPageNavigationHandler(), + maliciousSiteProtectionNavigationHandler: MaliciousSiteProtectionNavigationHandling & SpecialErrorPageActionHandler = MaliciousSiteProtectionNavigationHandler() + ) { + self.sslErrorPageNavigationHandler = sslErrorPageNavigationHandler + self.maliciousSiteProtectionNavigationHandler = maliciousSiteProtectionNavigationHandler + } + + func attachWebView(_ webView: WKWebView) { + self.webView = webView + } + + func setUserScript(_ userScript: SpecialErrorPageUserScript?) { + self.userScript = userScript + userScript?.delegate = self + } +} + +// MARK: - WebViewNavigationHandling + +extension SpecialErrorPageNavigationHandler: WebViewNavigationHandling { + + func handleSpecialErrorNavigation(navigationAction: WKNavigationAction, webView: WKWebView) async -> Bool { + let result = await maliciousSiteProtectionNavigationHandler.handleMaliciousSiteProtectionNavigation(for: navigationAction, webView: webView) + + return await MainActor.run { + switch result { + case let .navigationHandled(model): + var request = navigationAction.request + request.url = model.url + failedURL = model.url + errorData = model.errorData + errorPageType = .phishing + loadSpecialErrorPage(request: request) + return true + case .navigationNotHandled: + return false + } + } + } + + func handleWebView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { + guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust else { return } + + sslErrorPageNavigationHandler.handleServerTrustChallenge(challenge, completionHandler: completionHandler) + } + + func handleWebView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WebViewNavigation, withError error: NSError) { + guard let sslSpecialError = sslErrorPageNavigationHandler.makeNewRequestURLAndSpecialErrorDataIfEnabled(error: error) else { return } + failedURL = sslSpecialError.error.url + sslErrorPageNavigationHandler.errorPageVisited(errorType: sslSpecialError.type) + errorData = sslSpecialError.error.errorData + errorPageType = .ssl + loadSpecialErrorPage(url: sslSpecialError.error.url) + } + + func handleWebView(_ webView: WKWebView, didFinish navigation: WebViewNavigation) { + userScript?.isEnabled = webView.url == failedURL + if webView.url != failedURL { + isSpecialErrorPageVisible = false + } + } + +} + +// MARK: - SpecialErrorPageUserScriptDelegate + +extension SpecialErrorPageNavigationHandler: SpecialErrorPageUserScriptDelegate { + + func leaveSiteAction() { + switch errorPageType { + case .ssl: + sslErrorPageNavigationHandler.leaveSite() + case .phishing: + maliciousSiteProtectionNavigationHandler.leaveSite() + default: + break + } + + if webView?.canGoBack == true { + _ = webView?.goBack() + } else { + delegate?.closeSpecialErrorPageTab() + } + } + + func visitSiteAction() { + switch errorPageType { + case .ssl: + sslErrorPageNavigationHandler.visitSite() + case .phishing: + maliciousSiteProtectionNavigationHandler.visitSite() + default: + break + } + + isSpecialErrorPageVisible = false + _ = webView?.reload() + } + + func advancedInfoPresented() { + switch errorPageType { + case .ssl: + sslErrorPageNavigationHandler.advancedInfoPresented() + case .phishing: + maliciousSiteProtectionNavigationHandler.advancedInfoPresented() + default: + break + } + } +} + +// MARK: Private + +private extension SpecialErrorPageNavigationHandler { + + func loadSpecialErrorPage(url: URL) { + loadSpecialErrorPage(request: URLRequest(url: url)) + } + + func loadSpecialErrorPage(request: URLRequest) { + let html = SpecialErrorPageHTMLTemplate.htmlFromTemplate + webView?.loadSimulatedRequest(request, responseHTML: html) + isSpecialErrorPageVisible = true + } + +} diff --git a/DuckDuckGo/TabViewController.swift b/DuckDuckGo/TabViewController.swift index 19bb29eeec..4586314235 100644 --- a/DuckDuckGo/TabViewController.swift +++ b/DuckDuckGo/TabViewController.swift @@ -191,12 +191,7 @@ class TabViewController: UIViewController { private let refreshControl = UIRefreshControl() private let certificateTrustEvaluator: CertificateTrustEvaluating - private let urlCredentialCreator: URLCredentialCreating - private var shouldBypassSSLError = false - var errorData: SpecialErrorData? - var failedURL: URL? var storedSpecialErrorPageUserScript: SpecialErrorPageUserScript? - var isSpecialErrorPageVisible: Bool = false let syncService: DDGSyncing private let daxDialogsDebouncer = Debouncer(mode: .common) @@ -339,7 +334,6 @@ class TabViewController: UIViewController { contextualOnboardingPresenter: ContextualOnboardingPresenting, contextualOnboardingLogic: ContextualOnboardingLogic, onboardingPixelReporter: OnboardingCustomInteractionPixelReporting, - urlCredentialCreator: URLCredentialCreating = URLCredentialCreator(), featureFlagger: FeatureFlagger, subscriptionCookieManager: SubscriptionCookieManaging, textZoomCoordinator: TextZoomCoordinating, @@ -359,7 +353,6 @@ class TabViewController: UIViewController { contextualOnboardingPresenter: contextualOnboardingPresenter, contextualOnboardingLogic: contextualOnboardingLogic, onboardingPixelReporter: onboardingPixelReporter, - urlCredentialCreator: urlCredentialCreator, featureFlagger: featureFlagger, subscriptionCookieManager: subscriptionCookieManager, textZoomCoordinator: textZoomCoordinator, @@ -386,6 +379,7 @@ class TabViewController: UIViewController { let textZoomCoordinator: TextZoomCoordinating let fireproofing: Fireproofing let websiteDataManager: WebsiteDataManaging + let specialErrorPageNavigationHandler: SpecialErrorPageManaging required init?(coder aDecoder: NSCoder, tabModel: Tab, @@ -404,7 +398,8 @@ class TabViewController: UIViewController { subscriptionCookieManager: SubscriptionCookieManaging, textZoomCoordinator: TextZoomCoordinating, fireproofing: Fireproofing, - websiteDataManager: WebsiteDataManaging) { + websiteDataManager: WebsiteDataManaging, + specialErrorPageNavigationHandler: SpecialErrorPageManaging = SpecialErrorPageNavigationHandler()) { self.tabModel = tabModel self.appSettings = appSettings self.bookmarksDatabase = bookmarksDatabase @@ -421,17 +416,21 @@ class TabViewController: UIViewController { self.contextualOnboardingPresenter = contextualOnboardingPresenter self.contextualOnboardingLogic = contextualOnboardingLogic self.onboardingPixelReporter = onboardingPixelReporter - self.urlCredentialCreator = urlCredentialCreator self.featureFlagger = featureFlagger self.subscriptionCookieManager = subscriptionCookieManager self.textZoomCoordinator = textZoomCoordinator self.fireproofing = fireproofing self.websiteDataManager = websiteDataManager + self.specialErrorPageNavigationHandler = specialErrorPageNavigationHandler super.init(coder: aDecoder) // Assign itself as tabNavigationHandler for DuckPlayer duckPlayerNavigationHandler?.tabNavigationHandler = self + + // Assign itself as specialErrorPageNavigationDelegate for SpecialErrorPages + specialErrorPageNavigationHandler.delegate = self + } required init?(coder aDecoder: NSCoder) { @@ -569,6 +568,7 @@ class TabViewController: UIViewController { webView = WKWebView(frame: view.bounds, configuration: configuration) } textZoomCoordinator.onWebViewCreated(applyToWebView: webView) + specialErrorPageNavigationHandler.attachWebView(webView) webView.allowsLinkPreview = true webView.allowsBackForwardNavigationGestures = true @@ -1069,7 +1069,7 @@ class TabViewController: UIViewController { if let isValid { privacyInfo.serverTrust = isValid ? webView.serverTrust : nil } - privacyInfo.isSpecialErrorPageVisible = isSpecialErrorPageVisible + privacyInfo.isSpecialErrorPageVisible = specialErrorPageNavigationHandler.isSpecialErrorPageVisible previousPrivacyInfosByURL[url] = privacyInfo @@ -1284,7 +1284,8 @@ extension TabViewController: WKNavigationDelegate { if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodHTTPBasic { performBasicHTTPAuthentication(protectionSpace: challenge.protectionSpace, completionHandler: completionHandler) } else if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust { - handleServerTrustChallenge(challenge, completionHandler: completionHandler) + // Handle SSL challenge and present Special Error page if issues with SSL certificates are detected + specialErrorPageNavigationHandler.handleWebView(webView, didReceive: challenge, completionHandler: completionHandler) } else { completionHandler(.performDefaultHandling, nil) } @@ -1315,17 +1316,6 @@ extension TabViewController: WKNavigationDelegate { delegate?.tab(self, didRequestPresentingAlert: alert) } - private func handleServerTrustChallenge(_ challenge: URLAuthenticationChallenge, - completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { - guard shouldBypassSSLError, - let credential = urlCredentialCreator.urlCredentialFrom(trust: challenge.protectionSpace.serverTrust) else { - completionHandler(.performDefaultHandling, nil) - return - } - shouldBypassSSLError = false - completionHandler(.useCredential, credential) - } - func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) { if let url = webView.url { @@ -1490,10 +1480,8 @@ extension TabViewController: WKNavigationDelegate { includedParameters: [.appVersion, .atb]) } - specialErrorPageUserScript?.isEnabled = webView.url == failedURL - if webView.url != failedURL { - isSpecialErrorPageVisible = false - } + // Notify Special Error Page Navigation handler that webview successfully finished loading + specialErrorPageNavigationHandler.handleWebView(webView, didFinish: navigation) } var specialErrorPageUserScript: SpecialErrorPageUserScript? { @@ -1715,27 +1703,8 @@ extension TabViewController: WKNavigationDelegate { self.showErrorNow() } - loadSpecialErrorPageIfNeeded(error: error) - } - - private func loadSpecialErrorPageIfNeeded(error: NSError) { - guard featureFlagger.isFeatureOn(.sslCertificatesBypass), - error.isServerCertificateUntrusted, - let errorType = error.sslErrorType, - let failedURL = error.failedUrl, - let host = failedURL.host else { return } - - let tld = storageCache.tld - self.failedURL = failedURL - errorData = SpecialErrorData.ssl(type: errorType, domain: host, eTldPlus1: tld.eTLDplus1(host)) - loadSpecialErrorPage(url: failedURL) - Pixel.fire(pixel: .certificateWarningDisplayed(errorType.pixelParameter)) - } - - private func loadSpecialErrorPage(url: URL) { - let html = SpecialErrorPageHTMLTemplate.htmlFromTemplate - webView?.loadSimulatedRequest(URLRequest(url: url), responseHTML: html) - isSpecialErrorPageVisible = true + // Notify Special Error page that webview navigation failed and show special error page if needed. + specialErrorPageNavigationHandler.handleWebView(webView, didFailProvisionalNavigation: navigation, withError: error) } func webView(_ webView: WKWebView, didReceiveServerRedirectForProvisionalNavigation navigation: WKNavigation!) { @@ -1767,7 +1736,6 @@ extension TabViewController: WKNavigationDelegate { decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { - if let url = navigationAction.request.url { if !tabURLInterceptor.allowsNavigatingTo(url: url) { decisionHandler(.cancel) @@ -2571,7 +2539,9 @@ extension TabViewController: UserContentControllerDelegate { userScripts.printingUserScript.delegate = self userScripts.loginFormDetectionScript?.delegate = self userScripts.autoconsentUserScript.delegate = self - userScripts.specialErrorPageUserScript?.delegate = self + + // Special Error Page (SSL, Malicious Site protection) + specialErrorPageNavigationHandler.setUserScript(userScripts.specialErrorPageUserScript) // Setup DuckPlayer userScripts.duckPlayer = duckPlayerNavigationHandler?.duckPlayer @@ -3159,30 +3129,18 @@ extension UserContentController { } -extension TabViewController: SpecialErrorPageUserScriptDelegate { +// MARK: - SpecialErrorPageNavigationDelegate - func leaveSiteAction() { - Pixel.fire(pixel: .certificateWarningLeaveClicked) - guard webView?.canGoBack == true else { - delegate?.tabDidRequestClose(self) - return - } - _ = webView?.goBack() - } +extension TabViewController: SpecialErrorPageNavigationDelegate { - func visitSiteAction() { - Pixel.fire(pixel: .certificateWarningProceedClicked) - isSpecialErrorPageVisible = false - shouldBypassSSLError = true - _ = webView.reload() - } - - func advancedInfoPresented() { - Pixel.fire(pixel: .certificateWarningAdvancedClicked) + func closeSpecialErrorPageTab() { + delegate?.tabDidRequestClose(self) } } +// MARK: - DuckPlayerTabNavigationHandling + // This Protocol allows DuckPlayerHandler access tabs extension TabViewController: DuckPlayerTabNavigationHandling { diff --git a/DuckDuckGoTests/MockTabDelegate.swift b/DuckDuckGoTests/MockTabDelegate.swift index 47522ae542..84ac78bd22 100644 --- a/DuckDuckGoTests/MockTabDelegate.swift +++ b/DuckDuckGoTests/MockTabDelegate.swift @@ -145,7 +145,6 @@ extension TabViewController { contextualOnboardingPresenter: contextualOnboardingPresenter, contextualOnboardingLogic: contextualOnboardingLogic, onboardingPixelReporter: contextualOnboardingPixelReporter, - urlCredentialCreator: MockCredentialCreator(), featureFlagger: featureFlagger, subscriptionCookieManager: SubscriptionCookieManagerMock(), textZoomCoordinator: MockTextZoomCoordinator(), diff --git a/DuckDuckGoTests/SpecialErrorPage/SSLErrorPageNavigationHandlerTests.swift b/DuckDuckGoTests/SpecialErrorPage/SSLErrorPageNavigationHandlerTests.swift new file mode 100644 index 0000000000..d7b7ce923b --- /dev/null +++ b/DuckDuckGoTests/SpecialErrorPage/SSLErrorPageNavigationHandlerTests.swift @@ -0,0 +1,198 @@ +// +// SSLErrorPageNavigationHandlerTests.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// 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 Foundation +import Testing +import WebKit + +@testable import SpecialErrorPages +@testable import DuckDuckGo + +@Suite("Special Error Pages - SSLSpecialErrorPageTests Unit Tests", .serialized) +final class SSLSpecialErrorPageTests { + + private var sut: SSLErrorPageNavigationHandler! + + init() { + let featureFlagger = MockFeatureFlagger() + featureFlagger.enabledFeatureFlags = [.sslCertificatesBypass] + sut = SSLErrorPageNavigationHandler(urlCredentialCreator: MockCredentialCreator(), featureFlagger: featureFlagger) + } + + deinit { + sut = nil + } + + @Test + func whenCertificateExpiredThenExpectedErrorPageIsShown() throws { + // GIVEN + let error = NSError(domain: NSURLErrorDomain, + code: NSURLErrorServerCertificateUntrusted, + userInfo: ["_kCFStreamErrorCodeKey": errSSLCertExpired, + NSURLErrorFailingURLErrorKey: URL(string: "https://expired.badssl.com")!]) + + // WHEN + let sslError = try #require(sut.makeNewRequestURLAndSpecialErrorDataIfEnabled(error: error)) + + // THEN + #expect(sslError.error.url == URL(string: "https://expired.badssl.com")!) + #expect(sslError.type == .expired) + #expect(sslError.error.errorData == .ssl(type: .expired, + domain: "expired.badssl.com", + eTldPlus1: "badssl.com")) + } + + @Test + func whenCertificateWrongHostThenExpectedErrorPageIsShown() throws { + // GIVEN + let error = NSError(domain: NSURLErrorDomain, + code: NSURLErrorServerCertificateUntrusted, + userInfo: ["_kCFStreamErrorCodeKey": errSSLHostNameMismatch, + NSURLErrorFailingURLErrorKey: URL(string: "https://wrong.host.badssl.com")!]) + + // WHEN + let sslError = try #require(sut.makeNewRequestURLAndSpecialErrorDataIfEnabled(error: error)) + + // THEN + #expect(sslError.error.url == URL(string: "https://wrong.host.badssl.com")!) + #expect(sslError.type == .wrongHost) + #expect(sslError.error.errorData == .ssl(type: .wrongHost, + domain: "wrong.host.badssl.com", + eTldPlus1: "badssl.com")) + } + + @Test + func whenCertificateSelfSignedThenExpectedErrorPageIsShown() throws { + // GIVEN + let error = NSError(domain: NSURLErrorDomain, + code: NSURLErrorServerCertificateUntrusted, + userInfo: ["_kCFStreamErrorCodeKey": errSSLXCertChainInvalid, + NSURLErrorFailingURLErrorKey: URL(string: "https://self-signed.badssl.com")!]) + + // WHEN + let sslError = try #require(sut.makeNewRequestURLAndSpecialErrorDataIfEnabled(error: error)) + + // THEN + #expect(sslError.error.url == URL(string: "https://self-signed.badssl.com")!) + #expect(sslError.type == .selfSigned) + #expect(sslError.error.errorData == .ssl(type: .selfSigned, + domain: "self-signed.badssl.com", + eTldPlus1: "badssl.com")) + } + + @Test + func whenOtherCertificateIssueThenExpectedErrorPageIsShown() throws { + // GIVEN + let error = NSError(domain: NSURLErrorDomain, + code: NSURLErrorServerCertificateUntrusted, + userInfo: ["_kCFStreamErrorCodeKey": errSSLUnknownRootCert, + NSURLErrorFailingURLErrorKey: URL(string: "https://untrusted-root.badssl.com")!]) + + // WHEN + let sslError = try #require(sut.makeNewRequestURLAndSpecialErrorDataIfEnabled(error: error)) + + // THEN + #expect(sslError.error.url == URL(string: "https://untrusted-root.badssl.com")!) + #expect(sslError.type == .invalid) + #expect(sslError.error.errorData == .ssl(type: .invalid, + domain: "untrusted-root.badssl.com", + eTldPlus1: "badssl.com")) + } + + @Test + func whenDidReceiveChallengeIfChallengeForCertificateValidationAndNoBypassThenShouldNotReturnCredentials() { + // GIVEN + let protectionSpace = URLProtectionSpace(host: "", port: 4, protocol: nil, realm: nil, authenticationMethod: NSURLAuthenticationMethodServerTrust) + let challenge = URLAuthenticationChallenge(protectionSpace: protectionSpace, proposedCredential: nil, previousFailureCount: 0, failureResponse: nil, error: nil, sender: ChallengeSender()) + var expectedCredential: URLCredential? + + // WHEN + sut.handleServerTrustChallenge(challenge) { _, credential in + expectedCredential = credential + } + + // THEN + #expect(expectedCredential == nil) + } + + @Test + func whenDidReceiveChallengeIfChallengeForCertificateValidationAndUserRequestBypassThenReturnsCredentials() { + // GIVEN + let protectionSpace = URLProtectionSpace(host: "", port: 4, protocol: nil, realm: nil, authenticationMethod: NSURLAuthenticationMethodServerTrust) + let challenge = URLAuthenticationChallenge(protectionSpace: protectionSpace, proposedCredential: nil, previousFailureCount: 0, failureResponse: nil, error: nil, sender: ChallengeSender()) + var expectedCredential: URLCredential? + sut.visitSite() + + // WHEN + sut.handleServerTrustChallenge(challenge) { _, credential in + expectedCredential = credential + } + + // THEN + #expect(expectedCredential != nil) + } + +} + +final class ChallengeSender: URLAuthenticationChallengeSender { + func use(_ credential: URLCredential, for challenge: URLAuthenticationChallenge) {} + func continueWithoutCredential(for challenge: URLAuthenticationChallenge) {} + func cancel(_ challenge: URLAuthenticationChallenge) {} + func isEqual(_ object: Any?) -> Bool { + return false + } + var hash: Int = 0 + var superclass: AnyClass? + func `self`() -> Self { + self + } + func perform(_ aSelector: Selector!) -> Unmanaged! { + return nil + } + func perform(_ aSelector: Selector!, with object: Any!) -> Unmanaged! { + return nil + } + func perform(_ aSelector: Selector!, with object1: Any!, with object2: Any!) -> Unmanaged! { + return nil + } + func isProxy() -> Bool { + return false + } + func isKind(of aClass: AnyClass) -> Bool { + return false + } + func isMember(of aClass: AnyClass) -> Bool { + return false + } + func conforms(to aProtocol: Protocol) -> Bool { + return false + } + func responds(to aSelector: Selector!) -> Bool { + return false + } + var description: String = "" +} + +final class MockCredentialCreator: URLCredentialCreating { + + func urlCredentialFrom(trust: SecTrust?) -> URLCredential? { + return URLCredential(user: "", password: "", persistence: .forSession) + } + +} diff --git a/DuckDuckGoTests/SpecialErrorPage/SpecialErrorPageNavigationHandlerIntegrationTests.swift b/DuckDuckGoTests/SpecialErrorPage/SpecialErrorPageNavigationHandlerIntegrationTests.swift new file mode 100644 index 0000000000..415386e733 --- /dev/null +++ b/DuckDuckGoTests/SpecialErrorPage/SpecialErrorPageNavigationHandlerIntegrationTests.swift @@ -0,0 +1,253 @@ +// +// SpecialErrorPageNavigationHandlerIntegrationTests.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// 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 Testing +import WebKit +import SpecialErrorPages +@testable import DuckDuckGo + +@Suite("Special Error Pages - SSL Integration Tests", .serialized) +final class SpecialErrorPageNavigationHandlerIntegrationTests { + private var sut: SpecialErrorPageNavigationHandler! + private var webView: MockSpecialErrorWebView! + private var sslErrorPageNavigationHandler: SSLErrorPageNavigationHandler! + + @MainActor + init() { + let featureFlagger = MockFeatureFlagger() + featureFlagger.enabledFeatureFlags = [.sslCertificatesBypass] + webView = MockSpecialErrorWebView(frame: CGRect(), configuration: .nonPersistent()) + sslErrorPageNavigationHandler = SSLErrorPageNavigationHandler(featureFlagger: featureFlagger) + sut = SpecialErrorPageNavigationHandler( + sslErrorPageNavigationHandler: sslErrorPageNavigationHandler, + maliciousSiteProtectionNavigationHandler: DummyMaliciousSiteProtectionNavigationHandler() + ) + } + + deinit { + sslErrorPageNavigationHandler = nil + sut = nil + webView = nil + } + + @MainActor + @Test + func whenCertificateExpiredThenExpectedErrorPageIsShown() async throws { + // GIVEN + let error = NSError(domain: "test", + code: NSURLErrorServerCertificateUntrusted, + userInfo: ["_kCFStreamErrorCodeKey": errSSLCertExpired, + NSURLErrorFailingURLErrorKey: try #require(URL(string: "https://expired.badssl.com"))]) + sut.attachWebView(webView) + var expectedRequest: URLRequest? + var expectedHTML: String? + + try await confirmation { receivedHTML in + webView.loadRequestHandler = { request, html in + expectedRequest = request + expectedHTML = html + receivedHTML() + } + + // WHEN + sut.handleWebView(webView, didFailProvisionalNavigation: DummyWKNavigation(), withError: error) + + // THEN + let html = try #require(expectedHTML) + let url = try #require(expectedRequest?.url) + let expectedHost = try #require(URL(string: "https://expired.badssl.com")?.host) + #expect(html.contains("Warning: This site may be insecure")) + #expect(html.contains("is expired")) + #expect(url.host == expectedHost) + #expect(sut.failedURL == url) + #expect(sut.errorData == .ssl(type: .expired, domain: "expired.badssl.com", eTldPlus1: "badssl.com")) + } + } + + @MainActor + @Test + func whenCertificateWrongHostThenExpectedErrorPageIsShown() async throws { + // GIVEN + let error = NSError(domain: "test", + code: NSURLErrorServerCertificateUntrusted, + userInfo: ["_kCFStreamErrorCodeKey": errSSLHostNameMismatch, + NSURLErrorFailingURLErrorKey: try #require(URL(string: "https://wrong.host.badssl.com"))]) + sut.attachWebView(webView) + var expectedRequest: URLRequest? + var expectedHTML: String? + + try await confirmation { receivedHTML in + webView.loadRequestHandler = { request, html in + expectedRequest = request + expectedHTML = html + receivedHTML() + } + + // WHEN + sut.handleWebView(webView, didFailProvisionalNavigation: DummyWKNavigation(), withError: error) + + // THEN + let html = try #require(expectedHTML) + let url = try #require(expectedRequest?.url) + let expectedHost = try #require(URL(string: "https://wrong.host.badssl.com")?.host) + #expect(html.contains("Warning: This site may be insecure")) + #expect(html.contains("does not match")) + #expect(url.host == expectedHost) + #expect(sut.failedURL == url) + #expect(sut.errorData == .ssl(type: .wrongHost, domain: "wrong.host.badssl.com", eTldPlus1: "badssl.com")) + } + } + + @MainActor + @Test + func whenCertificateSelfSignedThenExpectedErrorPageIsShown() async throws { + // GIVEN + let error = NSError(domain: "test", + code: NSURLErrorServerCertificateUntrusted, + userInfo: ["_kCFStreamErrorCodeKey": errSSLXCertChainInvalid, + NSURLErrorFailingURLErrorKey: try #require(URL(string: "https://self-signed.badssl.com"))]) + sut.attachWebView(webView) + var expectedRequest: URLRequest? + var expectedHTML: String? + + try await confirmation { receivedHTML in + webView.loadRequestHandler = { request, html in + expectedRequest = request + expectedHTML = html + receivedHTML() + } + + // WHEN + sut.handleWebView(webView, didFailProvisionalNavigation: DummyWKNavigation(), withError: error) + + // THEN + let html = try #require(expectedHTML) + let url = try #require(expectedRequest?.url) + let expectedHost = try #require(URL(string: "https://self-signed.badssl.com")?.host) + #expect(html.contains("Warning: This site may be insecure")) + #expect(html.contains("is not trusted")) + #expect(url.host == expectedHost) + #expect(sut.failedURL == url) + #expect(sut.errorData == .ssl(type: .selfSigned, domain: "self-signed.badssl.com", eTldPlus1: "badssl.com")) + } + } + + @MainActor + @Test + func whenOtherCertificateIssueThenExpectedErrorPageIsShown() async throws { + // GIVEN + let error = NSError(domain: "test", + code: NSURLErrorServerCertificateUntrusted, + userInfo: ["_kCFStreamErrorCodeKey": errSSLUnknownRootCert, + NSURLErrorFailingURLErrorKey: try #require(URL(string: "https://untrusted-root.badssl.com"))]) + sut.attachWebView(webView) + var expectedRequest: URLRequest? + var expectedHTML: String? + + try await confirmation { receivedHTML in + webView.loadRequestHandler = { request, html in + expectedRequest = request + expectedHTML = html + receivedHTML() + } + + // WHEN + sut.handleWebView(webView, didFailProvisionalNavigation: DummyWKNavigation(), withError: error) + + // THEN + let html = try #require(expectedHTML) + let url = try #require(expectedRequest?.url) + let expectedHost = try #require(URL(string: "https://untrusted-root.badssl.com")?.host) + #expect(html.contains("Warning: This site may be insecure")) + #expect(html.contains("is not trusted")) + #expect(url.host == expectedHost) + #expect(sut.failedURL == url) + #expect(sut.errorData == .ssl(type: .invalid, domain: "untrusted-root.badssl.com", eTldPlus1: "badssl.com")) + } + } + + @MainActor + @Test + func whenNavigationEndedIfNoSSLFailureSSLUserScriptIsNotEnabled() throws { + // GIVEN + webView.setCurrentURL(try #require(URL(string: "https://self-signed.badssl.com"))) + let script = SpecialErrorPageUserScript(localeStrings: "", languageCode: "") + sut.setUserScript(script) + sut.attachWebView(webView) + #expect(script.isEnabled == false) + + // WHEN + sut.handleWebView(webView, didFinish: DummyWKNavigation()) + + // THEN + #expect(script.isEnabled == false) + } + + @MainActor + @Test + func whenNavigationEndedIfSSLFailureButURLIsDifferentFromNavigationURLThenSSLUserScriptIsNotEnabled() throws { + // GIVEN + let error = NSError(domain: "test", + code: NSURLErrorServerCertificateUntrusted, + userInfo: ["_kCFStreamErrorCodeKey": errSSLUnknownRootCert, + NSURLErrorFailingURLErrorKey: try #require(URL(string: "https://untrusted-root.badssl.com"))]) + + webView.setCurrentURL(try #require(URL(string: "https://self-signed.badssl.com"))) + let script = SpecialErrorPageUserScript(localeStrings: "", languageCode: "") + sut.setUserScript(script) + sut.attachWebView(webView) + // Fail the request with a different URL. + sut.handleWebView(webView, didFailProvisionalNavigation: DummyWKNavigation(), withError: error) + #expect(script.isEnabled == false) + + // WHEN + sut.handleWebView(webView, didFinish: DummyWKNavigation()) + + // THEN + #expect(script.isEnabled == false) + } + + @MainActor + @Test + func testWhenNavigationEndedIfSSLFailureAndNavigationURLIsTheSameAsFailingURLThenSSLUserScriptIsEnabled() throws { + // GIVEN + let url = try #require(URL(string: "https://self-signed.badssl.com")) + webView.setCurrentURL(url) + let script = SpecialErrorPageUserScript(localeStrings: "", languageCode: "") + sut.setUserScript(script) + sut.attachWebView(webView) + let navigation = DummyWKNavigation() + let error = NSError( + domain: "test", + code: NSURLErrorServerCertificateUntrusted, + userInfo: [ + "_kCFStreamErrorCodeKey": errSSLCertExpired, + NSURLErrorFailingURLErrorKey: url] + ) + sut.handleWebView(webView, didFailProvisionalNavigation: navigation, withError: error) + #expect(script.isEnabled == false) + + // WHEN + sut.handleWebView(webView, didFinish: navigation) + + // THEN + #expect(script.isEnabled) + } + +} diff --git a/DuckDuckGoTests/SpecialErrorPage/SpecialErrorPageNavigationHandlerTests.swift b/DuckDuckGoTests/SpecialErrorPage/SpecialErrorPageNavigationHandlerTests.swift new file mode 100644 index 0000000000..d5ea9bd078 --- /dev/null +++ b/DuckDuckGoTests/SpecialErrorPage/SpecialErrorPageNavigationHandlerTests.swift @@ -0,0 +1,181 @@ +// +// SpecialErrorPageNavigationHandlerTests.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// 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 Testing +import WebKit +import SpecialErrorPages +@testable import DuckDuckGo + +@Suite("Special Error Pages - SpecialErrorPageNavigationHandler Unit Tests", .serialized) +final class SpecialErrorPageNavigationHandlerTests { + private var sut: SpecialErrorPageNavigationHandler! + private var webView: MockSpecialErrorWebView! + private var sslErrorPageNavigationHandler: MockSSLErrorPageNavigationHandler! + + @MainActor + init() { + let featureFlagger = MockFeatureFlagger() + featureFlagger.enabledFeatureFlags = [.sslCertificatesBypass] + webView = MockSpecialErrorWebView(frame: CGRect(), configuration: .nonPersistent()) + sslErrorPageNavigationHandler = MockSSLErrorPageNavigationHandler() + sut = SpecialErrorPageNavigationHandler( + sslErrorPageNavigationHandler: sslErrorPageNavigationHandler, + maliciousSiteProtectionNavigationHandler: DummyMaliciousSiteProtectionNavigationHandler() + ) + } + + deinit { + sslErrorPageNavigationHandler = nil + sut = nil + webView = nil + } + + @Test("Receive Challenge forward event to SSL Error Page Navigation Handler") + func whenDidHandleWebViewReceiveChallengeIsCalledAskSSLErrorPageNavigationHandlerToHandleTheChallenge() { + // GIVEN + let protectionSpace = URLProtectionSpace(host: "", port: 4, protocol: nil, realm: nil, authenticationMethod: NSURLAuthenticationMethodServerTrust) + let challenge = URLAuthenticationChallenge(protectionSpace: protectionSpace, proposedCredential: nil, previousFailureCount: 0, failureResponse: nil, error: nil, sender: ChallengeSender()) + #expect(sslErrorPageNavigationHandler.didCallHandleServerTrustChallenge == false) + + // WHEN + sut.handleWebView(webView, didReceive: challenge) { _, _ in } + + // THEN + #expect(sslErrorPageNavigationHandler.didCallHandleServerTrustChallenge) + } + + @MainActor + @Test("Leave Site forward event to SSL Error Page Navigation Handler") + func whenLeaveSite_AndSSLError_ThenCallLeaveSiteOnSSLErrorPageNavigationHandler() { + // GIVEN + sut.handleWebView(webView, didFailProvisionalNavigation: DummyWKNavigation(), withError: .genericSSL) + #expect(!sslErrorPageNavigationHandler.didCallLeaveSite) + + // WHEN + sut.leaveSiteAction() + + // THEN + #expect(sslErrorPageNavigationHandler.didCallLeaveSite) + } + + @Test("Leave Site forward event to Malicious Site Protection Navigation Handler", .disabled("Will implement in upcoming PR")) + func whenLeaveSite_AndPhishingError_ThenCallLeaveSiteOnMaliciousSiteProtectioneNavigationHandler() { + + } + + @MainActor + @Test("Lave Site navigate Back") + func whenLeaveSite_AndWebViewCanNavigateBack_ThenNavigateBack() { + // GIVEN + webView.setCanGoBack(true) + sut.attachWebView(webView) + #expect(!webView.didCallGoBack) + + // WHEN + sut.leaveSiteAction() + + // THEN + #expect(webView.didCallGoBack) + } + + @MainActor + @Test("Lave Site close Tab") + func whenLeaveSite_AndWebViewCannotNavigateBack_ThenAskDelegateToCloseTab() { + // GIVEN + webView.setCanGoBack(false) + let delegate = SpySpecialErrorPageNavigationDelegate() + sut.delegate = delegate + sut.attachWebView(webView) + #expect(!delegate.didCallCloseSpecialErrorPageTab) + + // WHEN + sut.leaveSiteAction() + + // THEN + #expect(delegate.didCallCloseSpecialErrorPageTab) + } + + @MainActor + @Test("Visit Site forward event to SSL Error Page Navigation Handler") + func whenVisitSite_AndSSLError_ThenCallVisitSiteOnSSLErrorPageNavigationHandler() { + // GIVEN + sut.handleWebView(webView, didFailProvisionalNavigation: DummyWKNavigation(), withError: .genericSSL) + #expect(!sslErrorPageNavigationHandler.didCallVisitSite) + + // WHEN + sut.visitSiteAction() + + // THEN + #expect(sslErrorPageNavigationHandler.didCallVisitSite) + } + + @Test("Visit Site forward event to Malicious Site Protection Navigation Handler", .disabled("Will implement in upcoming PR")) + func whenVisitSite_AndPhishingError_ThenCallVisitSiteOnMaliciousSiteProtectioneNavigationHandler() { + + } + + @MainActor + @Test("Visit Site reset isSpecialErrorPageVisible and reload page") + func whenVisitSite_ThenSetIsSpecialErrorPageVisibleToFalseAndReloadPage() { + // GIVEN + sut.attachWebView(webView) + sut.handleWebView(webView, didFailProvisionalNavigation: DummyWKNavigation(), withError: .genericSSL) + #expect(sut.isSpecialErrorPageVisible) + #expect(!webView.didCallReload) + + // WHEN + sut.visitSiteAction() + + // THEN + #expect(!sut.isSpecialErrorPageVisible) + #expect(webView.didCallReload) + } + + @MainActor + @Test("Advanced Info Presented forward event to SSL Error Page Navigation Handler") + func whenAdvancedInfoPresented_AndSSLError_ThenCallAdvancedInfoPresentedOnSSLErrorPageNavigationHandler() { + // GIVEN + sut.handleWebView(webView, didFailProvisionalNavigation: DummyWKNavigation(), withError: .genericSSL) + #expect(!sslErrorPageNavigationHandler.didCalladvancedInfoPresented) + + // WHEN + sut.advancedInfoPresented() + + // THEN + #expect(sslErrorPageNavigationHandler.didCalladvancedInfoPresented) + } + + @Test("Advanced Info Presented forward event to Malicious Site Protection Navigation Handler", .disabled("Will implement in upcoming PR")) + func whenAdvancedInfoPresented_AndPhishingError_ThenCallAdvancedInfoPresentedOnMaliciousSiteProtectionNavigationHandler() { + + } +} + +private extension NSError { + + static let genericSSL = NSError( + domain: "test", + code: NSURLErrorServerCertificateUntrusted, + userInfo: [ + "_kCFStreamErrorCodeKey": errSSLUnknownRootCert, + NSURLErrorFailingURLErrorKey: URL(string: "https://untrusted-root.badssl.com")! + ] + ) + +} diff --git a/DuckDuckGoTests/SpecialErrorPageUserScriptTests.swift b/DuckDuckGoTests/SpecialErrorPage/SpecialErrorPageUserScriptTests.swift similarity index 63% rename from DuckDuckGoTests/SpecialErrorPageUserScriptTests.swift rename to DuckDuckGoTests/SpecialErrorPage/SpecialErrorPageUserScriptTests.swift index d770f9cc3b..8b27e95c9c 100644 --- a/DuckDuckGoTests/SpecialErrorPageUserScriptTests.swift +++ b/DuckDuckGoTests/SpecialErrorPage/SpecialErrorPageUserScriptTests.swift @@ -17,13 +17,15 @@ // limitations under the License. // -import XCTest +import Testing import SpecialErrorPages @testable import DuckDuckGo -final class SpecialErrorPageUserScriptTests: XCTestCase { +@Suite("Special Error Pages - SpecialErrorPageUserScript Unit Tests", .serialized) +struct SpecialErrorPageUserScriptTests { - func testLocaleStringsForNotSupportedLanguage() { + @Test + func localeStringsForNotSupportedLanguage() { // Given let languageCode = "ko" @@ -31,20 +33,21 @@ final class SpecialErrorPageUserScriptTests: XCTestCase { let result = SpecialErrorPageUserScript.localeStrings(for: languageCode) // Then - XCTAssertNil(result, "The result should be nil for Korean language") + #expect(result == nil, "The result should be nil for Korean language") } - func testLocaleStringsForPolishLanguage() { + @Test + func localeStringsForPolishLanguage() throws { // Given let languageCode = "pl" // When - let result = SpecialErrorPageUserScript.localeStrings(for: languageCode) + let result = try #require(SpecialErrorPageUserScript.localeStrings(for: languageCode)) // Then - XCTAssertNotNil(result, "The result should not be nil for the Polish language code.") + #expect(result != nil, "The result should not be nil for the Polish language code.") let expectedSubstring = "Ostrzeżenie: ta witryna może być niebezpieczna" - XCTAssertTrue(result!.contains(expectedSubstring), "The result should contain the expected Polish string.") + #expect(result.contains(expectedSubstring), "The result should contain the expected Polish string.") } } diff --git a/DuckDuckGoTests/SpecialErrorPage/TestDoubles/DummyMaliciousSiteProtectionNavigationHandler.swift b/DuckDuckGoTests/SpecialErrorPage/TestDoubles/DummyMaliciousSiteProtectionNavigationHandler.swift new file mode 100644 index 0000000000..e842c63cfa --- /dev/null +++ b/DuckDuckGoTests/SpecialErrorPage/TestDoubles/DummyMaliciousSiteProtectionNavigationHandler.swift @@ -0,0 +1,34 @@ +// +// DummyMaliciousSiteProtectionNavigationHandler.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// 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 Foundation +import WebKit +@testable import DuckDuckGo + +class DummyMaliciousSiteProtectionNavigationHandler: MaliciousSiteProtectionNavigationHandling & SpecialErrorPageActionHandler { + func handleMaliciousSiteProtectionNavigation(for navigationAction: WKNavigationAction, webView: WKWebView) async -> DuckDuckGo.MaliciousSiteProtectionNavigationResult { + .navigationNotHandled + } + + func visitSite() {} + + func leaveSite() {} + + func advancedInfoPresented() {} +} diff --git a/DuckDuckGoTests/SpecialErrorPage/TestDoubles/DummyWKNavigation.swift b/DuckDuckGoTests/SpecialErrorPage/TestDoubles/DummyWKNavigation.swift new file mode 100644 index 0000000000..7ec5952fbb --- /dev/null +++ b/DuckDuckGoTests/SpecialErrorPage/TestDoubles/DummyWKNavigation.swift @@ -0,0 +1,23 @@ +// +// DummyWKNavigation.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// 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 Foundation +@testable import DuckDuckGo + +final class DummyWKNavigation: WebViewNavigation {} diff --git a/DuckDuckGoTests/SpecialErrorPage/TestDoubles/MockSSLErrorPageNavigationHandler.swift b/DuckDuckGoTests/SpecialErrorPage/TestDoubles/MockSSLErrorPageNavigationHandler.swift new file mode 100644 index 0000000000..0580bb6f37 --- /dev/null +++ b/DuckDuckGoTests/SpecialErrorPage/TestDoubles/MockSSLErrorPageNavigationHandler.swift @@ -0,0 +1,65 @@ +// +// MockSSLErrorPageNavigationHandler.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// 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 Foundation +import SpecialErrorPages +@testable import DuckDuckGo + +final class MockSSLErrorPageNavigationHandler: SSLSpecialErrorPageNavigationHandling, SpecialErrorPageActionHandler { + private(set) var didCallHandleServerTrustChallenge = false + private(set) var capturedChallenge: URLAuthenticationChallenge? + var handleServerTrustChallengeHandler: (URLSession.AuthChallengeDisposition, URLCredential?) -> Void = { _, _ in } + + private(set) var didCallMakeNewRequestURLAndSpecialErrorDataIfEnabled = false + + private(set) var didCallErrorPageVisited = false + private(set) var capturedSpecialErrorType: SSLErrorType? + + private(set) var didCallLeaveSite = false + private(set) var didCallVisitSite = false + private(set) var didCalladvancedInfoPresented = false + + func handleServerTrustChallenge(_ challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { + didCallHandleServerTrustChallenge = true + capturedChallenge = challenge + handleServerTrustChallengeHandler(.performDefaultHandling, nil) + } + + func makeNewRequestURLAndSpecialErrorDataIfEnabled(error: NSError) -> SSLSpecialError? { + didCallMakeNewRequestURLAndSpecialErrorDataIfEnabled = true + return SSLSpecialError(type: .expired, error: SpecialErrorModel(url: URL(string: "www.example.com")!, errorData: .ssl(type: .expired, domain: "", eTldPlus1: nil))) + } + + func errorPageVisited(errorType: SSLErrorType) { + didCallErrorPageVisited = true + capturedSpecialErrorType = errorType + } + + func visitSite() { + didCallVisitSite = true + } + + func leaveSite() { + didCallLeaveSite = true + } + + func advancedInfoPresented() { + didCalladvancedInfoPresented = true + } +} diff --git a/DuckDuckGoTests/SpecialErrorPage/TestDoubles/MockSpecialErrorPageNavigationDelegate.swift b/DuckDuckGoTests/SpecialErrorPage/TestDoubles/MockSpecialErrorPageNavigationDelegate.swift new file mode 100644 index 0000000000..6a2e58628e --- /dev/null +++ b/DuckDuckGoTests/SpecialErrorPage/TestDoubles/MockSpecialErrorPageNavigationDelegate.swift @@ -0,0 +1,29 @@ +// +// MockSpecialErrorPageNavigationDelegate.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// 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 Foundation +@testable import DuckDuckGo + +final class SpySpecialErrorPageNavigationDelegate: SpecialErrorPageNavigationDelegate { + private(set) var didCallCloseSpecialErrorPageTab: Bool = false + + func closeSpecialErrorPageTab() { + didCallCloseSpecialErrorPageTab = true + } +} diff --git a/DuckDuckGoTests/SpecialErrorPage/TestDoubles/MockSpecialErrorWebView.swift b/DuckDuckGoTests/SpecialErrorPage/TestDoubles/MockSpecialErrorWebView.swift new file mode 100644 index 0000000000..05558490cb --- /dev/null +++ b/DuckDuckGoTests/SpecialErrorPage/TestDoubles/MockSpecialErrorWebView.swift @@ -0,0 +1,63 @@ +// +// MockSpecialErrorWebView.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// 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 Foundation +import WebKit + +class MockSpecialErrorWebView: WKWebView { + + var loadRequestHandler: ((URLRequest, String) -> Void)? + var currentURL: URL? + private var _canGoBack: Bool = false + + private(set) var didCallGoBack = false + private(set) var didCallReload = false + + override func loadSimulatedRequest(_ request: URLRequest, responseHTML string: String) -> WKNavigation { + loadRequestHandler?(request, string) + return super.loadSimulatedRequest(request, responseHTML: string) + } + + override var url: URL? { + currentURL + } + + func setCurrentURL(_ url: URL) { + self.currentURL = url + } + + override var canGoBack: Bool { + _canGoBack + } + + func setCanGoBack(_ canGoBack: Bool) { + _canGoBack = canGoBack + } + + override func goBack() -> WKNavigation? { + didCallGoBack = true + return super.goBack() + } + + override func reload() -> WKNavigation? { + didCallReload = true + return super.reload() + } + +}