diff --git a/.codecov.yml b/.codecov.yml index 749d6f52c..5d9e63362 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -1,7 +1,4 @@ coverage: - range: 80..100 - round: down - precision: 2 ignore: - Kinvey/KinveyApp/**/* - Kinvey/KinveyTests/**/* diff --git a/.travis.yml b/.travis.yml index 40f9c1527..f407181eb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -38,6 +38,9 @@ before_script: - if [ ! -d "Carthage/Build/iOS/KIF.framework" ]; then carthage build --platform iOS KIF; fi +- if [ ! -d "Carthage/Build/iOS/Nimble.framework" ]; then + carthage build --platform iOS Nimble; + fi - date script: - travis_retry make test diff --git a/Cartfile.private b/Cartfile.private index f61d9d0dc..a8be8f7a1 100644 --- a/Cartfile.private +++ b/Cartfile.private @@ -1 +1,2 @@ github "kif-framework/KIF" ~> 3.5 +github "Quick/Nimble" ~> 7.0 diff --git a/Cartfile.resolved b/Cartfile.resolved index 0e5f74c4a..fa9b9cf4b 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -1,7 +1,8 @@ github "kif-framework/KIF" "v3.5.2" github "kishikawakatsumi/KeychainAccess" "v3.0.2" github "tjboneman/NSPredicate-MongoDB-Adaptor" "2444d4a790527eb5c9fcb4e4f7b4af417048ae18" +github "Quick/Nimble" "v7.0.0" github "Hearst-DD/ObjectMapper" "2.2.6" -github "mxcl/PromiseKit" "4.2.0" +github "mxcl/PromiseKit" "4.2.1" github "DaveWoodCom/XCGLogger" "4.0.0" -github "realm/realm-cocoa" "v2.6.2" +github "realm/realm-cocoa" "v2.7.0" diff --git a/Kinvey.podspec b/Kinvey.podspec index 6b65ed0cf..0881fb944 100644 --- a/Kinvey.podspec +++ b/Kinvey.podspec @@ -16,7 +16,7 @@ Pod::Spec.new do |s| # s.name = "Kinvey" - s.version = "3.5.1" + s.version = "3.5.2" s.summary = "Kinvey iOS SDK" # This description is used to generate tags and improve search results. diff --git a/Kinvey/Kinvey.xcodeproj/project.pbxproj b/Kinvey/Kinvey.xcodeproj/project.pbxproj index 67cc537af..7a0aaa7f3 100644 --- a/Kinvey/Kinvey.xcodeproj/project.pbxproj +++ b/Kinvey/Kinvey.xcodeproj/project.pbxproj @@ -26,6 +26,8 @@ 5728213B1C6482C000373EC8 /* URLSessionTaskRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5728213A1C6482C000373EC8 /* URLSessionTaskRequest.swift */; }; 572C457B1C86690700A41935 /* Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = 572C457A1C86690700A41935 /* Date.swift */; }; 573150981CBD8D910022A05C /* QueryTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 573150971CBD8D910022A05C /* QueryTest.swift */; }; + 57373AB21ECCD8C6002842CE /* EntityTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57373AB11ECCD8C6002842CE /* EntityTestCase.swift */; }; + 57373ABF1ECE1849002842CE /* LogTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57373ABE1ECE1849002842CE /* LogTestCase.swift */; }; 57375F901E3FD71D0015A241 /* UploadAndPlayVideoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57375F8F1E3FD71D0015A241 /* UploadAndPlayVideoViewController.swift */; }; 57379DA51C72AAA900E240E9 /* Operation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57379DA41C72AAA900E240E9 /* Operation.swift */; }; 573851AC1D47C7EB00E4712A /* FileCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 573851AB1D47C7EB00E4712A /* FileCache.swift */; }; @@ -68,6 +70,15 @@ 577155511CA0F1D200C91B4B /* StoreTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5706FEC21C1F9A6D0037E7D0 /* StoreTestCase.swift */; }; 577155521CA0F1D400C91B4B /* FindOperationTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5765B8351C92365700080FFA /* FindOperationTest.swift */; }; 577155BB1CA21CC200C91B4B /* Migration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 577155BA1CA21CC200C91B4B /* Migration.swift */; }; + 5771CD611ECF6CDC0057E505 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 57BEAE2E1C98805E00479206 /* QuartzCore.framework */; }; + 5771CD621ECF6CDC0057E505 /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 57BEAE2C1C98805600479206 /* CoreGraphics.framework */; }; + 5771CD641ECF6CDC0057E505 /* RealmSwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 57B0C1DC1CDCE88900492D6C /* RealmSwift.framework */; }; + 5771CD661ECF6CDC0057E505 /* Media.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 57A2ED951C4D5F74006D26A9 /* Media.xcassets */; }; + 5771CD701ECF6D910057E505 /* ForgotToCallSuper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5771CD6F1ECF6D440057E505 /* ForgotToCallSuper.swift */; }; + 5771CD711ECF6ECB0057E505 /* Kinvey.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 57A27C811C178F17000DF951 /* Kinvey.framework */; }; + 5771CD721ECF6EDC0057E505 /* Kinvey.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 57A27C811C178F17000DF951 /* Kinvey.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 5771CD731ECF6FA10057E505 /* ObjectMapper.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 57B7687C1D10C01F0086AA38 /* ObjectMapper.framework */; }; + 5771CD751ECFE9B60057E505 /* MetadataTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5771CD741ECFE9B60057E505 /* MetadataTestCase.swift */; }; 5771F9FF1D301F6300903777 /* Event.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5771F9FE1D301F6300903777 /* Event.swift */; }; 5776EE9A1E2FFA38003B9DF0 /* XCGLogger.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 577751381DF8B8EA006C98F1 /* XCGLogger.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 5776EE9B1E2FFA7D003B9DF0 /* XCGLogger.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 577751381DF8B8EA006C98F1 /* XCGLogger.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -261,6 +272,20 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + 5771CD3C1ECF6CDC0057E505 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 57A27C781C178F17000DF951 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 57A27C801C178F17000DF951; + remoteInfo = Kinvey; + }; + 5771CD3E1ECF6CDC0057E505 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 57A27C781C178F17000DF951 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 5765B8431C9771BC00080FFA; + remoteInfo = KinveyApp; + }; 578870A41DD52EC80087FE78 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 57A27C781C178F17000DF951 /* Project object */; @@ -417,6 +442,16 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 5771CD671ECF6CDC0057E505 /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + 5771CD721ECF6EDC0057E505 /* Kinvey.framework in CopyFiles */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 578870D81DD52FE00087FE78 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; @@ -559,6 +594,9 @@ 5728213A1C6482C000373EC8 /* URLSessionTaskRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLSessionTaskRequest.swift; sourceTree = ""; }; 572C457A1C86690700A41935 /* Date.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Date.swift; sourceTree = ""; }; 573150971CBD8D910022A05C /* QueryTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QueryTest.swift; sourceTree = ""; }; + 57373AB11ECCD8C6002842CE /* EntityTestCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EntityTestCase.swift; sourceTree = ""; }; + 57373AB31ECCE670002842CE /* Nimble.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Nimble.framework; sourceTree = ""; }; + 57373ABE1ECE1849002842CE /* LogTestCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogTestCase.swift; sourceTree = ""; }; 57375F8F1E3FD71D0015A241 /* UploadAndPlayVideoViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UploadAndPlayVideoViewController.swift; sourceTree = ""; }; 57379DA41C72AAA900E240E9 /* Operation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Operation.swift; sourceTree = ""; }; 57379DA61C72AE7F00E240E9 /* GetOperationTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GetOperationTest.swift; sourceTree = ""; }; @@ -604,6 +642,9 @@ 576E95AE1C20DC0400258CC3 /* CacheStoreTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CacheStoreTests.swift; sourceTree = ""; }; 577155531CA1D65D00C91B4B /* CacheMigrationTestCaseStep1.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CacheMigrationTestCaseStep1.swift; sourceTree = ""; }; 577155BA1CA21CC200C91B4B /* Migration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Migration.swift; sourceTree = ""; }; + 5771CD6D1ECF6CDC0057E505 /* KinveyTests Forgot To Call Super.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "KinveyTests Forgot To Call Super.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + 5771CD6F1ECF6D440057E505 /* ForgotToCallSuper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForgotToCallSuper.swift; sourceTree = ""; }; + 5771CD741ECFE9B60057E505 /* MetadataTestCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MetadataTestCase.swift; sourceTree = ""; }; 5771F9FE1D301F6300903777 /* Event.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Event.swift; sourceTree = ""; }; 5776EEBF1E31740B003B9DF0 /* MIC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MIC.swift; sourceTree = ""; }; 577751381DF8B8EA006C98F1 /* XCGLogger.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = XCGLogger.framework; sourceTree = ""; }; @@ -756,6 +797,18 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 5771CD601ECF6CDC0057E505 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 5771CD611ECF6CDC0057E505 /* QuartzCore.framework in Frameworks */, + 5771CD621ECF6CDC0057E505 /* CoreGraphics.framework in Frameworks */, + 5771CD711ECF6ECB0057E505 /* Kinvey.framework in Frameworks */, + 5771CD641ECF6CDC0057E505 /* RealmSwift.framework in Frameworks */, + 5771CD731ECF6FA10057E505 /* ObjectMapper.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 5788708D1DD52EC70087FE78 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -945,7 +998,6 @@ 5759854D1CC833930054DF08 /* LongData.swift */, 5759854F1CC834FE0054DF08 /* MedData.swift */, 57375F8F1E3FD71D0015A241 /* UploadAndPlayVideoViewController.swift */, - 5754DDBC1EAEBC4A00122A7A /* DateTestCase.swift */, ); path = KinveyApp; sourceTree = ""; @@ -1034,6 +1086,7 @@ 578870C71DD52ECF0087FE78 /* SSOApp2Tests.xctest */, 578D9FF01E8DE53900C2B280 /* PushMissingConfiguration.xctest */, 578DA0271E8EC3CB00C2B280 /* UserUpgradeFromOldVersion.xctest */, + 5771CD6D1ECF6CDC0057E505 /* KinveyTests Forgot To Call Super.xctest */, ); name = Products; sourceTree = ""; @@ -1098,7 +1151,6 @@ 573851AB1D47C7EB00E4712A /* FileCache.swift */, 573851AD1D47C7F800E4712A /* RealmFileCache.swift */, 577155BA1CA21CC200C91B4B /* Migration.swift */, - 57D643471CAB3C8A00F6D16E /* MemoryCache.swift */, E9073D001C986D9600475E16 /* CustomEndpoint.swift */, 5781D1261CE29AA600369F40 /* ObjCRuntime.swift */, 577E6FA71D18E45F00B5DA36 /* Executor.swift */, @@ -1114,6 +1166,7 @@ children = ( 57A960A21CC6D729005E52A8 /* Models */, 57A27C921C178F18000DF951 /* Info.plist */, + 57D643471CAB3C8A00F6D16E /* MemoryCache.swift */, 5765B83C1C972D7000080FFA /* URLProtocols.swift */, 578F5C921C99EED100B20F17 /* KIF.swift */, 57A27C901C178F18000DF951 /* KinveyTestCase.swift */, @@ -1148,6 +1201,11 @@ 578D9FC01E8DE4D100C2B280 /* PushMissingConfiguration.swift */, 575465A31E66405D0063B4B6 /* PerformanceProductTestCase.swift */, 57E516381E9C3BE600A2AAD3 /* ClientTestCase.swift */, + 57373AB11ECCD8C6002842CE /* EntityTestCase.swift */, + 57373ABE1ECE1849002842CE /* LogTestCase.swift */, + 5771CD6F1ECF6D440057E505 /* ForgotToCallSuper.swift */, + 5771CD741ECFE9B60057E505 /* MetadataTestCase.swift */, + 5754DDBC1EAEBC4A00122A7A /* DateTestCase.swift */, ); path = KinveyTests; sourceTree = ""; @@ -1181,6 +1239,7 @@ 57B0C1DA1CDCE88900492D6C /* Realm.framework */, 57B0C1DC1CDCE88900492D6C /* RealmSwift.framework */, 57B0C1D21CDCE88900492D6C /* KIF.framework */, + 57373AB31ECCE670002842CE /* Nimble.framework */, ); name = iOS; path = ../Carthage/Build/iOS; @@ -1296,6 +1355,27 @@ productReference = 5765B8441C9771BC00080FFA /* KinveyApp.app */; productType = "com.apple.product-type.application"; }; + 5771CD3A1ECF6CDC0057E505 /* KinveyTests Forgot To Call Super */ = { + isa = PBXNativeTarget; + buildConfigurationList = 5771CD6A1ECF6CDC0057E505 /* Build configuration list for PBXNativeTarget "KinveyTests Forgot To Call Super" */; + buildPhases = ( + 5771CD3F1ECF6CDC0057E505 /* Sources */, + 5771CD601ECF6CDC0057E505 /* Frameworks */, + 5771CD651ECF6CDC0057E505 /* Resources */, + 5771CD671ECF6CDC0057E505 /* CopyFiles */, + 5771CD691ECF6CDC0057E505 /* Carthage Copy Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 5771CD3B1ECF6CDC0057E505 /* PBXTargetDependency */, + 5771CD3D1ECF6CDC0057E505 /* PBXTargetDependency */, + ); + name = "KinveyTests Forgot To Call Super"; + productName = KinveyTests; + productReference = 5771CD6D1ECF6CDC0057E505 /* KinveyTests Forgot To Call Super.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; 5788708F1DD52EC70087FE78 /* SSOApp1 */ = { isa = PBXNativeTarget; buildConfigurationList = 578870AA1DD52EC80087FE78 /* Build configuration list for PBXNativeTarget "SSOApp1" */; @@ -1580,6 +1660,9 @@ }; }; }; + 5771CD3A1ECF6CDC0057E505 = { + DevelopmentTeam = 5W7CYNR7UE; + }; 5788708F1DD52EC70087FE78 = { CreatedOnToolsVersion = 8.1; DevelopmentTeam = 5W7CYNR7UE; @@ -1672,6 +1755,7 @@ 57A27C801C178F17000DF951 /* Kinvey */, 57A27C8A1C178F18000DF951 /* KinveyTests */, 5793B86C1D2306A60088B5F9 /* KinveyTests Encrypted */, + 5771CD3A1ECF6CDC0057E505 /* KinveyTests Forgot To Call Super */, 57D642EF1CA3268000F6D16E /* KinveyTests Migration Database Step 1 */, 57D643161CA3268600F6D16E /* KinveyTests Migration Database Step 2 */, 57E447EB1DF62DC7003D1AFA /* KinveyTests No Cache */, @@ -1698,6 +1782,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 5771CD651ECF6CDC0057E505 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 5771CD661ECF6CDC0057E505 /* Media.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 5788708E1DD52EC70087FE78 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -1807,6 +1899,22 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 5771CD691ECF6CDC0057E505 /* Carthage Copy Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "$(SRCROOT)/../Carthage/Build/iOS/KIF.framework", + "$(SRCROOT)/../Carthage/Build/iOS/Nimble.framework", + ); + name = "Carthage Copy Frameworks"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "carthage copy-frameworks"; + }; 5777513E1DF8BDF4006C98F1 /* Carthage Copy Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -1834,6 +1942,7 @@ ); inputPaths = ( "$(SRCROOT)/../Carthage/Build/iOS/KIF.framework", + "$(SRCROOT)/../Carthage/Build/iOS/Nimble.framework", ); name = "Carthage Copy Frameworks"; outputPaths = ( @@ -1952,6 +2061,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 5771CD3F1ECF6CDC0057E505 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 5771CD701ECF6D910057E505 /* ForgotToCallSuper.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 5788708C1DD52EC70087FE78 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -2122,7 +2239,10 @@ 576A1D361CCA92CA006B261E /* DataTypeTestCase.swift in Sources */, 57A960A11CC6D6FE005E52A8 /* JsonTestCase.swift in Sources */, 57136F631D5D23BF00731DDB /* MockKinveyBackend.swift in Sources */, + 5771CD751ECFE9B60057E505 /* MetadataTestCase.swift in Sources */, + 57373AB21ECCD8C6002842CE /* EntityTestCase.swift in Sources */, 577155511CA0F1D200C91B4B /* StoreTestCase.swift in Sources */, + 57373ABF1ECE1849002842CE /* LogTestCase.swift in Sources */, 573E55F11CAC8BA8003D2F23 /* CustomEndpointTests.swift in Sources */, 57A4656F1CC00931009E7384 /* PerformanceTestCase.swift in Sources */, 57A9609F1CC6D6A9005E52A8 /* RefProject.swift in Sources */, @@ -2193,6 +2313,16 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ + 5771CD3B1ECF6CDC0057E505 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 57A27C801C178F17000DF951 /* Kinvey */; + targetProxy = 5771CD3C1ECF6CDC0057E505 /* PBXContainerItemProxy */; + }; + 5771CD3D1ECF6CDC0057E505 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 5765B8431C9771BC00080FFA /* KinveyApp */; + targetProxy = 5771CD3E1ECF6CDC0057E505 /* PBXContainerItemProxy */; + }; 578870A51DD52EC80087FE78 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 5788708F1DD52EC70087FE78 /* SSOApp1 */; @@ -2380,6 +2510,49 @@ }; name = Release; }; + 5771CD6B1ECF6CDC0057E505 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 57B0C1C01CDCE3E600492D6C /* KinveyTests.xcconfig */; + buildSettings = { + CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES; + CLANG_ENABLE_MODULES = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + DEVELOPMENT_TEAM = 5W7CYNR7UE; + INFOPLIST_FILE = KinveyTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + OTHER_LDFLAGS = ( + "-ObjC", + "-framework", + IOKit, + ); + PRODUCT_BUNDLE_IDENTIFIER = com.kinvey.KinveyTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 3.0; + }; + name = Debug; + }; + 5771CD6C1ECF6CDC0057E505 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 57B0C1C01CDCE3E600492D6C /* KinveyTests.xcconfig */; + buildSettings = { + CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES; + CLANG_ENABLE_MODULES = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + DEVELOPMENT_TEAM = 5W7CYNR7UE; + INFOPLIST_FILE = KinveyTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + OTHER_LDFLAGS = ( + "-ObjC", + "-framework", + IOKit, + ); + PRODUCT_BUNDLE_IDENTIFIER = com.kinvey.KinveyTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 3.0; + }; + name = Release; + }; 578870AB1DD52EC80087FE78 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -3070,6 +3243,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 5771CD6A1ECF6CDC0057E505 /* Build configuration list for PBXNativeTarget "KinveyTests Forgot To Call Super" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 5771CD6B1ECF6CDC0057E505 /* Debug */, + 5771CD6C1ECF6CDC0057E505 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 578870AA1DD52EC80087FE78 /* Build configuration list for PBXNativeTarget "SSOApp1" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/Kinvey/Kinvey.xcodeproj/xcshareddata/xcschemes/Kinvey.xcscheme b/Kinvey/Kinvey.xcodeproj/xcshareddata/xcschemes/Kinvey.xcscheme index f7da866e8..039a94d57 100644 --- a/Kinvey/Kinvey.xcodeproj/xcshareddata/xcschemes/Kinvey.xcscheme +++ b/Kinvey/Kinvey.xcodeproj/xcshareddata/xcschemes/Kinvey.xcscheme @@ -147,6 +147,16 @@ ReferencedContainer = "container:Kinvey.xcodeproj"> + + + + + + diff --git a/Kinvey/Kinvey/Acl.swift b/Kinvey/Kinvey/Acl.swift index 3dded263c..9d87e4b77 100644 --- a/Kinvey/Kinvey/Acl.swift +++ b/Kinvey/Kinvey/Acl.swift @@ -40,13 +40,7 @@ class AclTransformType: TransformType { } /// This class represents the ACL (Access Control List) for a record. -public final class Acl: Object, Mappable, BuilderType { - - static let CreatorKey = "creator" - static let GlobalReadKey = "gr" - static let GlobalWriteKey = "gw" - static let ReadersKey = "r" - static let WritersKey = "w" +public final class Acl: Object, BuilderType { /// The `userId` of the `User` used to create the record. open dynamic var creator: String? @@ -100,71 +94,64 @@ public final class Acl: Object, Mappable, BuilderType { } /// Constructs an Acl instance with the `userId` of the `User` used to create the record. - public init( + public convenience init( creator: String, globalRead: Bool? = nil, globalWrite: Bool? = nil, readers: [String]? = nil, writers: [String]? = nil ) { + self.init() self.creator = creator self.globalRead.value = globalRead self.globalWrite.value = globalWrite - super.init() self.readers = readers self.writers = writers } - /// Constructor that validates if the map contains at least the creator. - public required convenience init?(map: Map) { - var creator: String? - - creator <- map[Acl.CreatorKey] - - guard let creatorValue = creator else { - self.init() - return nil - } - - self.init(creator: creatorValue) - } - - /// Default Constructor. - public required init() { - super.init() - } - /** WARNING: This is an internal initializer not intended for public use. :nodoc: */ - public required init(value: Any, schema: RLMSchema) { - super.init(value: value, schema: schema) + open override class func ignoredProperties() -> [String] { + return ["readers", "writers"] } + +} + +extension Acl: Mappable { - /** - WARNING: This is an internal initializer not intended for public use. - :nodoc: - */ - public required init(realm: RLMRealm, schema: RLMObjectSchema) { - super.init(realm: realm, schema: schema) + /// Constructor that validates if the map contains at least the creator. + public convenience init?(map: Map) { + guard let _: String = map[Acl.Key.creator].value() else { + self.init() + return nil + } + + self.init() } /// This function is where all variable mappings should occur. It is executed by Mapper during the mapping (serialization and deserialization) process. open func mapping(map: Map) { - creator <- ("creator", map[Acl.CreatorKey]) - globalRead.value <- ("globalRead", map[Acl.GlobalReadKey]) - globalWrite.value <- ("globalWrite", map[Acl.GlobalWriteKey]) - readers <- ("readers", map[Acl.ReadersKey]) - writers <- ("writers", map[Acl.WritersKey]) + creator <- ("creator", map[Acl.Key.creator]) + globalRead.value <- ("globalRead", map[Acl.Key.globalRead]) + globalWrite.value <- ("globalWrite", map[Acl.Key.globalWrite]) + readers <- ("readers", map[Acl.Key.readers]) + writers <- ("writers", map[Acl.Key.writers]) } - /** - WARNING: This is an internal initializer not intended for public use. - :nodoc: - */ - open override class func ignoredProperties() -> [String] { - return ["readers", "writers"] - } +} +extension Acl { + + public struct Key { + + static let creator = "creator" + static let globalRead = "gr" + static let globalWrite = "gw" + static let readers = "r" + static let writers = "w" + + } + } diff --git a/Kinvey/Kinvey/AggregateOperation.swift b/Kinvey/Kinvey/AggregateOperation.swift index d6d5ea0fd..0934acb1e 100644 --- a/Kinvey/Kinvey/AggregateOperation.swift +++ b/Kinvey/Kinvey/AggregateOperation.swift @@ -22,9 +22,8 @@ class AggregateOperation: ReadOperation Request { let request = LocalRequest() request.execute { () -> Void in - if let cache = self.cache { - let result = cache.group(aggregation: aggregation, predicate: predicate) - completionHandler?(.success(result)) + if let _ = self.cache { + completionHandler?(.failure(Error.invalidOperation(description: "Custom Aggregation not supported against local cache"))) } else { completionHandler?(.success([])) } @@ -73,8 +72,6 @@ enum Aggregation { var resultKey: String { switch self { - case .custom(_, _, _): - fatalError("Custom does not have a resultKey") case .count: return "count" case .sum: @@ -85,6 +82,8 @@ enum Aggregation { return "min" case .max: return "max" + case .custom(_, _, _): + fatalError("Custom does not have a resultKey") } } diff --git a/Kinvey/Kinvey/Cache.swift b/Kinvey/Kinvey/Cache.swift index 2d73b0281..7663406ea 100644 --- a/Kinvey/Kinvey/Cache.swift +++ b/Kinvey/Kinvey/Cache.swift @@ -10,8 +10,6 @@ import Foundation internal protocol CacheType: class { - var persistenceId: String { get } - var collectionName: String { get } var ttl: TimeInterval? { get set } associatedtype `Type`: Persistable @@ -26,8 +24,6 @@ internal protocol CacheType: class { func findIdsLmts(byQuery query: Query) -> [String : String] - func findAll() -> [Type] - func count(query: Query?) -> Int @discardableResult @@ -39,14 +35,10 @@ internal protocol CacheType: class { @discardableResult func remove(byQuery query: Query) -> Int - func removeAll() - func clear(query: Query?) func detach(entities: [Type], query: Query?) -> [Type] - func group(aggregation: Aggregation, predicate: NSPredicate?) -> [JsonDictionary] - } extension CacheType { @@ -75,14 +67,6 @@ internal class Cache where T: NSObject { class AnyCache: CacheType { - var persistenceId: String { - return _getPersistenceId() - } - - var collectionName: String { - return _getCollectionName() - } - var ttl: TimeInterval? { get { return _getTTL() @@ -92,8 +76,6 @@ class AnyCache: CacheType { } } - private let _getPersistenceId: () -> String - private let _getCollectionName: () -> String private let _getTTL: () -> TimeInterval? private let _setTTL: (TimeInterval?) -> Void private let _saveEntity: (T) -> Void @@ -101,21 +83,16 @@ class AnyCache: CacheType { private let _findById: (String) -> T? private let _findByQuery: (Query) -> [T] private let _findIdsLmtsByQuery: (Query) -> [String : String] - private let _findAll: () -> [T] private let _count: (Query?) -> Int private let _removeEntity: (T) -> Bool private let _removeEntities: ([T]) -> Bool private let _removeByQuery: (Query) -> Int - private let _removeAll: () -> Void private let _clear: (Query?) -> Void private let _detach: ([T], Query?) -> [T] - private let _group: (Aggregation, NSPredicate?) -> [JsonDictionary] typealias `Type` = T init(_ cache: Cache) where Cache.`Type` == T { - _getPersistenceId = { return cache.persistenceId } - _getCollectionName = { return cache.collectionName } _getTTL = { return cache.ttl } _setTTL = { cache.ttl = $0 } _saveEntity = cache.save(entity:) @@ -123,15 +100,12 @@ class AnyCache: CacheType { _findById = cache.find(byId:) _findByQuery = cache.find(byQuery:) _findIdsLmtsByQuery = cache.findIdsLmts(byQuery:) - _findAll = cache.findAll _count = cache.count(query:) _removeEntity = cache.remove(entity:) _removeEntities = cache.remove(entities:) _removeByQuery = cache.remove(byQuery:) - _removeAll = cache.removeAll _clear = cache.clear(query:) _detach = cache.detach(entities: query:) - _group = cache.group(aggregation: predicate:) } func save(entity: T) { @@ -154,10 +128,6 @@ class AnyCache: CacheType { return _findIdsLmtsByQuery(query) } - func findAll() -> [T] { - return _findAll() - } - func count(query: Query?) -> Int { return _count(query) } @@ -177,10 +147,6 @@ class AnyCache: CacheType { return _removeByQuery(query) } - func removeAll() { - _removeAll() - } - func clear(query: Query?) { _clear(query) } @@ -189,8 +155,4 @@ class AnyCache: CacheType { return _detach(entities, query) } - func group(aggregation: Aggregation, predicate: NSPredicate?) -> [JsonDictionary] { - return _group(aggregation, predicate) - } - } diff --git a/Kinvey/Kinvey/CacheManager.swift b/Kinvey/Kinvey/CacheManager.swift index fdb4d2505..b18d19b90 100644 --- a/Kinvey/Kinvey/CacheManager.swift +++ b/Kinvey/Kinvey/CacheManager.swift @@ -36,8 +36,8 @@ internal class CacheManager: NSObject { return AnyCache(RealmCache(persistenceId: persistenceId, fileURL: fileURL, encryptionKey: encryptionKey, schemaVersion: schemaVersion)) } - func fileCache(fileURL: URL? = nil) -> FileCache? { - return RealmFileCache(persistenceId: persistenceId, fileURL: fileURL, encryptionKey: encryptionKey, schemaVersion: schemaVersion) + func fileCache(fileURL: URL? = nil) -> AnyFileCache? { + return AnyFileCache(RealmFileCache(persistenceId: persistenceId, fileURL: fileURL, encryptionKey: encryptionKey, schemaVersion: schemaVersion)) } func clearAll(_ tag: String? = nil) { diff --git a/Kinvey/Kinvey/Client.swift b/Kinvey/Kinvey/Client.swift index 6d7654ee4..67155f57b 100644 --- a/Kinvey/Kinvey/Client.swift +++ b/Kinvey/Kinvey/Client.swift @@ -47,6 +47,12 @@ open class Client: Credential { } } + internal var clientId: String? { + willSet { + keychain.clientId = clientId + } + } + private var accessGroup: String? private var keychain: Keychain { @@ -145,9 +151,7 @@ open class Client: Credential { private func validateInitialize(appKey: String, appSecret: String) { if appKey.isEmpty || appSecret.isEmpty { - let message = "Please provide a valid appKey and appSecret. Your app's key and secret can be found on the Kinvey management console." - log.severe(message) - fatalError(message) + fatalError("Please provide a valid appKey and appSecret. Your app's key and secret can be found on the Kinvey management console.") } } @@ -269,6 +273,7 @@ open class Client: Credential { if let user = keychain.user { user.client = self activeUser = user + clientId = keychain.clientId let customUser = user as! U completionHandler(.success(customUser)) } else if let kinveyAuth = sharedKeychain?.kinveyAuth { diff --git a/Kinvey/Kinvey/CustomEndpoint.swift b/Kinvey/Kinvey/CustomEndpoint.swift index 7fbfc8f89..090b69ffc 100644 --- a/Kinvey/Kinvey/CustomEndpoint.swift +++ b/Kinvey/Kinvey/CustomEndpoint.swift @@ -52,7 +52,7 @@ open class CustomEndpoint { request.request.httpBody = try! JSONSerialization.data(withJSONObject: object.toJSON().toJson(), options: []) } } - request.request.setValue(nil, forHTTPHeaderField: Header.requestId) + request.request.setValue(nil, forHTTPHeaderField: KinveyHeaderField.requestId) request.execute(completionHandler) return request } diff --git a/Kinvey/Kinvey/DataStore.swift b/Kinvey/Kinvey/DataStore.swift index 971b5dbe5..c37ae5218 100644 --- a/Kinvey/Kinvey/DataStore.swift +++ b/Kinvey/Kinvey/DataStore.swift @@ -104,9 +104,7 @@ open class DataStore where T: NSObject { */ open class func collection(_ type: StoreType = .cache, deltaSet: Bool? = nil, client: Client = sharedClient, tag: String = defaultTag) -> DataStore { if !client.isInitialized() { - let message = "Client is not initialized. Call Kinvey.sharedClient.initialize(...) to initialize the client before creating a DataStore." - log.severe(message) - fatalError(message) + fatalError("Client is not initialized. Call Kinvey.sharedClient.initialize(...) to initialize the client before creating a DataStore.") } let key = DataStoreTypeTag(persistableType: T.self, tag: tag, type: type) var dataStore = client.dataStoreInstances[key] as? DataStore @@ -172,9 +170,7 @@ open class DataStore where T: NSObject { private func validate(id: String) { if id.isEmpty { - let message = "id cannot be an empty string" - log.severe(message) - fatalError(message) + fatalError("id cannot be an empty string") } } diff --git a/Kinvey/Kinvey/Endpoint.swift b/Kinvey/Kinvey/Endpoint.swift index 60b2ce132..e130c43e1 100644 --- a/Kinvey/Kinvey/Endpoint.swift +++ b/Kinvey/Kinvey/Endpoint.swift @@ -39,7 +39,7 @@ internal enum Endpoint { case url(url: URL) case customEndpooint(client: Client, name: String) - case oauthAuth(client: Client, redirectURI: URL, loginPage: Bool) + case oauthAuth(client: Client, clientId: String?, redirectURI: URL, loginPage: Bool) case oauthToken(client: Client) var url: URL { @@ -144,7 +144,7 @@ internal enum Endpoint { return url case .customEndpooint(let client, let name): return client.apiHostName.appendingPathComponent("/rpc/\(client.appKey!)/custom/\(name)") - case .oauthAuth(let client, let redirectURI, let loginPage): + case .oauthAuth(let client, let clientId, let redirectURI, let loginPage): var url = client.authHostName if let micApiVersion = client.micApiVersion { url.appendPathComponent(micApiVersion.rawValue) @@ -153,7 +153,13 @@ internal enum Endpoint { if loginPage { var components = URLComponents(url: url, resolvingAgainstBaseURL: false)! var queryItems = [URLQueryItem]() - queryItems.append(URLQueryItem(name: "client_id", value: client.appKey!)) + if let appKey = client.appKey { + if let clientId = clientId { + queryItems.append(URLQueryItem(name: "client_id", value: "\(appKey):\(clientId)")) + } else { + queryItems.append(URLQueryItem(name: "client_id", value: appKey)) + } + } queryItems.append(URLQueryItem(name: "redirect_uri", value: redirectURI.absoluteString)) queryItems.append(URLQueryItem(name: "response_type", value: "code")) if let micApiVersion = client.micApiVersion, micApiVersion == .v3 { diff --git a/Kinvey/Kinvey/Entity.swift b/Kinvey/Kinvey/Entity.swift index 716c8047b..8d946a3e6 100644 --- a/Kinvey/Kinvey/Entity.swift +++ b/Kinvey/Kinvey/Entity.swift @@ -11,6 +11,18 @@ import Realm import RealmSwift import ObjectMapper +/// Key to map the `_id` column in your Persistable implementation class. +@available(*, deprecated: 3.5.2, message: "Please use Entity.Key.entityId instead") +public let PersistableIdKey = "_id" + +/// Key to map the `_acl` column in your Persistable implementation class. +@available(*, deprecated: 3.5.2, message: "Please use Entity.Key.acl instead") +public let PersistableAclKey = "_acl" + +/// Key to map the `_kmd` column in your Persistable implementation class. +@available(*, deprecated: 3.5.2, message: "Please use Entity.Key.metadata instead") +public let PersistableMetadataKey = "_kmd" + public typealias List = RealmSwift.List public typealias Object = RealmSwift.Object @@ -30,6 +42,19 @@ internal func StringFromClass(cls: AnyClass) -> String { /// Base class for entity classes that are mapped to a collection in Kinvey. open class Entity: Object, Persistable { + public struct Key { + + /// Key to map the `_id` column in your Persistable implementation class. + public static let entityId = "_id" + + /// Key to map the `_acl` column in your Persistable implementation class. + public static let acl = "_acl" + + /// Key to map the `_kmd` column in your Persistable implementation class. + public static let metadata = "_kmd" + + } + /// This function can be used to validate JSON prior to mapping. Return nil to cancel mapping at this point public required init?(map: Map) { super.init() @@ -37,9 +62,7 @@ open class Entity: Object, Persistable { /// Override this method and return the name of the collection for Kinvey. open class func collectionName() -> String { - let message = "Method \(#function) must be overridden" - log.severe(message) - fatalError(message) + fatalError("Method \(#function) must be overridden") } /// The `_id` property mapped in the Kinvey backend. @@ -74,9 +97,9 @@ open class Entity: Object, Persistable { /// Override this method to tell how to map your own objects. open func propertyMapping(_ map: Map) { - entityId <- ("entityId", map[PersistableIdKey]) - metadata <- ("metadata", map[PersistableMetadataKey]) - acl <- ("acl", map[PersistableAclKey]) + entityId <- ("entityId", map[Key.entityId]) + metadata <- ("metadata", map[Key.metadata]) + acl <- ("acl", map[Key.acl]) } /** @@ -140,8 +163,6 @@ open class StringValue: Object, ExpressibleByStringLiteral { public dynamic var value = "" - public typealias ExtendedGraphemeClusterLiteralType = StringLiteralType - public convenience required init(unicodeScalarLiteral value: String) { self.init() self.value = value @@ -168,8 +189,6 @@ open class IntValue: Object, ExpressibleByIntegerLiteral { public dynamic var value = 0 - public typealias IntegerLiteralType = Int - public convenience required init(integerLiteral value: Int) { self.init() self.value = value @@ -186,8 +205,6 @@ open class FloatValue: Object, ExpressibleByFloatLiteral { public dynamic var value = Float(0) - public typealias FloatLiteralType = Float - public convenience required init(floatLiteral value: Float) { self.init() self.value = value @@ -204,8 +221,6 @@ open class DoubleValue: Object, ExpressibleByFloatLiteral { public dynamic var value = 0.0 - public typealias FloatLiteralType = Double - public convenience required init(floatLiteral value: Double) { self.init() self.value = value @@ -222,8 +237,6 @@ open class BoolValue: Object, ExpressibleByBooleanLiteral { public dynamic var value = false - public typealias FloatLiteralType = BooleanLiteralType - public convenience required init(booleanLiteral value: Bool) { self.init() self.value = value diff --git a/Kinvey/Kinvey/Error.swift b/Kinvey/Kinvey/Error.swift index 6cd015824..6caeec9a6 100644 --- a/Kinvey/Kinvey/Error.swift +++ b/Kinvey/Kinvey/Error.swift @@ -135,7 +135,7 @@ public enum Error: Swift.Error, LocalizedError, CustomStringConvertible, CustomD /// Response Header `X-Kinvey-Request-Id` public var requestId: String? { - return httpResponse?.allHeaderFields[Header.requestId] as? String + return httpResponse?.allHeaderFields[KinveyHeaderField.requestId] as? String } /// Response Data Body object. diff --git a/Kinvey/Kinvey/File.swift b/Kinvey/Kinvey/File.swift index 5e799bca4..5eb4f97bf 100644 --- a/Kinvey/Kinvey/File.swift +++ b/Kinvey/Kinvey/File.swift @@ -115,9 +115,9 @@ open class File: Object, Mappable { } public func mapping(map: Map) { - fileId <- map[PersistableIdKey] - acl <- map[PersistableAclKey] - metadata <- map[PersistableMetadataKey] + fileId <- map[Entity.Key.entityId] + acl <- map[Entity.Key.acl] + metadata <- map[Entity.Key.metadata] publicAccessible <- map["_public"] fileName <- map["_filename"] mimeType <- map["mimeType"] diff --git a/Kinvey/Kinvey/FileCache.swift b/Kinvey/Kinvey/FileCache.swift index 3cb2722ef..4119c0988 100644 --- a/Kinvey/Kinvey/FileCache.swift +++ b/Kinvey/Kinvey/FileCache.swift @@ -10,10 +10,43 @@ import Foundation protocol FileCache { - func save(_ file: File, beforeSave: (() -> Void)?) + associatedtype FileType: File - func remove(_ file: File) + func save(_ file: FileType, beforeSave: (() -> Void)?) - func get(_ fileId: String) -> File? + func remove(_ file: FileType) + + func get(_ fileId: String) -> FileType? + +} + +class AnyFileCache: FileCache { + + typealias FileType = T + + private let _save: (T, (() -> Void)?) -> Void + private let _remove: (T) -> Void + private let _get: (String) -> T? + + let cache: Any + + init(_ cache: Cache) where Cache.FileType == T { + self.cache = cache + _save = cache.save(_:beforeSave:) + _remove = cache.remove(_:) + _get = cache.get(_:) + } + + func save(_ file: T, beforeSave: (() -> Void)?) { + return _save(file, beforeSave) + } + + func remove(_ file: T) { + _remove(file) + } + + func get(_ fileId: String) -> T? { + return _get(fileId) + } } diff --git a/Kinvey/Kinvey/FileStore.swift b/Kinvey/Kinvey/FileStore.swift index 50bb19290..8f3926171 100644 --- a/Kinvey/Kinvey/FileStore.swift +++ b/Kinvey/Kinvey/FileStore.swift @@ -15,26 +15,6 @@ import ObjectMapper import UIKit #endif -fileprivate func < (lhs: T?, rhs: T?) -> Bool { - switch (lhs, rhs) { - case let (l?, r?): - return l < r - case (nil, _?): - return true - default: - return false - } -} - -fileprivate func > (lhs: T?, rhs: T?) -> Bool { - switch (lhs, rhs) { - case let (l?, r?): - return l > r - default: - return rhs < lhs - } -} - public enum ImageRepresentation { case png @@ -61,23 +41,30 @@ public enum ImageRepresentation { } /// Class to interact with the `Files` collection in the backend. -open class FileStore { +open class FileStore { - public typealias FileCompletionHandler = (File?, Swift.Error?) -> Void - public typealias FileDataCompletionHandler = (File?, Data?, Swift.Error?) -> Void - public typealias FilePathCompletionHandler = (File?, URL?, Swift.Error?) -> Void + public typealias FileCompletionHandler = (FileType?, Swift.Error?) -> Void + public typealias FileDataCompletionHandler = (FileType?, Data?, Swift.Error?) -> Void + public typealias FilePathCompletionHandler = (FileType?, URL?, Swift.Error?) -> Void public typealias UIntCompletionHandler = (UInt?, Swift.Error?) -> Void - public typealias FileArrayCompletionHandler = ([File]?, Swift.Error?) -> Void + public typealias FileArrayCompletionHandler = ([FileType]?, Swift.Error?) -> Void internal let client: Client - internal let cache: FileCache? + internal let cache: AnyFileCache? /// Factory method that returns a `FileStore`. - open class func getInstance(_ client: Client = sharedClient) -> FileStore { - return FileStore(client: client) + @available(*, deprecated: 3.5.2, message: "Please use the constructor instead") + open class func getInstance(client: Client = sharedClient) -> FileStore { + return FileStore(client: client) } - fileprivate init(client: Client) { + /// Factory method that returns a `FileStore`. + @available(*, deprecated: 3.5.2, message: "Please use the constructor instead") + open class func getInstance(fileType: FileType.Type, client: Client = sharedClient) -> FileStore { + return FileStore(client: client) + } + + public init(client: Client = sharedClient) { self.client = client self.cache = client.cacheManager.fileCache(fileURL: client.fileURL()) } @@ -85,13 +72,13 @@ open class FileStore { #if !os(macOS) /// Uploads a `UIImage` in a PNG or JPEG format. @discardableResult - open func upload(_ file: File, image: UIImage, imageRepresentation: ImageRepresentation = .png, ttl: TTL? = nil, completionHandler: FileCompletionHandler? = nil) -> Request { + open func upload(_ file: FileType, image: UIImage, imageRepresentation: ImageRepresentation = .png, ttl: TTL? = nil, completionHandler: FileCompletionHandler? = nil) -> Request { return upload( file, image: image, imageRepresentation: imageRepresentation, ttl: ttl - ) { (result: Result) in + ) { (result: Result) in switch result { case .success(let file): completionHandler?(file, nil) @@ -103,7 +90,7 @@ open class FileStore { /// Uploads a `UIImage` in a PNG or JPEG format. @discardableResult - open func upload(_ file: File, image: UIImage, imageRepresentation: ImageRepresentation = .png, ttl: TTL? = nil, completionHandler: ((Result) -> Void)? = nil) -> Request { + open func upload(_ file: FileType, image: UIImage, imageRepresentation: ImageRepresentation = .png, ttl: TTL? = nil, completionHandler: ((Result) -> Void)? = nil) -> Request { let data = imageRepresentation.data(image: image)! file.mimeType = imageRepresentation.mimeType return upload(file, data: data, ttl: ttl, completionHandler: completionHandler) @@ -112,12 +99,12 @@ open class FileStore { /// Uploads a file using the file path. @discardableResult - open func upload(_ file: File, path: String, ttl: TTL? = nil, completionHandler: FileCompletionHandler? = nil) -> Request { + open func upload(_ file: FileType, path: String, ttl: TTL? = nil, completionHandler: FileCompletionHandler? = nil) -> Request { return upload( file, path: path, ttl: ttl - ) { (result: Result) in + ) { (result: Result) in switch result { case .success(let file): completionHandler?(file, nil) @@ -129,18 +116,18 @@ open class FileStore { /// Uploads a file using the file path. @discardableResult - open func upload(_ file: File, path: String, ttl: TTL? = nil, completionHandler: ((Result) -> Void)? = nil) -> Request { + open func upload(_ file: FileType, path: String, ttl: TTL? = nil, completionHandler: ((Result) -> Void)? = nil) -> Request { return upload(file, fromSource: .url(URL(fileURLWithPath: path)), ttl: ttl, completionHandler: completionHandler) } /// Uploads a file using a input stream. @discardableResult - open func upload(_ file: File, stream: InputStream, ttl: TTL? = nil, completionHandler: FileCompletionHandler? = nil) -> Request { + open func upload(_ file: FileType, stream: InputStream, ttl: TTL? = nil, completionHandler: FileCompletionHandler? = nil) -> Request { return upload( file, stream: stream, ttl: ttl - ) { (result: Result) in + ) { (result: Result) in switch result { case .success(let file): completionHandler?(file, nil) @@ -152,17 +139,17 @@ open class FileStore { /// Uploads a file using a input stream. @discardableResult - open func upload(_ file: File, stream: InputStream, ttl: TTL? = nil, completionHandler: ((Result) -> Void)? = nil) -> Request { + open func upload(_ file: FileType, stream: InputStream, ttl: TTL? = nil, completionHandler: ((Result) -> Void)? = nil) -> Request { return upload(file, fromSource: .stream(stream), ttl: ttl, completionHandler: completionHandler) } - fileprivate func getFileMetadata(_ file: File, ttl: TTL? = nil) -> (request: Request, promise: Promise) { + fileprivate func getFileMetadata(_ file: FileType, ttl: TTL? = nil) -> (request: Request, promise: Promise) { let request = self.client.networkRequestFactory.buildBlobDownloadFile(file, ttl: ttl) - let promise = Promise { fulfill, reject in + let promise = Promise { fulfill, reject in request.execute() { (data, response, error) -> Void in if let response = response, response.isOK, let json = self.client.responseParser.parse(data), - let newFile = File(JSON: json) { + let newFile = FileType(JSON: json) { newFile.path = file.path if let cache = self.cache { cache.save(newFile, beforeSave: nil) @@ -179,12 +166,12 @@ open class FileStore { /// Uploads a file using a `NSData`. @discardableResult - open func upload(_ file: File, data: Data, ttl: TTL? = nil, completionHandler: FileCompletionHandler? = nil) -> Request { + open func upload(_ file: FileType, data: Data, ttl: TTL? = nil, completionHandler: FileCompletionHandler? = nil) -> Request { return upload( file, data: data, ttl: ttl - ) { (result: Result) in + ) { (result: Result) in switch result { case .success(let file): completionHandler?(file, nil) @@ -196,7 +183,7 @@ open class FileStore { /// Uploads a file using a `NSData`. @discardableResult - open func upload(_ file: File, data: Data, ttl: TTL? = nil, completionHandler: ((Result) -> Void)? = nil) -> Request { + open func upload(_ file: FileType, data: Data, ttl: TTL? = nil, completionHandler: ((Result) -> Void)? = nil) -> Request { return upload(file, fromSource: .data(data), ttl: ttl, completionHandler: completionHandler) } @@ -209,7 +196,7 @@ open class FileStore { } /// Uploads a file using a `NSData`. - fileprivate func upload(_ file: File, fromSource source: InputSource, ttl: TTL? = nil, completionHandler: ((Result) -> Void)? = nil) -> Request { + fileprivate func upload(_ file: FileType, fromSource source: InputSource, ttl: TTL? = nil, completionHandler: ((Result) -> Void)? = nil) -> Request { if file.size.value == nil { switch source { case let .data(data): @@ -225,15 +212,15 @@ open class FileStore { } } let requests = MultiRequest() - Promise<(file: File, skip: Int?)> { fulfill, reject in //creating bucket + Promise<(file: FileType, skip: Int?)> { fulfill, reject in //creating bucket let createUpdateFileEntry = { let request = self.client.networkRequestFactory.buildBlobUploadFile(file) requests += request request.execute { (data, response, error) -> Void in if let response = response, response.isOK, let json = self.client.responseParser.parse(data), - let newFile = File(JSON: json) { - + let newFile = FileType(JSON: json) + { fulfill((file: newFile, skip: nil)) } else { reject(buildError(data, response, error, self.client)) @@ -305,7 +292,7 @@ open class FileStore { createUpdateFileEntry() } }.then { file, skip in //uploading data - return Promise { fulfill, reject in + return Promise { fulfill, reject in var request = URLRequest(url: file.uploadURL!) request.httpMethod = "PUT" if let uploadHeaders = file.uploadHeaders { @@ -399,11 +386,11 @@ open class FileStore { /// Refresh a `File` instance. @discardableResult - open func refresh(_ file: File, ttl: TTL? = nil, completionHandler: FileCompletionHandler? = nil) -> Request { + open func refresh(_ file: FileType, ttl: TTL? = nil, completionHandler: FileCompletionHandler? = nil) -> Request { return refresh( file, ttl: ttl - ) { (result: Result) in + ) { (result: Result) in switch result { case .success(let file): completionHandler?(file, nil) @@ -415,7 +402,7 @@ open class FileStore { /// Refresh a `File` instance. @discardableResult - open func refresh(_ file: File, ttl: TTL? = nil, completionHandler: ((Result) -> Void)? = nil) -> Request { + open func refresh(_ file: FileType, ttl: TTL? = nil, completionHandler: ((Result) -> Void)? = nil) -> Request { let (request, promise) = getFileMetadata(file, ttl: ttl) promise.then { file in completionHandler?(.success(file)) @@ -426,7 +413,7 @@ open class FileStore { } @discardableResult - fileprivate func downloadFileURL(_ file: File, storeType: StoreType = .cache, downloadURL: URL) -> (request: URLSessionTaskRequest, promise: Promise) { + fileprivate func downloadFileURL(_ file: FileType, storeType: StoreType = .cache, downloadURL: URL) -> (request: URLSessionTaskRequest, promise: Promise) { let downloadTaskRequest = URLSessionTaskRequest(client: client, url: downloadURL) let promise = Promise { fulfill, reject in let executor = Executor() @@ -487,7 +474,7 @@ open class FileStore { } @discardableResult - fileprivate func downloadFileData(_ file: File, downloadURL: URL) -> (request: URLSessionTaskRequest, promise: Promise) { + fileprivate func downloadFileData(_ file: FileType, downloadURL: URL) -> (request: URLSessionTaskRequest, promise: Promise) { let downloadTaskRequest = URLSessionTaskRequest(client: client, url: downloadURL) let promise = downloadTaskRequest.downloadTaskWithURL(file).then { data, response -> Promise in return Promise { fulfill, reject in @@ -498,38 +485,32 @@ open class FileStore { } /// Returns the cached file, if exists. - open func cachedFile(_ entityId: String) -> File? { - if let cache = cache { - return cache.get(entityId) - } - return nil + open func cachedFile(_ entityId: String) -> FileType? { + return cache?.get(entityId) } /// Returns the cached file, if exists. - open func cachedFile(_ file: inout File) { - guard let entityId = file.fileId else { - fatalError("fileId is required") - } - - if let cachedFile = cachedFile(entityId) { - file = cachedFile - } + open func cachedFile(_ file: FileType) -> FileType? { + let entityId = crashIfInvalid(file: file) + return cachedFile(entityId) } - fileprivate func crashIfInvalid(file: File) { - guard let _ = file.fileId else { + @discardableResult + fileprivate func crashIfInvalid(file: FileType) -> String { + guard let fileId = file.fileId else { fatalError("fileId is required") } + return fileId } /// Downloads a file using the `downloadURL` of the `File` instance. @discardableResult - open func download(_ file: File, storeType: StoreType = .cache, ttl: TTL? = nil, completionHandler: FilePathCompletionHandler? = nil) -> Request { + open func download(_ file: FileType, storeType: StoreType = .cache, ttl: TTL? = nil, completionHandler: FilePathCompletionHandler? = nil) -> Request { return download( file, storeType: storeType, ttl: ttl - ) { (result: Result<(File, URL), Swift.Error>) in + ) { (result: Result<(FileType, URL), Swift.Error>) in switch result { case .success(let file, let url): completionHandler?(file, url, nil) @@ -541,7 +522,7 @@ open class FileStore { /// Downloads a file using the `downloadURL` of the `File` instance. @discardableResult - open func download(_ file: File, storeType: StoreType = .cache, ttl: TTL? = nil, completionHandler: ((Result<(File, URL), Swift.Error>) -> Void)? = nil) -> Request { + open func download(_ file: FileType, storeType: StoreType = .cache, ttl: TTL? = nil, completionHandler: ((Result<(FileType, URL), Swift.Error>) -> Void)? = nil) -> Request { crashIfInvalid(file: file) if storeType == .sync || storeType == .cache, @@ -556,8 +537,8 @@ open class FileStore { if storeType == .cache || storeType == .network { let multiRequest = MultiRequest() - Promise<(File, URL)> { fulfill, reject in - if let downloadURL = file.downloadURL, file.publicAccessible || file.expiresAt?.timeIntervalSinceNow > 0 { + Promise<(FileType, URL)> { fulfill, reject in + if let downloadURL = file.downloadURL, file.publicAccessible || (file.expiresAt != nil && file.expiresAt!.timeIntervalSinceNow > 0) { fulfill((file, downloadURL)) } else { let (request, promise) = getFileMetadata(file, ttl: ttl) @@ -572,11 +553,11 @@ open class FileStore { reject(error) } } - }.then { (file, downloadURL) -> Promise<(File, URL)> in + }.then { (file, downloadURL) -> Promise<(FileType, URL)> in let (request, promise) = self.downloadFileURL(file, storeType: storeType, downloadURL: downloadURL) multiRequest += (request, true) return promise.then { localUrl in - return Promise<(File, URL)> { fulfill, reject in + return Promise<(FileType, URL)> { fulfill, reject in fulfill((file, localUrl)) } } @@ -593,11 +574,11 @@ open class FileStore { /// Downloads a file using the `downloadURL` of the `File` instance. @discardableResult - open func download(_ file: File, ttl: TTL? = nil, completionHandler: FileDataCompletionHandler? = nil) -> Request { + open func download(_ file: FileType, ttl: TTL? = nil, completionHandler: FileDataCompletionHandler? = nil) -> Request { return download( file, ttl: ttl - ) { (result: Result<(File, Data), Swift.Error>) in + ) { (result: Result<(FileType, Data), Swift.Error>) in switch result { case .success(let file, let data): completionHandler?(file, data, nil) @@ -607,27 +588,33 @@ open class FileStore { } } + private enum DownloadStage { + + case downloadURL(URL) + case data(Data) + + } + /// Downloads a file using the `downloadURL` of the `File` instance. @discardableResult - open func download(_ file: File, ttl: TTL? = nil, completionHandler: ((Result<(File, Data), Swift.Error>) -> Void)? = nil) -> Request { + open func download(_ file: FileType, ttl: TTL? = nil, completionHandler: ((Result<(FileType, Data), Swift.Error>) -> Void)? = nil) -> Request { crashIfInvalid(file: file) - if let entityId = file.fileId, let cachedFile = cachedFile(entityId), let path = file.path, let data = try? Data(contentsOf: URL(fileURLWithPath: path)) { - DispatchQueue.main.async { - completionHandler?(.success(cachedFile, data)) - } - } - let multiRequest = MultiRequest() - Promise<(File, URL)> { fulfill, reject in - if let downloadURL = file.downloadURL, file.publicAccessible || file.expiresAt?.timeIntervalSinceNow > 0 { - fulfill((file, downloadURL)) + Promise<(FileType, DownloadStage)> { fulfill, reject in + if let entityId = file.fileId, let cachedFile = cachedFile(entityId), let path = file.path, let data = try? Data(contentsOf: URL(fileURLWithPath: path)) { + fulfill((cachedFile, .data(data))) + return + } + + if let downloadURL = file.downloadURL, file.publicAccessible || (file.expiresAt != nil && file.expiresAt!.timeIntervalSinceNow > 0) { + fulfill((file, .downloadURL(downloadURL))) } else { let (request, promise) = getFileMetadata(file, ttl: ttl) multiRequest += request promise.then { file -> Void in - if let downloadURL = file.downloadURL, file.publicAccessible || file.expiresAt?.timeIntervalSinceNow > 0 { - fulfill(file, downloadURL) + if let downloadURL = file.downloadURL, file.publicAccessible || (file.expiresAt != nil && file.expiresAt!.timeIntervalSinceNow > 0) { + fulfill((file, .downloadURL(downloadURL))) } else { throw Error.invalidResponse(httpResponse: nil, data: nil) } @@ -635,10 +622,17 @@ open class FileStore { reject(error) } } - }.then { (file, downloadURL) -> Promise in - let (request, promise) = self.downloadFileData(file, downloadURL: downloadURL) - multiRequest += (request, addProgress: true) - return promise + }.then { (file, downloadStage) -> Promise in + switch downloadStage { + case .downloadURL(let downloadURL): + let (request, promise) = self.downloadFileData(file, downloadURL: downloadURL) + multiRequest += (request, addProgress: true) + return promise + case .data(let data): + return Promise { fulfill, reject in + fulfill(data) + } + } }.then { data in completionHandler?(.success(file, data)) }.catch { error in @@ -649,7 +643,7 @@ open class FileStore { /// Deletes a file instance in the backend. @discardableResult - open func remove(_ file: File, completionHandler: UIntCompletionHandler? = nil) -> Request { + open func remove(_ file: FileType, completionHandler: UIntCompletionHandler? = nil) -> Request { return remove(file) { (result: Result) in switch result { case .success(let count): @@ -662,7 +656,7 @@ open class FileStore { /// Deletes a file instance in the backend. @discardableResult - open func remove(_ file: File, completionHandler: ((Result) -> Void)? = nil) -> Request { + open func remove(_ file: FileType, completionHandler: ((Result) -> Void)? = nil) -> Request { let request = client.networkRequestFactory.buildBlobDeleteFile(file) Promise { fulfill, reject in request.execute({ (data, response, error) -> Void in @@ -693,7 +687,7 @@ open class FileStore { return find( query, ttl: ttl - ) { (result: Result<[File], Swift.Error>) in + ) { (result: Result<[FileType], Swift.Error>) in switch result { case .success(let files): completionHandler?(files, nil) @@ -705,14 +699,14 @@ open class FileStore { /// Gets a list of files that matches with the query passed by parameter. @discardableResult - open func find(_ query: Query = Query(), ttl: TTL? = nil, completionHandler: ((Result<[File], Swift.Error>) -> Void)? = nil) -> Request { + open func find(_ query: Query = Query(), ttl: TTL? = nil, completionHandler: ((Result<[FileType], Swift.Error>) -> Void)? = nil) -> Request { let request = client.networkRequestFactory.buildBlobQueryFile(query, ttl: ttl) - Promise<[File]> { fulfill, reject in + Promise<[FileType]> { fulfill, reject in request.execute { (data, response, error) -> Void in if let response = response, response.isOK, let jsonArray = self.client.responseParser.parseArray(data), - let files = [File](JSONArray: jsonArray) + let files = [FileType](JSONArray: jsonArray) { fulfill(files) } else { diff --git a/Kinvey/Kinvey/FindOperation.swift b/Kinvey/Kinvey/FindOperation.swift index abdc3677f..8f4366d75 100644 --- a/Kinvey/Kinvey/FindOperation.swift +++ b/Kinvey/Kinvey/FindOperation.swift @@ -53,7 +53,7 @@ internal class FindOperation: ReadOperation @discardableResult func executeNetwork(_ completionHandler: CompletionHandler? = nil) -> Request { let deltaSet = self.deltaSet && (cache != nil ? !cache!.isEmpty() : false) - let fields: Set? = deltaSet ? [PersistableIdKey, "\(PersistableMetadataKey).\(Metadata.LmtKey)"] : nil + let fields: Set? = deltaSet ? [Entity.Key.entityId, "\(Entity.Key.metadata).\(Metadata.Key.lastModifiedTime)"] : nil let request = client.networkRequestFactory.buildAppDataFindByQuery(collectionName: T.collectionName(), query: fields != nil ? Query(query) { $0.fields = fields } : query) request.execute() { data, response, error in if let response = response, response.isOK, @@ -63,7 +63,7 @@ internal class FindOperation: ReadOperation if let cache = self.cache, deltaSet { let refObjs = self.reduceToIdsLmts(jsonArray) let deltaSet = self.computeDeltaSet(self.query, refObjs: refObjs) - var allIds = Set() + var allIds = Set(minimumCapacity: deltaSet.created.count + deltaSet.updated.count + deltaSet.deleted.count) allIds.formUnion(deltaSet.created) allIds.formUnion(deltaSet.updated) allIds.formUnion(deltaSet.deleted) @@ -75,7 +75,7 @@ internal class FindOperation: ReadOperation let limit = min(offset + MaxIdsPerQuery, allIds.count - 1) let allIds = Set(allIds[offset...limit]) let promise = Promise<[AnyObject]> { fulfill, reject in - let query = Query(format: "\(PersistableIdKey) IN %@", allIds) + let query = Query(format: "\(Entity.Key.entityId) IN %@", allIds) let operation = FindOperation(query: query, deltaSet: false, readPolicy: .forceNetwork, cache: cache, client: self.client) { jsonArray in for (key, value) in self.reduceToIdsLmts(jsonArray) { newRefObjs[key] = value @@ -101,7 +101,7 @@ internal class FindOperation: ReadOperation completionHandler?(.failure(error)) } } else if allIds.count > 0 { - let query = Query(format: "\(PersistableIdKey) IN %@", allIds) + let query = Query(format: "\(Entity.Key.entityId) IN %@", allIds) var newRefObjs: [String : String]? = nil let operation = FindOperation(query: query, deltaSet: false, readPolicy: .forceNetwork, cache: cache, client: self.client) { jsonArray in newRefObjs = self.reduceToIdsLmts(jsonArray) @@ -121,20 +121,16 @@ internal class FindOperation: ReadOperation self.executeLocal(completionHandler) } } else { - let entities = [T](JSONArray: jsonArray) - if let entities = entities { - if let cache = self.cache { - if self.mustRemoveCachedRecords { - let refObjs = self.reduceToIdsLmts(jsonArray) - let deltaSet = self.computeDeltaSet(self.query, refObjs: refObjs) - self.removeCachedRecords(cache, keys: refObjs.keys, deleted: deltaSet.deleted) - } - cache.save(entities: entities) + let entities = [T](JSONArray: jsonArray)! + if let cache = self.cache { + if self.mustRemoveCachedRecords { + let refObjs = self.reduceToIdsLmts(jsonArray) + let deltaSet = self.computeDeltaSet(self.query, refObjs: refObjs) + self.removeCachedRecords(cache, keys: refObjs.keys, deleted: deltaSet.deleted) } - completionHandler?(.success(entities)) - } else { - completionHandler?(.failure(buildError(data, response, error, self.client))) + cache.save(entities: entities) } + completionHandler?(.success(entities)) } } else { completionHandler?(.failure(buildError(data, response, error, self.client))) diff --git a/Kinvey/Kinvey/Geolocation.swift b/Kinvey/Kinvey/Geolocation.swift index 531f541cd..8f325c56a 100644 --- a/Kinvey/Kinvey/Geolocation.swift +++ b/Kinvey/Kinvey/Geolocation.swift @@ -12,18 +12,11 @@ import ObjectMapper import CoreLocation import MapKit -open class GeoPoint: Object, Mappable { +public final class GeoPoint: Object { open dynamic var latitude: CLLocationDegrees = 0.0 open dynamic var longitude: CLLocationDegrees = 0.0 - public convenience required init?(map: Map) { - guard let _: Double = map["latitude"].value(), let _: Double = map["longitude"].value() else { - return nil - } - self.init() - } - public convenience init(latitude: CLLocationDegrees, longitude: CLLocationDegrees) { self.init() self.latitude = latitude @@ -38,6 +31,17 @@ open class GeoPoint: Object, Mappable { self.init(latitude: array[1], longitude: array[0]) } +} + +extension GeoPoint: Mappable { + + public convenience init?(map: Map) { + guard let _: Double = map["latitude"].value(), let _: Double = map["longitude"].value() else { + return nil + } + self.init() + } + public func mapping(map: Map) { latitude <- map["latitude"] longitude <- map["longitude"] diff --git a/Kinvey/Kinvey/HttpRequest.swift b/Kinvey/Kinvey/HttpRequest.swift index 3eeb21b1e..fbb3a29a6 100644 --- a/Kinvey/Kinvey/HttpRequest.swift +++ b/Kinvey/Kinvey/HttpRequest.swift @@ -14,22 +14,18 @@ import Foundation import WatchKit #endif -enum Header: String { +struct HeaderField { - case requestId = "X-Kinvey-Request-Id" - case clientAppVersion = "X-Kinvey-Client-App-Version" + static let userAgent = "User-Agent" } -extension URLRequest { - - mutating func setValue(_ value: String?, forHTTPHeaderField field: Header) { - setValue(value, forHTTPHeaderField: field.rawValue) - } +struct KinveyHeaderField { - func value(forHTTPHeaderField field: Header) -> String? { - return value(forHTTPHeaderField: field.rawValue) - } + static let requestId = "X-Kinvey-Request-Id" + static let clientAppVersion = "X-Kinvey-Client-App-Version" + static let apiVersion = "X-Kinvey-API-Version" + static let deviceInformation = "X-Kinvey-Device-Information" } @@ -108,13 +104,13 @@ enum HttpHeader { case .authorization: return HttpHeaderKey.authorization.rawValue case .apiVersion: - return "X-Kinvey-API-Version" + return KinveyHeaderField.apiVersion case .requestId: - return Header.requestId.rawValue + return KinveyHeaderField.requestId case .userAgent: - return "User-Agent" + return HeaderField.userAgent case .deviceInfo: - return "X-Kinvey-Device-Information" + return KinveyHeaderField.deviceInformation } } } @@ -320,7 +316,7 @@ internal class HttpRequest: TaskProgressRequest, Request { if let timeout = timeout { self.request.timeoutInterval = timeout } - self.request.setValue(UUID().uuidString, forHTTPHeaderField: .requestId) + self.request.setValue(UUID().uuidString, forHTTPHeaderField: KinveyHeaderField.requestId) } init( @@ -345,7 +341,7 @@ internal class HttpRequest: TaskProgressRequest, Request { if let body = body { body.attachTo(request: &request) } - self.request.setValue(UUID().uuidString, forHTTPHeaderField: .requestId) + self.request.setValue(UUID().uuidString, forHTTPHeaderField: KinveyHeaderField.requestId) } func prepareRequest() { @@ -360,7 +356,7 @@ internal class HttpRequest: TaskProgressRequest, Request { request.setValue(header.value, forHTTPHeaderField: header.name) } if let clientAppVersion = client.clientAppVersion { - request.setValue(clientAppVersion, forHTTPHeaderField: .clientAppVersion) + request.setValue(clientAppVersion, forHTTPHeaderField: KinveyHeaderField.clientAppVersion) } } @@ -392,7 +388,7 @@ internal class HttpRequest: TaskProgressRequest, Request { let kinveyAuthToken = socialIdentity.kinvey, let refreshToken = kinveyAuthToken["refresh_token"] as? String { - MIC.login(refreshToken: refreshToken) { user, error in + MIC.login(refreshToken: refreshToken, clientId: self.client.clientId) { user, error in if let user = user { self.credential = user self.execute(urlSession: urlSession, completionHandler) diff --git a/Kinvey/Kinvey/HttpRequestFactory.swift b/Kinvey/Kinvey/HttpRequestFactory.swift index b5da0bebe..3e64f0d66 100644 --- a/Kinvey/Kinvey/HttpRequestFactory.swift +++ b/Kinvey/Kinvey/HttpRequestFactory.swift @@ -169,7 +169,7 @@ class HttpRequestFactory: RequestFactory { func buildAppDataSave(_ persistable: T) -> HttpRequest { let collectionName = T.collectionName() var bodyObject = persistable.toJSON() - let objId = bodyObject[PersistableIdKey] as? String + let objId = bodyObject[Entity.Key.entityId] as? String let isNewObj = objId == nil || objId!.hasPrefix(ObjectIdTmpPrefix) let request = HttpRequest( httpMethod: isNewObj ? .post : .put, @@ -181,7 +181,7 @@ class HttpRequestFactory: RequestFactory { request.request.setValue("application/json", forHTTPHeaderField: "Content-Type") if (isNewObj) { - bodyObject[PersistableIdKey] = nil + bodyObject[Entity.Key.entityId] = nil } request.request.httpBody = try! JSONSerialization.data(withJSONObject: bodyObject, options: []) @@ -314,13 +314,23 @@ class HttpRequestFactory: RequestFactory { return request } - func buildOAuthToken(redirectURI: URL, code: String) -> HttpRequest { - let params = [ - "client_id" : client.appKey!, + func set(_ params: inout [String : String], clientId: String?) { + if let appKey = client.appKey { + if let clientId = clientId { + params["client_id"] = "\(appKey):\(clientId)" + } else { + params["client_id"] = appKey + } + } + } + + func buildOAuthToken(redirectURI: URL, code: String, clientId: String?) -> HttpRequest { + var params = [ "grant_type" : "authorization_code", "redirect_uri" : redirectURI.absoluteString, "code" : code ] + set(¶ms, clientId: clientId) let request = HttpRequest( httpMethod: .post, endpoint: Endpoint.oauthToken(client: client), @@ -331,15 +341,15 @@ class HttpRequestFactory: RequestFactory { return request } - func buildOAuthGrantAuth(redirectURI: URL) -> HttpRequest { - let json = [ - "client_id" : client.appKey!, + func buildOAuthGrantAuth(redirectURI: URL, clientId: String?) -> HttpRequest { + var json = [ "redirect_uri" : redirectURI.absoluteString, "response_type" : "code" ] + set(&json, clientId: clientId) let request = HttpRequest( httpMethod: .post, - endpoint: Endpoint.oauthAuth(client: client, redirectURI: redirectURI, loginPage: false), + endpoint: Endpoint.oauthAuth(client: client, clientId: clientId, redirectURI: redirectURI, loginPage: false), credential: client, body: Body.json(json: json), client: client @@ -347,14 +357,14 @@ class HttpRequestFactory: RequestFactory { return request } - func buildOAuthGrantAuthenticate(redirectURI: URL, tempLoginUri: URL, username: String, password: String) -> HttpRequest { - let params = [ - "client_id" : client.appKey!, + func buildOAuthGrantAuthenticate(redirectURI: URL, clientId: String?, tempLoginUri: URL, username: String, password: String) -> HttpRequest { + var params = [ "response_type" : "code", "redirect_uri" : redirectURI.absoluteString, "username" : username, "password" : password ] + set(¶ms, clientId: clientId) let request = HttpRequest( httpMethod: .post, endpoint: Endpoint.url(url: tempLoginUri), @@ -365,12 +375,12 @@ class HttpRequestFactory: RequestFactory { return request } - func buildOAuthGrantRefreshToken(refreshToken: String) -> HttpRequest { - let params = [ - "client_id" : client.appKey!, + func buildOAuthGrantRefreshToken(refreshToken: String, clientId: String?) -> HttpRequest { + var params = [ "grant_type" : "refresh_token", "refresh_token" : refreshToken ] + set(¶ms, clientId: clientId) let request = HttpRequest( httpMethod: .post, endpoint: Endpoint.oauthToken(client: client), diff --git a/Kinvey/Kinvey/Info.plist b/Kinvey/Kinvey/Info.plist index 849997713..cb3e1f5fb 100644 --- a/Kinvey/Kinvey/Info.plist +++ b/Kinvey/Kinvey/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 3.5.1 + 3.5.2 CFBundleSignature ???? CFBundleVersion diff --git a/Kinvey/Kinvey/Keychain.swift b/Kinvey/Kinvey/Keychain.swift index 9377acc24..a1b73ff93 100644 --- a/Kinvey/Kinvey/Keychain.swift +++ b/Kinvey/Kinvey/Keychain.swift @@ -30,30 +30,46 @@ class Keychain { self.keychain = KeychainAccess.Keychain(service: accessGroup, accessGroup: accessGroup).accessibility(.afterFirstUnlockThisDeviceOnly) } - fileprivate static let deviceTokenKey = "deviceToken" + enum Key: String { + + case deviceToken = "deviceToken" + case user = "user" + case clientId = "client_id" + case kinveyAuth = "kinveyAuth" + case defaultEncryptionKey = "defaultEncryptionKey" + + } + var deviceToken: Data? { get { - return keychain[data: Keychain.deviceTokenKey] + return keychain[data: Key.deviceToken] } set { - keychain[data: Keychain.deviceTokenKey] = newValue + keychain[data: Key.deviceToken] = newValue } } - fileprivate static let userKey = "user" var user: User? { get { - return client.responseParser.parseUser(keychain[Keychain.userKey]?.data(using: .utf8)) + return client.responseParser.parseUser(keychain[Key.user]?.data(using: .utf8)) + } + set { + keychain[Key.user] = newValue?.toJSONString() + } + } + + var clientId: String? { + get { + return keychain[Key.clientId] } set { - keychain[Keychain.userKey] = newValue?.toJSONString() + keychain[Key.clientId] = newValue } } - fileprivate static let kinveyAuthKey = AuthSource.kinvey.rawValue var kinveyAuth: [String : Any]? { get { - if let jsonString = keychain[Keychain.kinveyAuthKey], + if let jsonString = keychain[Key.kinveyAuth], let data = jsonString.data(using: .utf8), let jsonObject = try? JSONSerialization.jsonObject(with: data) { @@ -65,20 +81,19 @@ class Keychain { if let newValue = newValue, let data = try? JSONSerialization.data(withJSONObject: newValue) { - keychain[Keychain.kinveyAuthKey] = String(data: data, encoding: .utf8) + keychain[Key.kinveyAuth] = String(data: data, encoding: .utf8) } else { - keychain[Keychain.kinveyAuthKey] = nil + keychain[Key.kinveyAuth] = nil } } } - fileprivate static let defaultEncryptionKeyKey = "defaultEncryptionKey" var defaultEncryptionKey: Data? { get { - return keychain[data: Keychain.defaultEncryptionKeyKey] + return keychain[data: Key.defaultEncryptionKey] } set { - keychain[data: Keychain.defaultEncryptionKeyKey] = newValue + keychain[data: Key.defaultEncryptionKey] = newValue } } @@ -87,3 +102,25 @@ class Keychain { } } + +extension KeychainAccess.Keychain { + + subscript(key: Keychain.Key) -> String? { + get { + return self[key.rawValue] + } + set { + self[key.rawValue] = newValue + } + } + + subscript(data key: Keychain.Key) -> Data? { + get { + return self[data: key.rawValue] + } + set { + self[data: key.rawValue] = newValue + } + } + +} diff --git a/Kinvey/Kinvey/Kinvey.swift b/Kinvey/Kinvey/Kinvey.swift index 32cc8a9dd..26fbdb606 100644 --- a/Kinvey/Kinvey/Kinvey.swift +++ b/Kinvey/Kinvey/Kinvey.swift @@ -9,16 +9,6 @@ import Foundation import XCGLogger -/// Key to map the `_id` column in your Persistable implementation class. -public let PersistableIdKey = "_id" - -/// Key to map the `_acl` column in your Persistable implementation class. -public let PersistableAclKey = "_acl" - -/// Key to map the `_kmd` column in your Persistable implementation class. -public let PersistableMetadataKey = "_kmd" - -let PersistableMetadataLastRetrievedTimeKey = "lrt" let ObjectIdTmpPrefix = "tmp_" /// Shared client instance for simplicity. Use this instance if *you don't need* to handle with multiple Kinvey environments. @@ -58,8 +48,14 @@ extension XCGLogger.Level { let log = XCGLogger.default +func fatalError(_ message: @autoclosure () -> String = "", file: StaticString = #file, line: UInt = #line) -> Never { + let message = message() + log.severe(message) + Swift.fatalError(message, file: file, line: line) +} + /// Level of logging used to log messages inside the Kinvey library -public var logLevel: LogLevel = .warning { +public var logLevel: LogLevel = log.outputLevel.logLevel { didSet { log.outputLevel = logLevel.outputLevel } diff --git a/Kinvey/Kinvey/Localizable.strings b/Kinvey/Kinvey/Localizable.strings index 18e2336fc..06e7ee49a 100644 --- a/Kinvey/Kinvey/Localizable.strings +++ b/Kinvey/Kinvey/Localizable.strings @@ -19,3 +19,5 @@ "Error.userWithoutEmailOrUsername" = "User has no email or username"; "Error.clientNotInitialized" = "Client is not initialized. Please call the initialize() method to initialize the client and try again."; + +"Error.requestTimeout" = "Request Timeout"; diff --git a/Kinvey/Kinvey/MIC.swift b/Kinvey/Kinvey/MIC.swift index d662380da..f7c47ac29 100644 --- a/Kinvey/Kinvey/MIC.swift +++ b/Kinvey/Kinvey/MIC.swift @@ -44,18 +44,34 @@ open class MIC { return code } - open class func urlForLogin(redirectURI: URL, loginPage: Bool = true, client: Client = sharedClient) -> URL { - return Endpoint.oauthAuth(client: client, redirectURI: redirectURI, loginPage: loginPage).url + open class func urlForLogin( + redirectURI: URL, + loginPage: Bool = true, + clientId: String? = nil, + client: Client = sharedClient + ) -> URL { + return Endpoint.oauthAuth( + client: client, + clientId: clientId, + redirectURI: redirectURI, + loginPage: loginPage + ).url } @discardableResult - class func login(redirectURI: URL, code: String, client: Client = sharedClient, completionHandler: ((Result) -> Void)? = nil) -> Request { + class func login( + redirectURI: URL, + code: String, + clientId: String?, + client: Client = sharedClient, + completionHandler: ((Result) -> Void)? = nil + ) -> Request { let requests = MultiRequest() Promise { fulfill, reject in - let request = client.networkRequestFactory.buildOAuthToken(redirectURI: redirectURI, code: code) + let request = client.networkRequestFactory.buildOAuthToken(redirectURI: redirectURI, code: code, clientId: clientId) request.execute { (data, response, error) in if let response = response, response.isOK, let authData = client.responseParser.parse(data) { - requests += User.login(authSource: .kinvey, authData, client: client) { (result: Result) in + requests += User.login(authSource: .kinvey, authData, clientId: clientId, client: client) { (result: Result) in switch result { case .success(let user): fulfill(user) @@ -77,9 +93,16 @@ open class MIC { } @discardableResult - class func login(redirectURI: URL, username: String, password: String, client: Client = sharedClient, completionHandler: ((Result) -> Void)? = nil) -> Request { + class func login( + redirectURI: URL, + username: String, + password: String, + clientId: String?, + client: Client = sharedClient, + completionHandler: ((Result) -> Void)? = nil + ) -> Request { let requests = MultiRequest() - let request = client.networkRequestFactory.buildOAuthGrantAuth(redirectURI: redirectURI) + let request = client.networkRequestFactory.buildOAuthGrantAuth(redirectURI: redirectURI, clientId: clientId) Promise { fulfill, reject in request.execute { (data, response, error) in if let response = response, @@ -96,7 +119,7 @@ open class MIC { requests += request }.then { tempLoginUrl in return Promise { fulfill, reject in - let request = client.networkRequestFactory.buildOAuthGrantAuthenticate(redirectURI: redirectURI, tempLoginUri: tempLoginUrl, username: username, password: password) + let request = client.networkRequestFactory.buildOAuthGrantAuthenticate(redirectURI: redirectURI, clientId: clientId, tempLoginUri: tempLoginUrl, username: username, password: password) let urlSession = URLSession(configuration: client.urlSession.configuration, delegate: URLSessionDelegateAdapter(), delegateQueue: nil) request.execute(urlSession: urlSession) { (data, response, error) in if let response = response, @@ -106,7 +129,7 @@ open class MIC { let url = URL(string: location), let code = parseCode(redirectURI: redirectURI, url: url) { - requests += login(redirectURI: redirectURI, code: code, client: client) { result in + requests += login(redirectURI: redirectURI, code: code, clientId: clientId, client: client) { result in switch result { case .success(let user): fulfill(user as! U) @@ -121,7 +144,7 @@ open class MIC { } requests += request } - }.then { user in + }.then { user -> Void in completionHandler?(.success(user)) }.catch { error in completionHandler?(.failure(error)) @@ -130,9 +153,14 @@ open class MIC { } @discardableResult - class func login(refreshToken: String, client: Client = sharedClient, completionHandler: User.UserHandler? = nil) -> Request { + class func login( + refreshToken: String, + clientId: String?, + client: Client = sharedClient, + completionHandler: User.UserHandler? = nil + ) -> Request { let requests = MultiRequest() - let request = client.networkRequestFactory.buildOAuthGrantRefreshToken(refreshToken: refreshToken) + let request = client.networkRequestFactory.buildOAuthGrantRefreshToken(refreshToken: refreshToken, clientId: clientId) request.execute { (data, response, error) in if let response = response, response.isOK, let authData = client.responseParser.parse(data) { requests += User.login(authSource: .kinvey, authData, client: client, completionHandler: completionHandler) @@ -188,6 +216,7 @@ class MICLoginViewController: UIViewController, WKNavigationDelegate, UIWebViewD let redirectURI: URL let timeout: TimeInterval? let forceUIWebView: Bool + let clientId: String? let client: Client let completionHandler: UserHandler @@ -200,10 +229,11 @@ class MICLoginViewController: UIViewController, WKNavigationDelegate, UIWebViewD } } - init(redirectURI: URL, userType: UserType.Type, timeout: TimeInterval? = nil, forceUIWebView: Bool = false, client: Client = sharedClient, completionHandler: @escaping UserHandler) { + init(redirectURI: URL, userType: UserType.Type, timeout: TimeInterval? = nil, forceUIWebView: Bool = false, clientId: String?, client: Client = sharedClient, completionHandler: @escaping UserHandler) { self.redirectURI = redirectURI self.timeout = timeout self.forceUIWebView = forceUIWebView + self.clientId = clientId self.client = client self.completionHandler = { switch $0 { @@ -319,7 +349,7 @@ class MICLoginViewController: UIViewController, WKNavigationDelegate, UIWebViewD override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - let url = MIC.urlForLogin(redirectURI: redirectURI, client: client) + let url = MIC.urlForLogin(redirectURI: redirectURI, clientId: clientId, client: client) let request = URLRequest(url: url) webView( wkWebView: { $0.load(request) }, @@ -342,8 +372,6 @@ class MICLoginViewController: UIViewController, WKNavigationDelegate, UIWebViewD wkWebView(webView) } else if let webView = webView as? UIWebView { uiWebView(webView) - } else { - fatalError() } } @@ -372,7 +400,7 @@ class MICLoginViewController: UIViewController, WKNavigationDelegate, UIWebViewD func success(code: String) { activityIndicatorView.startAnimating() - MIC.login(redirectURI: redirectURI, code: code, client: client) { result in + MIC.login(redirectURI: redirectURI, code: code, clientId: clientId, client: client) { result in self.activityIndicatorView.stopAnimating() self.closeViewControllerUserInteraction(result) diff --git a/Kinvey/Kinvey/Metadata.swift b/Kinvey/Kinvey/Metadata.swift index 58c289640..877a30cd3 100644 --- a/Kinvey/Kinvey/Metadata.swift +++ b/Kinvey/Kinvey/Metadata.swift @@ -15,17 +15,33 @@ import ObjectMapper public class Metadata: Object, Mappable { /// Last Modification Time Key. + @available(*, deprecated: 3.5.2, message: "Please use Metadata.Key.lastModifiedTime instead") open static let LmtKey = "lmt" /// Entity Creation Time Key. + @available(*, deprecated: 3.5.2, message: "Please use Metadata.Key.entityCreationTime instead") open static let EctKey = "ect" - /// Last Read Time Key. - internal static let LrtKey = "lrt" - /// Authentication Token Key. + @available(*, deprecated: 3.5.2, message: "Please use Metadata.Key.authToken instead") open static let AuthTokenKey = "authtoken" + public struct Key { + + /// Last Modification Time Key. + public static let lastModifiedTime = "lmt" + + /// Entity Creation Time Key. + public static let entityCreationTime = "ect" + + /// Authentication Token Key. + public static let authtoken = "authtoken" + + /// Last Read Time Key. + internal static let lastReadTime = "lrt" + + } + internal dynamic var lmt: String? internal dynamic var ect: String? internal dynamic var lrt: Date = Date() @@ -35,9 +51,6 @@ public class Metadata: Object, Mappable { get { return self.lrt } - set { - lrt = newValue - } } /// Last Modification Time. @@ -63,11 +76,6 @@ public class Metadata: Object, Mappable { /// Authentication Token. open internal(set) dynamic var authtoken: String? - /// Constructor that validates if the map can be build a new instance of Metadata. - public required init?(map: Map) { - super.init() - } - /// Default Constructor. public required init() { super.init() @@ -77,31 +85,40 @@ public class Metadata: Object, Mappable { WARNING: This is an internal initializer not intended for public use. :nodoc: */ - public required init(realm: RLMRealm, schema: RLMObjectSchema) { - super.init(realm: realm, schema: schema) + open override class func ignoredProperties() -> [String] { + return ["lastModifiedTime", "entityCreationTime", "lastReadTime"] + } + + // MARK: Mappable + + /// Constructor that validates if the map can be build a new instance of Metadata. + public required init?(map: Map) { + super.init() } + /// This function is where all variable mappings should occur. It is executed by Mapper during the mapping (serialization and deserialization) process. + public func mapping(map: Map) { + lmt <- map[Key.lastModifiedTime] + ect <- map[Key.entityCreationTime] + authtoken <- map[Key.authtoken] + } + + // MARK: Realm + /** WARNING: This is an internal initializer not intended for public use. :nodoc: */ - public required init(value: Any, schema: RLMSchema) { - super.init(value: value, schema: schema) - } - - /// This function is where all variable mappings should occur. It is executed by Mapper during the mapping (serialization and deserialization) process. - open func mapping(map: Map) { - lmt <- map[Metadata.LmtKey] - ect <- map[Metadata.EctKey] - authtoken <- map[Metadata.AuthTokenKey] + public required init(realm: RLMRealm, schema: RLMObjectSchema) { + super.init(realm: realm, schema: schema) } /** WARNING: This is an internal initializer not intended for public use. :nodoc: */ - open override class func ignoredProperties() -> [String] { - return ["lastModifiedTime", "entityCreationTime", "lastReadTime"] + public required init(value: Any, schema: RLMSchema) { + super.init(value: value, schema: schema) } } @@ -112,7 +129,7 @@ public final class UserMetadata: Metadata { open internal(set) var passwordReset: PasswordReset? open internal(set) var userStatus: UserStatus? - open override func mapping(map: Map) { + public override func mapping(map: Map) { super.mapping(map: map) emailVerification <- map["emailVerification"] @@ -122,29 +139,20 @@ public final class UserMetadata: Metadata { } -public final class EmailVerification: Object, Mappable { +public final class EmailVerification: Object { open internal(set) var status: String? - open internal(set) var lastStateChangeAt:Date? - open internal(set) var lastConfirmedAt:Date? - open internal(set) var emailAddress:String? - - /// Constructor that validates if the map can be build a new instance of Metadata. - public required init?(map: Map) { - super.init() - } - - /// Default Constructor. - public required init() { - super.init() - } + open internal(set) var lastStateChangeAt: Date? + open internal(set) var lastConfirmedAt: Date? + open internal(set) var emailAddress: String? - public required init(realm: RLMRealm, schema: RLMObjectSchema) { - super.init(realm: realm, schema: schema) - } +} + +extension EmailVerification: Mappable { - public required init(value: Any, schema: RLMSchema) { - super.init(value: value, schema: schema) + /// Constructor that validates if the map can be build a new instance of Metadata. + public convenience init?(map: Map) { + self.init() } /// This function is where all variable mappings should occur. It is executed by Mapper during the mapping (serialization and deserialization) process. @@ -154,29 +162,21 @@ public final class EmailVerification: Object, Mappable { lastConfirmedAt <- (map["lastConfirmedAt"], KinveyDateTransform()) emailAddress <- map["emailAddress"] } + } -public final class PasswordReset: Object, Mappable { +public final class PasswordReset: Object { open internal(set) var status: String? open internal(set) var lastStateChangeAt: Date? - /// Constructor that validates if the map can be build a new instance of Metadata. - public required init?(map: Map) { - super.init() - } - - /// Default Constructor. - public required init() { - super.init() - } - - public required init(realm: RLMRealm, schema: RLMObjectSchema) { - super.init(realm: realm, schema: schema) - } +} + +extension PasswordReset: Mappable { - public required init(value: Any, schema: RLMSchema) { - super.init(value: value, schema: schema) + /// Constructor that validates if the map can be build a new instance of Metadata. + public convenience init?(map: Map) { + self.init() } /// This function is where all variable mappings should occur. It is executed by Mapper during the mapping (serialization and deserialization) process. @@ -184,29 +184,21 @@ public final class PasswordReset: Object, Mappable { status <- map["status"] lastStateChangeAt <- (map["lastStateChangeAt"], KinveyDateTransform()) } + } -public final class UserStatus: Object, Mappable { +public final class UserStatus: Object { open internal(set) var value: String? open internal(set) var lastChange: Date? - /// Constructor that validates if the map can be build a new instance of Metadata. - public required init?(map: Map) { - super.init() - } - - /// Default Constructor. - public required init() { - super.init() - } - - public required init(realm: RLMRealm, schema: RLMObjectSchema) { - super.init(realm: realm, schema: schema) - } +} + +extension UserStatus: Mappable { - public required init(value: Any, schema: RLMSchema) { - super.init(value: value, schema: schema) + /// Constructor that validates if the map can be build a new instance of Metadata. + public convenience init?(map: Map) { + self.init() } /// This function is where all variable mappings should occur. It is executed by Mapper during the mapping (serialization and deserialization) process. @@ -214,5 +206,5 @@ public final class UserStatus: Object, Mappable { value <- map["val"] lastChange <- (map["lastChange"], KinveyDateTransform()) } - + } diff --git a/Kinvey/Kinvey/Migration.swift b/Kinvey/Kinvey/Migration.swift index df88faf45..3ab8df444 100644 --- a/Kinvey/Kinvey/Migration.swift +++ b/Kinvey/Kinvey/Migration.swift @@ -75,16 +75,14 @@ open class Migration: NSObject { if let oldObjectSchema = oldObjectSchema { let oldProperties = oldObjectSchema.properties.map { $0.name } realmMigration.enumerateObjects(ofType: oldSchemaClassName) { (oldObject, newObject) in - if let oldObject = oldObject { + if let oldObject = oldObject, let newObject = newObject { let oldDictionary = oldObject.dictionaryWithValues(forKeys: oldProperties) - let newDictionary = migrationObjectHandler?(oldDictionary) - if let newObject = newObject { + if let newDictionary = migrationObjectHandler?(oldDictionary) { + newObject.setValuesForKeys(newDictionary) + } else { self.realmMigration.delete(newObject) } - if let newDictionary = newDictionary { - self.realmMigration.create(className, value: newDictionary) - } } } } diff --git a/Kinvey/Kinvey/MultiRequest.swift b/Kinvey/Kinvey/MultiRequest.swift index 71bfa6993..e9c11208b 100644 --- a/Kinvey/Kinvey/MultiRequest.swift +++ b/Kinvey/Kinvey/MultiRequest.swift @@ -45,7 +45,7 @@ internal class MultiRequest: NSObject, Request { } } - var _cancelled = false + private var _cancelled = false internal var cancelled: Bool { get { for request in requests { diff --git a/Kinvey/Kinvey/ObjCRuntime.swift b/Kinvey/Kinvey/ObjCRuntime.swift index fe13fd588..ec3cb2588 100644 --- a/Kinvey/Kinvey/ObjCRuntime.swift +++ b/Kinvey/Kinvey/ObjCRuntime.swift @@ -11,9 +11,6 @@ import ObjectiveC internal class ObjCRuntime: NSObject { - fileprivate override init() { - } - internal class func type(_ target: AnyClass, isSubtypeOf cls: AnyClass) -> Bool { if target == cls { return true @@ -26,13 +23,13 @@ internal class ObjCRuntime: NSObject { } internal class func typeForPropertyName(_ cls: AnyClass, propertyName: String) -> AnyClass? { - let regexClassName = try! NSRegularExpression(pattern: "@\"(\\w+)(?:<(\\w+)>)?\"", options: []) + let regexClassName = try! NSRegularExpression(pattern: "@\"(\\w+)(?:<(\\w+)>)?\"") let property = class_getProperty(cls, propertyName) let attributeValueCString = property_copyAttributeValue(property, "T") defer { free(attributeValueCString) } if let attributeValue = String(validatingUTF8: attributeValueCString!), - let textCheckingResult = regexClassName.matches(in: attributeValue, options: [], range: NSMakeRange(0, attributeValue.characters.count)).first + let textCheckingResult = regexClassName.matches(in: attributeValue, range: NSMakeRange(0, attributeValue.characters.count)).first { let attributeValueNSString = attributeValue as NSString let propertyTypeName = attributeValueNSString.substring(with: textCheckingResult.rangeAt(1)) @@ -48,40 +45,43 @@ internal class ObjCRuntime: NSObject { var results = [String : (String?, String?)]() while cls != nil { var propertyCount = UInt32(0) - guard let properties = class_copyPropertyList(cls, &propertyCount) else { break } - defer { free(properties) } - for i in UInt32(0) ..< propertyCount { - guard let property = properties[Int(i)] else { break } - if let propertyName = String(validatingUTF8: property_getName(property)) - { - var attributeCount = UInt32(0) - guard let attributes = property_copyAttributeList(property, &attributeCount) else { break } - defer { free(attributes) } - for x in UInt32(0) ..< attributeCount { - let attribute = attributes[Int(x)] - if let attributeName = String(validatingUTF8: attribute.name), - attributeName == "T", - let attributeValue = String(validatingUTF8: attribute.value), - let textCheckingResult = regexClassName.matches(in: attributeValue, range: NSMakeRange(0, attributeValue.characters.count)).first + if let properties = class_copyPropertyList(cls, &propertyCount) { + defer { free(properties) } + for i in UInt32(0) ..< propertyCount { + if let property = properties[Int(i)] { + if let propertyName = String(validatingUTF8: property_getName(property)) { - var tuple: (type: String?, subType: String?) = (nil, nil) - if let range = textCheckingResult.rangeAt(1).toRange() { - tuple.type = attributeValue.substring(with: range) - } - - if textCheckingResult.numberOfRanges > 1, - let range = textCheckingResult.rangeAt(2).toRange() { - tuple.subType = attributeValue.substring(with: range) + var attributeCount = UInt32(0) + if let attributes = property_copyAttributeList(property, &attributeCount) { + defer { free(attributes) } + for x in UInt32(0) ..< attributeCount { + let attribute = attributes[Int(x)] + if let attributeName = String(validatingUTF8: attribute.name), + attributeName == "T", + let attributeValue = String(validatingUTF8: attribute.value), + let textCheckingResult = regexClassName.matches(in: attributeValue, range: NSMakeRange(0, attributeValue.characters.count)).first + { + var tuple: (type: String?, subType: String?) = (nil, nil) + if let range = textCheckingResult.rangeAt(1).toRange() { + tuple.type = attributeValue.substring(with: range) + } + + if textCheckingResult.numberOfRanges > 1, + let range = textCheckingResult.rangeAt(2).toRange() { + tuple.subType = attributeValue.substring(with: range) + } + results[propertyName] = tuple + } + } } - results[propertyName] = tuple } } } - } - if cls == Entity.self { - cls = nil - } else { - cls = class_getSuperclass(cls) + if cls == Entity.self { + cls = nil + } else { + cls = class_getSuperclass(cls) + } } } return results diff --git a/Kinvey/Kinvey/Operation.swift b/Kinvey/Kinvey/Operation.swift index ea0740c02..a3e9c2095 100644 --- a/Kinvey/Kinvey/Operation.swift +++ b/Kinvey/Kinvey/Operation.swift @@ -123,9 +123,9 @@ internal class Operation: NSObject where T: NSObject { func reduceToIdsLmts(_ jsonArray: [JsonDictionary]) -> [String : String] { var items = [String : String](minimumCapacity: jsonArray.count) for json in jsonArray { - if let id = json[PersistableIdKey] as? String, - let kmd = json[PersistableMetadataKey] as? JsonDictionary, - let lmt = kmd[Metadata.LmtKey] as? String + if let id = json[Entity.Key.entityId] as? String, + let kmd = json[Entity.Key.metadata] as? JsonDictionary, + let lmt = kmd[Metadata.Key.lastModifiedTime] as? String { items[id] = lmt } diff --git a/Kinvey/Kinvey/Persistable.swift b/Kinvey/Kinvey/Persistable.swift index 77e8db198..1cc73be68 100644 --- a/Kinvey/Kinvey/Persistable.swift +++ b/Kinvey/Kinvey/Persistable.swift @@ -30,9 +30,26 @@ public protocol Persistable: Mappable { /// Default Constructor. init() - /// Override this method to tell how to map your own objects. - mutating func propertyMapping(_ map: Map) +} + +struct AnyTransform: TransformType { + + private let _transformFromJSON: (Any?) -> Any? + private let _transformToJSON: (Any?) -> Any? + + init(_ transform: Transform) { + _transformFromJSON = { transform.transformFromJSON($0) } + _transformToJSON = { transform.transformToJSON($0 as? Transform.Object) } + } + func transformFromJSON(_ value: Any?) -> Any? { + return _transformFromJSON(value) + } + + func transformToJSON(_ value: Any?) -> Any? { + return _transformToJSON(value) + } + } internal func kinveyMappingType(left: String, right: String) { @@ -41,7 +58,19 @@ internal func kinveyMappingType(left: String, right: String) { let className = kinveyMappingType.first?.0, var classMapping = kinveyMappingType[className] { - classMapping[left] = right + classMapping[left] = (right, nil) + kinveyMappingType[className] = classMapping + currentThread.threadDictionary[KinveyMappingTypeKey] = kinveyMappingType + } +} + +internal func kinveyMappingType(left: String, right: String, transform: Transform) { + let currentThread = Thread.current + if var kinveyMappingType = currentThread.threadDictionary[KinveyMappingTypeKey] as? [String : PropertyMap], + let className = kinveyMappingType.first?.0, + var classMapping = kinveyMappingType[className] + { + classMapping[left] = (right, AnyTransform(transform)) kinveyMappingType[className] = classMapping currentThread.threadDictionary[KinveyMappingTypeKey] = kinveyMappingType } @@ -92,21 +121,47 @@ public func <- (left: inout T!, right: (String, Map)) { /// Override operator used during the `propertyMapping(_:)` method. public func <- (left: inout Transform.Object, right: (String, Map, Transform)) { let (right, map, transform) = right - kinveyMappingType(left: right, right: map.currentKey!) + kinveyMappingType(left: right, right: map.currentKey!, transform: transform) left <- (map, transform) } /// Override operator used during the `propertyMapping(_:)` method. public func <- (left: inout Transform.Object?, right: (String, Map, Transform)) { let (right, map, transform) = right - kinveyMappingType(left: right, right: map.currentKey!) + kinveyMappingType(left: right, right: map.currentKey!, transform: transform) left <- (map, transform) } /// Override operator used during the `propertyMapping(_:)` method. public func <- (left: inout Transform.Object!, right: (String, Map, Transform)) { let (right, map, transform) = right - kinveyMappingType(left: right, right: map.currentKey!) + kinveyMappingType(left: right, right: map.currentKey!, transform: transform) + left <- (map, transform) +} + +// MARK: Default Date Transform + +/// Override operator used during the `propertyMapping(_:)` method. +public func <- (left: inout Date, right: (String, Map)) { + let (right, map) = right + let transform = KinveyDateTransform() + kinveyMappingType(left: right, right: map.currentKey!, transform: transform) + left <- (map, transform) +} + +/// Override operator used during the `propertyMapping(_:)` method. +public func <- (left: inout Date?, right: (String, Map)) { + let (right, map) = right + let transform = KinveyDateTransform() + kinveyMappingType(left: right, right: map.currentKey!, transform: transform) + left <- (map, transform) +} + +/// Override operator used during the `propertyMapping(_:)` method. +public func <- (left: inout Date!, right: (String, Map)) { + let (right, map) = right + let transform = KinveyDateTransform() + kinveyMappingType(left: right, right: map.currentKey!, transform: transform) left <- (map, transform) } @@ -341,7 +396,7 @@ internal let KinveyMappingTypeKey = "Kinvey Mapping Type" struct PropertyMap: Sequence, IteratorProtocol, ExpressibleByDictionaryLiteral { typealias Key = String - typealias Value = String + typealias Value = (String, AnyTransform?) typealias Element = (Key, Value) private var map = [Key : Value]() @@ -383,7 +438,7 @@ extension Persistable { static func propertyMappingReverse() -> [String : [String]] { var results = [String : [String]]() - for (key, value) in propertyMapping() { + for (key, (value, _)) in propertyMapping() { var properties = results[value] if properties == nil { properties = [String]() @@ -391,15 +446,17 @@ extension Persistable { properties!.append(key) results[value] = properties } - guard - results[PersistableIdKey] != nil, - results[PersistableMetadataKey] != nil - else { + let entityIdMapped = results[Entity.Key.entityId] != nil + let metadataMapped = results[Entity.Key.metadata] != nil + if !(entityIdMapped && metadataMapped) { let isEntity = self is Entity.Type let hintMessage = isEntity ? "Please call super.propertyMapping() inside your propertyMapping() method." : "Please add properties in your Persistable model class to map the missing properties." - precondition(results[PersistableIdKey] != nil, "Property \(PersistableIdKey) (PersistableIdKey) is missing in the propertyMapping() method. \(hintMessage)") - precondition(results[PersistableMetadataKey] != nil, "Property \(PersistableMetadataKey) (PersistableMetadataKey) is missing in the propertyMapping() method. \(hintMessage)") - fatalError(hintMessage) + guard entityIdMapped else { + fatalError("Property \(Entity.Key.entityId) (Entity.Key.entityId) is missing in the propertyMapping() method. \(hintMessage)") + } + guard metadataMapped else { + fatalError("Property \(Entity.Key.metadata) (Entity.Key.metadata) is missing in the propertyMapping() method. \(hintMessage)") + } } return results } @@ -418,20 +475,20 @@ extension Persistable { return [:] } - static func propertyMapping(_ propertyName: String) -> String? { + static func propertyMapping(_ propertyName: String) -> PropertyMap.Value? { return propertyMapping()[propertyName] } internal static func entityIdProperty() -> String { - return propertyMappingReverse()[PersistableIdKey]!.last! + return propertyMappingReverse()[Entity.Key.entityId]!.last! } internal static func aclProperty() -> String? { - return propertyMappingReverse()[PersistableAclKey]?.last + return propertyMappingReverse()[Entity.Key.acl]?.last } internal static func metadataProperty() -> String? { - return propertyMappingReverse()[PersistableMetadataKey]?.last + return propertyMappingReverse()[Entity.Key.metadata]?.last } } diff --git a/Kinvey/Kinvey/PullOperation.swift b/Kinvey/Kinvey/PullOperation.swift index d6b84713f..b384c3a49 100644 --- a/Kinvey/Kinvey/PullOperation.swift +++ b/Kinvey/Kinvey/PullOperation.swift @@ -15,9 +15,7 @@ internal class PullOperation: FindOperation where T: NSObject } override var mustRemoveCachedRecords: Bool { - get { - return true - } + return true } } diff --git a/Kinvey/Kinvey/Push.swift b/Kinvey/Kinvey/Push.swift index 84fbc639f..e749da136 100644 --- a/Kinvey/Kinvey/Push.swift +++ b/Kinvey/Kinvey/Push.swift @@ -215,25 +215,39 @@ open class Push { /// Unregister the current device to receive push notifications. open func unRegisterDeviceToken(_ completionHandler: BoolCompletionHandler? = nil) { - guard let deviceToken = deviceToken else { - log.error("Device token not found") - fatalError("Device token not found") + unRegisterDeviceToken { (result: Result) in + switch result { + case .success: + completionHandler?(true, nil) + case .failure(let error): + completionHandler?(false, error) + } } - - Promise { fulfill, reject in + } + + /// Unregister the current device to receive push notifications. + open func unRegisterDeviceToken(_ completionHandler: ((Result) -> Void)? = nil) { + Promise { fulfill, reject in + guard let deviceToken = deviceToken else { + reject(Error.invalidOperation(description: "Device token not found")) + return + } + let request = self.client.networkRequestFactory.buildPushUnRegisterDevice(deviceToken) - request.execute({ (data, response, error) -> Void in - if let response = response, response.isOK { + request.execute { (data, response, error) -> Void in + if let response = response, + response.isOK + { self.deviceToken = nil - fulfill(true) + fulfill() } else { reject(buildError(data, response, error, self.client)) } - }) + } }.then { success in - completionHandler?(success, nil) + completionHandler?(.success()) }.catch { error in - completionHandler?(false, error) + completionHandler?(.failure(error)) } } diff --git a/Kinvey/Kinvey/Query.swift b/Kinvey/Kinvey/Query.swift index e9cb1b2ef..458cae400 100644 --- a/Kinvey/Kinvey/Query.swift +++ b/Kinvey/Kinvey/Query.swift @@ -47,7 +47,7 @@ public final class Query: NSObject, BuilderType, Mappable { /// Impose a limit of records in the results of the query. open var limit: Int? - internal func translate(expression: NSExpression) -> NSExpression { + internal func translate(expression: NSExpression, otherSideExpression: NSExpression) -> NSExpression { switch expression.expressionType { case .keyPath: var keyPath = expression.keyPath @@ -55,16 +55,28 @@ public final class Query: NSObject, BuilderType, Mappable { if keyPath.contains(".") { var keyPaths = [String]() for item in keyPath.components(separatedBy: ".") { - keyPaths.append(persistableType?.propertyMapping(item) ?? item) + if let (keyPath, _) = persistableType?.propertyMapping(item) { + keyPaths.append(keyPath) + } else { + keyPaths.append(item) + } if let persistableTypeTmp = persistableType { persistableType = ObjCRuntime.typeForPropertyName(persistableTypeTmp as! AnyClass, propertyName: item) as? Persistable.Type } } keyPath = keyPaths.joined(separator: ".") - } else if let translatedKeyPath = persistableType?.propertyMapping(keyPath) { + } else if let (translatedKeyPath, _) = persistableType?.propertyMapping(keyPath) { keyPath = translatedKeyPath } return NSExpression(forKeyPath: keyPath) + case .constantValue: + if otherSideExpression.expressionType == .keyPath, + let (_, optionalTransform) = persistableType?.propertyMapping(otherSideExpression.keyPath), + let transform = optionalTransform + { + return NSExpression(forConstantValue: transform.transformToJSON(expression.constantValue)) + } + return expression default: return expression } @@ -73,8 +85,8 @@ public final class Query: NSObject, BuilderType, Mappable { fileprivate func translate(predicate: NSPredicate) -> NSPredicate { if let predicate = predicate as? NSComparisonPredicate { return NSComparisonPredicate( - leftExpression: translate(expression: predicate.leftExpression), - rightExpression: translate(expression: predicate.rightExpression), + leftExpression: translate(expression: predicate.leftExpression, otherSideExpression: predicate.rightExpression), + rightExpression: translate(expression: predicate.rightExpression, otherSideExpression: predicate.leftExpression), modifier: predicate.comparisonPredicateModifier, type: predicate.predicateOperatorType, options: predicate.options @@ -90,22 +102,24 @@ public final class Query: NSObject, BuilderType, Mappable { } var isEmpty: Bool { - return predicate == nil && sortDescriptors == nil && skip == nil && limit == nil + return predicate == nil && + (sortDescriptors == nil || sortDescriptors!.isEmpty) && + skip == nil && + limit == nil && + (fields == nil || fields!.isEmpty) } fileprivate var queryStringEncoded: String? { - get { - if let predicate = predicate { - let translatedPredicate = translate(predicate: predicate) - let queryObj = translatedPredicate.mongoDBQuery! - - let data = try! JSONSerialization.data(withJSONObject: queryObj, options: []) - let queryStr = String(data: data, encoding: String.Encoding.utf8)! - return queryStr.trimmingCharacters(in: CharacterSet.whitespaces) - } - - return "{}" + guard let predicate = predicate else { + return nil } + + let translatedPredicate = translate(predicate: predicate) + let queryObj = translatedPredicate.mongoDBQuery! + + let data = try! JSONSerialization.data(withJSONObject: queryObj, options: []) + let queryStr = String(data: data, encoding: String.Encoding.utf8)! + return queryStr.trimmingCharacters(in: CharacterSet.whitespaces) } internal var urlQueryItems: [URLQueryItem]? { diff --git a/Kinvey/Kinvey/RealmCache.swift b/Kinvey/Kinvey/RealmCache.swift index d8e96766d..fd37a718c 100644 --- a/Kinvey/Kinvey/RealmCache.swift +++ b/Kinvey/Kinvey/RealmCache.swift @@ -35,9 +35,7 @@ internal class RealmCache: Cache, CacheType where T: NSObject required init(persistenceId: String, fileURL: URL? = nil, encryptionKey: Data? = nil, schemaVersion: UInt64) { if !(T.self is Entity.Type) { - let message = "\(T.self) needs to be a Entity" - log.severe(message) - fatalError(message) + fatalError("\(T.self) needs to be a Entity") } var configuration = Realm.Configuration() if let fileURL = fileURL { @@ -267,10 +265,6 @@ internal class RealmCache: Cache, CacheType where T: NSObject return AnyRandomAccessCollection(realmResults) } - - fileprivate func newInstance(_ type: P.Type) -> P { - return type.init() - } fileprivate func detach(_ entity: Object, props: [String]) -> Object { log.verbose("Detaching object: \(entity)") @@ -384,15 +378,6 @@ internal class RealmCache: Cache, CacheType where T: NSObject return results } - func findAll() -> [T] { - log.verbose("Finding All") - var results = [T]() - executor.executeAndWait { - results = self.detach(AnyRandomAccessCollection(self.realm.objects(self.entityType)), query: nil) - } - return results - } - func count(query: Query? = nil) -> Int { log.verbose("Counting by query: \(String(describing: query))") var result = 0 @@ -454,15 +439,6 @@ internal class RealmCache: Cache, CacheType where T: NSObject return result } - func removeAll() { - log.verbose("Removing all objects") - executor.executeAndWait { - try! self.realm.write { - self.realm.delete(self.realm.objects(self.entityType)) - } - } - } - func clear(query: Query? = nil) { log.verbose("Clearing cache") executor.executeAndWait { @@ -486,10 +462,6 @@ internal class RealmCache: Cache, CacheType where T: NSObject } } - func group(aggregation: Aggregation, predicate: NSPredicate?) -> [JsonDictionary] { - fatalError("Custom Aggregation not supported against local cache") - } - } extension NSComparisonPredicate { @@ -590,7 +562,7 @@ internal class RealmPendingOperation: Object, PendingOperationType { convenience init(request: URLRequest, collectionName: String, objectId: String?) { self.init() - requestId = request.value(forHTTPHeaderField: .requestId)! + requestId = request.value(forHTTPHeaderField: KinveyHeaderField.requestId)! self.collectionName = collectionName self.objectId = objectId method = request.httpMethod ?? "GET" diff --git a/Kinvey/Kinvey/RealmFileCache.swift b/Kinvey/Kinvey/RealmFileCache.swift index efe71c7be..379811147 100644 --- a/Kinvey/Kinvey/RealmFileCache.swift +++ b/Kinvey/Kinvey/RealmFileCache.swift @@ -9,7 +9,9 @@ import Foundation import RealmSwift -class RealmFileCache: FileCache { +class RealmFileCache: FileCache { + + typealias FileType = T let persistenceId: String let realm: Realm @@ -39,7 +41,7 @@ class RealmFileCache: FileCache { executor = Executor() } - func save(_ file: File, beforeSave: (() -> Void)?) { + func save(_ file: FileType, beforeSave: (() -> Void)?) { executor.executeAndWait { try! self.realm.write { beforeSave?() @@ -48,7 +50,7 @@ class RealmFileCache: FileCache { } } - func remove(_ file: File) { + func remove(_ file: FileType) { executor.executeAndWait { try! self.realm.write { if let fileId = file.fileId, let file = self.realm.object(ofType: File.self, forPrimaryKey: fileId) { @@ -69,11 +71,11 @@ class RealmFileCache: FileCache { } } - func get(_ fileId: String) -> File? { - var file: File? = nil + func get(_ fileId: String) -> FileType? { + var file: FileType? = nil executor.executeAndWait { - file = self.realm.object(ofType: File.self, forPrimaryKey: fileId) + file = self.realm.object(ofType: FileType.self, forPrimaryKey: fileId) } return file diff --git a/Kinvey/Kinvey/RealmSync.swift b/Kinvey/Kinvey/RealmSync.swift index 360ffe189..1da5387a4 100644 --- a/Kinvey/Kinvey/RealmSync.swift +++ b/Kinvey/Kinvey/RealmSync.swift @@ -23,9 +23,7 @@ class RealmSync: SyncType where T: NSObject { required init(persistenceId: String, fileURL: URL? = nil, encryptionKey: Data? = nil, schemaVersion: UInt64) { if !(T.self is Entity.Type) { - let message = "\(T.self) needs to be a Entity" - log.severe(message) - fatalError(message) + fatalError("\(T.self) needs to be a Entity") } var configuration = Realm.Configuration() if let fileURL = fileURL { @@ -41,12 +39,6 @@ class RealmSync: SyncType where T: NSObject { self.persistenceId = persistenceId log.debug("Sync File: \(self.realm.configuration.fileURL!.path)") } - - required init(persistenceId: String) { - let message = "Method \(#function) must be overridden" - log.severe(message) - fatalError(message) - } func createPendingOperation(_ request: URLRequest, objectId: String?) -> PendingOperationType { return RealmPendingOperation(request: request, collectionName: T.collectionName(), objectId: objectId) @@ -67,15 +59,11 @@ class RealmSync: SyncType where T: NSObject { } } - func pendingOperations(_ objectId: String?) -> AnyCollection { - log.verbose("Fetching pending operations by object id: \(String(describing: objectId))") + func pendingOperations() -> AnyCollection { + log.verbose("Fetching pending operations") var results: [PendingOperationType]? executor.executeAndWait { - var realmResults = self.realm.objects(RealmPendingOperation.self) - if let objectId = objectId { - realmResults = realmResults.filter("objectId == %@", objectId) - } - results = realmResults.map { + results = self.realm.objects(RealmPendingOperation.self).map { return RealmPendingOperationThreadSafeReference($0) } } diff --git a/Kinvey/Kinvey/RemoveByIdOperation.swift b/Kinvey/Kinvey/RemoveByIdOperation.swift index d12e3004c..d580802de 100644 --- a/Kinvey/Kinvey/RemoveByIdOperation.swift +++ b/Kinvey/Kinvey/RemoveByIdOperation.swift @@ -12,14 +12,11 @@ internal class RemoveByIdOperation: RemoveOperation where T: let objectId: String - override func buildRequest() -> HttpRequest { - return client.networkRequestFactory.buildAppDataRemoveById(collectionName: T.collectionName(), objectId: objectId) - } - internal init(objectId: String, writePolicy: WritePolicy, sync: AnySync? = nil, cache: AnyCache? = nil, client: Client) { self.objectId = objectId let query = Query(format: "\(T.entityIdProperty()) == %@", objectId as Any) - super.init(query: query, writePolicy: writePolicy, sync: sync, cache: cache, client: client) + let httpRequest = client.networkRequestFactory.buildAppDataRemoveById(collectionName: T.collectionName(), objectId: objectId) + super.init(query: query, httpRequest: httpRequest, writePolicy: writePolicy, sync: sync, cache: cache, client: client) } } diff --git a/Kinvey/Kinvey/RemoveByQueryOperation.swift b/Kinvey/Kinvey/RemoveByQueryOperation.swift index 8045dbabb..f5724724f 100644 --- a/Kinvey/Kinvey/RemoveByQueryOperation.swift +++ b/Kinvey/Kinvey/RemoveByQueryOperation.swift @@ -10,12 +10,9 @@ import Foundation internal class RemoveByQueryOperation: RemoveOperation where T: NSObject { - override init(query: Query, writePolicy: WritePolicy, sync: AnySync? = nil, cache: AnyCache? = nil, client: Client) { - super.init(query: query, writePolicy: writePolicy, sync: sync, cache: cache, client: client) - } - - override func buildRequest() -> HttpRequest { - return client.networkRequestFactory.buildAppDataRemoveByQuery(collectionName: T.collectionName(), query: query) + init(query: Query, writePolicy: WritePolicy, sync: AnySync? = nil, cache: AnyCache? = nil, client: Client) { + let httpRequest = client.networkRequestFactory.buildAppDataRemoveByQuery(collectionName: T.collectionName(), query: query) + super.init(query: query, httpRequest: httpRequest, writePolicy: writePolicy, sync: sync, cache: cache, client: client) } } diff --git a/Kinvey/Kinvey/RemoveOperation.swift b/Kinvey/Kinvey/RemoveOperation.swift index 706f4f3ca..b16bb3b9d 100644 --- a/Kinvey/Kinvey/RemoveOperation.swift +++ b/Kinvey/Kinvey/RemoveOperation.swift @@ -11,23 +11,19 @@ import Foundation class RemoveOperation: WriteOperation, WriteOperationType where T: NSObject { let query: Query - lazy var request: HttpRequest = self.buildRequest() + private let httpRequest: () -> HttpRequest + lazy var request: HttpRequest = self.httpRequest() - init(query: Query, writePolicy: WritePolicy, sync: AnySync? = nil, cache: AnyCache? = nil, client: Client) { + init(query: Query, httpRequest: @autoclosure @escaping () -> HttpRequest, writePolicy: WritePolicy, sync: AnySync? = nil, cache: AnyCache? = nil, client: Client) { self.query = query + self.httpRequest = httpRequest super.init(writePolicy: writePolicy, sync: sync, cache: cache, client: client) } - func buildRequest() -> HttpRequest { - let message = "Method \(#function) must be overridden" - log.severe(message) - fatalError(message) - } - func executeLocal(_ completionHandler: CompletionHandler? = nil) -> Request { let request = LocalRequest() request.execute { () -> Void in - var count: Int? + var count = 0 if let cache = self.cache { let realmObjects = cache.find(byQuery: self.query) count = realmObjects.count @@ -43,15 +39,9 @@ class RemoveOperation: WriteOperation, WriteOperationTyp } } } - } else { - count = 0 } } - if let count = count { - completionHandler?(.success(count)) - } else { - completionHandler?(.failure(buildError(client: client))) - } + completionHandler?(.success(count)) } return request } diff --git a/Kinvey/Kinvey/RequestFactory.swift b/Kinvey/Kinvey/RequestFactory.swift index 8a0f6cc9f..27144663e 100644 --- a/Kinvey/Kinvey/RequestFactory.swift +++ b/Kinvey/Kinvey/RequestFactory.swift @@ -44,10 +44,10 @@ protocol RequestFactory { func buildCustomEndpoint(_ name: String) -> HttpRequest - func buildOAuthToken(redirectURI: URL, code: String) -> HttpRequest + func buildOAuthToken(redirectURI: URL, code: String, clientId: String?) -> HttpRequest - func buildOAuthGrantAuth(redirectURI: URL) -> HttpRequest - func buildOAuthGrantAuthenticate(redirectURI: URL, tempLoginUri: URL, username: String, password: String) -> HttpRequest - func buildOAuthGrantRefreshToken(refreshToken: String) -> HttpRequest + func buildOAuthGrantAuth(redirectURI: URL, clientId: String?) -> HttpRequest + func buildOAuthGrantAuthenticate(redirectURI: URL, clientId: String?, tempLoginUri: URL, username: String, password: String) -> HttpRequest + func buildOAuthGrantRefreshToken(refreshToken: String, clientId: String?) -> HttpRequest } diff --git a/Kinvey/Kinvey/String.swift b/Kinvey/Kinvey/String.swift index 1bb15eead..0bf0c917f 100644 --- a/Kinvey/Kinvey/String.swift +++ b/Kinvey/Kinvey/String.swift @@ -49,4 +49,8 @@ extension String { return self[startIndex.. String { + return substring(with: range.location ..< range.location + range.length) + } + } diff --git a/Kinvey/Kinvey/Sync.swift b/Kinvey/Kinvey/Sync.swift index 17e7e3367..d41a3c134 100644 --- a/Kinvey/Kinvey/Sync.swift +++ b/Kinvey/Kinvey/Sync.swift @@ -10,14 +10,11 @@ import Foundation internal protocol SyncType { - var persistenceId: String { get } - var collectionName: String { get } - //Create func createPendingOperation(_ request: URLRequest, objectId: String?) -> PendingOperationType //Read - func pendingOperations(_ objectId: String?) -> AnyCollection + func pendingOperations() -> AnyCollection //Update func savePendingOperation(_ pendingOperation: PendingOperationType) @@ -31,25 +28,13 @@ internal protocol SyncType { internal final class AnySync: SyncType { - var persistenceId: String { - return _getPersistenceId() - } - - var collectionName: String { - return _getCollectionName() - } - - private let _getPersistenceId: () -> String - private let _getCollectionName: () -> String private let _createPendingOperation: (URLRequest, String?) -> PendingOperationType - private let _pendingOperations: (String?) -> AnyCollection + private let _pendingOperations: () -> AnyCollection private let _savePendingOperation: (PendingOperationType) -> Void private let _removePendingOperation: (PendingOperationType) -> Void private let _removeAllPendingOperations: (String?, [String]?) -> Void init(_ sync: Sync) { - _getPersistenceId = { return sync.persistenceId } - _getCollectionName = { return sync.collectionName } _createPendingOperation = sync.createPendingOperation _pendingOperations = sync.pendingOperations _savePendingOperation = sync.savePendingOperation @@ -61,8 +46,8 @@ internal final class AnySync: SyncType { return _createPendingOperation(request, objectId) } - func pendingOperations(_ objectId: String? = nil) -> AnyCollection { - return _pendingOperations(objectId) + func pendingOperations() -> AnyCollection { + return _pendingOperations() } func savePendingOperation(_ pendingOperation: PendingOperationType) { diff --git a/Kinvey/Kinvey/User.swift b/Kinvey/Kinvey/User.swift index d753356c5..0fe7f53ab 100644 --- a/Kinvey/Kinvey/User.swift +++ b/Kinvey/Kinvey/User.swift @@ -132,7 +132,7 @@ open class User: NSObject, Credential, Mappable { - parameter completionHandler: Completion handler to be called once the response returns from the server */ @discardableResult - open class func login(authSource: AuthSource, _ authData: [String : Any], createIfNotExists: Bool = true, client: Client = Kinvey.sharedClient, completionHandler: UserHandler? = nil) -> Request { + open class func login(authSource: AuthSource, _ authData: [String : Any], createIfNotExists: Bool = true, clientId: String? = nil, client: Client = Kinvey.sharedClient, completionHandler: UserHandler? = nil) -> Request { return login( authSource: authSource, authData, @@ -156,7 +156,7 @@ open class User: NSObject, Credential, Mappable { - parameter completionHandler: Completion handler to be called once the response returns from the server */ @discardableResult - open class func login(authSource: AuthSource, _ authData: [String : Any], createIfNotExists: Bool = true, client: Client = Kinvey.sharedClient, completionHandler: ((Result) -> Void)? = nil) -> Request { + open class func login(authSource: AuthSource, _ authData: [String : Any], createIfNotExists: Bool = true, clientId: String? = nil, client: Client = Kinvey.sharedClient, completionHandler: ((Result) -> Void)? = nil) -> Request { if let error = client.validate() { DispatchQueue.main.async { completionHandler?(.failure(error)) @@ -170,13 +170,11 @@ open class User: NSObject, Credential, Mappable { request.execute() { (data, response, error) in if let response = response { if response.isOK, let user = client.responseParser.parseUser(data) as? U { - client.activeUser = user fulfill(user) } else if response.isNotFound, createIfNotExists { let request = client.networkRequestFactory.buildUserSocialCreate(authSource, authData: authData) request.execute { (data, response, error) in if let response = response, response.isOK, let user = client.responseParser.parseUser(data) as? U { - client.activeUser = user fulfill(user) } else { reject(buildError(data, response, error, client)) @@ -191,7 +189,9 @@ open class User: NSObject, Credential, Mappable { } } requests += request - }.then { user in + }.then { user -> Void in + client.activeUser = user + client.clientId = clientId completionHandler?(.success(user)) }.catch { error in completionHandler?(.failure(error)) @@ -525,21 +525,21 @@ open class User: NSObject, Credential, Mappable { var acl: Acl? var metadata: UserMetadata? - userId <- map[PersistableIdKey] + userId <- map[Entity.Key.entityId] guard let userIdValue = userId else { return nil } - acl <- map[PersistableAclKey] - metadata <- map[PersistableMetadataKey] + acl <- map[Entity.Key.acl] + metadata <- map[Entity.Key.metadata] self.init(userId: userIdValue, acl: acl, metadata: metadata) } /// This function is where all variable mappings should occur. It is executed by Mapper during the mapping (serialization and deserialization) process. open func mapping(map: Map) { - _userId <- map[PersistableIdKey] - acl <- map[PersistableAclKey] - metadata <- map[PersistableMetadataKey] + _userId <- map[Entity.Key.entityId] + acl <- map[Entity.Key.acl] + metadata <- map[Entity.Key.metadata] socialIdentity <- map["_socialIdentity"] username <- map["username"] email <- map["email"] @@ -642,11 +642,19 @@ open class User: NSObject, Credential, Mappable { /** Login with MIC using Automated Authorization Grant Flow. We strongly recommend use [Authorization Code Grant Flow](http://devcenter.kinvey.com/rest/guides/mobile-identity-connect#authorization-grant) instead of [Automated Authorization Grant Flow](http://devcenter.kinvey.com/rest/guides/mobile-identity-connect#automated-authorization-grant) for security reasons. */ - open class func login(redirectURI: URL, username: String, password: String, client: Client = sharedClient, completionHandler: UserHandler? = nil) { + open class func login( + redirectURI: URL, + username: String, + password: String, + clientId: String? = nil, + client: Client = sharedClient, + completionHandler: UserHandler? = nil + ) { return login( redirectURI: redirectURI, username: username, password: password, + clientId: clientId, client: client ) { (result: Result) in switch result { @@ -661,8 +669,22 @@ open class User: NSObject, Credential, Mappable { /** Login with MIC using Automated Authorization Grant Flow. We strongly recommend use [Authorization Code Grant Flow](http://devcenter.kinvey.com/rest/guides/mobile-identity-connect#authorization-grant) instead of [Automated Authorization Grant Flow](http://devcenter.kinvey.com/rest/guides/mobile-identity-connect#automated-authorization-grant) for security reasons. */ - open class func login(redirectURI: URL, username: String, password: String, client: Client = sharedClient, completionHandler: ((Result) -> Void)? = nil) { - MIC.login(redirectURI: redirectURI, username: username, password: password, client: client, completionHandler: completionHandler) + open class func login( + redirectURI: URL, + username: String, + password: String, + clientId: String? = nil, + client: Client = sharedClient, + completionHandler: ((Result) -> Void)? = nil + ) { + MIC.login( + redirectURI: redirectURI, + username: username, + password: password, + clientId: clientId, + client: client, + completionHandler: completionHandler + ) } #if os(iOS) @@ -689,9 +711,9 @@ open class User: NSObject, Credential, Mappable { } /// Performs a login using the MIC Redirect URL that contains a temporary token. - open class func login(redirectURI: URL, micURL: URL, client: Client = sharedClient) -> Bool { + open class func login(redirectURI: URL, micURL: URL, clientId: String? = nil, client: Client = sharedClient) -> Bool { if let code = MIC.parseCode(redirectURI: redirectURI, url: micURL) { - MIC.login(redirectURI: redirectURI, code: code, client: client) { result in + MIC.login(redirectURI: redirectURI, code: code, clientId: clientId, client: client) { result in switch result { case .success(let user): NotificationCenter.default.post( @@ -712,17 +734,40 @@ open class User: NSObject, Credential, Mappable { /// Presents the MIC View Controller to sign in a user using MIC (Mobile Identity Connect). @available(*, deprecated: 3.3.2, message: "Please use the method presentMICViewController(micUserInterface:) instead") - open class func presentMICViewController(redirectURI: URL, timeout: TimeInterval = 0, forceUIWebView: Bool, client: Client = Kinvey.sharedClient, completionHandler: UserHandler? = nil) { - presentMICViewController(redirectURI: redirectURI, timeout: timeout, micUserInterface: forceUIWebView ? .uiWebView : .wkWebView, client: client, completionHandler: completionHandler) + open class func presentMICViewController( + redirectURI: URL, + timeout: TimeInterval = 0, + forceUIWebView: Bool, + clientId: String? = nil, + client: Client = sharedClient, + completionHandler: UserHandler? = nil + ) { + presentMICViewController( + redirectURI: redirectURI, + timeout: timeout, + micUserInterface: forceUIWebView ? .uiWebView : .wkWebView, + clientId: clientId, + client: client, + completionHandler: completionHandler + ) } /// Presents the MIC View Controller to sign in a user using MIC (Mobile Identity Connect). - open class func presentMICViewController(redirectURI: URL, timeout: TimeInterval = 0, micUserInterface: MICUserInterface = .safari, currentViewController: UIViewController? = nil, client: Client = Kinvey.sharedClient, completionHandler: UserHandler? = nil) { + open class func presentMICViewController( + redirectURI: URL, + timeout: TimeInterval = 0, + micUserInterface: MICUserInterface = .safari, + currentViewController: UIViewController? = nil, + clientId: String? = nil, + client: Client = sharedClient, + completionHandler: UserHandler? = nil + ) { presentMICViewController( redirectURI: redirectURI, timeout: timeout, micUserInterface: micUserInterface, currentViewController: currentViewController, + clientId: clientId, client: client ) { (result: Result) in switch result { @@ -735,7 +780,15 @@ open class User: NSObject, Credential, Mappable { } /// Presents the MIC View Controller to sign in a user using MIC (Mobile Identity Connect). - open class func presentMICViewController(redirectURI: URL, timeout: TimeInterval = 0, micUserInterface: MICUserInterface = .safari, currentViewController: UIViewController? = nil, client: Client = Kinvey.sharedClient, completionHandler: ((Result) -> Void)? = nil) { + open class func presentMICViewController( + redirectURI: URL, + timeout: TimeInterval = 0, + micUserInterface: MICUserInterface = .safari, + currentViewController: UIViewController? = nil, + clientId: String? = nil, + client: Client = sharedClient, + completionHandler: ((Result) -> Void)? = nil + ) { if let error = client.validate() { DispatchQueue.main.async { completionHandler?(.failure(error)) @@ -748,7 +801,7 @@ open class User: NSObject, Credential, Mappable { switch micUserInterface { case .safari: - let url = MIC.urlForLogin(redirectURI: redirectURI, client: client) + let url = MIC.urlForLogin(redirectURI: redirectURI, clientId: clientId, client: client) micVC = SFSafariViewController(url: url) micVC.modalPresentationStyle = .overCurrentContext MICSafariViewControllerSuccessNotificationObserver = NotificationCenter.default.addObserver( @@ -788,6 +841,7 @@ open class User: NSObject, Credential, Mappable { userType: client.userType, timeout: timeout, forceUIWebView: forceUIWebView, + clientId: clientId, client: client ) { (result) in switch result { @@ -808,7 +862,7 @@ open class User: NSObject, Credential, Mappable { } } viewController?.present(micVC, animated: true) - }.then { user in + }.then { user -> Void in completionHandler?(.success(user)) }.catch { error in completionHandler?(.failure(error)) diff --git a/Kinvey/Kinvey/UserQuery.swift b/Kinvey/Kinvey/UserQuery.swift index 2a4abcf64..290f2c3e2 100644 --- a/Kinvey/Kinvey/UserQuery.swift +++ b/Kinvey/Kinvey/UserQuery.swift @@ -64,7 +64,8 @@ public final class UserQuery: Mappable, BuilderType { } /// Constructor for object mapping. - public init?(map: Map) { + public convenience init?(map: Map) { + self.init() } /// Performs the object mapping. diff --git a/Kinvey/KinveyApp/DateTestCase.swift b/Kinvey/KinveyApp/DateTestCase.swift deleted file mode 100644 index fde4158ab..000000000 --- a/Kinvey/KinveyApp/DateTestCase.swift +++ /dev/null @@ -1,51 +0,0 @@ -// -// DateTestCase.swift -// Kinvey -// -// Created by Victor Hugo on 2017-04-24. -// Copyright © 2017 Kinvey. All rights reserved. -// - -import XCTest -@testable import Kinvey - -class DateTestCase: XCTestCase { - - func testDateFormatWithMillis() { - XCTAssertEqual("2017-03-30T12:30:00.733Z".toDate(), Date(timeIntervalSince1970: 1490877000.733)) - } - - func testDateFormatWithoutMillis() { - XCTAssertEqual("2017-03-30T12:30:00Z".toDate(), Date(timeIntervalSince1970: 1490877000)) - } - - func testDateFormatNotSupported() { - XCTAssertNil("2017-03-30 12:30:00".toDate()) - } - - func testTTLSeconds() { - let (time, unit) = 10.seconds - XCTAssertEqual(unit.toTimeInterval(time), TimeInterval(10)) - } - - func testTTLMinutes() { - let (time, unit) = 10.minutes - XCTAssertEqual(unit.toTimeInterval(time), TimeInterval(10 * 60)) - } - - func testTTLHours() { - let (time, unit) = 10.hours - XCTAssertEqual(unit.toTimeInterval(time), TimeInterval(10 * 60 * 60)) - } - - func testTTLDays() { - let (time, unit) = 10.days - XCTAssertEqual(unit.toTimeInterval(time), TimeInterval(10 * 60 * 60 * 24)) - } - - func testTTLWeeks() { - let (time, unit) = 10.weeks - XCTAssertEqual(unit.toTimeInterval(time), TimeInterval(10 * 60 * 60 * 24 * 7)) - } - -} diff --git a/Kinvey/KinveyTests/CacheMigrationTestCaseStep1.swift b/Kinvey/KinveyTests/CacheMigrationTestCaseStep1.swift index a59b1edbb..87e0d30fa 100644 --- a/Kinvey/KinveyTests/CacheMigrationTestCaseStep1.swift +++ b/Kinvey/KinveyTests/CacheMigrationTestCaseStep1.swift @@ -15,7 +15,6 @@ import ObjectMapper class Person: Entity { - dynamic var personId: String? dynamic var firstName: String? dynamic var lastName: String? @@ -26,7 +25,6 @@ class Person: Entity { override func propertyMapping(_ map: Map) { super.propertyMapping(map) - personId <- map[PersistableIdKey] firstName <- map["firstName"] lastName <- map["lastName"] } diff --git a/Kinvey/KinveyTests/CacheMigrationTestCaseStep2.swift b/Kinvey/KinveyTests/CacheMigrationTestCaseStep2.swift index 37b8791c2..cdff60eeb 100644 --- a/Kinvey/KinveyTests/CacheMigrationTestCaseStep2.swift +++ b/Kinvey/KinveyTests/CacheMigrationTestCaseStep2.swift @@ -14,7 +14,6 @@ import ObjectMapper class Person: Entity { - dynamic var personId: String? dynamic var fullName: String? override class func collectionName() -> String { @@ -24,7 +23,6 @@ class Person: Entity { override func propertyMapping(_ map: Map) { super.propertyMapping(map) - personId <- map[PersistableIdKey] fullName <- map["fullName"] } diff --git a/Kinvey/KinveyTests/CacheStoreTests.swift b/Kinvey/KinveyTests/CacheStoreTests.swift index 83761b494..4e8e6ca51 100644 --- a/Kinvey/KinveyTests/CacheStoreTests.swift +++ b/Kinvey/KinveyTests/CacheStoreTests.swift @@ -267,4 +267,108 @@ class CacheStoreTests: StoreTestCase { } } + func testFindCache() { + let book = Book() + book.title = "Swift for the win!" + book.authorNames.append("Victor Barros") + + if useMockData { + var mockJson: JsonDictionary? = nil + var count = 0 + mockResponse { request in + defer { + count += 1 + } + switch count { + case 0: + var json = try! JSONSerialization.jsonObject(with: request) as! JsonDictionary + json += [ + "_id" : UUID().uuidString, + "_acl" : [ + "creator" : UUID().uuidString + ], + "_kmd" : [ + "lmt" : Date().toString(), + "ect" : Date().toString() + ] + ] + mockJson = json + return HttpResponse(json: json) + case 1: + return HttpResponse(json: [mockJson!]) + default: + Swift.fatalError() + } + } + } + defer { + if useMockData { + setURLProtocol(nil) + } + } + + do { + weak var expectationSaveNetwork = expectation(description: "Save Network") + weak var expectationSaveLocal = expectation(description: "Save Local") + + let store = DataStore.collection(.cache) + store.save(book) { book, error in + XCTAssertNotNil(book) + XCTAssertNil(error) + + if let book = book { + XCTAssertEqual(book.title, "Swift for the win!") + + XCTAssertEqual(book.authorNames.count, 1) + XCTAssertEqual(book.authorNames.first?.value, "Victor Barros") + } + + if expectationSaveLocal != nil { + expectationSaveLocal?.fulfill() + expectationSaveLocal = nil + } else { + expectationSaveNetwork?.fulfill() + } + } + + waitForExpectations(timeout: defaultTimeout) { error in + expectationSaveNetwork = nil + expectationSaveLocal = nil + } + } + + do { + weak var expectationFindLocal = expectation(description: "Save Local") + weak var expectationFindNetwork = expectation(description: "Save Network") + + let store = DataStore.collection(.cache) + store.find { books, error in + XCTAssertNotNil(books) + XCTAssertNil(error) + + if let books = books { + if expectationFindLocal != nil { + expectationFindLocal?.fulfill() + expectationFindLocal = nil + } else { + expectationFindNetwork?.fulfill() + } + + XCTAssertEqual(books.count, 1) + if let book = books.first { + XCTAssertEqual(book.title, "Swift for the win!") + + XCTAssertEqual(book.authorNames.count, 1) + XCTAssertEqual(book.authorNames.first?.value, "Victor Barros") + } + } + } + + waitForExpectations(timeout: defaultTimeout) { error in + expectationFindLocal = nil + expectationFindNetwork = nil + } + } + } + } diff --git a/Kinvey/KinveyTests/ClientTestCase.swift b/Kinvey/KinveyTests/ClientTestCase.swift index acffef1ca..f97d41d35 100644 --- a/Kinvey/KinveyTests/ClientTestCase.swift +++ b/Kinvey/KinveyTests/ClientTestCase.swift @@ -8,6 +8,7 @@ import XCTest import Kinvey +import Nimble class ClientTestCase: KinveyTestCase { @@ -119,4 +120,17 @@ class ClientTestCase: KinveyTestCase { XCTAssertNil(EnvironmentInfo(JSON: [:])) } + func testClientAppKeyAndAppSecretEmpty() { + expect { () -> Void in + let _ = Client(appKey: "", appSecret: "") + }.to(throwAssertion()) + } + + func testDataStoreWithoutInitilizeClient() { + expect { () -> Void in + let client = Client() + let _ = DataStore.collection(client: client) + }.to(throwAssertion()) + } + } diff --git a/Kinvey/KinveyTests/DataTypeTestCase.swift b/Kinvey/KinveyTests/DataTypeTestCase.swift index ef4b29ff8..db74215cd 100644 --- a/Kinvey/KinveyTests/DataTypeTestCase.swift +++ b/Kinvey/KinveyTests/DataTypeTestCase.swift @@ -252,8 +252,11 @@ class DataType: Entity { dynamic var fullName2: FullName2? dynamic var objectValue: NSObject? - - //dynamic var dateValue: Date? + dynamic var stringValueNotOptional: String! = "" + dynamic var fullName2DefaultValue = FullName2() + dynamic var fullName2DefaultValueNotOptional: FullName2! = FullName2() + dynamic var fullName2DefaultValueTransformed = FullName2() + dynamic var fullName2DefaultValueNotOptionalTransformed: FullName2! = FullName2() fileprivate dynamic var colorValueString: String? dynamic var colorValue: UIColor? { @@ -289,12 +292,24 @@ class DataType: Entity { boolValue <- map["boolValue"] colorValue <- (map["colorValue"], UIColorTransformType()) fullName <- map["fullName"] - fullName2 <- (map["fullName2"], FullName2TransformType()) - //dateValue <- (map["date"], KinveyDateTransform()) + fullName2 <- ("fullName2", map["fullName2"], FullName2TransformType()) + stringValueNotOptional <- ("stringValueNotOptional", map["stringValueNotOptional"]) + fullName2DefaultValue <- ("fullName2DefaultValue", map["fullName2DefaultValue"]) + fullName2DefaultValueNotOptional <- ("fullName2DefaultValueNotOptional", map["fullName2DefaultValueNotOptional"]) + fullName2DefaultValueTransformed <- ("fullName2DefaultValueTransformed", map["fullName2DefaultValueTransformed"], FullName2TransformType()) + fullName2DefaultValueNotOptionalTransformed <- ("fullName2DefaultValueNotOptionalTransformed", map["fullName2DefaultValueNotOptionalTransformed"], FullName2TransformType()) } override class func ignoredProperties() -> [String] { - return ["objectValue", "colorValue", "fullName2"] + return [ + "objectValue", + "colorValue", + "fullName2", + "fullName2DefaultValue", + "fullName2DefaultValueNotOptional", + "fullName2DefaultValueTransformed", + "fullName2DefaultValueNotOptionalTransformed" + ] } } diff --git a/Kinvey/KinveyTests/DateTestCase.swift b/Kinvey/KinveyTests/DateTestCase.swift new file mode 100644 index 000000000..02c0dfbfb --- /dev/null +++ b/Kinvey/KinveyTests/DateTestCase.swift @@ -0,0 +1,147 @@ +// +// DateTestCase.swift +// Kinvey +// +// Created by Victor Hugo on 2017-04-24. +// Copyright © 2017 Kinvey. All rights reserved. +// + +import XCTest +@testable import Kinvey + +class DateTestCase: KinveyTestCase { + + func testDateFormatWithMillis() { + XCTAssertEqual("2017-03-30T12:30:00.733Z".toDate(), Date(timeIntervalSince1970: 1490877000.733)) + } + + func testDateFormatWithoutMillis() { + XCTAssertEqual("2017-03-30T12:30:00Z".toDate(), Date(timeIntervalSince1970: 1490877000)) + } + + func testDateFormatNotSupported() { + XCTAssertNil("2017-03-30 12:30:00".toDate()) + } + + func testTTLSeconds() { + let (time, unit) = 10.seconds + XCTAssertEqual(unit.toTimeInterval(time), TimeInterval(10)) + } + + func testTTLMinutes() { + let (time, unit) = 10.minutes + XCTAssertEqual(unit.toTimeInterval(time), TimeInterval(10 * 60)) + } + + func testTTLHours() { + let (time, unit) = 10.hours + XCTAssertEqual(unit.toTimeInterval(time), TimeInterval(10 * 60 * 60)) + } + + func testTTLDays() { + let (time, unit) = 10.days + XCTAssertEqual(unit.toTimeInterval(time), TimeInterval(10 * 60 * 60 * 24)) + } + + func testTTLWeeks() { + let (time, unit) = 10.weeks + XCTAssertEqual(unit.toTimeInterval(time), TimeInterval(10 * 60 * 60 * 24 * 7)) + } + + func testDateTransform() { + let transform = AnyTransform(KinveyDateTransform()) + + let date = Date() + let dateString = date.toString() + + XCTAssertEqualWithAccuracy(date.timeIntervalSinceReferenceDate, (transform.transformFromJSON(dateString) as! Date).timeIntervalSinceReferenceDate, accuracy: 0.0009) + XCTAssertEqual(dateString, transform.transformToJSON(date) as? String) + } + + func testQueryDate() { + signUp() + + let store = DataStore.collection(.network) + + let publishDate = Date() + + client.logNetworkEnabled = true + let nEvents = 4 + + var mockObjects = [JsonDictionary]() + + for _ in 1...nEvents { + if useMockData { + let json: JsonDictionary = [ + "date" : publishDate.toString(), + "_acl" : [ + "creator" : client.activeUser!.userId + ], + "_kmd" : [ + "lmt" : Date().toString(), + "ect" : Date().toString() + ], + "_id" : UUID().uuidString + ] + mockObjects.append(json) + mockResponse(json: json) + } + defer { + if useMockData { + setURLProtocol(nil) + } + } + + weak var expectationSave = self.expectation(description: "Save") + + let event = Event() + event.publishDate = publishDate + store.save(event) { event, error in + XCTAssertNotNil(event) + XCTAssertNil(error) + + expectationSave?.fulfill() + } + + waitForExpectations(timeout: defaultTimeout) { (error) in + expectationSave = nil + } + } + + do { + if useMockData { + mockResponse(json: mockObjects) + } + defer { + if useMockData { + setURLProtocol(nil) + } + } + + weak var expectationFind = self.expectation(description: "Find") + + let query = Query(format: "acl.creator == %@ AND publishDate >= %@", Kinvey.sharedClient.activeUser!.userId, Date(timeIntervalSinceNow: -60)) + store.find(query) { events, error in + XCTAssertNotNil(events) + XCTAssertNil(error) + + XCTAssertEqual(events?.count, nEvents) + if let events = events { + for event in events { + XCTAssertNotNil(event.publishDate) + if let date = event.publishDate { + XCTAssertEqualWithAccuracy(date.timeIntervalSinceReferenceDate, publishDate.timeIntervalSinceReferenceDate, accuracy: 0.0009) + } + } + } + + expectationFind?.fulfill() + } + + waitForExpectations(timeout: defaultTimeout) { (error) in + expectationFind = nil + } + } + } + +} diff --git a/Kinvey/KinveyTests/DeltaSetCacheTestCase.swift b/Kinvey/KinveyTests/DeltaSetCacheTestCase.swift index 3e4f65a60..4037bb03c 100644 --- a/Kinvey/KinveyTests/DeltaSetCacheTestCase.swift +++ b/Kinvey/KinveyTests/DeltaSetCacheTestCase.swift @@ -1413,4 +1413,344 @@ class DeltaSetCacheTestCase: KinveyTestCase { } } + func testFindOneRecordDeltaSetTimeoutError2ndRequest() { + let store = DataStore.collection(.sync, deltaSet: true) + + do { + let person = Person() + person.name = "Victor" + + weak var expectationSave = expectation(description: "Save") + + store.save(person) { (person, error) in + XCTAssertTrue(Thread.isMainThread) + XCTAssertNotNil(person) + XCTAssertNil(error) + + expectationSave?.fulfill() + } + + waitForExpectations(timeout: defaultTimeout) { (error) in + expectationSave = nil + } + } + + var count = 0 + mockResponse { (request) -> HttpResponse in + defer { + count += 1 + } + switch count { + case 0: + return HttpResponse(json: [ + [ + "_id" : UUID().uuidString, + "_kmd" : [ + "lmt" : Date().toString() + ] + ] + ]) + case 1: + return HttpResponse(error: timeoutError) + default: + Swift.fatalError() + } + } + defer { + setURLProtocol(nil) + } + + weak var expectationFind = expectation(description: "Find") + + store.find(readPolicy: .forceNetwork) { (persons, error) in + XCTAssertNil(persons) + XCTAssertNotNil(error) + + XCTAssertTimeoutError(error) + + expectationFind?.fulfill() + } + + waitForExpectations(timeout: defaultTimeout) { (error) in + expectationFind = nil + } + } + + func testFind201RecordsDeltaSet() { + signUp() + + let store = DataStore.collection(.sync, deltaSet: true) + + let person = Person() + person.name = "Victor" + + weak var expectationSave = expectation(description: "Save") + + store.save(person) { (person, error) in + XCTAssertTrue(Thread.isMainThread) + XCTAssertNotNil(person) + XCTAssertNil(error) + + expectationSave?.fulfill() + } + + waitForExpectations(timeout: defaultTimeout) { (error) in + expectationSave = nil + } + + do { + mockResponse(statusCode: 201, json: [ + "_id" : UUID().uuidString, + "name" : "Victor", + "age" : 0, + "_acl" : [ + "creator" : client.activeUser?.userId + ], + "_kmd" : [ + "lmt" : Date().toString(), + "ect" : Date().toString() + ] + ]) + defer { + setURLProtocol(nil) + } + + weak var expectationPush = expectation(description: "Push") + + store.push() { (count, error) in + XCTAssertTrue(Thread.isMainThread) + XCTAssertNotNil(count) + XCTAssertNil(error) + + expectationPush?.fulfill() + } + + waitForExpectations(timeout: defaultTimeout) { (error) in + expectationPush = nil + } + } + + let query = Query(format: "\(Person.aclProperty() ?? PersistableAclKey).creator == %@", client.activeUser!.userId) + + var jsonArray = [JsonDictionary]() + for _ in 1...201 { + jsonArray.append([ + "_id" : UUID().uuidString, + "name" : UUID().uuidString, + "age" : 0, + "_acl" : [ + "creator" : self.client.activeUser!.userId + ], + "_kmd" : [ + "lmt" : Date().toString(), + "ect" : Date().toString() + ] + ]) + } + mockResponse { request in + let urlComponents = URLComponents(url: request.url!, resolvingAgainstBaseURL: false) + + if let fieldsString = urlComponents?.queryItems?.filter({ $0.name == "fields" }).first?.value { + let fields = fieldsString.components(separatedBy: ",") + let mockResponse = jsonArray.map { (item: [String : Any]) -> [String : Any] in + var json = [String : Any]() + for field in fields { + switch field { + case "_id": + json[field] = item[field] + case "_kmd.lmt": + let itemKmd = item["_kmd"] as! [String : Any] + let kmd = ["lmt" : itemKmd["lmt"]] + json["_kmd"] = kmd + default: + Swift.fatalError() + } + } + return json + } + return HttpResponse(json: mockResponse) + } + + guard let queryString = urlComponents?.queryItems?.filter({ $0.name == "query" }).first?.value, + let data = queryString.data(using: .utf8), + let jsonObject = try? JSONSerialization.jsonObject(with: data), + let queryDict = jsonObject as? [String : Any] + else { + Swift.fatalError() + } + + if let idFilter = queryDict["_id"] as? [String : Any], + let ids = idFilter["$in"] as? [String] + { + let mockResponse = jsonArray.filter { + let id = $0["_id"] as! String + return ids.contains(id) + } + return HttpResponse(json: mockResponse) + } + + Swift.fatalError() + } + defer { + setURLProtocol(nil) + } + + weak var expectationFind = expectation(description: "Find") + + store.find(readPolicy: .forceNetwork) { results, error in + XCTAssertNotNil(results) + XCTAssertNil(error) + + XCTAssertEqual(results?.count, 201) + + expectationFind?.fulfill() + } + + waitForExpectations(timeout: defaultTimeout) { (error) in + expectationFind = nil + } + } + + func testFind201RecordsDeltaSetTimeoutOn2ndRequest() { + signUp() + + let store = DataStore.collection(.sync, deltaSet: true) + + let person = Person() + person.name = "Victor" + + weak var expectationSave = expectation(description: "Save") + + store.save(person) { (person, error) in + XCTAssertTrue(Thread.isMainThread) + XCTAssertNotNil(person) + XCTAssertNil(error) + + expectationSave?.fulfill() + } + + waitForExpectations(timeout: defaultTimeout) { (error) in + expectationSave = nil + } + + do { + mockResponse(statusCode: 201, json: [ + "_id" : UUID().uuidString, + "name" : "Victor", + "age" : 0, + "_acl" : [ + "creator" : client.activeUser?.userId + ], + "_kmd" : [ + "lmt" : Date().toString(), + "ect" : Date().toString() + ] + ]) + defer { + setURLProtocol(nil) + } + + weak var expectationPush = expectation(description: "Push") + + store.push() { (count, error) in + XCTAssertTrue(Thread.isMainThread) + XCTAssertNotNil(count) + XCTAssertNil(error) + + expectationPush?.fulfill() + } + + waitForExpectations(timeout: defaultTimeout) { (error) in + expectationPush = nil + } + } + + let query = Query(format: "\(Person.aclProperty() ?? PersistableAclKey).creator == %@", client.activeUser!.userId) + + var jsonArray = [JsonDictionary]() + for _ in 1...201 { + jsonArray.append([ + "_id" : UUID().uuidString, + "name" : UUID().uuidString, + "age" : 0, + "_acl" : [ + "creator" : self.client.activeUser!.userId + ], + "_kmd" : [ + "lmt" : Date().toString(), + "ect" : Date().toString() + ] + ]) + } + var count = 0 + mockResponse { request in + let urlComponents = URLComponents(url: request.url!, resolvingAgainstBaseURL: false) + + if let fieldsString = urlComponents?.queryItems?.filter({ $0.name == "fields" }).first?.value { + let fields = fieldsString.components(separatedBy: ",") + let mockResponse = jsonArray.map { (item: [String : Any]) -> [String : Any] in + var json = [String : Any]() + for field in fields { + switch field { + case "_id": + json[field] = item[field] + case "_kmd.lmt": + let itemKmd = item["_kmd"] as! [String : Any] + let kmd = ["lmt" : itemKmd["lmt"]] + json["_kmd"] = kmd + default: + Swift.fatalError() + } + } + return json + } + return HttpResponse(json: mockResponse) + } + + guard let queryString = urlComponents?.queryItems?.filter({ $0.name == "query" }).first?.value, + let data = queryString.data(using: .utf8), + let jsonObject = try? JSONSerialization.jsonObject(with: data), + let queryDict = jsonObject as? [String : Any] + else { + Swift.fatalError() + } + + if let idFilter = queryDict["_id"] as? [String : Any], + let ids = idFilter["$in"] as? [String] + { + defer { + count += 1 + } + switch count { + case 0: + let mockResponse = jsonArray.filter { + let id = $0["_id"] as! String + return ids.contains(id) + } + return HttpResponse(json: mockResponse) + default: + return HttpResponse(error: timeoutError) + } + } + + Swift.fatalError() + } + defer { + setURLProtocol(nil) + } + + weak var expectationFind = expectation(description: "Find") + + store.find(readPolicy: .forceNetwork) { results, error in + XCTAssertNil(results) + XCTAssertNotNil(error) + + expectationFind?.fulfill() + } + + waitForExpectations(timeout: defaultTimeout) { (error) in + expectationFind = nil + } + } + } diff --git a/Kinvey/KinveyTests/EntityTestCase.swift b/Kinvey/KinveyTests/EntityTestCase.swift new file mode 100644 index 000000000..f992be8aa --- /dev/null +++ b/Kinvey/KinveyTests/EntityTestCase.swift @@ -0,0 +1,105 @@ +// +// EntityTestCase.swift +// Kinvey +// +// Created by Victor Hugo on 2017-05-17. +// Copyright © 2017 Kinvey. All rights reserved. +// + +import XCTest +@testable import Kinvey +import Nimble + +class EntityTestCase: XCTestCase { + + func testCollectionName() { + expect { () -> Void in + let _ = Entity.collectionName() + }.to(throwAssertion()) + } + + func testBoolValue() { + let value = true + XCTAssertEqual(BoolValue(booleanLiteral: true).value, value) + XCTAssertEqual(BoolValue(true).value, value) + } + + func testDoubleValue() { + let value: Double = 3.14159 + XCTAssertEqual(DoubleValue(floatLiteral: value).value, value) + XCTAssertEqual(DoubleValue(value).value, value) + } + + func testFloatValue() { + let value: Float = 3.14159 + XCTAssertEqual(FloatValue(floatLiteral: value).value, value) + XCTAssertEqual(FloatValue(value).value, value) + } + + func testIntValue() { + let value = 314159 + XCTAssertEqual(IntValue(integerLiteral: value).value, value) + XCTAssertEqual(IntValue(value).value, value) + } + + func testStringValue() { + let value = "314159" + XCTAssertEqual(StringValue(unicodeScalarLiteral: value).value, value) + XCTAssertEqual(StringValue(extendedGraphemeClusterLiteral: value).value, value) + XCTAssertEqual(StringValue(stringLiteral: value).value, value) + XCTAssertEqual(StringValue(value).value, value) + } + + func testGeoPointValidationParse() { + let latitude = 42.3133521 + let longitude = -71.1271963 + XCTAssertNotNil(GeoPoint(JSON: ["latitude" : latitude, "longitude" : longitude])) + XCTAssertNil(GeoPoint(JSON: ["latitude" : latitude])) + XCTAssertNil(GeoPoint(JSON: ["longitude" : longitude])) + } + + func testGeoPointMapping() { + var geoPoint = GeoPoint() + let latitude = 42.3133521 + let longitude = -71.1271963 + geoPoint <- ("geoPoint", Map(mappingType: .fromJSON, JSON: ["location" : [longitude, latitude]])["location"]) + XCTAssertEqual(geoPoint.latitude, latitude) + XCTAssertEqual(geoPoint.longitude, longitude) + } + + func testGeoPointMapping2() { + var geoPoint: GeoPoint! + let latitude = 42.3133521 + let longitude = -71.1271963 + geoPoint <- ("geoPoint", Map(mappingType: .fromJSON, JSON: ["location" : [longitude, latitude]])["location"]) + XCTAssertEqual(geoPoint.latitude, latitude) + XCTAssertEqual(geoPoint.longitude, longitude) + } + + func testPropertyType() { + var clazz: AnyClass? = ObjCRuntime.typeForPropertyName(Person.self, propertyName: "name") + XCTAssertNotNil(clazz) + if let clazz = clazz { + let clazzName = NSStringFromClass(clazz) + XCTAssertEqual(clazzName, "NSString") + } + + clazz = ObjCRuntime.typeForPropertyName(Person.self, propertyName: "geolocation") + XCTAssertNotNil(clazz) + if let clazz = clazz { + let clazzName = NSStringFromClass(clazz) + XCTAssertEqual(clazzName, "Kinvey.GeoPoint") + } + + clazz = ObjCRuntime.typeForPropertyName(Person.self, propertyName: "address") + XCTAssertNotNil(clazz) + if let clazz = clazz { + let clazzName = NSStringFromClass(clazz) + XCTAssertEqual(clazzName, "KinveyTests.Address") + } + + clazz = ObjCRuntime.typeForPropertyName(Person.self, propertyName: "age") + XCTAssertNil(clazz) + } + +} diff --git a/Kinvey/KinveyTests/Event.swift b/Kinvey/KinveyTests/Event.swift index a4dfcc988..a564ab6b1 100644 --- a/Kinvey/KinveyTests/Event.swift +++ b/Kinvey/KinveyTests/Event.swift @@ -7,7 +7,6 @@ // import Kinvey -import ObjectMapper /// Event.swift - an entity in the 'Events' collection class Event : Entity { @@ -24,7 +23,7 @@ class Event : Entity { super.propertyMapping(map) name <- ("name", map["name"]) - publishDate <- ("date", map["date"], ISO8601DateTransform()) + publishDate <- ("publishDate", map["date"]) location <- ("location", map["location"]) } diff --git a/Kinvey/KinveyTests/FileTestCase.swift b/Kinvey/KinveyTests/FileTestCase.swift index 34dfdb010..b18ed2f93 100644 --- a/Kinvey/KinveyTests/FileTestCase.swift +++ b/Kinvey/KinveyTests/FileTestCase.swift @@ -8,12 +8,30 @@ import XCTest @testable import Kinvey +import Nimble + +class MyFile: File { + + dynamic var label: String? + + public convenience required init?(map: Map) { + self.init() + } + + override func mapping(map: Map) { + super.mapping(map: map) + + label <- ("label", map["label"]) + } + +} class FileTestCase: StoreTestCase { let caminandes3TrailerURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent("Caminandes 3 - TRAILER.mp4") let caminandes3TrailerImageURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent("Caminandes 3 - TRAILER.jpg") var file: File? + var myFile: MyFile? var caminandes3TrailerFileSize: UInt64 { return try! FileManager.default.attributesOfItem(atPath: caminandes3TrailerURL.path).filter { $0.key == .size }.first!.value as! UInt64 } @@ -144,15 +162,397 @@ class FileTestCase: StoreTestCase { return nil } - func testUpload() { + func testDownloadMissingFileId() { + signUp() + + expect { () -> Void in + self.fileStore.download(File()) { (file, data: Data?, error) in + XCTFail() + } + }.to(throwAssertion()) + } + + func testDownloadTimeoutError() { + signUp() + + var file = File() { + $0.fileId = UUID().uuidString + } + + mockResponse(error: timeoutError) + defer { + setURLProtocol(nil) + } + + weak var expectationDownload = expectation(description: "Download") + + fileStore.download(file) { (file, data: Data?, error) in + XCTAssertTrue(Thread.isMainThread) + + XCTAssertNil(file) + XCTAssertNil(data) + XCTAssertNotNil(error) + XCTAssertTimeoutError(error) + + expectationDownload?.fulfill() + } + + waitForExpectations(timeout: defaultTimeout) { error in + expectationDownload = nil + } + } + + func testDownloadDataTimeoutError() { + signUp() + + var file = File() { + $0.fileId = UUID().uuidString + } + + mockResponse(error: timeoutError) + defer { + setURLProtocol(nil) + } + + weak var expectationDownload = expectation(description: "Download") + + fileStore.download(file) { (file, data: Data?, error) in + XCTAssertTrue(Thread.isMainThread) + + XCTAssertNil(file) + XCTAssertNil(data) + XCTAssertNotNil(error) + XCTAssertTimeoutError(error) + + expectationDownload?.fulfill() + } + + waitForExpectations(timeout: defaultTimeout) { error in + expectationDownload = nil + } + } + + func testDownloadPathTimeoutError() { + signUp() + + var file = File() { + $0.fileId = UUID().uuidString + } + + mockResponse(error: timeoutError) + defer { + setURLProtocol(nil) + } + + weak var expectationDownload = expectation(description: "Download") + + fileStore.download(file) { (file, url: URL?, error) in + XCTAssertTrue(Thread.isMainThread) + + XCTAssertNil(file) + XCTAssertNil(url) + XCTAssertNotNil(error) + XCTAssertTimeoutError(error) + + expectationDownload?.fulfill() + } + + waitForExpectations(timeout: defaultTimeout) { error in + expectationDownload = nil + } + } + + func testUploadFileMetadataTimeoutError() { + signUp() + + var file = File() { + $0.publicAccessible = true + } + self.file = file + let path = caminandes3TrailerURL.path + + var count = 0 + mockResponse(error: timeoutError) + defer { + setURLProtocol(nil) + } + + weak var expectationUpload = expectation(description: "Upload") + + fileStore.upload(file, path: path) { (file, error) in + XCTAssertTrue(Thread.isMainThread) + + XCTAssertNil(file) + XCTAssertNotNil(error) + XCTAssertTimeoutError(error) + + expectationUpload?.fulfill() + } + + waitForExpectations(timeout: defaultTimeout) { error in + expectationUpload = nil + } + } + + func testUploadTimeoutError() { + signUp() + + var file = File() { + $0.publicAccessible = true + } + self.file = file + let path = caminandes3TrailerURL.path + + var count = 0 + mockResponse { request in + defer { + count += 1 + } + switch count { + case 0: + return HttpResponse(statusCode: 201, json: [ + "_public": true, + "_id": "2a37d253-752f-42cd-987e-db319a626077", + "_filename": "a2f88ffc-e7fe-4d17-aa69-063088cb24fa", + "_acl": [ + "creator": "584287c3b1c6f88d1990e1e8" + ], + "_kmd": [ + "lmt": "2016-12-03T08:52:19.204Z", + "ect": "2016-12-03T08:52:19.204Z" + ], + "_uploadURL": "https://www.googleapis.com/upload/storage/v1/b/0b5b1cd673164e3185a2e75e815f5cfe/o?name=2a37d253-752f-42cd-987e-db319a626077%2Fa2f88ffc-e7fe-4d17-aa69-063088cb24fa&uploadType=resumable&predefinedAcl=publicRead&upload_id=AEnB2Uqwlm2GQ0JWMApi0ApeBHQ0PxjY3hSe_VNs5geuZFxLBkrwiI0gLldrE8GgkqX4ahWtRJ1MHombFq8hQc9o5772htAvDQ", + "_expiresAt": "2016-12-10T08:52:19.488Z", + "_requiredHeaders": [ + ] + ]) + case 1: + return HttpResponse(error: timeoutError) + default: + Swift.fatalError() + } + } + defer { + setURLProtocol(nil) + } + + weak var expectationUpload = expectation(description: "Upload") + + fileStore.upload(file, path: path) { (file, error) in + XCTAssertTrue(Thread.isMainThread) + + XCTAssertNil(file) + XCTAssertNotNil(error) + XCTAssertTimeoutError(error) + + expectationUpload?.fulfill() + } + + waitForExpectations(timeout: defaultTimeout) { error in + expectationUpload = nil + } + } + + func testUploadDataTimeoutError() { + signUp() + + var file = File() { + $0.publicAccessible = true + } + self.file = file + let path = caminandes3TrailerURL.path + + var count = 0 + mockResponse { request in + defer { + count += 1 + } + switch count { + case 0: + return HttpResponse(statusCode: 201, json: [ + "_public": true, + "_id": "2a37d253-752f-42cd-987e-db319a626077", + "_filename": "a2f88ffc-e7fe-4d17-aa69-063088cb24fa", + "_acl": [ + "creator": "584287c3b1c6f88d1990e1e8" + ], + "_kmd": [ + "lmt": "2016-12-03T08:52:19.204Z", + "ect": "2016-12-03T08:52:19.204Z" + ], + "_uploadURL": "https://www.googleapis.com/upload/storage/v1/b/0b5b1cd673164e3185a2e75e815f5cfe/o?name=2a37d253-752f-42cd-987e-db319a626077%2Fa2f88ffc-e7fe-4d17-aa69-063088cb24fa&uploadType=resumable&predefinedAcl=publicRead&upload_id=AEnB2Uqwlm2GQ0JWMApi0ApeBHQ0PxjY3hSe_VNs5geuZFxLBkrwiI0gLldrE8GgkqX4ahWtRJ1MHombFq8hQc9o5772htAvDQ", + "_expiresAt": "2016-12-10T08:52:19.488Z", + "_requiredHeaders": [ + ] + ]) + case 1: + return HttpResponse(error: timeoutError) + default: + Swift.fatalError() + } + } + defer { + setURLProtocol(nil) + } + + let data = try! Data(contentsOf: URL(fileURLWithPath: path)) + + weak var expectationUpload = expectation(description: "Upload") + + fileStore.upload(file, data: data) { (file, error) in + XCTAssertTrue(Thread.isMainThread) + + XCTAssertNil(file) + XCTAssertNotNil(error) + XCTAssertTimeoutError(error) + + expectationUpload?.fulfill() + } + + waitForExpectations(timeout: defaultTimeout) { error in + expectationUpload = nil + } + } + + func testUploadImageTimeoutError() { + signUp() + + var file = File() { + $0.publicAccessible = true + } + self.file = file + let path = caminandes3TrailerImageURL.path + + var count = 0 + mockResponse { request in + defer { + count += 1 + } + switch count { + case 0: + return HttpResponse(statusCode: 201, json: [ + "_public": true, + "_id": "2a37d253-752f-42cd-987e-db319a626077", + "_filename": "a2f88ffc-e7fe-4d17-aa69-063088cb24fa", + "_acl": [ + "creator": "584287c3b1c6f88d1990e1e8" + ], + "_kmd": [ + "lmt": "2016-12-03T08:52:19.204Z", + "ect": "2016-12-03T08:52:19.204Z" + ], + "_uploadURL": "https://www.googleapis.com/upload/storage/v1/b/0b5b1cd673164e3185a2e75e815f5cfe/o?name=2a37d253-752f-42cd-987e-db319a626077%2Fa2f88ffc-e7fe-4d17-aa69-063088cb24fa&uploadType=resumable&predefinedAcl=publicRead&upload_id=AEnB2Uqwlm2GQ0JWMApi0ApeBHQ0PxjY3hSe_VNs5geuZFxLBkrwiI0gLldrE8GgkqX4ahWtRJ1MHombFq8hQc9o5772htAvDQ", + "_expiresAt": "2016-12-10T08:52:19.488Z", + "_requiredHeaders": [ + ] + ]) + case 1: + return HttpResponse(error: timeoutError) + default: + Swift.fatalError() + } + } + defer { + setURLProtocol(nil) + } + + let image = UIImage(contentsOfFile: path)! + + weak var expectationUpload = expectation(description: "Upload") + + fileStore.upload(file, image: image) { (file, error) in + XCTAssertTrue(Thread.isMainThread) + + XCTAssertNil(file) + XCTAssertNotNil(error) + XCTAssertTimeoutError(error) + + expectationUpload?.fulfill() + } + + waitForExpectations(timeout: defaultTimeout) { error in + expectationUpload = nil + } + } + + func testUploadInputStreamTimeoutError() { signUp() var file = File() { $0.publicAccessible = true } self.file = file + let path = caminandes3TrailerImageURL.path + + var count = 0 + mockResponse { request in + defer { + count += 1 + } + switch count { + case 0: + return HttpResponse(statusCode: 201, json: [ + "_public": true, + "_id": "2a37d253-752f-42cd-987e-db319a626077", + "_filename": "a2f88ffc-e7fe-4d17-aa69-063088cb24fa", + "_acl": [ + "creator": "584287c3b1c6f88d1990e1e8" + ], + "_kmd": [ + "lmt": "2016-12-03T08:52:19.204Z", + "ect": "2016-12-03T08:52:19.204Z" + ], + "_uploadURL": "https://www.googleapis.com/upload/storage/v1/b/0b5b1cd673164e3185a2e75e815f5cfe/o?name=2a37d253-752f-42cd-987e-db319a626077%2Fa2f88ffc-e7fe-4d17-aa69-063088cb24fa&uploadType=resumable&predefinedAcl=publicRead&upload_id=AEnB2Uqwlm2GQ0JWMApi0ApeBHQ0PxjY3hSe_VNs5geuZFxLBkrwiI0gLldrE8GgkqX4ahWtRJ1MHombFq8hQc9o5772htAvDQ", + "_expiresAt": "2016-12-10T08:52:19.488Z", + "_requiredHeaders": [ + ] + ]) + case 1: + return HttpResponse(error: timeoutError) + default: + Swift.fatalError() + } + } + defer { + setURLProtocol(nil) + } + + let inputStream = InputStream(fileAtPath: path)! + + weak var expectationUpload = expectation(description: "Upload") + + fileStore.upload(file, stream: inputStream) { (file, error) in + XCTAssertTrue(Thread.isMainThread) + + XCTAssertNil(file) + XCTAssertNotNil(error) + XCTAssertTimeoutError(error) + + expectationUpload?.fulfill() + } + + waitForExpectations(timeout: defaultTimeout) { error in + expectationUpload = nil + } + } + + func testUpload() { + signUp() + + let originalLogNetworkEnabled = client.logNetworkEnabled + client.logNetworkEnabled = true + defer { + client.logNetworkEnabled = originalLogNetworkEnabled + } + + var file = MyFile() + file.label = "trailer" + file.publicAccessible = true + self.file = file let path = caminandes3TrailerURL.path + let fileStore = FileStore.getInstance(fileType: MyFile.self) + var uploadProgressCount = 0 do { @@ -257,10 +657,11 @@ class FileTestCase: StoreTestCase { "lmt": "2016-12-03T08:52:19.204Z", "ect": "2016-12-03T08:52:19.204Z" ], - "_downloadURL": "https://storage.googleapis.com/0b5b1cd673164e3185a2e75e815f5cfe/2a37d253-752f-42cd-987e-db319a626077/a2f88ffc-e7fe-4d17-aa69-063088cb24fa" + "label" : "trailer", + "_downloadURL": "https://storage.googleapis.com/0b5b1cd673164e3185a2e75e815f5cfe/aae29e81-1930-43a5-97e9-bae7964d3820/715dcece-2a05-4d88-a771-d6c7c5cac197" ]) default: - fatalError() + Swift.fatalError() } } } @@ -343,8 +744,12 @@ class FileTestCase: StoreTestCase { XCTAssertNotNil(data) XCTAssertNil(error) + if let file = file { + XCTAssertEqual(file.label, "trailer") + } + if let data = data { - XCTAssertEqual(data.count, 10899706) + XCTAssertEqual(data.count, 8578265) } expectationDownload?.fulfill() @@ -491,7 +896,7 @@ class FileTestCase: StoreTestCase { "_downloadURL": "https://storage.googleapis.com/0b5b1cd673164e3185a2e75e815f5cfe/f85b3eb0-fc22-4147-ae51-19bb201edfdf/0cab2b78-3142-4c10-987a-e837d1a9e269" ]) default: - fatalError() + Swift.fatalError() } } } @@ -727,7 +1132,7 @@ class FileTestCase: StoreTestCase { "_downloadURL": "https://storage.googleapis.com/0b5b1cd673164e3185a2e75e815f5cfe/429fb893-4bb2-4651-b907-a42145c31015/videoplayback.png" ]) default: - fatalError() + Swift.fatalError() } } } @@ -964,7 +1369,7 @@ class FileTestCase: StoreTestCase { "_downloadURL": "https://storage.googleapis.com/0b5b1cd673164e3185a2e75e815f5cfe/757a357e-341b-4119-8e38-cd7e96edd28b/videoplayback.jpg" ]) default: - fatalError() + Swift.fatalError() } } } @@ -1167,12 +1572,14 @@ class FileTestCase: StoreTestCase { func testDownloadAndResume() { signUp() - let file = File() { - $0.publicAccessible = true - } + let file = MyFile() + file.label = "trailer" + file.publicAccessible = true self.file = file let path = caminandes3TrailerURL.path + let fileStore: FileStore = FileStore.getInstance() + do { if useMockData { var count = 0 @@ -1260,10 +1667,11 @@ class FileTestCase: StoreTestCase { "lmt": Date().toString(), "ect": Date().toString() ], + "label": "trailer", "_downloadURL": "https://storage.googleapis.com/\(UUID().uuidString)/\(UUID().uuidString)/\(UUID().uuidString)" ]) default: - fatalError() + Swift.fatalError() } } } @@ -1277,7 +1685,7 @@ class FileTestCase: StoreTestCase { fileStore.upload(file, path: path) { (file, error) in XCTAssertNotNil(file) - self.file = file + self.myFile = file XCTAssertNil(error) expectationUpload?.fulfill() @@ -1288,7 +1696,7 @@ class FileTestCase: StoreTestCase { } } - XCTAssertNotNil(self.file?.fileId) + XCTAssertNotNil(self.myFile?.fileId) if useMockData { let url = caminandes3TrailerURL @@ -1318,14 +1726,21 @@ class FileTestCase: StoreTestCase { do { weak var expectationDownload = expectation(description: "Download") - let request = fileStore.download(self.file!) { (file, data: Data?, error) in - self.file = file + let request = fileStore.download(self.myFile!) { (file, data: Data?, error) in + self.myFile = file XCTFail() } let delayTime = DispatchTime.now() + Double(Int64(0.5 * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC) DispatchQueue.main.asyncAfter(deadline: delayTime) { + XCTAssertTrue(request.executing) + XCTAssertFalse(request.cancelled) + request.cancel() + + XCTAssertFalse(request.executing) + XCTAssertTrue(request.cancelled) + expectationDownload?.fulfill() } @@ -1334,19 +1749,23 @@ class FileTestCase: StoreTestCase { } } - XCTAssertNotNil(self.file?.resumeDownloadData) - if let resumeData = self.file?.resumeDownloadData { + XCTAssertNotNil(self.myFile?.resumeDownloadData) + if let resumeData = self.myFile?.resumeDownloadData { XCTAssertGreaterThan(resumeData.count, 0) } do { weak var expectationDownload = expectation(description: "Download") - fileStore.download(self.file!) { (file, data: Data?, error) in + fileStore.download(self.myFile!) { (file, data: Data?, error) in XCTAssertNotNil(file) XCTAssertNotNil(data) XCTAssertNil(error) + if let file = file { + XCTAssertEqual(file.label, "trailer") + } + if let data = data { XCTAssertEqual(UInt64(data.count), self.caminandes3TrailerFileSize) } @@ -1861,7 +2280,7 @@ class FileTestCase: StoreTestCase { "_expiresAt": Date(timeIntervalSinceNow: 3600).toString() ]) default: - fatalError() + Swift.fatalError() } } } @@ -1922,7 +2341,7 @@ class FileTestCase: StoreTestCase { case 1: return HttpResponse(data: data) default: - fatalError() + Swift.fatalError() } } } @@ -1968,9 +2387,9 @@ class FileTestCase: StoreTestCase { func testGetInstance() { let appKey = "file-get_instance-\(UUID().uuidString)" let client = Client(appKey: appKey, appSecret: "unit-test") - let fileStore = FileStore.getInstance(client) + let fileStore = FileStore.getInstance(client: client) - let fileCache = fileStore.cache as? RealmFileCache + let fileCache = fileStore.cache?.cache as? RealmFileCache XCTAssertNotNil(fileCache) if let fileCache = fileCache { let fileURL = fileCache.realm.configuration.fileURL @@ -2094,7 +2513,7 @@ class FileTestCase: StoreTestCase { "_expiresAt" : Date(timeIntervalSinceNow: TimeUnit.hour.timeInterval).toString() ]) default: - fatalError() + Swift.fatalError() } } } @@ -2235,7 +2654,7 @@ class FileTestCase: StoreTestCase { case 1: return HttpResponse(data: "secret message".data(using: .utf8)) default: - fatalError() + Swift.fatalError() } } } @@ -2489,4 +2908,99 @@ class FileTestCase: StoreTestCase { } } + func testRefreshTimeoutError() { + signUp() + + let fileStore = FileStore.getInstance() + var _file: File? = nil + + do { + if useMockData { + mockResponse(json: [ + [ + "_id" : UUID().uuidString, + "_filename" : "image.png", + "size" : 4096, + "mimeType" : "image/png", + "_acl" : [ + "gr" : true, + "creator" : UUID().uuidString + ], + "_kmd" : [ + "lmt" : Date().toString(), + "ect" : Date().toString() + ], + "_downloadURL" : "https://storage.googleapis.com/image.png", + "_expiresAt" : Date(timeIntervalSinceNow: 3).toString() + ] + ]) + } + defer { + if useMockData { + setURLProtocol(nil) + } + } + + weak var expectationFind = expectation(description: "Find") + + fileStore.find(ttl: (5, .second)) { files, error in + XCTAssertTrue(Thread.isMainThread) + XCTAssertNotNil(files) + XCTAssertNil(error) + + if var files = files { + files = files.filter { $0.expiresAt != nil } + XCTAssertEqual(files.count, 1) + + if let file = files.first { + _file = file + let expiresInSeconds = file.expiresAt?.timeIntervalSinceNow + XCTAssertNotNil(expiresInSeconds) + if let expiresInSeconds = expiresInSeconds { + XCTAssertGreaterThan(expiresInSeconds, 0) + XCTAssertLessThanOrEqual(expiresInSeconds, 5) + } + } + } + + expectationFind?.fulfill() + } + + waitForExpectations(timeout: defaultTimeout) { error in + expectationFind = nil + } + } + + XCTAssertNotNil(_file) + + if let file = _file { + mockResponse(error: timeoutError) + defer { + setURLProtocol(nil) + } + + weak var expectationRefresh = expectation(description: "Refresh") + + fileStore.refresh(file, ttl: (5, .second)) { file, error in + XCTAssertTrue(Thread.isMainThread) + XCTAssertNil(file) + XCTAssertNotNil(error) + XCTAssertTimeoutError(error) + + expectationRefresh?.fulfill() + } + + waitForExpectations(timeout: defaultTimeout) { error in + expectationRefresh = nil + } + } + } + + func testCachedFileNotFound() { + let file = File() { + $0.fileId = UUID().uuidString + } + XCTAssertNil(fileStore.cachedFile(file)) + } + } diff --git a/Kinvey/KinveyTests/ForgotToCallSuper.swift b/Kinvey/KinveyTests/ForgotToCallSuper.swift new file mode 100644 index 000000000..5cd76ab3c --- /dev/null +++ b/Kinvey/KinveyTests/ForgotToCallSuper.swift @@ -0,0 +1,83 @@ +// +// ForgotToCallSuper.swift +// Kinvey +// +// Created by Victor Hugo on 2017-05-19. +// Copyright © 2017 Kinvey. All rights reserved. +// + +import XCTest +@testable import Kinvey +import Nimble + +class ForgotToCallSuperEntity: Entity { + + dynamic var myProperty: String? + + override class func collectionName() -> String { + return "ForgotToCallSuper" + } + + override func propertyMapping(_ map: Map) { + myProperty <- ("myProperty", map["myProperty"]) + } + +} + +class ForgotToCallSuperEntity2: Entity { + + dynamic var myId: String? + dynamic var myProperty: String? + + override class func collectionName() -> String { + return "ForgotToCallSuper" + } + + override func propertyMapping(_ map: Map) { + myId <- ("myId", map[Key.entityId]) + myProperty <- ("myProperty", map["myProperty"]) + } + +} + +class ForgotToCallSuperPersistable: Persistable { + + dynamic var myProperty: String? + + class func collectionName() -> String { + return "ForgotToCallSuper" + } + + required init() { + } + + required init?(map: Map) { + } + + func mapping(map: Map) { + myProperty <- ("myProperty", map["myProperty"]) + } + +} + +class ForgotToCallSuper: XCTestCase { + + func testForgotToCallSuper() { + expect { () -> Void in + let _ = ForgotToCallSuperEntity.propertyMappingReverse() + }.to(throwAssertion()) + } + + func testForgotToCallSuper2() { + expect { () -> Void in + let _ = ForgotToCallSuperEntity2.propertyMappingReverse() + }.to(throwAssertion()) + } + + func testForgotToCallSuperPersistable() { + expect { () -> Void in + let _ = ForgotToCallSuperPersistable.propertyMappingReverse() + }.to(throwAssertion()) + } + +} diff --git a/Kinvey/KinveyTests/KinveyTestCase.swift b/Kinvey/KinveyTests/KinveyTestCase.swift index 27e4891ef..1009ddb18 100644 --- a/Kinvey/KinveyTests/KinveyTestCase.swift +++ b/Kinvey/KinveyTests/KinveyTestCase.swift @@ -102,7 +102,7 @@ extension JSONSerialization { } return try jsonObject(with: inputStream, options: opt) } else { - fatalError() + Swift.fatalError() } } @@ -128,7 +128,7 @@ extension URLRequest { } while read > 0 return data } else { - fatalError() + Swift.fatalError() } } @@ -307,9 +307,14 @@ class KinveyTestCase: XCTestCase { } + private var originalLogLevel: LogLevel! + override func setUp() { super.setUp() + originalLogLevel = Kinvey.logLevel + Kinvey.logLevel = .verbose + if KinveyTestCase.appInitialize == KinveyTestCase.appInitializeDevelopment { initializeDevelopment() } else { @@ -462,6 +467,8 @@ class KinveyTestCase: XCTestCase { client.cacheManager.clearAll() removeAll(Person.self) + Kinvey.logLevel = originalLogLevel + super.tearDown() } @@ -470,7 +477,7 @@ class KinveyTestCase: XCTestCase { var json = try! JSONSerialization.jsonObject(with: request) as! JsonDictionary json[PersistableIdKey] = UUID().uuidString json[PersistableAclKey] = [ - Acl.CreatorKey : self.client.activeUser!.userId + Acl.Key.creator : self.client.activeUser!.userId ] json[PersistableMetadataKey] = [ Metadata.LmtKey : Date().toString(), diff --git a/Kinvey/KinveyTests/LogTestCase.swift b/Kinvey/KinveyTests/LogTestCase.swift new file mode 100644 index 000000000..ee870b2b1 --- /dev/null +++ b/Kinvey/KinveyTests/LogTestCase.swift @@ -0,0 +1,72 @@ +// +// LogTestCase.swift +// Kinvey +// +// Created by Victor Hugo on 2017-05-18. +// Copyright © 2017 Kinvey. All rights reserved. +// + +import XCTest +@testable import Kinvey +import XCGLogger + +class LogTestCase: XCTestCase { + + var originalLogLevel: LogLevel! + + override func setUp() { + originalLogLevel = logLevel + } + + override func tearDown() { + logLevel = originalLogLevel + } + + func testLogLevelInitialState() { + XCTAssertEqual(Kinvey.log.outputLevel, XCGLogger.Level.debug) + XCTAssertEqual(Kinvey.LogLevel.debug, XCGLogger.Level.debug.logLevel) + } + + func testLogLevelVerbose() { + logLevel = .verbose + XCTAssertEqual(Kinvey.log.outputLevel, XCGLogger.Level.verbose) + XCTAssertEqual(logLevel, XCGLogger.Level.verbose.logLevel) + } + + func testLogLevelDebug() { + logLevel = .debug + XCTAssertEqual(Kinvey.log.outputLevel, XCGLogger.Level.debug) + XCTAssertEqual(logLevel, XCGLogger.Level.debug.logLevel) + } + + func testLogLevelInfo() { + logLevel = .info + XCTAssertEqual(Kinvey.log.outputLevel, XCGLogger.Level.info) + XCTAssertEqual(logLevel, XCGLogger.Level.info.logLevel) + } + + func testLogLevelWarning() { + logLevel = .warning + XCTAssertEqual(Kinvey.log.outputLevel, XCGLogger.Level.warning) + XCTAssertEqual(logLevel, XCGLogger.Level.warning.logLevel) + } + + func testLogLevelError() { + logLevel = .error + XCTAssertEqual(Kinvey.log.outputLevel, XCGLogger.Level.error) + XCTAssertEqual(logLevel, XCGLogger.Level.error.logLevel) + } + + func testLogLevelSevere() { + logLevel = .severe + XCTAssertEqual(Kinvey.log.outputLevel, XCGLogger.Level.severe) + XCTAssertEqual(logLevel, XCGLogger.Level.severe.logLevel) + } + + func testLogLevelNone() { + logLevel = .none + XCTAssertEqual(Kinvey.log.outputLevel, XCGLogger.Level.none) + XCTAssertEqual(logLevel, XCGLogger.Level.none.logLevel) + } + +} diff --git a/Kinvey/Kinvey/MemoryCache.swift b/Kinvey/KinveyTests/MemoryCache.swift similarity index 100% rename from Kinvey/Kinvey/MemoryCache.swift rename to Kinvey/KinveyTests/MemoryCache.swift diff --git a/Kinvey/KinveyTests/MetadataTestCase.swift b/Kinvey/KinveyTests/MetadataTestCase.swift new file mode 100644 index 000000000..0201edb64 --- /dev/null +++ b/Kinvey/KinveyTests/MetadataTestCase.swift @@ -0,0 +1,37 @@ +// +// MetadataTestCase.swift +// Kinvey +// +// Created by Victor Hugo on 2017-05-19. +// Copyright © 2017 Kinvey. All rights reserved. +// + +import XCTest +@testable import Kinvey +import ObjectMapper + +class MetadataTestCase: XCTestCase { + + func testMetadata() { + let authToken = UUID().uuidString + + let lrt = Date() + let lmt = Date(timeIntervalSinceNow: 1) + let ect = Date(timeIntervalSinceNow: 2) + + let json = [ + Metadata.Key.lastModifiedTime: lmt.toString(), + Metadata.Key.entityCreationTime: ect.toString(), + Metadata.Key.authtoken: authToken + ] + let metadata = Metadata(JSON: json) + XCTAssertNotNil(metadata) + if let metadata = metadata { + XCTAssertEqualWithAccuracy(metadata.lastModifiedTime!.timeIntervalSinceReferenceDate, lmt.timeIntervalSinceReferenceDate, accuracy: 0.0009) + XCTAssertEqualWithAccuracy(metadata.entityCreationTime!.timeIntervalSinceReferenceDate, ect.timeIntervalSinceReferenceDate, accuracy: 0.0009) + XCTAssertEqualWithAccuracy(metadata.lastReadTime.timeIntervalSinceReferenceDate, lrt.timeIntervalSinceReferenceDate, accuracy: 0.9999) + XCTAssertEqual(metadata.authtoken, authToken) + } + } + +} diff --git a/Kinvey/KinveyTests/NetworkStoreTests.swift b/Kinvey/KinveyTests/NetworkStoreTests.swift index 4c88dffaf..8af234028 100644 --- a/Kinvey/KinveyTests/NetworkStoreTests.swift +++ b/Kinvey/KinveyTests/NetworkStoreTests.swift @@ -11,6 +11,7 @@ import Foundation @testable import Kinvey import CoreLocation import MapKit +import Nimble class NetworkStoreTests: StoreTestCase { @@ -2086,4 +2087,19 @@ class NetworkStoreTests: StoreTestCase { } } + func testGroupCustomResultKey() { + expect { () -> Void in + let _ = Aggregation.custom(keys: [], initialObject: [:], reduceJSFunction: "").resultKey + }.to(throwAssertion()) + } + + func testRemoveByEmptyId() { + let store = DataStore.collection(.network) + expect { () -> Void in + store.remove(byId: "") { count, error in + XCTFail() + } + }.to(throwAssertion()) + } + } diff --git a/Kinvey/KinveyTests/PerformanceProductTestCase.swift b/Kinvey/KinveyTests/PerformanceProductTestCase.swift index 315871df7..e095d3da9 100644 --- a/Kinvey/KinveyTests/PerformanceProductTestCase.swift +++ b/Kinvey/KinveyTests/PerformanceProductTestCase.swift @@ -8,7 +8,7 @@ import XCTest import RealmSwift -@testable import Kinvey +import Kinvey import PromiseKit class Product: Entity { diff --git a/Kinvey/KinveyTests/PushTestCase.swift b/Kinvey/KinveyTests/PushTestCase.swift index 874cc9522..89ea50b46 100644 --- a/Kinvey/KinveyTests/PushTestCase.swift +++ b/Kinvey/KinveyTests/PushTestCase.swift @@ -77,6 +77,24 @@ class PushTestCase: KinveyTestCase { } } + func testUnregisterDeviceToken() { + let client = Client(appKey: "_appKey_", appSecret: "_appSecret_") + + weak var expectaionUnRegister = expectation(description: "UnRegister") + + client.push.unRegisterDeviceToken { (success, error) in + XCTAssertFalse(success) + XCTAssertNotNil(error) + XCTAssertEqual(error?.localizedDescription, "Device token not found") + + expectaionUnRegister?.fulfill() + } + + waitForExpectations(timeout: defaultTimeout) { error in + expectaionUnRegister = nil + } + } + func testBadgeNumber() { UIApplication.shared.applicationIconBadgeNumber = 1 diff --git a/Kinvey/KinveyTests/QueryTest.swift b/Kinvey/KinveyTests/QueryTest.swift index bcb95c84b..6fb831da8 100644 --- a/Kinvey/KinveyTests/QueryTest.swift +++ b/Kinvey/KinveyTests/QueryTest.swift @@ -38,7 +38,7 @@ class QueryTest: XCTestCase { } else if let value = value as? [String : Any] { result[key] = String(data: try! JSONSerialization.data(withJSONObject: value), encoding: .utf8)! } else { - fatalError() + Swift.fatalError() } } return result @@ -189,23 +189,23 @@ class QueryTest: XCTestCase { } func testSortAscending() { - XCTAssertEqual(encodeQuery(Query(sortDescriptors: [NSSortDescriptor(key: "name", ascending: true)])), "query=%7B%7D&sort=\(encodeURL(["name" : 1]))") + XCTAssertEqual(encodeQuery(Query(sortDescriptors: [NSSortDescriptor(key: "name", ascending: true)])), "sort=\(encodeURL(["name" : 1]))") } func testSortDescending() { - XCTAssertEqual(encodeQuery(Query(sortDescriptors: [NSSortDescriptor(key: "name", ascending: false)])), "query=%7B%7D&sort=\(encodeURL(["name" : -1]))") + XCTAssertEqual(encodeQuery(Query(sortDescriptors: [NSSortDescriptor(key: "name", ascending: false)])), "sort=\(encodeURL(["name" : -1]))") } func testSkip() { - XCTAssertEqual(encodeQuery(Query { $0.skip = 100 }), "query=%7B%7D&skip=100") + XCTAssertEqual(encodeQuery(Query { $0.skip = 100 }), "skip=100") } func testLimit() { - XCTAssertEqual(encodeQuery(Query { $0.limit = 100 }), "query=%7B%7D&limit=100") + XCTAssertEqual(encodeQuery(Query { $0.limit = 100 }), "limit=100") } func testSkipAndLimit() { - XCTAssertEqual(encodeQuery(Query { $0.skip = 100; $0.limit = 300 }), "query=%7B%7D&skip=100&limit=300") + XCTAssertEqual(encodeQuery(Query { $0.skip = 100; $0.limit = 300 }), "skip=100&limit=300") } func testPredicateSortSkipAndLimit() { @@ -334,7 +334,7 @@ class QueryTest: XCTestCase { let queryItems = query.urlQueryItems XCTAssertNotNil(queryItems) - XCTAssertEqual(queryItems?.count, 2) + XCTAssertEqual(queryItems?.count, 1) XCTAssertEqual(queryItems?.filter { $0.name == "sort" }.first?.value, "{\"name\":1}") } @@ -344,7 +344,7 @@ class QueryTest: XCTestCase { let queryItems = query.urlQueryItems XCTAssertNotNil(queryItems) - XCTAssertEqual(queryItems?.count, 2) + XCTAssertEqual(queryItems?.count, 1) XCTAssertEqual(queryItems?.filter { $0.name == "sort" }.first?.value, "{\"name\":-1}") } @@ -356,4 +356,49 @@ class QueryTest: XCTestCase { XCTAssertNil(NSPredicate(JSON: [:])) } + func testGeoPointConvertionToCLLocationCoordinate2D() { + let geopoint = GeoPoint(latitude: 40.74, longitude: -74.56) + let locationCoordinate2D = CLLocationCoordinate2D(geoPoint: geopoint) + XCTAssertEqual(geopoint.latitude, locationCoordinate2D.latitude) + XCTAssertEqual(geopoint.longitude, locationCoordinate2D.longitude) + } + + func testCLLocationCoordinate2DConvertionToGeoPoint() { + let locationCoordinate2D = CLLocationCoordinate2D(latitude: 40.74, longitude: -74.56) + let geopoint = GeoPoint(coordinate: locationCoordinate2D) + XCTAssertEqual(geopoint.latitude, locationCoordinate2D.latitude) + XCTAssertEqual(geopoint.longitude, locationCoordinate2D.longitude) + } + + func testMKPolyline() { + let locationCoordinate2D = CLLocationCoordinate2D(latitude: 40.74, longitude: -74.56) + let locationCoordinate2DArray = [locationCoordinate2D] + let polyline = MKPolyline(coordinates: locationCoordinate2DArray, count: 1) + let query = Query(format: "location == %@", polyline) + guard let result = query.predicate?.mongoDBQuery else { + XCTAssertNotNil(query.predicate?.mongoDBQuery) + return + } + + XCTAssertEqual(result.count, 1) + + guard let location = result["location"] as? [String : Any] else { + XCTAssertNotNil(result["location"] as? [String : Any]) + return + } + + XCTAssertEqual(location.count, 1) + guard let geoWithin = location["$geoWithin"] else { + XCTAssertNotNil(location["$geoWithin"]) + return + } + XCTAssertEqual("\(geoWithin)", "nil") + } + + func testInvalidGeoPointParse() { + XCTAssertNil(GeoPointTransform().transformFromJSON([-74.56])) + XCTAssertNil(GeoPointTransform().transformFromJSON([])) + XCTAssertNil(GeoPointTransform().transformFromJSON([-74.56, 40.74, 5.22])) + } + } diff --git a/Kinvey/KinveyTests/SyncStoreTests.swift b/Kinvey/KinveyTests/SyncStoreTests.swift index bbb99b3a3..42cf777d1 100644 --- a/Kinvey/KinveyTests/SyncStoreTests.swift +++ b/Kinvey/KinveyTests/SyncStoreTests.swift @@ -8,6 +8,7 @@ import XCTest @testable import Kinvey +import Nimble class SyncStoreTests: StoreTestCase { @@ -261,7 +262,7 @@ class SyncStoreTests: StoreTestCase { case 1: return HttpResponse(json: persons.toJSON()) default: - fatalError() + Swift.fatalError() } }) } @@ -400,7 +401,7 @@ class SyncStoreTests: StoreTestCase { case 1: return HttpResponse(json: persons.toJSON()) default: - fatalError() + Swift.fatalError() } }) } @@ -501,7 +502,7 @@ class SyncStoreTests: StoreTestCase { var json = try! JSONSerialization.jsonObject(with: request) as! JsonDictionary json[PersistableIdKey] = UUID().uuidString json[PersistableAclKey] = [ - Acl.CreatorKey : self.client.activeUser!.userId + Acl.Key.creator : self.client.activeUser!.userId ] json[PersistableMetadataKey] = [ Metadata.LmtKey : Date().toString(), @@ -514,7 +515,7 @@ class SyncStoreTests: StoreTestCase { XCTAssertNotNil(personMockJson) return HttpResponse(statusCode: 200, json: [personMockJson!]) default: - fatalError() + Swift.fatalError() } } } @@ -560,7 +561,7 @@ class SyncStoreTests: StoreTestCase { var json = try! JSONSerialization.jsonObject(with: request) as! JsonDictionary json[PersistableIdKey] = UUID().uuidString json[PersistableAclKey] = [ - Acl.CreatorKey : self.client.activeUser!.userId + Acl.Key.creator : self.client.activeUser!.userId ] json[PersistableMetadataKey] = [ Metadata.LmtKey : Date().toString(), @@ -571,7 +572,7 @@ class SyncStoreTests: StoreTestCase { case 1: return HttpResponse(error: timeoutError) default: - fatalError() + Swift.fatalError() } } } @@ -677,7 +678,7 @@ class SyncStoreTests: StoreTestCase { var json = try! JSONSerialization.jsonObject(with: request) as! JsonDictionary json[PersistableIdKey] = UUID().uuidString json[PersistableAclKey] = [ - Acl.CreatorKey : self.client.activeUser!.userId + Acl.Key.creator : self.client.activeUser!.userId ] json[PersistableMetadataKey] = [ Metadata.LmtKey : Date().toString(), @@ -990,8 +991,8 @@ class SyncStoreTests: StoreTestCase { XCTAssertNil(results) XCTAssertNotNil(error) - if let error = error as? NSError? { - XCTAssertEqual(error, Kinvey.Error.invalidDataStoreType as NSError) + if let error = error { + XCTAssertEqual(error as NSError, Kinvey.Error.invalidDataStoreType as NSError) } expectationPull?.fulfill() @@ -1586,7 +1587,7 @@ class SyncStoreTests: StoreTestCase { var json = try! JSONSerialization.jsonObject(with: request) as! JsonDictionary json[PersistableIdKey] = UUID().uuidString json[PersistableAclKey] = [ - Acl.CreatorKey : self.client.activeUser!.userId + Acl.Key.creator : self.client.activeUser!.userId ] json[PersistableMetadataKey] = [ Metadata.LmtKey : Date().toString(), @@ -1598,7 +1599,7 @@ class SyncStoreTests: StoreTestCase { XCTAssertNotNil(personMockJson) return HttpResponse(statusCode: 200, json: [personMockJson!]) default: - fatalError() + Swift.fatalError() } } } @@ -1970,4 +1971,76 @@ class SyncStoreTests: StoreTestCase { } } + func testRealmCacheNotEntity() { + class NotEntityPersistable: NSObject, Persistable { + + static func collectionName() -> String { + return "NotEntityPersistable" + } + + required override init() { + } + + required init?(map: Map) { + } + + func mapping(map: Map) { + } + + } + + expect { () -> Void in + let _ = RealmCache(persistenceId: UUID().uuidString, schemaVersion: 0) + }.to(throwAssertion()) + } + + func testRealmSyncNotEntity() { + class NotEntityPersistable: NSObject, Persistable { + + static func collectionName() -> String { + return "NotEntityPersistable" + } + + required override init() { + } + + required init?(map: Map) { + } + + func mapping(map: Map) { + } + + } + + expect { () -> Void in + let _ = RealmSync(persistenceId: UUID().uuidString, schemaVersion: 0) + }.to(throwAssertion()) + } + + func testCancelLocalRequest() { + let query = Query(format: "propertyNotMapped == %@", 10) + + weak var expectationFind = expectation(description: "Find") + + let request = store.find(query) { persons, error in + XCTAssertNotNil(persons) + XCTAssertNil(error) + + XCTAssertEqual(persons?.count, 0) + + expectationFind?.fulfill() + expectationFind = nil + } + request.cancel() + + waitForExpectations(timeout: defaultTimeout) { error in + expectationFind = nil + } + } + + func testNewTypeDataStore() { + var store = DataStore.getInstance() + store = store.collection(newType: Book.self).collection(newType: Person.self) + } + } diff --git a/Kinvey/KinveyTests/UserTests.swift b/Kinvey/KinveyTests/UserTests.swift index b50cd17b3..65d81f521 100644 --- a/Kinvey/KinveyTests/UserTests.swift +++ b/Kinvey/KinveyTests/UserTests.swift @@ -12,6 +12,7 @@ import KinveyApp @testable import Kinvey import ObjectMapper import SafariServices +import Nimble typealias MICLoginViewController = KinveyApp.MICLoginViewController @@ -1084,6 +1085,10 @@ class UserTests: KinveyTestCase { } } + func testUserQueryMapping() { + XCTAssertNotNil(UserQuery(JSON: [:])) + } + func testLogoutLogin() { guard !useMockData else { return @@ -2079,7 +2084,7 @@ class UserTests: KinveyTestCase { case 1: return HttpResponse(error: NSError(domain: NSURLErrorDomain, code: NSURLErrorTimedOut, userInfo: nil)) default: - fatalError() + Swift.fatalError() } } defer { @@ -3099,7 +3104,7 @@ class UserTests: KinveyTestCase { "_acl" : [ "creator" : "57c788d168d976c525ee4602" ] - ] as [String : Any] + ] as [String : Any] let data = try! JSONSerialization.data(withJSONObject: json, options: []) client?.urlProtocol(self, didLoad: data) client?.urlProtocolDidFinishLoading(self) @@ -3387,7 +3392,7 @@ class UserTests: KinveyTestCase { weak var expectationLogin = expectation(description: "Login") - MIC.login(redirectURI: URL(string: "myCustomURIScheme://")!, code: "1234") { result in + MIC.login(redirectURI: URL(string: "myCustomURIScheme://")!, code: "1234", clientId: nil) { result in XCTAssertTrue(Thread.isMainThread) switch result { @@ -3465,7 +3470,7 @@ class UserTests: KinveyTestCase { weak var expectationLogin = expectation(description: "Login") - MIC.login(redirectURI: URL(string: "myCustomURIScheme://")!, code: "1234") { result in + MIC.login(redirectURI: URL(string: "myCustomURIScheme://")!, code: "1234", clientId: nil) { result in XCTAssertTrue(Thread.isMainThread) switch result { @@ -3520,7 +3525,7 @@ class UserTests: KinveyTestCase { ] ]) default: - fatalError() + Swift.fatalError() } } defer { @@ -3552,7 +3557,7 @@ class UserTests: KinveyTestCase { weak var expectationLogin = expectation(description: "Login") - MIC.login(redirectURI: URL(string: "myCustomURIScheme://")!, username: UUID().uuidString, password: UUID().uuidString) { result in + MIC.login(redirectURI: URL(string: "myCustomURIScheme://")!, username: UUID().uuidString, password: UUID().uuidString, clientId: nil) { result in XCTAssertTrue(Thread.isMainThread) switch result { @@ -3622,7 +3627,7 @@ class UserTests: KinveyTestCase { weak var expectationLogin = expectation(description: "Login") - MIC.login(redirectURI: URL(string: "myCustomURIScheme://")!, username: UUID().uuidString, password: UUID().uuidString) { result in + MIC.login(redirectURI: URL(string: "myCustomURIScheme://")!, username: UUID().uuidString, password: UUID().uuidString, clientId: nil) { result in XCTAssertTrue(Thread.isMainThread) switch result { @@ -3675,7 +3680,7 @@ class UserTests: KinveyTestCase { weak var expectationLogin = expectation(description: "Login") - MIC.login(redirectURI: URL(string: "myCustomURIScheme://")!, username: UUID().uuidString, password: UUID().uuidString) { result in + MIC.login(redirectURI: URL(string: "myCustomURIScheme://")!, username: UUID().uuidString, password: UUID().uuidString, clientId: nil) { result in XCTAssertTrue(Thread.isMainThread) switch result { @@ -3698,5 +3703,82 @@ class UserTests: KinveyTestCase { func testUserWithoutUserID() { XCTAssertNil(User(JSON: ["username" : "Test"])) } + + func testUserMicViewControllerCoding() { + expect { () -> Void in + let _ = Kinvey.MICLoginViewController(coder: NSKeyedArchiver()) + }.to(throwAssertion()) + } + + func testMICTimeoutAction() { + mockResponse { (request) -> HttpResponse in + RunLoop.current.run(until: Date(timeIntervalSinceNow: 10)) + return HttpResponse(error: timeoutError) + } + defer { + setURLProtocol(nil) + } + + weak var expectationLogin = expectation(description: "Login") + + let redirectURI = URL(string: "throwAnError://")! + User.presentMICViewController(redirectURI: redirectURI, timeout: 3, forceUIWebView: true) { (user, error) -> Void in + XCTAssertTrue(Thread.isMainThread) + XCTAssertNotNil(error) + + XCTAssertTrue(error is Kinvey.Error) + if let error = error as? Kinvey.Error { + switch error { + case .requestTimeout: + break + default: + XCTFail() + } + } + + expectationLogin?.fulfill() + } + + waitForExpectations(timeout: defaultTimeout) { error in + expectationLogin = nil + } + } + + func testMICCancelUserAction() { + mockResponse { (request) -> HttpResponse in + RunLoop.current.run(until: Date(timeIntervalSinceNow: 10)) + DispatchQueue.main.async { + self.tester().tapView(withAccessibilityLabel: " X ") + } + return HttpResponse(error: timeoutError) + } + defer { + setURLProtocol(nil) + } + + weak var expectationLogin = expectation(description: "Login") + + let redirectURI = URL(string: "throwAnError://")! + User.presentMICViewController(redirectURI: redirectURI, timeout: 60, forceUIWebView: true) { (user, error) -> Void in + XCTAssertTrue(Thread.isMainThread) + XCTAssertNotNil(error) + + XCTAssertTrue(error is Kinvey.Error) + if let error = error as? Kinvey.Error { + switch error { + case .requestCancelled: + break + default: + XCTFail() + } + } + + expectationLogin?.fulfill() + } + + waitForExpectations(timeout: defaultTimeout) { error in + expectationLogin = nil + } + } } diff --git a/Kinvey/SSOApp/SSOApp1/AppDelegate.swift b/Kinvey/SSOApp/SSOApp1/AppDelegate.swift index 285e23634..00d284964 100644 --- a/Kinvey/SSOApp/SSOApp1/AppDelegate.swift +++ b/Kinvey/SSOApp/SSOApp1/AppDelegate.swift @@ -10,6 +10,7 @@ import UIKit import Kinvey let micRedirectURI = URL(string: "ssoApp1://")! +let clientId = "sso_app1_client_id" @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { @@ -50,7 +51,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } func application(_ application: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool { - if User.login(redirectURI: micRedirectURI, micURL: url) { + if User.login(redirectURI: micRedirectURI, micURL: url, clientId: clientId) { return true } diff --git a/Kinvey/SSOApp/SSOApp1Tests/SSOApp1Tests.swift b/Kinvey/SSOApp/SSOApp1Tests/SSOApp1Tests.swift index 957bf028f..db310f938 100644 --- a/Kinvey/SSOApp/SSOApp1Tests/SSOApp1Tests.swift +++ b/Kinvey/SSOApp/SSOApp1Tests/SSOApp1Tests.swift @@ -64,6 +64,32 @@ class SSOApp1Tests: KinveyTestCase { let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: "1.1", headerFields: [:])! client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) + let urlComponents = URLComponents(url: request.url!, resolvingAgainstBaseURL: false)! + XCTAssertNotNil(urlComponents.queryItems) + if let queryItems = urlComponents.queryItems { + let clientId = queryItems.filter { $0.name == "client_id" }.first?.value + XCTAssertNotNil(clientId) + if let clientId = clientId { + XCTAssertTrue(clientId.contains(":")) + let regex = try? NSRegularExpression(pattern: "([^:]+):([^:]+)") + XCTAssertNotNil(regex) + if let regex = regex { + let match = regex.firstMatch(in: clientId, range: NSMakeRange(0, clientId.characters.count)) + XCTAssertNotNil(match) + if let match = match { + XCTAssertEqual(match.numberOfRanges, 3) + let appKey = clientId.substring(with: match.rangeAt(1)) + XCTAssertNotNil(appKey) + XCTAssertFalse(appKey.isEmpty) + + let clientIdValue = clientId.substring(with: match.rangeAt(1)) + XCTAssertNotNil(clientIdValue) + XCTAssertFalse(clientIdValue.isEmpty) + } + } + } + } + let url = Bundle(for: SSOApp1Tests.self).url(forResource: "auth", withExtension: "html")! let data = try! Data(contentsOf: url) var html = String(data: data, encoding: .utf8)! diff --git a/Kinvey/SSOApp/SSOApp2/AppDelegate.swift b/Kinvey/SSOApp/SSOApp2/AppDelegate.swift index 00e667adc..49169b725 100644 --- a/Kinvey/SSOApp/SSOApp2/AppDelegate.swift +++ b/Kinvey/SSOApp/SSOApp2/AppDelegate.swift @@ -10,6 +10,7 @@ import UIKit import Kinvey let micRedirectURI = URL(string: "ssoApp2://")! +let clientId = "sso_app1_client_id" @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { diff --git a/Kinvey/SSOApp/Sources/ViewController.swift b/Kinvey/SSOApp/Sources/ViewController.swift index a1210aca2..d9e81fc48 100644 --- a/Kinvey/SSOApp/Sources/ViewController.swift +++ b/Kinvey/SSOApp/Sources/ViewController.swift @@ -39,7 +39,11 @@ class ViewController: UIViewController { } else { Kinvey.sharedClient.micApiVersion = .v3 URLCache.shared.removeAllCachedResponses() - User.presentMICViewController(redirectURI: micRedirectURI, micUserInterface: micUserInterface) { user, error in + User.presentMICViewController( + redirectURI: micRedirectURI, + micUserInterface: micUserInterface, + clientId: clientId + ) { user, error in self.completionHandler?(user, error) if let user = user { self.display(title: "Success", message: "User: \(user)") diff --git a/README.md b/README.md index 801f7a239..543d5afe8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Kinvey iOS SDK -![badge-pod] ![badge-languages] ![badge-pms] ![badge-platforms] ![badge-status] +![badge-pod] ![badge-languages] ![badge-pms] ![badge-platforms] ![badge-status] ![badge-coverage] The Kinvey iOS SDK is a package that can be used to develop iOS applications on the Kinvey platform. Refer to the Kinvey [DevCenter](http://devcenter.kinvey.com/ios-v3.0) for documentation on using Kinvey. @@ -75,6 +75,6 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for details on reporting bugs and making [badge-languages]: https://img.shields.io/badge/languages-Swift%20%7C%20ObjC-orange.svg [badge-mit]: https://img.shields.io/badge/license-MIT-blue.svg [badge-pms]: https://img.shields.io/badge/supports-CocoaPods%20%7C%20Carthage-green.svg -[badge-status]: https://travis-ci.org/Kinvey/ios-library.svg?branch=master -[badge-coverage]: https://codecov.io/gh/Kinvey/ios-library/graph/badge.svg +[badge-status]: https://travis-ci.org/Kinvey/swift-sdk.svg?branch=master +[badge-coverage]: https://codecov.io/gh/Kinvey/swift-sdk/graph/badge.svg [badge-codebeat]: https://codebeat.co/badges/e1a944a5-3090-4d76-bfde-e408a6f97278