diff --git a/Alicerce.xcodeproj/project.pbxproj b/Alicerce.xcodeproj/project.pbxproj index 5984523f..b4de65b9 100644 --- a/Alicerce.xcodeproj/project.pbxproj +++ b/Alicerce.xcodeproj/project.pbxproj @@ -29,6 +29,10 @@ 0A01F88E20B370F400BA7C4D /* MinderaAlicerceRootCA.cer in Resources */ = {isa = PBXBuildFile; fileRef = 0A01F88D20B370F300BA7C4D /* MinderaAlicerceRootCA.cer */; }; 0A01F89420B383EB00BA7C4D /* DigiCertGlobalRootG2.pub in Resources */ = {isa = PBXBuildFile; fileRef = 0A01F89320B383EB00BA7C4D /* DigiCertGlobalRootG2.pub */; }; 0A01F89620B383F500BA7C4D /* GeoTrust_Universal_CA.pub in Resources */ = {isa = PBXBuildFile; fileRef = 0A01F89520B383F500BA7C4D /* GeoTrust_Universal_CA.pub */; }; + 0A02BDDD220D08BA00CF14C9 /* HTTPResourceEndpointTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A02BDDC220D08BA00CF14C9 /* HTTPResourceEndpointTestCase.swift */; }; + 0A02BDDF220D919600CF14C9 /* AuthenticatedHTTPNetworkResourceTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A02BDDE220D919600CF14C9 /* AuthenticatedHTTPNetworkResourceTestCase.swift */; }; + 0A02BDE1220D927C00CF14C9 /* HTTPNetworkResourceTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A02BDE0220D927C00CF14C9 /* HTTPNetworkResourceTestCase.swift */; }; + 0A02BDE3220D92DD00CF14C9 /* MockHTTPResourceEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A02BDE2220D92DD00CF14C9 /* MockHTTPResourceEndpoint.swift */; }; 0A09A1AA20EEEA5A008EFAF3 /* PerformanceMetricsTrackerTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A09A1A620EEEA28008EFAF3 /* PerformanceMetricsTrackerTestCase.swift */; }; 0A1083E220CD8322003969AB /* DispatchQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A1083E120CD8322003969AB /* DispatchQueue.swift */; }; 0A1083E520CDA8AF003969AB /* MockLogLevelFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A1083E320CDA8AA003969AB /* MockLogLevelFormatter.swift */; }; @@ -178,6 +182,10 @@ 0A83885E1EB1F6B000C1E835 /* NSPersistentStoreCoordinator+CoreDataStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A8388551EB1F6B000C1E835 /* NSPersistentStoreCoordinator+CoreDataStack.swift */; }; 0A83885F1EB1F6B000C1E835 /* NSPersistentStoreDescription+CoreDataStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A8388561EB1F6B000C1E835 /* NSPersistentStoreDescription+CoreDataStack.swift */; }; 0A8388601EB1F6B000C1E835 /* SiblingContextCoreDataStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A8388571EB1F6B000C1E835 /* SiblingContextCoreDataStack.swift */; }; + 0A848DE2220B4B7500B690E8 /* AuthenticatedHTTPNetworkResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A848DE1220B4B7500B690E8 /* AuthenticatedHTTPNetworkResource.swift */; }; + 0A848DE4220B4C0100B690E8 /* HTTPResourceEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A848DE3220B4C0100B690E8 /* HTTPResourceEndpoint.swift */; }; + 0A848DE6220B4D3F00B690E8 /* HTTPNetworkResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A848DE5220B4D3F00B690E8 /* HTTPNetworkResource.swift */; }; + 0A848DE9220C83D100B690E8 /* MockResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A848DE7220C489F00B690E8 /* MockResource.swift */; }; 0A85F0E720B3177E0095AFFB /* PublicKeyAlgorithmTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A85F0E620B3177E0095AFFB /* PublicKeyAlgorithmTestCase.swift */; }; 0A85F0E920B31A810095AFFB /* Data+SPKIHash.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A85F0E820B31A810095AFFB /* Data+SPKIHash.swift */; }; 0A85F0EB20B31AE30095AFFB /* Data+SPKIHashTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A85F0EA20B31AE30095AFFB /* Data+SPKIHashTestCase.swift */; }; @@ -240,14 +248,13 @@ 0AEEE11820CF208A00B47687 /* MockLogItemFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AEEE11720CF208A00B47687 /* MockLogItemFormatter.swift */; }; 0AFB0DC620F8E77300A58515 /* MockURLSessionDataTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AFB0DC420F8E76900A58515 /* MockURLSessionDataTask.swift */; }; 0AFB0DC720F8E77700A58515 /* MockURLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AFB0DC220F8E74700A58515 /* MockURLSession.swift */; }; - 0AFB0DCA20F8E7AE00A58515 /* MockNetworkAuthenticator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AFB0DC920F8E7AE00A58515 /* MockNetworkAuthenticator.swift */; }; + 0AFB0DCA20F8E7AE00A58515 /* MockRequestAuthenticator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AFB0DC920F8E7AE00A58515 /* MockRequestAuthenticator.swift */; }; 0AFB0DCC20F8E7F400A58515 /* MockRequestInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AFB0DCB20F8E7F400A58515 /* MockRequestInterceptor.swift */; }; 0AFB0DCE20F8E81700A58515 /* MockAuthenticationChallengeHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AFB0DCD20F8E81700A58515 /* MockAuthenticationChallengeHandler.swift */; }; 0AFB0DD220F8FD1C00A58515 /* RetryableResourceTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AFB0DD120F8FD1C00A58515 /* RetryableResourceTestCase.swift */; }; 1B14CDC11ECCC84D00CFAC15 /* KeyboardObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B14CDC01ECCC84D00CFAC15 /* KeyboardObserver.swift */; }; - 1B327F99207BE6C300ACEEBD /* ResourceTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B327F97207BE39400ACEEBD /* ResourceTestCase.swift */; }; 1B327F9D207CF3C500ACEEBD /* JSON+Mappable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B327F9C207CF3C500ACEEBD /* JSON+Mappable.swift */; }; - 1B3C01791F0A93CD00DF8394 /* NetworkAuthenticator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B3C01781F0A93CD00DF8394 /* NetworkAuthenticator.swift */; }; + 1B3C01791F0A93CD00DF8394 /* RequestAuthenticator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B3C01781F0A93CD00DF8394 /* RequestAuthenticator.swift */; }; 1B40334D1ED6E57200B4B03D /* Serialize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B40334C1ED6E57200B4B03D /* Serialize.swift */; }; 1B4033581ED8927E00B4B03D /* ViewModelCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B4033571ED8927E00B4B03D /* ViewModelCollectionViewCell.swift */; }; 1B40335A1ED8A59100B4B03D /* ViewModelTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B4033591ED8A59100B4B03D /* ViewModelTableViewCell.swift */; }; @@ -308,6 +315,10 @@ 0A01F88D20B370F300BA7C4D /* MinderaAlicerceRootCA.cer */ = {isa = PBXFileReference; lastKnownFileType = file; path = MinderaAlicerceRootCA.cer; sourceTree = ""; }; 0A01F89320B383EB00BA7C4D /* DigiCertGlobalRootG2.pub */ = {isa = PBXFileReference; lastKnownFileType = file; path = DigiCertGlobalRootG2.pub; sourceTree = ""; }; 0A01F89520B383F500BA7C4D /* GeoTrust_Universal_CA.pub */ = {isa = PBXFileReference; lastKnownFileType = file; path = GeoTrust_Universal_CA.pub; sourceTree = ""; }; + 0A02BDDC220D08BA00CF14C9 /* HTTPResourceEndpointTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPResourceEndpointTestCase.swift; sourceTree = ""; }; + 0A02BDDE220D919600CF14C9 /* AuthenticatedHTTPNetworkResourceTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticatedHTTPNetworkResourceTestCase.swift; sourceTree = ""; }; + 0A02BDE0220D927C00CF14C9 /* HTTPNetworkResourceTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPNetworkResourceTestCase.swift; sourceTree = ""; }; + 0A02BDE2220D92DD00CF14C9 /* MockHTTPResourceEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockHTTPResourceEndpoint.swift; sourceTree = ""; }; 0A09A1A620EEEA28008EFAF3 /* PerformanceMetricsTrackerTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PerformanceMetricsTrackerTestCase.swift; sourceTree = ""; }; 0A09A1A820EEEA4B008EFAF3 /* PerformanceMetrics+MultiTrackerTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PerformanceMetrics+MultiTrackerTestCase.swift"; sourceTree = ""; }; 0A1083E120CD8322003969AB /* DispatchQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DispatchQueue.swift; sourceTree = ""; }; @@ -470,6 +481,10 @@ 0A83887F1EB206C800C1E835 /* InvalidCoreDataStackModel.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = InvalidCoreDataStackModel.bundle; sourceTree = ""; }; 0A83888C1EB2088C00C1E835 /* DiskMemoryPersistenceTestCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiskMemoryPersistenceTestCase.swift; sourceTree = ""; }; 0A83888E1EB209D100C1E835 /* MockNetworkStack.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockNetworkStack.swift; sourceTree = ""; }; + 0A848DE1220B4B7500B690E8 /* AuthenticatedHTTPNetworkResource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticatedHTTPNetworkResource.swift; sourceTree = ""; }; + 0A848DE3220B4C0100B690E8 /* HTTPResourceEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPResourceEndpoint.swift; sourceTree = ""; }; + 0A848DE5220B4D3F00B690E8 /* HTTPNetworkResource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPNetworkResource.swift; sourceTree = ""; }; + 0A848DE7220C489F00B690E8 /* MockResource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockResource.swift; sourceTree = ""; }; 0A85F0E620B3177E0095AFFB /* PublicKeyAlgorithmTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicKeyAlgorithmTestCase.swift; sourceTree = ""; }; 0A85F0E820B31A810095AFFB /* Data+SPKIHash.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+SPKIHash.swift"; sourceTree = ""; }; 0A85F0EA20B31AE30095AFFB /* Data+SPKIHashTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+SPKIHashTestCase.swift"; sourceTree = ""; }; @@ -523,14 +538,13 @@ 0AEEE11720CF208A00B47687 /* MockLogItemFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockLogItemFormatter.swift; sourceTree = ""; }; 0AFB0DC220F8E74700A58515 /* MockURLSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockURLSession.swift; sourceTree = ""; }; 0AFB0DC420F8E76900A58515 /* MockURLSessionDataTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockURLSessionDataTask.swift; sourceTree = ""; }; - 0AFB0DC920F8E7AE00A58515 /* MockNetworkAuthenticator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockNetworkAuthenticator.swift; sourceTree = ""; }; + 0AFB0DC920F8E7AE00A58515 /* MockRequestAuthenticator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRequestAuthenticator.swift; sourceTree = ""; }; 0AFB0DCB20F8E7F400A58515 /* MockRequestInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRequestInterceptor.swift; sourceTree = ""; }; 0AFB0DCD20F8E81700A58515 /* MockAuthenticationChallengeHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAuthenticationChallengeHandler.swift; sourceTree = ""; }; 0AFB0DD120F8FD1C00A58515 /* RetryableResourceTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RetryableResourceTestCase.swift; sourceTree = ""; }; 1B14CDC01ECCC84D00CFAC15 /* KeyboardObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyboardObserver.swift; sourceTree = ""; }; - 1B327F97207BE39400ACEEBD /* ResourceTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResourceTestCase.swift; sourceTree = ""; }; 1B327F9C207CF3C500ACEEBD /* JSON+Mappable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSON+Mappable.swift"; sourceTree = ""; }; - 1B3C01781F0A93CD00DF8394 /* NetworkAuthenticator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkAuthenticator.swift; sourceTree = ""; }; + 1B3C01781F0A93CD00DF8394 /* RequestAuthenticator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestAuthenticator.swift; sourceTree = ""; }; 1B40334C1ED6E57200B4B03D /* Serialize.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Serialize.swift; sourceTree = ""; }; 1B4033571ED8927E00B4B03D /* ViewModelCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewModelCollectionViewCell.swift; sourceTree = ""; }; 1B4033591ED8A59100B4B03D /* ViewModelTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewModelTableViewCell.swift; sourceTree = ""; }; @@ -748,10 +762,10 @@ 1B327F9C207CF3C500ACEEBD /* JSON+Mappable.swift */, 0A3C2CA11EA7E18500EFB7D4 /* Mappable.swift */, 0A3C2CA21EA7E18500EFB7D4 /* Network.swift */, - 1B3C01781F0A93CD00DF8394 /* NetworkAuthenticator.swift */, 0A3C2CA31EA7E18500EFB7D4 /* NetworkStack.swift */, 0A3C2CA41EA7E18500EFB7D4 /* Parse.swift */, 0ACFA72D20AEC8E400D2E280 /* Pinning */, + 1B3C01781F0A93CD00DF8394 /* RequestAuthenticator.swift */, 1B4D4CB41F04F67E00FA4260 /* RequestInterceptor.swift */, 1B40334C1ED6E57200B4B03D /* Serialize.swift */, 0A3C2CA51EA7E18500EFB7D4 /* URLSessionNetworkStack.swift */, @@ -775,6 +789,9 @@ 0A3C2CB61EA7E18500EFB7D4 /* Resource */ = { isa = PBXGroup; children = ( + 0A848DE1220B4B7500B690E8 /* AuthenticatedHTTPNetworkResource.swift */, + 0A848DE5220B4D3F00B690E8 /* HTTPNetworkResource.swift */, + 0A848DE3220B4C0100B690E8 /* HTTPResourceEndpoint.swift */, 0A3C2CB71EA7E18500EFB7D4 /* NetworkResource.swift */, 0A3C2CB81EA7E18500EFB7D4 /* Resource.swift */, 0A77982E20FFF29D008E269A /* ResourceRetry.swift */, @@ -866,7 +883,6 @@ 0A3C2D221EA7E1EE00EFB7D4 /* NetworkTestCase.swift */, 0A3C2D231EA7E1EE00EFB7D4 /* ParseTestCase.swift */, 0A85F0E320B314E10095AFFB /* Pinning */, - 1B327F97207BE39400ACEEBD /* ResourceTestCase.swift */, 0A3C2D241EA7E1EE00EFB7D4 /* URLSessionNetworkStackTestCase.swift */, ); path = Network; @@ -1226,8 +1242,8 @@ isa = PBXGroup; children = ( 0AFB0DCD20F8E81700A58515 /* MockAuthenticationChallengeHandler.swift */, - 0AFB0DC920F8E7AE00A58515 /* MockNetworkAuthenticator.swift */, 0A83888E1EB209D100C1E835 /* MockNetworkStack.swift */, + 0AFB0DC920F8E7AE00A58515 /* MockRequestAuthenticator.swift */, 0AFB0DCB20F8E7F400A58515 /* MockRequestInterceptor.swift */, 0AFB0DC220F8E74700A58515 /* MockURLSession.swift */, 0AFB0DC420F8E76900A58515 /* MockURLSessionDataTask.swift */, @@ -1238,6 +1254,11 @@ 0AFB0DD020F8FD0800A58515 /* Resource */ = { isa = PBXGroup; children = ( + 0A02BDDE220D919600CF14C9 /* AuthenticatedHTTPNetworkResourceTestCase.swift */, + 0A02BDE0220D927C00CF14C9 /* HTTPNetworkResourceTestCase.swift */, + 0A02BDDC220D08BA00CF14C9 /* HTTPResourceEndpointTestCase.swift */, + 0A02BDE2220D92DD00CF14C9 /* MockHTTPResourceEndpoint.swift */, + 0A848DE7220C489F00B690E8 /* MockResource.swift */, 0A77982820FCCD24008E269A /* ResourceRetryTestCase.swift */, 0AFB0DD120F8FD1C00A58515 /* RetryableResourceTestCase.swift */, ); @@ -1643,6 +1664,7 @@ 0A3236E720D88ED400D225CB /* LoggerTestCase.swift in Sources */, 0A266F9C1ED59FB6009CD0D7 /* FileLogDestinationTestCase.swift in Sources */, 73C6051D20F3BD0E00D0B643 /* TokenTests.swift in Sources */, + 0A848DE9220C83D100B690E8 /* MockResource.swift in Sources */, 1B4033621ED8C9D400B4B03D /* MockReusableViewModelView.swift in Sources */, 0A266F9D1ED59FB6009CD0D7 /* JSONLogItemFormatterTestCase.swift in Sources */, 0A3907F91FC366BC0050714A /* ViewModelCollectionReusableViewTestCase.swift in Sources */, @@ -1650,6 +1672,7 @@ F19A442D20384DD400AD6448 /* UIViewConstraintTestCase.swift in Sources */, 0A266F9E1ED59FB6009CD0D7 /* DefaultLoggerTestCase.swift in Sources */, 0A266FA01ED59FB6009CD0D7 /* StringLogItemFormatterTestCase.swift in Sources */, + 0A02BDDD220D08BA00CF14C9 /* HTTPResourceEndpointTestCase.swift in Sources */, 0A266FA11ED59FB6009CD0D7 /* JSONTests.swift in Sources */, 0A9AF8C21FC33B070076458E /* ReusableViewCollectionViewTestCase.swift in Sources */, 0A266FA21ED59FB6009CD0D7 /* MappableModel.swift in Sources */, @@ -1673,15 +1696,16 @@ 0A265E1D20EAEA8A00A55ED9 /* BoxTestCase.swift in Sources */, 0A7B505220B6D769005A08E7 /* SecCertificate+PublicKeyTestCase.swift in Sources */, 0AECE6832177799C003A2509 /* DummyCancelableTestCase.swift in Sources */, + 0A02BDE3220D92DD00CF14C9 /* MockHTTPResourceEndpoint.swift in Sources */, 0A266FAB1ED59FB6009CD0D7 /* DiskMemoryPersistenceTestCase.swift in Sources */, 0A9AF8C01FC336F60076458E /* ReusableViewTestCase.swift in Sources */, 0A266FAC1ED59FB6009CD0D7 /* MockPersistenceStack.swift in Sources */, 0A266FAD1ED59FB6009CD0D7 /* CAGradientLayerTestCase.swift in Sources */, 0A266FAE1ED59FB6009CD0D7 /* CALayerTestCase.swift in Sources */, 0A9AF8B61FC30B660076458E /* NibViewTestCase.swift in Sources */, - 0AFB0DCA20F8E7AE00A58515 /* MockNetworkAuthenticator.swift in Sources */, + 0AFB0DCA20F8E7AE00A58515 /* MockRequestAuthenticator.swift in Sources */, 1BBEB60C1F333E6E00D06526 /* UIImageTestCase.swift in Sources */, - 1B327F99207BE6C300ACEEBD /* ResourceTestCase.swift in Sources */, + 0A02BDDF220D919600CF14C9 /* AuthenticatedHTTPNetworkResourceTestCase.swift in Sources */, 0AECE6812177741A003A2509 /* MockCancelable.swift in Sources */, 9DEC00AE209A052300F94353 /* BuilderCacheTestCase.swift in Sources */, 0A266FB11ED59FB6009CD0D7 /* NetworkPersistableStoreTestCase.swift in Sources */, @@ -1689,6 +1713,7 @@ 0A266FB21ED59FB6009CD0D7 /* CollectionReusableViewTestCase.swift in Sources */, 0A3907F71FC35E760050714A /* ReusableViewTableViewTestCase.swift in Sources */, 0A266FB31ED59FB6009CD0D7 /* CollectionViewCellTestCase.swift in Sources */, + 0A02BDE1220D927C00CF14C9 /* HTTPNetworkResourceTestCase.swift in Sources */, 0A266FB41ED59FB6009CD0D7 /* TableViewCellTestCase.swift in Sources */, 0A266FB51ED59FB6009CD0D7 /* TableViewHeaderFooterViewTestCase.swift in Sources */, 0AEEE11820CF208A00B47687 /* MockLogItemFormatter.swift in Sources */, @@ -1730,6 +1755,7 @@ 0A7476C81ECB3F88003024D1 /* ReusableViewModelView.swift in Sources */, 0A83885C1EB1F6B000C1E835 /* NestedContextCoreDataStack.swift in Sources */, 1B40335A1ED8A59100B4B03D /* ViewModelTableViewCell.swift in Sources */, + 0A848DE2220B4B7500B690E8 /* AuthenticatedHTTPNetworkResource.swift in Sources */, 0A3C2D921EA7E5DD00EFB7D4 /* String.swift in Sources */, 0A3C2DA01EA7E5DD00EFB7D4 /* LogItemFormatter.swift in Sources */, 1B57E97F1EB1510D0027AB30 /* AnalyticsTracker.swift in Sources */, @@ -1739,6 +1765,7 @@ 0A708F5420E932EB001784DA /* ModuleLogger.swift in Sources */, 0AE330961EBB71F8003E8506 /* Cancelable.swift in Sources */, 1B687C971FE432BC00F39CFE /* PerformanceMetricsTracker.swift in Sources */, + 0A848DE6220B4D3F00B690E8 /* HTTPNetworkResource.swift in Sources */, 0A8388601EB1F6B000C1E835 /* SiblingContextCoreDataStack.swift in Sources */, 0A1083E220CD8322003969AB /* DispatchQueue.swift in Sources */, 0A3C2DC01EA7E5DD00EFB7D4 /* Placeholder.swift in Sources */, @@ -1759,7 +1786,7 @@ 0A3C2DB61EA7E5DD00EFB7D4 /* Resource.swift in Sources */, 0A3C2D961EA7E5DD00EFB7D4 /* Log+ConsoleLogDestination.swift in Sources */, 0A3C2D901EA7E5DD00EFB7D4 /* Optional.swift in Sources */, - 1B3C01791F0A93CD00DF8394 /* NetworkAuthenticator.swift in Sources */, + 1B3C01791F0A93CD00DF8394 /* RequestAuthenticator.swift in Sources */, 1B327F9D207CF3C500ACEEBD /* JSON+Mappable.swift in Sources */, 0A83885B1EB1F6B000C1E835 /* ManagedObjectReflectable.swift in Sources */, 0A3C2DB01EA7E5DD00EFB7D4 /* PersistenceStack.swift in Sources */, @@ -1822,6 +1849,7 @@ 0A9084391EBCBFB200616076 /* NibView.swift in Sources */, 0ABFFAC41EA8D08B00CFC8BD /* JSON.swift in Sources */, 0A3C2DA31EA7E5DD00EFB7D4 /* Network.swift in Sources */, + 0A848DE4220B4C0100B690E8 /* HTTPResourceEndpoint.swift in Sources */, 0A3C2D8A1EA7E5DD00EFB7D4 /* Route+Tree.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Sources/Network/HTTP.swift b/Sources/Network/HTTP.swift index c53f79f8..ccb9ddde 100644 --- a/Sources/Network/HTTP.swift +++ b/Sources/Network/HTTP.swift @@ -4,7 +4,6 @@ import Foundation public enum HTTP { public typealias Headers = [String : String] - public typealias Query = [String : String] /// An enum describing the HTTP methods. public enum Method: String, Hashable { diff --git a/Sources/Network/Network.swift b/Sources/Network/Network.swift index cfa128c5..86b4b8e8 100644 --- a/Sources/Network/Network.swift +++ b/Sources/Network/Network.swift @@ -25,11 +25,11 @@ public enum Network { public enum Error: Swift.Error { + case noRequest(Swift.Error) case http(code: HTTP.StatusCode, apiError: Swift.Error?, response: URLResponse) case noData(response: URLResponse) case url(Swift.Error, response: URLResponse?) case badResponse(response: URLResponse?) - case authenticator(Swift.Error) case retry(errors: [Swift.Error], totalDelay: ResourceRetry.Delay, retryError: ResourceRetry.Error, @@ -42,18 +42,15 @@ public enum Network { let authenticationChallengeHandler: AuthenticationChallengeHandler? - let authenticator: NetworkAuthenticator? - let requestInterceptors: [RequestInterceptor] let retryQueue: DispatchQueue public init(authenticationChallengeHandler: AuthenticationChallengeHandler? = nil, - authenticator: NetworkAuthenticator? = nil, requestInterceptors: [RequestInterceptor] = [], retryQueue: DispatchQueue) { + self.authenticationChallengeHandler = authenticationChallengeHandler - self.authenticator = authenticator self.requestInterceptors = requestInterceptors self.retryQueue = retryQueue } @@ -63,6 +60,8 @@ public enum Network { extension Network.Error { var response: URLResponse? { switch self { + case .noRequest: + return nil case let .http(_, _, response): return response case let .noData(response): @@ -71,8 +70,6 @@ extension Network.Error { return response case let .badResponse(response): return response - case .authenticator: - return nil case let .retry(_, _, _, response): return response } diff --git a/Sources/Network/NetworkAuthenticator.swift b/Sources/Network/NetworkAuthenticator.swift deleted file mode 100644 index 62b77057..00000000 --- a/Sources/Network/NetworkAuthenticator.swift +++ /dev/null @@ -1,16 +0,0 @@ -import Foundation -import Result - -public protocol NetworkAuthenticator: class { - - typealias PerformRequestClosure = (Result) -> Cancelable - - func authenticate(request: URLRequest, performRequest: @escaping PerformRequestClosure) -> Cancelable -} - -public protocol RetryableNetworkAuthenticator: NetworkAuthenticator { - - typealias RetryPolicy = ResourceRetry.Policy - - func retryPolicyRule() -> RetryPolicy.Rule -} diff --git a/Sources/Network/RequestAuthenticator.swift b/Sources/Network/RequestAuthenticator.swift new file mode 100644 index 00000000..9f87235f --- /dev/null +++ b/Sources/Network/RequestAuthenticator.swift @@ -0,0 +1,53 @@ +import Foundation +import Result + +// A type representing a request authenticator. +public protocol RequestAuthenticator: class { + + /// A type that represents a network request. + associatedtype Request + + /// A type that represents a request authentication error. + associatedtype Error: Swift.Error + + /// The authenticator's authentication handler closure, invoked when a request's authentication finishes. + typealias AuthenticationHandler = (Result) -> Cancelable + + /// Authenticates a request. + /// + /// - Important: The cancelable returned by the `handler` closure *when called asynchronously* should be added + /// as a child of the cancelable returned by this method, so that the async work gets chained and can be cancelled. + /// + /// - Parameters: + /// - request: The request to authenticate. + /// - handler: The closure to handle the request authentication's result (i.e. either the authenticated request + /// or an error). + /// - Returns: A cancelable to cancel the operation. + @discardableResult + func authenticate(_ request: Request, handler: @escaping AuthenticationHandler) -> Cancelable +} + +/// A type representing a request authenticator that provides a retry policy rule (to handle authentication errors). +public protocol RetryableRequestAuthenticator: RequestAuthenticator { + + /// A type that represents a resource's remote type. + associatedtype Remote + + /// A type that represent a network response. + associatedtype Response + + /// The authenticator's specialized retry policy. + typealias RetryPolicy = ResourceRetry.Policy + + /// The retry policy used to evaluate which action to take when an error occurs. + var retryPolicyRule: RetryPolicy.Rule { get } +} + +// A type representing a request authenticator specialized to authenticate `URLRequest`'s. +public protocol URLRequestAuthenticator: RequestAuthenticator +where Request == URLRequest {} + +/// A type representing a request authenticator specialized to authenticate `URLRequest`'s that provides a retry policy +/// rule (to handle authentication errors) specialized for `Data` remote type, `URLRequest`'s and `URLResponse`'s. +public protocol RetryableURLRequestAuthenticator: RetryableRequestAuthenticator +where Remote == Data, Request == URLRequest, Response == URLResponse {} diff --git a/Sources/Network/URLSessionNetworkStack.swift b/Sources/Network/URLSessionNetworkStack.swift index fc7b2981..c5bd6cf6 100644 --- a/Sources/Network/URLSessionNetworkStack.swift +++ b/Sources/Network/URLSessionNetworkStack.swift @@ -12,7 +12,6 @@ public extension Network { public typealias URLSessionDataTaskClosure = (Data?, URLResponse?, Swift.Error?) -> Void private let authenticationChallengeHandler: AuthenticationChallengeHandler? - private let authenticator: NetworkAuthenticator? private let requestInterceptors: [RequestInterceptor] private let retryQueue: DispatchQueue @@ -34,34 +33,38 @@ public extension Network { } public init(authenticationChallengeHandler: AuthenticationChallengeHandler? = nil, - authenticator: NetworkAuthenticator? = nil, requestInterceptors: [RequestInterceptor] = [], retryQueue: DispatchQueue) { + self.authenticationChallengeHandler = authenticationChallengeHandler - self.authenticator = authenticator self.requestInterceptors = requestInterceptors self.retryQueue = retryQueue } public convenience init(configuration: Network.Configuration) { + self.init(authenticationChallengeHandler: configuration.authenticationChallengeHandler, - authenticator: configuration.authenticator, requestInterceptors: configuration.requestInterceptors, retryQueue: configuration.retryQueue) } @discardableResult - public func fetch(resource: R, completion: @escaping Network.CompletionClosure) - -> Cancelable + public func fetch(resource: R, completion: @escaping Network.CompletionClosure) -> Cancelable where R: NetworkResource & RetryableResource, R.Remote == Remote, R.Request == Request, R.Response == Response { - guard let authenticator = authenticator else { - let request = resource.request + return resource.makeRequest { [ weak self] result -> Cancelable in - return perform(request: request, resource: resource, completion: completion) - } + guard let strongSelf = self else { return DummyCancelable() } + + switch result { + case let .success(request): + return strongSelf.perform(request: request, resource: resource, completion: completion) - return authenticatedFetch(using: authenticator, resource: resource, completion: completion) + case let .failure(error): + completion(.failure(.noRequest(error.error))) + return DummyCancelable() + } + } } // MARK: - URLSessionDelegate Methods @@ -109,6 +112,7 @@ public extension Network { return cancelableBag } + // swiftlint:disable:next function_body_length private func handleHTTPResponse(with completion: @escaping Network.CompletionClosure, request: Request, resource: R, @@ -177,28 +181,6 @@ public extension Network { } } - private func authenticatedFetch(using authenticator: NetworkAuthenticator, - resource: R, - completion: @escaping Network.CompletionClosure) -> Cancelable - where R: NetworkResource & RetryableResource, R.Remote == Remote, R.Request == Request, R.Response == Response { - - let request = resource.request - - return authenticator.authenticate(request: request) { [weak self] result -> Cancelable in - - guard let strongSelf = self else { return DummyCancelable() } - - switch result { - case let .success(authenticatedRequest): - return strongSelf.perform(request: authenticatedRequest, resource: resource, completion: completion) - - case let .failure(error): - completion(.failure(.authenticator(error.error))) - return DummyCancelable() - } - } - } - // swiftlint:disable:next function_body_length function_parameter_count private func handleError(with completion: @escaping Network.CompletionClosure, request: Request, diff --git a/Sources/Resource/AuthenticatedHTTPNetworkResource.swift b/Sources/Resource/AuthenticatedHTTPNetworkResource.swift new file mode 100644 index 00000000..f4e4a0da --- /dev/null +++ b/Sources/Resource/AuthenticatedHTTPNetworkResource.swift @@ -0,0 +1,21 @@ +import Foundation +import struct Result.AnyError + +/// A type representing a resource that is fetched via HTTP using a specific type of endpoint requiring authentication. +public protocol AuthenticatedHTTPNetworkResource: HTTPNetworkResource { + + /// A type that represents a request authenticator. + associatedtype Authenticator: RequestAuthenticator where Authenticator.Request == Request + + /// The resource's request authenticator. + var authenticator: Authenticator { get } +} + +extension AuthenticatedHTTPNetworkResource { + + @discardableResult + public func makeRequest(_ handler: @escaping MakeRequestHandler) -> Cancelable { + + return authenticator.authenticate(endpoint.request) { handler($0.mapError(AnyError.init)) } + } +} diff --git a/Sources/Resource/HTTPNetworkResource.swift b/Sources/Resource/HTTPNetworkResource.swift new file mode 100644 index 00000000..5917f199 --- /dev/null +++ b/Sources/Resource/HTTPNetworkResource.swift @@ -0,0 +1,20 @@ +import Foundation + +/// A type representing a resource that is fetched via HTTP using a specific type of endpoint. +public protocol HTTPNetworkResource: NetworkResource where Request == URLRequest { + + /// A type that represents an HTTP endpoint. + associatedtype Endpoint: HTTPResourceEndpoint + + /// The resource's endpoint. + var endpoint: Endpoint { get } +} + +extension HTTPNetworkResource { + + @discardableResult + public func makeRequest(_ handler: @escaping MakeRequestHandler) -> Cancelable { + + return handler(.success(endpoint.request)) + } +} diff --git a/Sources/Resource/HTTPResourceEndpoint.swift b/Sources/Resource/HTTPResourceEndpoint.swift new file mode 100644 index 00000000..ca34a02b --- /dev/null +++ b/Sources/Resource/HTTPResourceEndpoint.swift @@ -0,0 +1,66 @@ +import Foundation + +/// A type representing an HTTP resource's endpoint, to generate its request. +/// +/// Especially useful when conformed to by an enum, allowing a type safe modelling of an API's endpoints. +public protocol HTTPResourceEndpoint { + + /// The HTTP method. + var method: HTTP.Method { get } + + /// The base URL. + var baseURL: URL { get } + + /// The URL's path subcomponent. + var path: String? { get } + + /// The URL's query string items. + var queryItems: [URLQueryItem]? { get } + + /// The HTTP header fields. + var headers: HTTP.Headers? { get } + + // The HTTP message body data. + var body: Data? { get } +} + +public extension HTTPResourceEndpoint { + + var path: String? { return nil } + var queryItems: [URLQueryItem]? { return nil } + var headers: HTTP.Headers? { return nil } + var body: Data? { return nil } +} + +public extension HTTPResourceEndpoint { + + /// The endpoint's generated request. + var request: URLRequest { + + guard var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: false) else { + assertionFailure("๐Ÿ˜ฑ: failed to create components from URL: \(baseURL) on \(type(of: self))!") + return URLRequest(url: baseURL) + } + + if let queryItems = queryItems { + components.queryItems = (components.queryItems ?? []) + queryItems + } + + if let path = path { + components.path = components.path.appending(path).replacingOccurrences(of: "//", with: "/") + } + + guard let url = components.url else { + assertionFailure("๐Ÿ˜ฑ: failed to extract URL from components: \(components) on \(type(of: self))!") + return URLRequest(url: baseURL) + } + + var urlRequest = URLRequest(url: url) + + urlRequest.httpMethod = method.rawValue + urlRequest.allHTTPHeaderFields = headers + urlRequest.httpBody = body + + return urlRequest + } +} diff --git a/Sources/Resource/NetworkResource.swift b/Sources/Resource/NetworkResource.swift index a2ec7eb9..942a03e8 100644 --- a/Sources/Resource/NetworkResource.swift +++ b/Sources/Resource/NetworkResource.swift @@ -1,81 +1,26 @@ import Foundation +import Result +/// A type representing a resource that is fetched from the network. public protocol NetworkResource: Resource { - var request: URLRequest { get } + /// A type representing the network request. + associatedtype Request - static var empty: Remote { get } -} - -public protocol RelativeNetworkResource: NetworkResource { - - static var baseURL: URL { get } - - var path: String { get } - var method: HTTP.Method { get } - var headers: HTTP.Headers? { get } - var query: HTTP.Query? { get } - var body: Data? { get } -} - -extension RelativeNetworkResource { - - public var request: URLRequest { - var url = Self.baseURL - - if var components = URLComponents(url: url, resolvingAgainstBaseURL: false) { - components.queryItems = build(queryItems: query) - components.path = components.path - .appending(path) - .replacingOccurrences(of: "//", with: "/") - - components.url.then { - url = $0 - } - } - - return buildRequest(for: url, method: method, headers: headers, body: body) - } -} - -public protocol StaticNetworkResource: NetworkResource { - - var url: URL { get } - var method: HTTP.Method { get } - var headers: HTTP.Headers? { get } - var query: HTTP.Query? { get } - var body: Data? { get } -} - -extension StaticNetworkResource { - - public var request: URLRequest { - - var newUrl = url - - if var components = URLComponents(url: url, resolvingAgainstBaseURL: false) { - build(queryItems: query).then { components.queryItems = (components.queryItems ?? []) + $0 } - - components.url.then { newUrl = $0 } - } - - return buildRequest(for: newUrl, method: method, headers: headers, body: body) - } -} - -private func build(queryItems: HTTP.Query?) -> [URLQueryItem]? { - guard let queryItems = queryItems, queryItems.isEmpty == false else { return nil } - - return queryItems.map { URLQueryItem(name: $0, value: $1) } -} + /// The resource's make request handler closure, invoked when the request generation finishes. + typealias MakeRequestHandler = (Result) -> Cancelable -private func buildRequest(for url: URL, method: HTTP.Method, headers: HTTP.Headers?, body: Data?) -> URLRequest { + /// Generates a new request to fetch the resource (to be scheduled by the network client). + /// + /// - Important: The cancelable returned by the `handler` closure *when called asynchronously* should be added + /// as a child of the cancelable returned by this method, so that the async work gets chained and can be cancelled. + /// + /// - Parameter completion: The closure to handle the request generation's result (i.e. either the new request or + /// an error). + /// - Returns: A cancelable to cancel the operation. + @discardableResult + func makeRequest(_ handler: @escaping MakeRequestHandler) -> Cancelable - var urlRequest = URLRequest(url: url) - - urlRequest.allHTTPHeaderFields = headers - urlRequest.httpBody = body - urlRequest.httpMethod = method.rawValue - - return urlRequest + /// An empty instance of the resource's remote type (e.g. used for returning a value on 204's HTTP status codes). + static var empty: Remote { get } } diff --git a/Tests/AlicerceTests/Network/Mocks/MockNetworkAuthenticator.swift b/Tests/AlicerceTests/Network/Mocks/MockNetworkAuthenticator.swift deleted file mode 100644 index 9aeb949e..00000000 --- a/Tests/AlicerceTests/Network/Mocks/MockNetworkAuthenticator.swift +++ /dev/null @@ -1,21 +0,0 @@ -import Foundation -import Result -@testable import Alicerce - -final class MockNetworkAuthenticator: NetworkAuthenticator, RetryableNetworkAuthenticator { - - enum Error: Swift.Error { - case ๐Ÿšซ - } - - var mockAuthenticateClosure: ((URLRequest) -> Result)? - var mockRetryPolicyRule: RetryPolicy.Rule = { _, _, _, _, _, _ in .noRetry(.custom(Error.๐Ÿšซ)) } - - func authenticate(request: URLRequest, - performRequest: @escaping NetworkAuthenticator.PerformRequestClosure) -> Cancelable { - - return performRequest(mockAuthenticateClosure?(request) ?? .success(request)) - } - - func retryPolicyRule() -> RetryPolicy.Rule { return mockRetryPolicyRule } -} diff --git a/Tests/AlicerceTests/Network/Mocks/MockRequestAuthenticator.swift b/Tests/AlicerceTests/Network/Mocks/MockRequestAuthenticator.swift new file mode 100644 index 00000000..beef3e56 --- /dev/null +++ b/Tests/AlicerceTests/Network/Mocks/MockRequestAuthenticator.swift @@ -0,0 +1,22 @@ +import Foundation +import Result +@testable import Alicerce + +final class MockRequestAuthenticator: RetryableURLRequestAuthenticator { + + typealias Request = URLRequest + typealias Remote = Data + typealias Response = URLResponse + + enum Error: Swift.Error { case ๐Ÿšซ } + + var mockAuthenticate: (Request) -> Result = { .success($0) } + var mockRetryPolicyRule: RetryPolicy.Rule = { _, _, _, _, _, _ in .noRetry(.custom(Error.๐Ÿšซ)) } + + func authenticate(_ request: Request, handler: @escaping AuthenticationHandler) -> Cancelable { + + return handler(mockAuthenticate(request)) + } + + var retryPolicyRule: RetryPolicy.Rule { return mockRetryPolicyRule } +} diff --git a/Tests/AlicerceTests/Network/Mocks/MockURLSession.swift b/Tests/AlicerceTests/Network/Mocks/MockURLSession.swift index 20827ab3..eceafef2 100644 --- a/Tests/AlicerceTests/Network/Mocks/MockURLSession.swift +++ b/Tests/AlicerceTests/Network/Mocks/MockURLSession.swift @@ -7,7 +7,7 @@ final class MockURLSession: URLSession { var mockDataTaskError: Error? = nil var mockURLResponse: URLResponse = URLResponse() - var mockDataTaskResumeInvokedClosure: (() -> Void)? + var mockDataTaskResumeInvokedClosure: ((URLRequest) -> Void)? var mockDataTaskCancelInvokedClosure: (() -> Void)? var mockAuthenticationChallenge: URLAuthenticationChallenge = URLAuthenticationChallenge() @@ -47,7 +47,7 @@ final class MockURLSession: URLSession { dataTask.resumeInvokedClosure = { [weak self] in guard let strongSelf = self else { fatalError("๐Ÿ”ฅ: `self` must be defined!") } - strongSelf.mockDataTaskResumeInvokedClosure?() + strongSelf.mockDataTaskResumeInvokedClosure?(request) strongSelf.delegate?.urlSession?(strongSelf, didReceive: strongSelf.mockAuthenticationChallenge, diff --git a/Tests/AlicerceTests/Network/NetworkTestCase.swift b/Tests/AlicerceTests/Network/NetworkTestCase.swift index d018c576..561dfdde 100644 --- a/Tests/AlicerceTests/Network/NetworkTestCase.swift +++ b/Tests/AlicerceTests/Network/NetworkTestCase.swift @@ -9,7 +9,6 @@ final class NetworkTestCase: XCTestCase { let networkConfiguration = Network.Configuration(retryQueue: DispatchQueue(label: "configuration-retry-queue")) XCTAssertNil(networkConfiguration.authenticationChallengeHandler) - XCTAssertNil(networkConfiguration.authenticator) XCTAssertTrue(networkConfiguration.requestInterceptors.isEmpty) } @@ -22,7 +21,6 @@ final class NetworkTestCase: XCTestCase { retryQueue: DispatchQueue(label: "configuration-retry-queue")) XCTAssertNil(networkConfiguration.authenticationChallengeHandler) - XCTAssertNil(networkConfiguration.authenticator) XCTAssertEqual(networkConfiguration.requestInterceptors.count, 1) guard let configurationDummyRequestInterceptor diff --git a/Tests/AlicerceTests/Network/ResourceTestCase.swift b/Tests/AlicerceTests/Network/ResourceTestCase.swift deleted file mode 100644 index 6d4a9a7e..00000000 --- a/Tests/AlicerceTests/Network/ResourceTestCase.swift +++ /dev/null @@ -1,82 +0,0 @@ -import XCTest - -@testable import Alicerce - -final class ResourceTestCase: XCTestCase { - - // MARK: - StaticNetworkResource - - func testStaticNetworkResource_WithoutQueryItemsOnPathAndNoQueryItems_ItShouldReturnTheSameURL() { - let url = URL(string: "http://test.com/somepath/")! - let resource = MockStaticNetworkResource(url: url) - - let builtRequest = resource.request - - XCTAssertNotNil(builtRequest.url) - XCTAssertEqual(builtRequest.url, url, "๐Ÿ’ฅ the URLs should be the same") - } - - func testStaticNetworkResource_WithQueryItemsOnPathAndNoQueryItems_ItShouldReturnTheSameURL() { - let url = URL(string: "http://test.com/somepath/?item1=one&item2=two")! - let resource = MockStaticNetworkResource(url: url) - - let builtRequest = resource.request - - XCTAssertNotNil(builtRequest.url) - XCTAssertEqual(builtRequest.url, url, "๐Ÿ’ฅ the URLs should be the same") - } - - func testStaticNetworkResource_WithoutQueryItemsOnPathAndQueryItems_ItShouldReturnTheURLWithTheQueryItems() { - let url = URL(string: "http://test.com/somepath/")! - let resource = MockStaticNetworkResource(url: url, query: ["item1" : "one", "item2" : "two"]) - - let builtRequest = resource.request - - var components = URLComponents(url: url, resolvingAgainstBaseURL: false)! - components.queryItems = resource.query.flatMap { $0.map { URLQueryItem(name: $0, value: $1) } } - - let builtURL = components.url! - - XCTAssertNotNil(builtRequest.url) - XCTAssertEqual(builtRequest.url, builtURL, "๐Ÿ’ฅ the URLs should be the same") - } - - func testStaticNetworkResource_WithQueryItemsOnPathAndQueryItems_ItShouldReturnTheURLWithBothQueryItems() { - let url = URL(string: "http://test.com/somepath/?item1=one&item2=two")! - let resource = MockStaticNetworkResource(url: url, query: ["item3" : "three", "item4" : "four"]) - - let builtRequest = resource.request - - var components = URLComponents(url: url, resolvingAgainstBaseURL: false)! - components.queryItems = components.queryItems! + resource.query.flatMap { $0.map { URLQueryItem(name: $0, value: $1) } }! - - let builtURL = components.url! - - XCTAssertNotNil(builtRequest.url) - XCTAssertEqual(builtRequest.url, builtURL, "๐Ÿ’ฅ the URLs should be the same") - } -} - -private enum NoError: Swift.Error { case empty } - -private struct MockStaticNetworkResource: StaticNetworkResource { - static var empty: Void = () - - let parse: ResourceMapClosure = { _ in XCTFail("๐Ÿ’ฅ How was this possible? ๐Ÿ˜ณ") } - let serialize: ResourceMapClosure = { _ in XCTFail("๐Ÿ’ฅ How was this possible? ๐Ÿ˜ณ") } - let errorParser: ResourceErrorParseClosure = { _ in XCTFail("๐Ÿ’ฅ How was this possible? ๐Ÿ˜ณ"); return NoError.empty } - - let url: URL - let headers: HTTP.Headers? - let method: HTTP.Method - let query: HTTP.Query? - let body: Data? - - init(url: URL, headers: HTTP.Headers? = nil, method: HTTP.Method = .GET, query: HTTP.Query? = nil, body: Data? = nil) { - self.url = url - self.headers = headers - self.method = method - self.query = query - self.body = body - } -} diff --git a/Tests/AlicerceTests/Network/URLSessionNetworkStackTestCase.swift b/Tests/AlicerceTests/Network/URLSessionNetworkStackTestCase.swift index 8522666b..635bd9fa 100644 --- a/Tests/AlicerceTests/Network/URLSessionNetworkStackTestCase.swift +++ b/Tests/AlicerceTests/Network/URLSessionNetworkStackTestCase.swift @@ -2,144 +2,76 @@ import XCTest import Result @testable import Alicerce -private struct URLSessionMockResource: StaticNetworkResource & RetryableResource { - - static var empty: Data { return Data() } - - var url: URL - var path: String - var method: HTTP.Method - var headers: HTTP.Headers? - var query: HTTP.Query? - var body: Data? - - var retryErrors: [Swift.Error] - var totalRetriedDelay: ResourceRetry.Delay - var retryPolicies: [ResourceRetry.Policy] - - let parse: ResourceMapClosure - let serialize: ResourceMapClosure - let errorParser: ResourceErrorParseClosure -} - final class URLSessionNetworkStackTestCase: XCTestCase { - private var networkStackRetryQueue: DispatchQueue! - private var networkStack: Network.URLSessionNetworkStack! - private var mockSession: MockURLSession! - - private var authenticatorNetworkStackRetryQueue: DispatchQueue! - private var authenticatorNetworkStack: Network.URLSessionNetworkStack! - private var mockAuthenticator: MockNetworkAuthenticator! - private var mockAuthenticatorSession: MockURLSession! - - private var mockRequestHandler: MockRequestInterceptor! - private var requestHandlerNetworkStackRetryQueue: DispatchQueue! - private var requestHandlerNetworkStack: Network.URLSessionNetworkStack! - private var mockRequestHandlerSession: MockURLSession! + private typealias Resource = MockResource + private typealias RetryPolicy = Resource.RetryPolicy private enum MockError: Error { case ๐Ÿ”ฅ } - private enum APIError: Error { - case ๐Ÿ’ฉ - case ๐Ÿ’ฅ - } + private var networkStackRetryQueue: DispatchQueue! + private var networkStack: Network.URLSessionNetworkStack! + private var mockSession: MockURLSession! + private var requestInterceptor: MockRequestInterceptor! - private typealias Resource = URLSessionMockResource - private typealias RetryPolicy = Resource.RetryPolicy + private var resource: Resource! + + private let successResponse = HTTPURLResponse(url: URL(string: "https://mindera.com")!, + statusCode: 200, + httpVersion: nil, + headerFields: nil)! + private let failureResponse = HTTPURLResponse(url: URL(string: "https://mindera.com")!, + statusCode: 500, + httpVersion: nil, + headerFields: nil)! fileprivate let expectationTimeout: TimeInterval = 5 override func setUp() { super.setUp() + requestInterceptor = MockRequestInterceptor() + networkStackRetryQueue = DispatchQueue(label: "network-stack.retry-queue") - networkStack = Network.URLSessionNetworkStack(retryQueue: networkStackRetryQueue) + networkStack = Network.URLSessionNetworkStack(requestInterceptors: [requestInterceptor], + retryQueue: networkStackRetryQueue) mockSession = MockURLSession(delegate: networkStack) networkStack.session = mockSession - mockAuthenticator = MockNetworkAuthenticator() - authenticatorNetworkStackRetryQueue = DispatchQueue(label: "authenticator-network-stack.retry-queue") - authenticatorNetworkStack = Network.URLSessionNetworkStack(authenticator: mockAuthenticator, - retryQueue: authenticatorNetworkStackRetryQueue) - mockAuthenticatorSession = MockURLSession(delegate: authenticatorNetworkStack) - - authenticatorNetworkStack.session = mockAuthenticatorSession - - mockRequestHandler = MockRequestInterceptor() - requestHandlerNetworkStackRetryQueue = DispatchQueue(label: "request-handler-network-stack.retry-queue") - requestHandlerNetworkStack = Network.URLSessionNetworkStack(requestInterceptors: [mockRequestHandler], - retryQueue: requestHandlerNetworkStackRetryQueue) - mockRequestHandlerSession = MockURLSession(delegate: requestHandlerNetworkStack) - - requestHandlerNetworkStack.session = mockRequestHandlerSession + resource = Resource() } override func tearDown() { networkStackRetryQueue = nil networkStack = nil mockSession = nil + requestInterceptor = nil - mockAuthenticatorSession = nil - authenticatorNetworkStackRetryQueue = nil - authenticatorNetworkStack = nil - mockAuthenticator = nil - - mockRequestHandlerSession = nil - requestHandlerNetworkStackRetryQueue = nil - requestHandlerNetworkStack = nil - mockRequestHandler = nil + resource = nil super.tearDown() } - private func buildResource(url: URL = URL(string: "http://0.0.0.0")!, - parse: @escaping ResourceMapClosure = { _ in () }, - serialize: @escaping ResourceMapClosure = { _ in Data() }, - errorParser: @escaping ResourceErrorParseClosure = { _ in APIError.๐Ÿ’ฅ }) - -> Resource { - return URLSessionMockResource(url: url, - path: "", - method: .GET, - headers: nil, - query: nil, - body: nil, - retryErrors: [], - totalRetriedDelay: 0, - retryPolicies: [], - parse: parse, - serialize: serialize, - errorParser: errorParser) - } - // MARK: - Success tests - // MARK: without authenticator - func testConvenienceInit_WithValidProperties_ShouldPopulateAllProperties() { let expectation = self.expectation(description: "testConvenienceInit") defer { waitForExpectations(timeout: expectationTimeout) } - let url = URL(string: "http://0.0.0.0")! let networkConfiguration = Network.Configuration(retryQueue: networkStackRetryQueue) networkStack = Network.URLSessionNetworkStack(configuration: networkConfiguration) mockSession = MockURLSession(delegate: networkStack) - mockSession.mockURLResponse = HTTPURLResponse(url: url, - statusCode: 200, - httpVersion: nil, - headerFields: nil)! - - let mockData = "๐ŸŽ‰".data(using: .utf8) - mockSession.mockDataTaskData = mockData - networkStack.session = mockSession - let resource = buildResource(url: url) + mockSession.mockDataTaskData = "๐ŸŽ‰".data(using: .utf8) + mockSession.mockURLResponse = successResponse + + resource.mockParse = { _ in () } networkStack.fetch(resource: resource) { result in @@ -155,18 +87,13 @@ final class URLSessionNetworkStackTestCase: XCTestCase { let expectation = self.expectation(description: "testFetch") defer { waitForExpectations(timeout: expectationTimeout) } - let baseURL = URL(string: "http://")! - let mockResponse = HTTPURLResponse(url: baseURL, - statusCode: 200, - httpVersion: nil, - headerFields: nil)! - - mockSession.mockURLResponse = mockResponse - let mockData = "๐ŸŽ‰".data(using: .utf8) + let mockResponse = successResponse + mockSession.mockDataTaskData = mockData + mockSession.mockURLResponse = mockResponse - let resource = buildResource(url: baseURL) + resource.mockParse = { _ in () } networkStack.fetch(resource: resource) { result in @@ -182,160 +109,36 @@ final class URLSessionNetworkStackTestCase: XCTestCase { } } - func testFetchCancel_ShouldCancelTask() { - let expectation = self.expectation(description: "testFetchCancel") - defer { waitForExpectations(timeout: expectationTimeout) } - - let baseURL = URL(string: "http://")! - - mockSession.mockURLResponse = HTTPURLResponse(url: baseURL, - statusCode: 200, - httpVersion: nil, - headerFields: nil)! - - mockSession.mockDataTaskData = "๐ŸŽ‰".data(using: .utf8) - mockSession.mockDataTaskCancelInvokedClosure = { - expectation.fulfill() - } - - let resource = buildResource(url: baseURL) - - let cancelable = networkStack.fetch(resource: resource) { _ in } - - cancelable.cancel() - } - - // MARK: with authenticator - - func testConvenienceInitWithAuthenticator_WithValidProperties_ShouldPopulateAllProperties() { - let expectation = self.expectation(description: "testConvenienceInitWithAuthenticator") - let expectation2 = self.expectation(description: "authenticate") + func testFetch_WithSuccessfulMakeRequest_ShouldPerformRequest() { + let expectation = self.expectation(description: "testFetch") + let expectation2 = self.expectation(description: "makeRequest") + let expectation3 = self.expectation(description: "performRequest") + let expectation4 = self.expectation(description: "session dataTask") defer { waitForExpectations(timeout: expectationTimeout) } - let url = URL(string: "http://0.0.0.0")! - mockAuthenticator = MockNetworkAuthenticator() - let networkConfiguration = Network.Configuration(authenticator: mockAuthenticator, - retryQueue: authenticatorNetworkStackRetryQueue) - - authenticatorNetworkStack = Network.URLSessionNetworkStack(configuration: networkConfiguration) - mockAuthenticatorSession = MockURLSession(delegate: authenticatorNetworkStack) - - mockAuthenticatorSession.mockURLResponse = HTTPURLResponse(url: url, - statusCode: 200, - httpVersion: nil, - headerFields: nil)! - let mockData = "๐ŸŽ‰".data(using: .utf8) - mockAuthenticatorSession.mockDataTaskData = mockData - - authenticatorNetworkStack.session = mockAuthenticatorSession - - mockAuthenticator.mockAuthenticateClosure = { - expectation2.fulfill() - return .success($0) - } - - let resource = buildResource(url: url) - - authenticatorNetworkStack.fetch(resource: resource) { result in - - if let error = result.error { - XCTFail("๐Ÿ”ฅ: unexpected error \(error)") - } - expectation.fulfill() - } - } - - func testFetchWithAuthenticator_WhenResponseIsSuccessful_ShouldCallCompletionClosureWithData() { - let expectation = self.expectation(description: "testFetchWithAuthenticator") - let expectation2 = self.expectation(description: "authenticate") - defer { waitForExpectations(timeout: expectationTimeout) } + let mockResponse = successResponse - let baseURL = URL(string: "http://")! + mockSession.mockDataTaskData = mockData + mockSession.mockURLResponse = mockResponse - let mockResponse = HTTPURLResponse(url: baseURL, - statusCode: 200, - httpVersion: nil, - headerFields: nil)! - mockAuthenticatorSession.mockURLResponse = mockResponse + let mockRequest = URLRequest(url: URL(string: "https://mindera.com")!) + resource.mockMakeRequest = .success(mockRequest) - let mockData = "๐ŸŽ‰".data(using: .utf8) - mockAuthenticatorSession.mockDataTaskData = mockData - - mockAuthenticator.mockAuthenticateClosure = { + resource.didInvokeMakeRequest = { expectation2.fulfill() - return .success($0) - } - - let resource = buildResource(url: baseURL) - - authenticatorNetworkStack.fetch(resource: resource) { result in - - switch result { - case let .success(response): - XCTAssertEqual(response.value, mockData) - XCTAssertEqual(response.response, mockResponse) - case let .failure(error): - XCTFail("๐Ÿ”ฅ received unexpected error ๐Ÿ‘‰ \(error) ๐Ÿ˜ฑ") - } - - expectation.fulfill() } - } - - func testFetchWithAuthenticator_WhenRequestFailsAndAuthenticatorAllowsRetry_ShouldCallAuthenticateRequestAgain() { - let expectation = self.expectation(description: "testFetchWithAuthenticator") - let expectation2 = self.expectation(description: "authenticate") - let expectation3 = self.expectation(description: "authenticatorRetry") - - defer { waitForExpectations(timeout: expectationTimeout) } - - let baseURL = URL(string: "http://")! - let mockData = "๐ŸŽ‰".data(using: .utf8) - let mockError = MockError.๐Ÿ”ฅ - let mockResponse = HTTPURLResponse(url: baseURL, statusCode: 200, httpVersion: nil, headerFields: nil)! - - mockAuthenticatorSession.mockDataTaskData = mockData - mockAuthenticatorSession.mockDataTaskError = mockError - mockAuthenticatorSession.mockURLResponse = mockResponse - - let numRetriesBeforeSuccess = 2 - var retryCount = 0 - - expectation2.expectedFulfillmentCount = numRetriesBeforeSuccess + 1 - expectation3.expectedFulfillmentCount = numRetriesBeforeSuccess - mockAuthenticator.mockAuthenticateClosure = { - expectation2.fulfill() - return .success($0) + resource.didInvokeMakeRequestHandler = { _ in + expectation3.fulfill() } - var resource = buildResource(url: baseURL) - - mockAuthenticator.mockRetryPolicyRule = { previousErrors, totalDelay, request, error, payload, response in - defer { expectation3.fulfill() } - - XCTAssertEqual(previousErrors.count, retryCount) - previousErrors.forEach { XCTAssertDumpsEqual($0, MockError.๐Ÿ”ฅ) } - XCTAssertEqual(totalDelay, 0) - XCTAssertEqual(request, resource.request) - XCTAssertDumpsEqual(error, MockError.๐Ÿ”ฅ) - XCTAssertEqual(payload, mockData) - XCTAssertEqual(response, mockResponse) - - retryCount += 1 - - // return success after N retries - if retryCount == numRetriesBeforeSuccess { - self.mockAuthenticatorSession.mockDataTaskError = nil - } - - return .retry + mockSession.mockDataTaskResumeInvokedClosure = { + XCTAssertEqual($0, mockRequest) + expectation4.fulfill() } - resource.retryPolicies = [.custom(mockAuthenticator.retryPolicyRule())] - - authenticatorNetworkStack.fetch(resource: resource) { result in + networkStack.fetch(resource: resource) { result in switch result { case let .success(response): @@ -349,32 +152,19 @@ final class URLSessionNetworkStackTestCase: XCTestCase { } } - func testFetchCancelWithAuthenticator_ShouldCancelTask() { - let expectation = self.expectation(description: "testFetchCancelWithAuthenticator") - let expectation2 = self.expectation(description: "authenticate") + func testFetchCancel_ShouldCancelTask() { + let expectation = self.expectation(description: "testFetchCancel") defer { waitForExpectations(timeout: expectationTimeout) } - let baseURL = URL(string: "http://")! - - mockAuthenticatorSession.mockURLResponse = HTTPURLResponse(url: baseURL, - statusCode: 200, - httpVersion: nil, - headerFields: nil)! - - mockAuthenticatorSession.mockDataTaskData = "๐ŸŽ‰".data(using: .utf8) - mockAuthenticatorSession.mockDataTaskCancelInvokedClosure = { + mockSession.mockDataTaskData = "๐ŸŽ‰".data(using: .utf8) + mockSession.mockURLResponse = successResponse + mockSession.mockDataTaskCancelInvokedClosure = { expectation.fulfill() } - mockAuthenticator.mockAuthenticateClosure = { - expectation2.fulfill() - return .success($0) - } - - var resource = buildResource(url: baseURL) - resource.retryPolicies = [.custom(mockAuthenticator.retryPolicyRule())] + resource.mockParse = { _ in () } - let cancelable = authenticatorNetworkStack.fetch(resource: resource) { _ in } + let cancelable = networkStack.fetch(resource: resource) { _ in } cancelable.cancel() } @@ -386,30 +176,28 @@ final class URLSessionNetworkStackTestCase: XCTestCase { let expectation2 = self.expectation(description: "mockRule") defer { waitForExpectations(timeout: expectationTimeout) } - let baseURL = URL(string: "http://")! let mockData = "๐ŸŽ‰".data(using: .utf8) + let mockResponse = successResponse let mockError = MockError.๐Ÿ”ฅ - let mockResponse = HTTPURLResponse(url: baseURL, statusCode: 200, httpVersion: nil, headerFields: nil)! mockSession.mockDataTaskData = mockData - mockSession.mockDataTaskError = mockError mockSession.mockURLResponse = mockResponse + mockSession.mockDataTaskError = mockError + let mockRequest = URLRequest(url: URL(string: "https://mindera.com")!) let numRetriesBeforeSuccess = 2 var retryCount = 0 expectation2.expectedFulfillmentCount = numRetriesBeforeSuccess - var resource = buildResource(url: baseURL) - let mockRule: RetryPolicy.Rule = { previousErrors, totalDelay, request, error, payload, response in defer { expectation2.fulfill() } XCTAssertEqual(previousErrors.count, retryCount) - previousErrors.forEach { XCTAssertDumpsEqual($0, MockError.๐Ÿ”ฅ) } + previousErrors.forEach { XCTAssertDumpsEqual($0, mockError) } XCTAssertEqual(totalDelay, 0) - XCTAssertEqual(request, resource.request) - XCTAssertDumpsEqual(error, MockError.๐Ÿ”ฅ) + XCTAssertEqual(request, mockRequest) + XCTAssertDumpsEqual(error, mockError) XCTAssertEqual(payload, mockData) XCTAssertEqual(response, mockResponse) @@ -423,7 +211,8 @@ final class URLSessionNetworkStackTestCase: XCTestCase { return .retry } - resource.retryPolicies = [.custom(mockRule)] + resource.mockMakeRequest = .success(mockRequest) + resource.mockRetryPolicies = [.custom(mockRule)] networkStack.fetch(resource: resource) { result in @@ -444,31 +233,29 @@ final class URLSessionNetworkStackTestCase: XCTestCase { let expectation2 = self.expectation(description: "mockRule") defer { waitForExpectations(timeout: expectationTimeout) } - let baseURL = URL(string: "http://")! let mockData = "๐ŸŽ‰".data(using: .utf8) + let mockResponse = successResponse let mockError = MockError.๐Ÿ”ฅ - let mockResponse = HTTPURLResponse(url: baseURL, statusCode: 200, httpVersion: nil, headerFields: nil)! mockSession.mockDataTaskData = mockData - mockSession.mockDataTaskError = mockError mockSession.mockURLResponse = mockResponse + mockSession.mockDataTaskError = mockError + let mockRequest = URLRequest(url: URL(string: "https://mindera.com")!) let numRetriesBeforeSuccess = 3 var retryCount = 0 let baseRetryDelay: ResourceRetry.Delay = 0.01 expectation2.expectedFulfillmentCount = numRetriesBeforeSuccess - var resource = buildResource(url: baseURL) - let mockRule: RetryPolicy.Rule = { previousErrors, totalDelay, request, error, payload, response in defer { expectation2.fulfill() } XCTAssertEqual(previousErrors.count, retryCount) - previousErrors.forEach { XCTAssertDumpsEqual($0, MockError.๐Ÿ”ฅ) } + previousErrors.forEach { XCTAssertDumpsEqual($0, mockError) } XCTAssertEqual(totalDelay, baseRetryDelay * Double(retryCount)) - XCTAssertEqual(request, resource.request) - XCTAssertDumpsEqual(error, MockError.๐Ÿ”ฅ) + XCTAssertEqual(request, mockRequest) + XCTAssertDumpsEqual(error, mockError) XCTAssertEqual(payload, mockData) XCTAssertEqual(response, mockResponse) @@ -482,7 +269,8 @@ final class URLSessionNetworkStackTestCase: XCTestCase { return .retryAfter(baseRetryDelay) } - resource.retryPolicies = [.custom(mockRule)] + resource.mockMakeRequest = .success(mockRequest) + resource.mockRetryPolicies = [.custom(mockRule)] networkStack.fetch(resource: resource) { result in @@ -498,36 +286,44 @@ final class URLSessionNetworkStackTestCase: XCTestCase { } } - // MARK: - RequestHandler tests + // MARK: with request interceptor - func testFetch_WithRequestHandler_ShouldCallHandleAndRequest() { - let expectationRequestHandlerHandle = self.expectation(description: "RequestHandler:handle ๐Ÿค™") - let expectationRequestHandlerRequest = self.expectation(description: "RequestHandler:request ๐Ÿค™") + func testFetch_WithRequestInterceptor_ShouldCallHandleAndRequest() { + let expectationRequestInterceptorHandleRequest = self.expectation(description: "intercept request ๐Ÿค™") + let expectationRequestInterceptorHandleResponse = self.expectation(description: "intercept response ๐Ÿค™") defer { waitForExpectations(timeout: expectationTimeout) } - - let baseURL = URL(string: "http://")! - - mockSession.mockURLResponse = HTTPURLResponse(url: baseURL, - statusCode: 200, - httpVersion: nil, - headerFields: nil)! - + + let mockResponse = HTTPURLResponse(url: URL(string: "https://mindera.com")!, + statusCode: 200, + httpVersion: nil, + headerFields: nil)! + let mockData = "๐ŸŽ‰".data(using: .utf8) + + mockSession.mockURLResponse = mockResponse mockSession.mockDataTaskData = mockData + + let mockRequest = URLRequest(url: URL(string: "https://mindera.com")!) + resource.mockMakeRequest = .success(mockRequest) - mockRequestHandler.interceptRequestClosure = { _ in - expectationRequestHandlerHandle.fulfill() + requestInterceptor.interceptRequestClosure = { + + XCTAssertEqual($0, mockRequest) + + expectationRequestInterceptorHandleRequest.fulfill() } - mockRequestHandler.interceptResponseClosure = { _, _, _, _ in - expectationRequestHandlerRequest.fulfill() - } + requestInterceptor.interceptResponseClosure = { response, data, error, request in - let resource = buildResource(url: baseURL) - - let cancelable = requestHandlerNetworkStack.fetch(resource: resource) { _ in } + XCTAssertEqual(response, mockResponse) + XCTAssertEqual(data, mockData) + XCTAssertNil(error) + XCTAssertEqual(request, mockRequest) + + expectationRequestInterceptorHandleResponse.fulfill() + } - cancelable.cancel() + networkStack.fetch(resource: resource) { _ in } } // MARK: - Error tests @@ -536,19 +332,12 @@ final class URLSessionNetworkStackTestCase: XCTestCase { let expectation = self.expectation(description: "testFetch") defer { waitForExpectations(timeout: expectationTimeout) } - let baseURL = URL(string: "http://")! - let statusCode = 500 - let mockError = NSError(domain: "โ˜ ๏ธ", code: statusCode, userInfo: nil) - let mockResponse = HTTPURLResponse(url: baseURL, - statusCode: statusCode, - httpVersion: nil, - headerFields: nil)! + let mockError = NSError(domain: "โ˜ ๏ธ", code: failureResponse.statusCode, userInfo: nil) + let mockResponse = failureResponse mockSession.mockURLResponse = mockResponse mockSession.mockDataTaskError = mockError - let resource = buildResource(url: baseURL) - networkStack.fetch(resource: resource) { result in switch result { @@ -569,8 +358,6 @@ final class URLSessionNetworkStackTestCase: XCTestCase { let expectation = self.expectation(description: "testFetch") defer { waitForExpectations(timeout: expectationTimeout) } - let resource = buildResource() - networkStack.fetch(resource: resource) { result in switch result { @@ -591,26 +378,18 @@ final class URLSessionNetworkStackTestCase: XCTestCase { let expectation = self.expectation(description: "testFetch") defer { waitForExpectations(timeout: expectationTimeout) } - let baseURL = URL(string: "http://")! - let statusCode = 500 - let mockResponse = HTTPURLResponse(url: baseURL, - statusCode: statusCode, - httpVersion: nil, - headerFields: nil)! - + let mockResponse = failureResponse mockSession.mockURLResponse = mockResponse mockSession.mockDataTaskData = nil - let resource = buildResource(url: baseURL) - networkStack.fetch(resource: resource) { result in switch result { case .success: XCTFail("๐Ÿ”ฅ should throw an error ๐Ÿค”") case let .failure(.http(code: receiveStatusCode, apiError: nil, response: receivedResponse)): - XCTAssertEqual(receiveStatusCode.statusCode, statusCode) + XCTAssertEqual(receiveStatusCode.statusCode, mockResponse.statusCode) XCTAssertEqual(receivedResponse, mockResponse) case let .failure(error): XCTFail("๐Ÿ”ฅ received unexpected error ๐Ÿ‘‰ \(error) ๐Ÿ˜ฑ") @@ -624,27 +403,26 @@ final class URLSessionNetworkStackTestCase: XCTestCase { let expectation = self.expectation(description: "testFetch") defer { waitForExpectations(timeout: expectationTimeout) } - let baseURL = URL(string: "http://")! - let statusCode = 500 - let mockResponse = HTTPURLResponse(url: baseURL, statusCode: statusCode, httpVersion: nil, headerFields: nil)! - - mockSession.mockURLResponse = mockResponse - let mockData = "๐Ÿ’ฉ".data(using: .utf8)! + let mockResponse = failureResponse + mockSession.mockDataTaskData = mockData + mockSession.mockURLResponse = mockResponse - let resource = buildResource(url: baseURL, errorParser: { + resource.mockErrorParser = { XCTAssertEqual($0, mockData) - return APIError.๐Ÿ’ฉ - }) + return Resource.MockAPIError.๐Ÿ’ฉ + } networkStack.fetch(resource: resource) { result in switch result { case .success: XCTFail("๐Ÿ”ฅ should throw an error ๐Ÿค”") - case let .failure(.http(code: receiveStatusCode, apiError: APIError.๐Ÿ’ฉ?, response: receivedResponse)): - XCTAssertEqual(receiveStatusCode.statusCode, statusCode) + case let .failure(.http(code: receiveStatusCode, + apiError: Resource.MockAPIError.๐Ÿ’ฉ?, + response: receivedResponse)): + XCTAssertEqual(receiveStatusCode.statusCode, mockResponse.statusCode) XCTAssertEqual(receivedResponse, mockResponse) case let .failure(error): XCTFail("๐Ÿ”ฅ received unexpected error ๐Ÿ‘‰ \(error) ๐Ÿ˜ฑ") @@ -658,13 +436,10 @@ final class URLSessionNetworkStackTestCase: XCTestCase { let expectation = self.expectation(description: "testFetch") defer { waitForExpectations(timeout: expectationTimeout) } - let baseURL = URL(string: "http://")! - let mockResponse = HTTPURLResponse(url: baseURL, statusCode: 200, httpVersion: nil, headerFields: nil)! + let mockResponse = successResponse - mockSession.mockURLResponse = mockResponse mockSession.mockDataTaskData = nil - - let resource = buildResource(url: baseURL) + mockSession.mockURLResponse = mockResponse networkStack.fetch(resource: resource) { result in @@ -692,8 +467,6 @@ final class URLSessionNetworkStackTestCase: XCTestCase { expectation1.fulfill() } - let resource = buildResource() - networkStack.fetch(resource: resource) { result in expectation2.fulfill() } @@ -729,27 +502,33 @@ final class URLSessionNetworkStackTestCase: XCTestCase { networkStack.session = mockSession - let resource = buildResource() - networkStack.fetch(resource: resource) { result in expectation3.fulfill() } } - func testFetchWithAuthenticator_WithThrowingAuthenticate_ShouldThrowTheAuthenticateError() { + func testFetch_WithThrowingMakeRequest_ShouldThrowTheNoRequestError() { let expectation = self.expectation(description: "testFetch") + let expectation2 = self.expectation(description: "makeRequest") + let expectation3 = self.expectation(description: "performRequest") defer { waitForExpectations(timeout: expectationTimeout) } - mockAuthenticator.mockAuthenticateClosure = { _ in .failure(AnyError(MockError.๐Ÿ”ฅ)) } + resource.mockMakeRequest = .failure(AnyError(MockError.๐Ÿ”ฅ)) - let resource = buildResource() + resource.didInvokeMakeRequest = { + expectation2.fulfill() + } - authenticatorNetworkStack.fetch(resource: resource) { result in + resource.didInvokeMakeRequestHandler = { _ in + expectation3.fulfill() + } + + networkStack.fetch(resource: resource) { result in switch result { case .success: XCTFail("๐Ÿ”ฅ should throw an error ๐Ÿค”") - case .failure(.authenticator(MockError.๐Ÿ”ฅ)): + case .failure(.noRequest(MockError.๐Ÿ”ฅ)): // ๐Ÿค  well done sir break case let .failure(error): @@ -767,31 +546,29 @@ final class URLSessionNetworkStackTestCase: XCTestCase { let expectation2 = self.expectation(description: "mockRule") defer { waitForExpectations(timeout: expectationTimeout) } - let baseURL = URL(string: "http://")! let mockData = "๐ŸŽ‰".data(using: .utf8) + let mockResponse = successResponse let mockError = MockError.๐Ÿ”ฅ - let mockResponse = HTTPURLResponse(url: baseURL, statusCode: 200, httpVersion: nil, headerFields: nil)! mockSession.mockDataTaskData = mockData mockSession.mockDataTaskError = mockError mockSession.mockURLResponse = mockResponse + let mockRequest = URLRequest(url: URL(string: "https://mindera.com")!) let numRetries = 3 var retryCount = 0 let baseRetryDelay: ResourceRetry.Delay = 0.01 expectation2.expectedFulfillmentCount = numRetries - var resource = buildResource(url: baseURL) - let mockRule: RetryPolicy.Rule = { previousErrors, totalDelay, request, error, payload, response in defer { expectation2.fulfill() } XCTAssertEqual(previousErrors.count, retryCount) - previousErrors.forEach { XCTAssertDumpsEqual($0, MockError.๐Ÿ”ฅ) } + previousErrors.forEach { XCTAssertDumpsEqual($0, mockError) } XCTAssertEqual(totalDelay, baseRetryDelay * Double(retryCount)) - XCTAssertEqual(request, resource.request) - XCTAssertDumpsEqual(error, MockError.๐Ÿ”ฅ) + XCTAssertEqual(request, mockRequest) + XCTAssertDumpsEqual(error, mockError) XCTAssertEqual(payload, mockData) XCTAssertEqual(response, mockResponse) @@ -799,10 +576,11 @@ final class URLSessionNetworkStackTestCase: XCTestCase { return retryCount < numRetries ? .retryAfter(baseRetryDelay) - : .noRetry(.custom(MockNetworkAuthenticator.Error.๐Ÿšซ)) + : .noRetry(.custom(MockRequestAuthenticator.Error.๐Ÿšซ)) } - resource.retryPolicies = [.custom(mockRule)] + resource.mockMakeRequest = .success(mockRequest) + resource.mockRetryPolicies = [.custom(mockRule)] networkStack.fetch(resource: resource) { result in @@ -811,10 +589,10 @@ final class URLSessionNetworkStackTestCase: XCTestCase { XCTFail("๐Ÿ”ฅ should throw an error ๐Ÿค”") case let .failure(.retry(errors, delay, - ResourceRetry.Error.custom(MockNetworkAuthenticator.Error.๐Ÿšซ), + ResourceRetry.Error.custom(MockRequestAuthenticator.Error.๐Ÿšซ), response)): XCTAssertEqual(response, mockResponse) - XCTAssertDumpsEqual(errors, (0.. + + func testMakeRequest_WithDefaultImplementation_ShouldReturnAuthenticatedEndpointRequestAndPropagateCancelable() { + + let expectation = self.expectation(description: "makeRequest") + let expectation2 = self.expectation(description: "authenticate") + + let resource = Resource() + + var authenticatedRequest = resource.endpoint.request + + authenticatedRequest.allHTTPHeaderFields = { + var headers = $0 ?? [:] + headers["Authorization"] = "Bearer ๐Ÿ”‘" + return headers + }(authenticatedRequest.allHTTPHeaderFields) + + resource.authenticator.mockAuthenticate = { + + defer { expectation2.fulfill() } + + XCTAssertEqual($0, resource.endpoint.request) + + return .success(authenticatedRequest) + } + + let testCancelable = DummyCancelable() + + let cancelable = resource.makeRequest { result in + + defer { expectation.fulfill() } + + switch result { + case .success(let request): + XCTAssertEqual(request, authenticatedRequest) + case .failure(let error): + XCTFail("unexpected error: \(error)!") + } + + return testCancelable + } + + waitForExpectations(timeout: 1) + + XCTAssert(cancelable === testCancelable) + } +} + +class MockAuthenticatedHTTPNetworkResource: MockHTTPNetworkResource, AuthenticatedHTTPNetworkResource { + + typealias Authenticator = MockRequestAuthenticator + + // AuthenticatedHTTPNetworkResource + + var authenticator: Authenticator = MockRequestAuthenticator() +} diff --git a/Tests/AlicerceTests/Resource/HTTPNetworkResourceTestCase.swift b/Tests/AlicerceTests/Resource/HTTPNetworkResourceTestCase.swift new file mode 100644 index 00000000..665dad3d --- /dev/null +++ b/Tests/AlicerceTests/Resource/HTTPNetworkResourceTestCase.swift @@ -0,0 +1,67 @@ +import XCTest +@testable import Alicerce + +class HTTPNetworkResourceTestCase: XCTestCase { + + private typealias Resource = MockHTTPNetworkResource + + func testMakeRequest_WithDefaultImplementation_ShouldReturnEndpointRequestAndPropagateCancelable() { + + let expectation = self.expectation(description: "makeRequest") + + let resource = Resource() + let testCancelable = DummyCancelable() + + let cancelable = resource.makeRequest { result in + + defer { expectation.fulfill() } + + switch result { + case .success(let request): + XCTAssertEqual(request, resource.endpoint.request) + case .failure(let error): + XCTFail("unexpected error: \(error)!") + } + + return testCancelable + } + + waitForExpectations(timeout: 1) + + XCTAssert(cancelable === testCancelable) + } +} + +class MockHTTPNetworkResource: HTTPNetworkResource { + + typealias Remote = Data + typealias Local = T + typealias Error = MockAPIError + typealias Request = URLRequest + typealias Endpoint = MockHTTPResourceEndpoint + + enum MockError: Swift.Error { case ๐Ÿ˜ฑ, ๐Ÿ˜ญ } + enum MockAPIError: Swift.Error { case ๐Ÿคฌ } + + // Mocks + + var mockParse: (Remote) throws -> Local = { _ in throw MockError.๐Ÿ˜ฑ } + var mockSerialize: (Local) throws -> Remote = { _ in throw MockError.๐Ÿ˜ญ } + var mockErrorParser: (Remote) -> Error? = { _ in return MockAPIError.๐Ÿคฌ } + + var mockEndpoint: Endpoint = MockHTTPResourceEndpoint(baseURL: URL(string: "https://mindera.com")!) + + // Resource + + var parse: (Remote) throws -> Local { return mockParse } + var serialize: (Local) throws -> Remote { return mockSerialize } + var errorParser: (Remote) -> Error? { return mockErrorParser } + + // NetworkResource + + static var empty: Remote { return Data() } + + // HTTPNetworkResource + + var endpoint: MockHTTPResourceEndpoint { return mockEndpoint } +} diff --git a/Tests/AlicerceTests/Resource/HTTPResourceEndpointTestCase.swift b/Tests/AlicerceTests/Resource/HTTPResourceEndpointTestCase.swift new file mode 100644 index 00000000..23c62235 --- /dev/null +++ b/Tests/AlicerceTests/Resource/HTTPResourceEndpointTestCase.swift @@ -0,0 +1,160 @@ +import XCTest +import Alicerce + +class HTTPResourceEndpointTestCase: XCTestCase { + + // basic resource (i.e. using extension's default `nil property values for path, query items, headers and body) + + func testRequest_WithResourceUsingExtensionDefaultProperties_ShouldReturnRequestWithJustMethodAndBaseURL() { + + let method = HTTP.Method.GET + let url = URL(string: "https://mindera.com/somepath")! + + let resource = MockBasicHTTPResourceEndpoint(method: method, baseURL: url) + + let builtRequest = resource.request + + XCTAssertEqual(builtRequest.url, url, "๐Ÿ’ฅ the URLs should be the same") + XCTAssertEqual(builtRequest.httpMethod, method.rawValue) + XCTAssertEqual(builtRequest.allHTTPHeaderFields, [:]) + XCTAssertNil(builtRequest.httpBody) + } + + // directly assignable properties (method, headers, body) + + func testRequest_WithDirectlyAssignableProperties_ShouldReturnRequestWithCorrectValues() { + + let method = HTTP.Method.HEAD + let url = URL(string: "https://mindera.com")! + let queryItems = [URLQueryItem(name: "item1", value: "one"), + URLQueryItem(name: "item2", value: "two")] + let headers = ["header1" : "value1", + "header2" : "value2"] + + let body = "๐Ÿš€".data(using: .utf8)! + + let resource = MockHTTPResourceEndpoint(method: method, + baseURL: url, + path: "/somepath/another", + queryItems: queryItems, + headers: headers, + body: body) + + let builtRequest = resource.request + + let expectedURL = URL(string: "https://mindera.com/somepath/another?item1=one&item2=two")! + + XCTAssertEqual(builtRequest.url, expectedURL, "๐Ÿ’ฅ the URLs should be the same") + XCTAssertEqual(builtRequest.httpMethod, method.rawValue) + XCTAssertEqual(builtRequest.allHTTPHeaderFields, headers) + XCTAssertEqual(builtRequest.httpBody, body) + } + + // no custom path + + func testRequest_WithoutPathOnBaseURLAndNoCustomPath_ShouldReturnTheSameURL() { + + let url = URL(string: "https://mindera.com")! + let resource = MockHTTPResourceEndpoint(baseURL: url) + + let builtRequest = resource.request + + XCTAssertEqual(builtRequest.url, url, "๐Ÿ’ฅ the URLs should be the same") + } + + func testRequest_WithPathOnBaseURLAndNoCustomPath_ShouldReturnTheSameURL() { + + let url = URL(string: "https://mindera.com/somepath/")! + + let resource = MockHTTPResourceEndpoint(baseURL: url) + + let builtRequest = resource.request + + XCTAssertEqual(builtRequest.url, url, "๐Ÿ’ฅ the URLs should be the same") + } + + // custom path + + func testRequest_WithoutPathOnBaseURLAndCustomPath_ShouldReturnTheURLWithTheCustomPath() { + + let url = URL(string: "https://mindera.com")! + let path = "/somepath" + + let resource = MockHTTPResourceEndpoint(baseURL: url, path: path) + + let builtRequest = resource.request + + let expectedURL = URL(string: "https://mindera.com/somepath")! + + XCTAssertEqual(builtRequest.url, expectedURL, "๐Ÿ’ฅ the URLs should be the same") + } + + func testRequest_WithPathOnBaseURLAndCustomPath_ShouldReturnTheURLWithConatenatedPaths() { + + let url = URL(string: "https://mindera.com/somepath/")! + let path = "/another/path" + + let resource = MockHTTPResourceEndpoint(baseURL: url, path: path) + + let builtRequest = resource.request + + let expectedURL = URL(string: "https://mindera.com/somepath/another/path")! + + XCTAssertEqual(builtRequest.url, expectedURL, "๐Ÿ’ฅ the URLs should be the same") + } + + // no custom queryItems + + func testRequest_WithoutQueryItemsOnBaseURLAndNoCustomQueryItems_ShouldReturnTheSameURL() { + + let url = URL(string: "https://mindera.com/somepath/")! + let resource = MockHTTPResourceEndpoint(baseURL: url) + + let builtRequest = resource.request + + XCTAssertEqual(builtRequest.url, url, "๐Ÿ’ฅ the URLs should be the same") + } + + func testRequest_WithQueryItemsOnBaseURLAndNoCustomQueryItems_ShouldReturnTheSameURL() { + + let url = URL(string: "https://mindera.com/somepath/?item1=one&item2=two")! + + let resource = MockHTTPResourceEndpoint(baseURL: url) + + let builtRequest = resource.request + + XCTAssertEqual(builtRequest.url, url, "๐Ÿ’ฅ the URLs should be the same") + } + + // custom queryItems + + func testRequest_WithoutQueryItemsOnBaseURLAndCustomQueryItems_ShouldReturnTheURLWithCustomQueryItems() { + + let url = URL(string: "https://mindera.com/somepath/")! + let queryItems = [URLQueryItem(name: "item1", value: "one"), + URLQueryItem(name: "item2", value: "two")] + + let resource = MockHTTPResourceEndpoint(baseURL: url, queryItems: queryItems) + + let builtRequest = resource.request + + let expectedURL = URL(string: "https://mindera.com/somepath/?item1=one&item2=two")! + + XCTAssertEqual(builtRequest.url, expectedURL, "๐Ÿ’ฅ the URLs should be the same") + } + + func testRequest_WithQueryItemsOnBaseURLAndCustomQueryItems_ShouldReturnTheURLWithAllQueryItems() { + + let url = URL(string: "https://mindera.com/somepath/?item1=one&item2=two")! + let queryItems = [URLQueryItem(name: "item3", value: "three"), + URLQueryItem(name: "item4", value: "four")] + + let resource = MockHTTPResourceEndpoint(baseURL: url, queryItems: queryItems) + + let builtRequest = resource.request + + let expectedURL = URL(string: "https://mindera.com/somepath/?item1=one&item2=two&item3=three&item4=four")! + + XCTAssertEqual(builtRequest.url, expectedURL, "๐Ÿ’ฅ the URLs should be the same") + } +} diff --git a/Tests/AlicerceTests/Resource/MockHTTPResourceEndpoint.swift b/Tests/AlicerceTests/Resource/MockHTTPResourceEndpoint.swift new file mode 100644 index 00000000..08a5b909 --- /dev/null +++ b/Tests/AlicerceTests/Resource/MockHTTPResourceEndpoint.swift @@ -0,0 +1,34 @@ +import Foundation +import Alicerce + +struct MockHTTPResourceEndpoint: HTTPResourceEndpoint { + + var method: HTTP.Method + var baseURL: URL + + var path: String? = nil + var queryItems: [URLQueryItem]? = nil + var headers: HTTP.Headers? = nil + var body: Data? = nil + + init(method: HTTP.Method = .GET, + baseURL: URL, + path: String? = nil, + queryItems: [URLQueryItem]? = nil, + headers: HTTP.Headers? = nil, + body: Data? = nil) { + + self.method = method + self.baseURL = baseURL + self.path = path + self.queryItems = queryItems + self.headers = headers + self.body = body + } +} + +struct MockBasicHTTPResourceEndpoint: HTTPResourceEndpoint { + + var method: HTTP.Method + var baseURL: URL +} diff --git a/Tests/AlicerceTests/Resource/MockResource.swift b/Tests/AlicerceTests/Resource/MockResource.swift new file mode 100644 index 00000000..290d9440 --- /dev/null +++ b/Tests/AlicerceTests/Resource/MockResource.swift @@ -0,0 +1,68 @@ +import Foundation +import Result +@testable import Alicerce + +struct MockResource: NetworkResource & RetryableResource & PersistableResource & StrategyFetchResource { + + typealias Remote = Data + typealias Local = T + typealias Error = MockAPIError + + typealias Request = URLRequest + typealias Response = URLResponse + + enum MockError: Swift.Error { case ๐Ÿ’ฃ, ๐Ÿงจ } + enum MockAPIError: Swift.Error { case ๐Ÿ’ฉ } + + // Mocks + + var mockParse: (Remote) throws -> Local = { _ in throw MockError.๐Ÿ’ฃ } + var mockSerialize: (Local) throws -> Remote = { _ in throw MockError.๐Ÿงจ } + var mockErrorParser: (Remote) -> Error? = { _ in return MockAPIError.๐Ÿ’ฉ } + + var didInvokeMakeRequest: (() -> Void)? + var didInvokeMakeRequestHandler: ((Cancelable) -> Void)? + var mockMakeRequest: Result = .success(URLRequest(url: URL(string: "https://mindera.com")!)) + + var mockRetryPolicies: [ResourceRetry.Policy] = [] + + var mockPersistenceKey: String = "๐Ÿ’ฝ" + + var mockStrategy: StoreFetchStrategy = .networkThenPersistence + + // Resource + + var parse: (Remote) throws -> Local { return mockParse } + var serialize: (Local) throws -> Remote { return mockSerialize } + var errorParser: (Remote) -> Error? { return mockErrorParser } + + // NetworkResource + + static var empty: Remote { return Data() } + + @discardableResult + func makeRequest(_ handler: @escaping MakeRequestHandler) -> Cancelable { + + didInvokeMakeRequest?() + + let cancelable = handler(mockMakeRequest) + + didInvokeMakeRequestHandler?(cancelable) + + return cancelable + } + + // RetryableResource + + var retryErrors: [Swift.Error] = [] + var totalRetriedDelay: ResourceRetry.Delay = 0 + var retryPolicies: [ResourceRetry.Policy] { return mockRetryPolicies } + + // PersistableResource + + var persistenceKey: Persistence.Key { return mockPersistenceKey } + + // StrategyFetchResource + + var strategy: StoreFetchStrategy { return mockStrategy } +} diff --git a/Tests/AlicerceTests/Stores/NetworkPersistableStoreTestCase.swift b/Tests/AlicerceTests/Stores/NetworkPersistableStoreTestCase.swift index 5799796d..fa8e52cd 100644 --- a/Tests/AlicerceTests/Stores/NetworkPersistableStoreTestCase.swift +++ b/Tests/AlicerceTests/Stores/NetworkPersistableStoreTestCase.swift @@ -4,60 +4,9 @@ import Result class NetworkPersistableStoreTestCase: XCTestCase { - private enum MockAPIError: Error { case ๐Ÿ”ฅ } - private enum TestParseError: Error { case ๐Ÿ’ฉ } - private enum TestSerializeError: Error { case ๐Ÿ’ฉ } - - private struct MockResource: NetworkResource, PersistableResource, StrategyFetchResource, RetryableResource { - - let value: String - let strategy: StoreFetchStrategy - let parse: (Data) throws -> String - let serialize: (String) throws -> Data - let errorParser: (Data) -> MockAPIError? - - var persistenceKey: Persistence.Key { - return value - } + private typealias Resource = MockResource - let request = URLRequest(url: URL(string: "http://localhost")!) - static var empty = Data() - - var retryErrors: [Error] - var totalRetriedDelay: ResourceRetry.Delay - var retryPolicies: [ResourceRetry.Policy] - } - - private let testValueNetwork = "network" - private let testValuePersistence = "persistence" - - private lazy var testDataNetwork: Data = { - return self.testValueNetwork.data(using: .utf8)! - }() - private lazy var testDataPersistence: Data = { - return self.testValuePersistence.data(using: .utf8)! - }() - - private lazy var testResourceNetworkThenPersistence: MockResource = { - return MockResource(value: "network", - strategy: .networkThenPersistence, - parse: { String(data: $0, encoding: .utf8)! }, - serialize: { $0.data(using: .utf8)! }, - errorParser: { _ in .๐Ÿ”ฅ }, - retryErrors: [], - totalRetriedDelay: 0, - retryPolicies: []) - }() - private lazy var testResourcePersistenceThenNetwork: MockResource = { - return MockResource(value: "persistence", - strategy: .persistenceThenNetwork, - parse: { String(data: $0, encoding: .utf8)! }, - serialize: { $0.data(using: .utf8)! }, - errorParser: { _ in .๐Ÿ”ฅ }, - retryErrors: [], - totalRetriedDelay: 0, - retryPolicies: []) - }() + private enum TestParseError: Error { case ๐Ÿ’ฉ } private let expectationTimeout: TimeInterval = 5 private let expectationHandler: XCWaitCompletionHandler = { error in @@ -66,10 +15,27 @@ class NetworkPersistableStoreTestCase: XCTestCase { } } - var networkStack: MockNetworkStack! - var persistenceStack: MockPersistenceStack! + private var networkStack: MockNetworkStack! + private var persistenceStack: MockPersistenceStack! - var store: NetworkPersistableStore! + private var store: NetworkPersistableStore! + + private var resource: Resource! + + private let networkValue = "๐ŸŒ" + private let persistenceValue = "๐Ÿ’พ" + + private lazy var networkData = networkValue.data(using: .utf8)! + private lazy var persistenceData = persistenceValue.data(using: .utf8)! + + private let successResponse = HTTPURLResponse(url: URL(string: "https://mindera.com")!, + statusCode: 200, + httpVersion: nil, + headerFields: nil)! + private let failureResponse = HTTPURLResponse(url: URL(string: "https://mindera.com")!, + statusCode: 500, + httpVersion: nil, + headerFields: nil)! override func setUp() { super.setUp() @@ -80,13 +46,18 @@ class NetworkPersistableStoreTestCase: XCTestCase { store = NetworkPersistableStore(networkStack: networkStack, persistenceStack: persistenceStack, performanceMetrics: nil) + + resource = Resource() } override func tearDown() { networkStack = nil persistenceStack = nil + store = nil + resource = nil + super.tearDown() } @@ -102,15 +73,13 @@ class NetworkPersistableStoreTestCase: XCTestCase { defer { waitForExpectations(timeout: expectationTimeout, handler: expectationHandler) } // Given - let baseURL = URL(string: "http://")! - let mockResponse = HTTPURLResponse(url: baseURL, - statusCode: 200, - httpVersion: nil, - headerFields: nil)! + let mockResponse = successResponse networkStack.mockError = .noData(response: mockResponse) persistenceStack.mockObjectResult = .success(nil) - let resource = testResourcePersistenceThenNetwork // Parser is OK + + resource.mockStrategy = .persistenceThenNetwork + // When store.fetch(resource: resource) { result in defer { fetchExpectation.fulfill() } @@ -138,15 +107,12 @@ class NetworkPersistableStoreTestCase: XCTestCase { defer { waitForExpectations(timeout: expectationTimeout, handler: expectationHandler) } // Given - let baseURL = URL(string: "http://")! - let mockResponse = HTTPURLResponse(url: baseURL, - statusCode: 200, - httpVersion: nil, - headerFields: nil)! + let mockResponse = successResponse networkStack.mockError = .noData(response: mockResponse) persistenceStack.mockObjectResult = .success(nil) - let resource = testResourceNetworkThenPersistence // Parser is OK + + resource.mockStrategy = .networkThenPersistence // When store.fetch(resource: resource) { result in @@ -175,16 +141,11 @@ class NetworkPersistableStoreTestCase: XCTestCase { defer { waitForExpectations(timeout: expectationTimeout, handler: expectationHandler) } // Given - networkStack.mockData = testDataNetwork + networkStack.mockData = networkData persistenceStack.mockObjectResult = .success(nil) - let resource = MockResource(value: "๐Ÿ’ฅ", - strategy: .persistenceThenNetwork, - parse: { _ in throw Parse.Error.json(TestParseError.๐Ÿ’ฉ) }, - serialize: { _ in throw Serialize.Error.json(TestSerializeError.๐Ÿ’ฉ) }, - errorParser: { _ in nil }, - retryErrors: [], - totalRetriedDelay: 0, - retryPolicies: []) + + resource.mockStrategy = .persistenceThenNetwork + resource.mockParse = { _ in throw Parse.Error.json(TestParseError.๐Ÿ’ฉ) } store.fetch(resource: resource) { result in defer { fetchExpectation.fulfill() } @@ -212,16 +173,11 @@ class NetworkPersistableStoreTestCase: XCTestCase { defer { waitForExpectations(timeout: expectationTimeout, handler: expectationHandler) } // Given - networkStack.mockData = testDataNetwork - persistenceStack.mockObjectResult = .success(testDataPersistence) - let resource = MockResource(value: "๐Ÿ’ฅ", - strategy: .persistenceThenNetwork, - parse: { _ in throw Parse.Error.json(TestParseError.๐Ÿ’ฉ) }, - serialize: { _ in throw Serialize.Error.json(TestSerializeError.๐Ÿ’ฉ) }, - errorParser: { _ in nil }, - retryErrors: [], - totalRetriedDelay: 0, - retryPolicies: []) + networkStack.mockData = networkData + persistenceStack.mockObjectResult = .success(persistenceData) + + resource.mockStrategy = .persistenceThenNetwork + resource.mockParse = { _ in throw Parse.Error.json(TestParseError.๐Ÿ’ฉ) } // When store.fetch(resource: resource) { result in @@ -250,16 +206,11 @@ class NetworkPersistableStoreTestCase: XCTestCase { defer { waitForExpectations(timeout: expectationTimeout, handler: expectationHandler) } // Given - networkStack.mockData = testDataNetwork - persistenceStack.mockObjectResult = .success(testDataPersistence) - let resource = MockResource(value: "๐Ÿ’ฅ", - strategy: .networkThenPersistence, - parse: { _ in throw Parse.Error.json(TestParseError.๐Ÿ’ฉ) }, - serialize: { _ in throw Serialize.Error.json(TestSerializeError.๐Ÿ’ฉ) }, - errorParser: { _ in nil }, - retryErrors: [], - totalRetriedDelay: 0, - retryPolicies: []) + networkStack.mockData = networkData + persistenceStack.mockObjectResult = .success(persistenceData) + + resource.mockStrategy = .networkThenPersistence + resource.mockParse = { _ in throw Parse.Error.json(TestParseError.๐Ÿ’ฉ) } // When store.fetch(resource: resource) { result in @@ -288,15 +239,12 @@ class NetworkPersistableStoreTestCase: XCTestCase { defer { waitForExpectations(timeout: expectationTimeout, handler: expectationHandler) } // Given - let baseURL = URL(string: "http://")! - let mockResponse = HTTPURLResponse(url: baseURL, - statusCode: 200, - httpVersion: nil, - headerFields: nil)! + let mockResponse = successResponse networkStack.mockError = .noData(response: mockResponse) persistenceStack.mockObjectResult = .failure(.๐Ÿ’ฅ) - let resource = testResourceNetworkThenPersistence + + resource.mockStrategy = .networkThenPersistence // When store.fetch(resource: resource) { result in @@ -327,15 +275,12 @@ class NetworkPersistableStoreTestCase: XCTestCase { defer { waitForExpectations(timeout: expectationTimeout, handler: expectationHandler) } // Given - let baseURL = URL(string: "http://")! - let mockResponse = HTTPURLResponse(url: baseURL, - statusCode: 200, - httpVersion: nil, - headerFields: nil)! + let mockResponse = successResponse networkStack.mockError = .noData(response: mockResponse) persistenceStack.mockObjectResult = .failure(.๐Ÿ’ฅ) - let resource = testResourcePersistenceThenNetwork + + resource.mockStrategy = .persistenceThenNetwork // When store.fetch(resource: resource) { result in @@ -372,7 +317,8 @@ class NetworkPersistableStoreTestCase: XCTestCase { cancelable.cancel() } persistenceStack.mockObjectResult = .success(nil) - let resource = testResourcePersistenceThenNetwork + + resource.mockStrategy = .persistenceThenNetwork // When cancelable += store.fetch(resource: resource) { result in @@ -407,7 +353,8 @@ class NetworkPersistableStoreTestCase: XCTestCase { cancelable.cancel() } persistenceStack.mockObjectResult = .success(nil) - let resource = testResourceNetworkThenPersistence + + resource.mockStrategy = .networkThenPersistence // When cancelable += store.fetch(resource: resource) { result in @@ -438,12 +385,13 @@ class NetworkPersistableStoreTestCase: XCTestCase { defer { waitForExpectations(timeout: expectationTimeout, handler: expectationHandler) } // Given - networkStack.mockData = testDataNetwork + networkStack.mockData = networkData networkStack.mockCancelable.mockCancelClosure = { cancelExpectation.fulfill() } persistenceStack.mockObjectResult = .success(nil) - let resource = testResourcePersistenceThenNetwork + + resource.mockStrategy = .persistenceThenNetwork // When let cancelable = store.fetch(resource: resource) { result in @@ -479,11 +427,12 @@ class NetworkPersistableStoreTestCase: XCTestCase { defer { waitForExpectations(timeout: expectationTimeout, handler: expectationHandler) } // Given - networkStack.mockData = testDataNetwork + networkStack.mockData = networkData networkStack.mockCancelable.mockCancelClosure = { cancelExpectation.fulfill() } - let resource = testResourceNetworkThenPersistence + + resource.mockStrategy = .networkThenPersistence // When let cancelable = store.fetch(resource: resource) { result in @@ -529,7 +478,8 @@ class NetworkPersistableStoreTestCase: XCTestCase { networkStack.mockCancelable.mockCancelClosure = { cancelExpectation.fulfill() } - let resource = testResourceNetworkThenPersistence + + resource.mockStrategy = .networkThenPersistence // When let cancelable = store.fetch(resource: resource) { result in @@ -565,7 +515,7 @@ class NetworkPersistableStoreTestCase: XCTestCase { defer { waitForExpectations(timeout: expectationTimeout, handler: expectationHandler) } // Given - networkStack.mockData = testDataNetwork + networkStack.mockData = networkData networkStack.mockCancelable.mockCancelClosure = { cancelExpectation.fulfill() } @@ -573,20 +523,12 @@ class NetworkPersistableStoreTestCase: XCTestCase { // closure to cancel the cancelable var cancelClosure: (() -> Void)? - let cancellingParse: (Data) -> String = { + resource.mockStrategy = .persistenceThenNetwork + resource.mockParse = { cancelClosure?() return String(data: $0, encoding: .utf8)! } - let resource = MockResource(value: self.testValuePersistence, - strategy: .persistenceThenNetwork, - parse: cancellingParse, - serialize: { _ in throw Serialize.Error.json(TestSerializeError.๐Ÿ’ฉ) }, - errorParser: { _ in nil }, - retryErrors: [], - totalRetriedDelay: 0, - retryPolicies: []) - // When let cancelable = store.fetch(resource: resource) { result in defer { fetchExpectation.fulfill() } @@ -640,9 +582,14 @@ class NetworkPersistableStoreTestCase: XCTestCase { defer { waitForExpectations(timeout: expectationTimeout, handler: expectationHandler) } // Given - networkStack.mockData = testDataNetwork + networkStack.mockData = networkData persistenceStack.mockObjectResult = .success(nil) - let resource = testResourcePersistenceThenNetwork + + resource.mockStrategy = .persistenceThenNetwork + resource.mockParse = { + XCTAssertEqual($0, self.networkData) + return self.networkValue + } // When store.fetch(resource: resource) { result in @@ -654,7 +601,7 @@ class NetworkPersistableStoreTestCase: XCTestCase { } XCTAssertTrue(value.isNetwork) - XCTAssertEqual(value.value, self.testValueNetwork) + XCTAssertEqual(value.value, self.networkValue) } networkStack.runMockFetch() @@ -670,9 +617,23 @@ class NetworkPersistableStoreTestCase: XCTestCase { defer { waitForExpectations(timeout: expectationTimeout, handler: expectationHandler) } // Given - networkStack.mockData = testDataNetwork - persistenceStack.mockObjectResult = .success(testDataPersistence) - let resource = testResourcePersistenceThenNetwork + networkStack.mockData = networkData + persistenceStack.mockObjectResult = .success(persistenceData) + + resource.mockStrategy = .persistenceThenNetwork + + var count = 0 + resource.mockParse = { + defer { count += 1 } + + if count == 0 { + XCTAssertEqual($0, self.persistenceData) + return self.persistenceValue + } else { + XCTAssertEqual($0, self.networkData) + return self.networkValue + } + } // When store.fetch(resource: resource) { result in @@ -684,7 +645,7 @@ class NetworkPersistableStoreTestCase: XCTestCase { } XCTAssertTrue(value.isPersistence) - XCTAssertEqual(value.value, self.testValuePersistence) + XCTAssertEqual(value.value, self.persistenceValue) } networkStack.runMockFetch() @@ -700,10 +661,15 @@ class NetworkPersistableStoreTestCase: XCTestCase { defer { waitForExpectations(timeout: expectationTimeout, handler: expectationHandler) } // Given - networkStack.mockData = testDataNetwork + networkStack.mockData = networkData persistenceStack.mockObjectResult = .failure(.๐Ÿ’ฅ) persistenceStack.mockSetObjectResult = .failure(.๐Ÿ’ฅ) - let resource = testResourcePersistenceThenNetwork + + resource.mockStrategy = .persistenceThenNetwork + resource.mockParse = { + XCTAssertEqual($0, self.networkData) + return self.networkValue + } // When store.fetch(resource: resource) { result in @@ -715,7 +681,7 @@ class NetworkPersistableStoreTestCase: XCTestCase { } XCTAssertTrue(value.isNetwork) - XCTAssertEqual(value.value, self.testValueNetwork) + XCTAssertEqual(value.value, self.networkValue) } networkStack.runMockFetch() @@ -731,9 +697,14 @@ class NetworkPersistableStoreTestCase: XCTestCase { defer { waitForExpectations(timeout: expectationTimeout, handler: expectationHandler) } // Given - networkStack.mockData = testDataNetwork - persistenceStack.mockObjectResult = .success(testDataPersistence) - let resource = testResourceNetworkThenPersistence + networkStack.mockData = networkData + persistenceStack.mockObjectResult = .success(persistenceData) + + resource.mockStrategy = .networkThenPersistence + resource.mockParse = { + XCTAssertEqual($0, self.networkData) + return self.networkValue + } // When store.fetch(resource: resource) { result in @@ -745,7 +716,7 @@ class NetworkPersistableStoreTestCase: XCTestCase { } XCTAssertTrue(value.isNetwork) - XCTAssertEqual(value.value, self.testValueNetwork) + XCTAssertEqual(value.value, self.networkValue) } networkStack.runMockFetch() @@ -768,8 +739,13 @@ class NetworkPersistableStoreTestCase: XCTestCase { headerFields: nil)! networkStack.mockError = .noData(response: mockResponse) - persistenceStack.mockObjectResult = .success(testDataPersistence) - let resource = testResourceNetworkThenPersistence + persistenceStack.mockObjectResult = .success(persistenceData) + + resource.mockStrategy = .networkThenPersistence + resource.mockParse = { + XCTAssertEqual($0, self.persistenceData) + return self.persistenceValue + } // When store.fetch(resource: resource) { result in @@ -781,7 +757,7 @@ class NetworkPersistableStoreTestCase: XCTestCase { } XCTAssertTrue(value.isPersistence) - XCTAssertEqual(value.value, self.testValuePersistence) + XCTAssertEqual(value.value, self.persistenceValue) } networkStack.runMockFetch() @@ -797,9 +773,23 @@ class NetworkPersistableStoreTestCase: XCTestCase { defer { waitForExpectations(timeout: expectationTimeout, handler: expectationHandler) } // Given - networkStack.mockData = testDataNetwork - persistenceStack.mockObjectResult = .success(testDataPersistence) - let resource = testResourcePersistenceThenNetwork + networkStack.mockData = networkData + persistenceStack.mockObjectResult = .success(persistenceData) + + resource.mockStrategy = .persistenceThenNetwork + + var count = 0 + resource.mockParse = { + defer { count += 1 } + + if count == 0 { + XCTAssertEqual($0, self.persistenceData) + return self.persistenceValue + } else { + XCTAssertEqual($0, self.networkData) + return self.networkValue + } + } // When store.fetch(resource: resource) { result in @@ -811,7 +801,7 @@ class NetworkPersistableStoreTestCase: XCTestCase { } XCTAssertTrue(value.isPersistence) - XCTAssertEqual(value.value, self.testValuePersistence) + XCTAssertEqual(value.value, self.persistenceValue) } networkStack.runMockFetch() @@ -827,9 +817,14 @@ class NetworkPersistableStoreTestCase: XCTestCase { defer { waitForExpectations(timeout: expectationTimeout, handler: expectationHandler) } // Given - networkStack.mockData = testDataNetwork + networkStack.mockData = networkData persistenceStack.mockObjectResult = .failure(.๐Ÿ’ฅ) - let resource = testResourcePersistenceThenNetwork + + resource.mockStrategy = .persistenceThenNetwork + resource.mockParse = { + XCTAssertEqual($0, self.networkData) + return self.networkValue + } // When store.fetch(resource: resource) { result in @@ -841,7 +836,7 @@ class NetworkPersistableStoreTestCase: XCTestCase { } XCTAssertTrue(value.isNetwork) - XCTAssertEqual(value.value, self.testValueNetwork) + XCTAssertEqual(value.value, self.networkValue) } networkStack.runMockFetch() @@ -865,16 +860,21 @@ class NetworkPersistableStoreTestCase: XCTestCase { performanceMetrics: performanceMetrics) // Given - networkStack.mockData = testDataNetwork + networkStack.mockData = networkData persistenceStack.mockObjectResult = .failure(.๐Ÿ’ฅ) - let resource = testResourcePersistenceThenNetwork + + resource.mockStrategy = .persistenceThenNetwork + resource.mockParse = { + XCTAssertEqual($0, self.networkData) + return self.networkValue + } performanceMetrics.measureSyncInvokedClosure = { identifier, metadata in XCTAssertEqual(identifier, - performanceMetrics.makeParseIdentifier(for: resource, payload: self.testDataNetwork)) + performanceMetrics.makeParseIdentifier(for: self.resource, payload: self.networkData)) XCTAssertDumpsEqual(metadata, - [performanceMetrics.modelTypeMetadataKey : "\(MockResource.Local.self)", - performanceMetrics.payloadSizeMetadataKey : UInt(self.testDataNetwork.count)]) + [performanceMetrics.modelTypeMetadataKey : "\(Resource.Local.self)", + performanceMetrics.payloadSizeMetadataKey : UInt(self.networkData.count)]) measureExpectation.fulfill() } @@ -888,7 +888,7 @@ class NetworkPersistableStoreTestCase: XCTestCase { } XCTAssertTrue(value.isNetwork) - XCTAssertEqual(value.value, self.testValueNetwork) + XCTAssertEqual(value.value, self.networkValue) } networkStack.runMockFetch() diff --git a/Tests/AlicerceTests/Stores/NetworkStoreTestCase.swift b/Tests/AlicerceTests/Stores/NetworkStoreTestCase.swift index ea4f8669..e42677c7 100644 --- a/Tests/AlicerceTests/Stores/NetworkStoreTestCase.swift +++ b/Tests/AlicerceTests/Stores/NetworkStoreTestCase.swift @@ -8,52 +8,25 @@ extension MockNetworkStack: NetworkStore { class NetworkStoreTestCase: XCTestCase { - private enum MockAPIError: Error { case ๐Ÿ”ฅ } + private typealias Resource = MockResource + private typealias NetworkStoreResult = Result, NetworkPersistableStoreError> + private enum MockParseError: Error { case ๐Ÿ’ฉ } private enum MockOtherError: Error { case ๐Ÿ’ฅ } - private struct MockResource: NetworkResource & PersistableResource & StrategyFetchResource & RetryableResource { - - let value: String - let strategy: StoreFetchStrategy - - let parse: (Data) throws -> String - let serialize: (String) throws -> Data - let errorParser: (Data) -> MockAPIError? - - var persistenceKey: Persistence.Key { return value } - - let request = URLRequest(url: URL(string: "http://localhost")!) - static var empty = Data() - - var retryErrors: [Error] - var totalRetriedDelay: ResourceRetry.Delay - var retryPolicies: [ResourceRetry.Policy] - } - - private typealias NetworkStoreResult = Result, NetworkPersistableStoreError> - - private lazy var testResource: MockResource = { - return MockResource(value: "network", - strategy: .networkThenPersistence, - parse: { String(data: $0, encoding: .utf8)! }, - serialize: { $0.data(using: .utf8)! }, - errorParser: { _ in .๐Ÿ”ฅ }, - retryErrors: [], - totalRetriedDelay: 0, - retryPolicies: []) - }() - - var networkStack: MockNetworkStack! + private var networkStack: MockNetworkStack! + private var testResource: Resource! override func setUp() { super.setUp() networkStack = MockNetworkStack() + testResource = Resource() } override func tearDown() { networkStack = nil + testResource = nil super.tearDown() } @@ -64,6 +37,8 @@ class NetworkStoreTestCase: XCTestCase { let expectation = self.expectation(description: "testFetch") defer { waitForExpectations(timeout: 1.0) } + testResource.mockParse = { String(data: $0, encoding: .utf8)! } + let mockValue = "๐ŸŽ‰" networkStack.mockData = mockValue.data(using: .utf8) @@ -121,18 +96,11 @@ class NetworkStoreTestCase: XCTestCase { let expectation = self.expectation(description: "testFetch") defer { waitForExpectations(timeout: 1.0) } - let resource = MockResource(value: "network", - strategy: .networkThenPersistence, - parse: { _ in throw Parse.Error.json(MockParseError.๐Ÿ’ฉ) }, - serialize: { $0.data(using: .utf8)! }, - errorParser: { _ in .๐Ÿ”ฅ }, - retryErrors: [], - totalRetriedDelay: 0, - retryPolicies: []) + testResource.mockParse = { _ in throw Parse.Error.json(MockParseError.๐Ÿ’ฉ) } networkStack.mockData = "๐Ÿค”".data(using: .utf8) - networkStack.fetch(resource: resource) { (result: NetworkStoreResult) in + networkStack.fetch(resource: testResource) { (result: NetworkStoreResult) in switch result { case .success: @@ -181,18 +149,11 @@ class NetworkStoreTestCase: XCTestCase { let expectation = self.expectation(description: "testFetch") defer { waitForExpectations(timeout: 1.0) } - let resource = MockResource(value: "network", - strategy: .networkThenPersistence, - parse: { _ in throw MockOtherError.๐Ÿ’ฅ }, - serialize: { $0.data(using: .utf8)! }, - errorParser: { _ in .๐Ÿ”ฅ }, - retryErrors: [], - totalRetriedDelay: 0, - retryPolicies: []) + testResource.mockParse = { _ in throw MockOtherError.๐Ÿ’ฅ } networkStack.mockData = "๐Ÿค”".data(using: .utf8) - networkStack.fetch(resource: resource) { (result: NetworkStoreResult) in + networkStack.fetch(resource: testResource) { (result: NetworkStoreResult) in switch result { case .success: