From 6a94ad329b116ecf440b7a351e0cc0e9a6c1ee43 Mon Sep 17 00:00:00 2001 From: nishant-clevertap <96819882+nishant-clevertap@users.noreply.github.com> Date: Tue, 25 Jun 2024 18:54:12 +0530 Subject: [PATCH] [MC-1563] File downloading (#339) * Adds CTFileDownloadManager class for file downloading Adds callback for each file and all files download * Added unit tests for CTFileDownloadManager * Added unit tests for CTFileDownloadDelegate protocol callback methods. * Updated callback approach to use completion block. Updated unit tests. * - Added class CTFileDownloader to handle file downloading from inapps, file variables. - Added expiry logic for files downloaded. - Added unit tests. * Added public method to get file downloaded path. * Added some unit tests for clearFileAssets method. * - Added logic for handling url download in progress, only one request to the url to download the file. - Added methods for image preloading cases. - Added unit test cases. * - Added thread safety check for updating active and inactive dictionary. - Added changes for CS InApps to use the new CTFileDownloader class for image preloading. - Removed previous usage of CTInAppImagePrefetchManager class for image preloading. * Added callback for File variables only, and code cleanup. * Address comments and improvements * Rename methods * Move duplicate methods to test helper * Move tests to group * Move tests headers and mocks to separate files * Use longer resource timeout * Improve tests * Improve clear expired files * Fix clear expired assets * Save files to directory inside documents Remove all files removes the files inside the directory Add Unit tests * Move to group --------- Co-authored-by: Nikola Zagorchev --- CleverTapSDK.xcodeproj/project.pbxproj | 90 ++- .../contents.xcworkspacedata | 3 - CleverTapSDK/CTConstants.h | 7 + CleverTapSDK/CTInAppNotification.h | 4 +- CleverTapSDK/CTInAppNotification.m | 8 +- CleverTapSDK/CleverTap.m | 15 +- .../FileDownload/CTFileDownloadManager.h | 68 ++ .../FileDownload/CTFileDownloadManager.m | 258 ++++++++ CleverTapSDK/FileDownload/CTFileDownloader.h | 20 + CleverTapSDK/FileDownload/CTFileDownloader.m | 233 +++++++ CleverTapSDK/InApps/CTInAppDisplayManager.h | 3 +- CleverTapSDK/InApps/CTInAppDisplayManager.m | 9 +- .../InApps/CTInAppImagePrefetchManager.h | 19 - .../InApps/CTInAppImagePrefetchManager.m | 254 -------- CleverTapSDK/InApps/CTInAppStore.h | 4 +- CleverTapSDK/InApps/CTInAppStore.m | 47 +- CleverTapSDKTests/CTEventBuilderTest.m | 10 +- .../CTFileDownloadManager+Tests.h | 27 + .../FileDownload/CTFileDownloadManagerTests.m | 579 ++++++++++++++++++ .../FileDownload/CTFileDownloadTestHelper.h | 28 + .../FileDownload/CTFileDownloadTestHelper.m | 108 ++++ .../FileDownload/CTFileDownloader+Tests.h | 33 + .../FileDownload/CTFileDownloaderMock.h | 27 + .../FileDownload/CTFileDownloaderMock.m | 55 ++ .../FileDownload/CTFileDownloaderTests.m | 459 ++++++++++++++ .../FileDownload/NSFileManagerMock.h | 23 + .../FileDownload/NSFileManagerMock.m | 50 ++ .../InApps/CTInAppEvaluationManagerTest.m | 2 +- .../InApps/CTInAppFCManagerTest.m | 38 +- .../CTInAppImagePrefetchManager+Tests.h | 20 - .../InApps/CTInAppImagePrefetchManagerTest.m | 295 --------- CleverTapSDKTests/InApps/CTInAppStoreTest.m | 2 +- CleverTapSDKTests/InApps/InAppHelper.h | 4 +- CleverTapSDKTests/InApps/InAppHelper.m | 6 +- .../Stub Responses/samplePDFStub.pdf | Bin 0 -> 13264 bytes .../Stub Responses/sampleTXTStub.txt | 1 + 36 files changed, 2140 insertions(+), 669 deletions(-) create mode 100644 CleverTapSDK/FileDownload/CTFileDownloadManager.h create mode 100644 CleverTapSDK/FileDownload/CTFileDownloadManager.m create mode 100644 CleverTapSDK/FileDownload/CTFileDownloader.h create mode 100644 CleverTapSDK/FileDownload/CTFileDownloader.m delete mode 100644 CleverTapSDK/InApps/CTInAppImagePrefetchManager.h delete mode 100644 CleverTapSDK/InApps/CTInAppImagePrefetchManager.m create mode 100644 CleverTapSDKTests/FileDownload/CTFileDownloadManager+Tests.h create mode 100644 CleverTapSDKTests/FileDownload/CTFileDownloadManagerTests.m create mode 100644 CleverTapSDKTests/FileDownload/CTFileDownloadTestHelper.h create mode 100644 CleverTapSDKTests/FileDownload/CTFileDownloadTestHelper.m create mode 100644 CleverTapSDKTests/FileDownload/CTFileDownloader+Tests.h create mode 100644 CleverTapSDKTests/FileDownload/CTFileDownloaderMock.h create mode 100644 CleverTapSDKTests/FileDownload/CTFileDownloaderMock.m create mode 100644 CleverTapSDKTests/FileDownload/CTFileDownloaderTests.m create mode 100644 CleverTapSDKTests/FileDownload/NSFileManagerMock.h create mode 100644 CleverTapSDKTests/FileDownload/NSFileManagerMock.m delete mode 100644 CleverTapSDKTests/InApps/CTInAppImagePrefetchManager+Tests.h delete mode 100644 CleverTapSDKTests/InApps/CTInAppImagePrefetchManagerTest.m create mode 100644 CleverTapSDKTests/Stub Responses/samplePDFStub.pdf create mode 100644 CleverTapSDKTests/Stub Responses/sampleTXTStub.txt diff --git a/CleverTapSDK.xcodeproj/project.pbxproj b/CleverTapSDK.xcodeproj/project.pbxproj index 583bc56f..3a57681c 100644 --- a/CleverTapSDK.xcodeproj/project.pbxproj +++ b/CleverTapSDK.xcodeproj/project.pbxproj @@ -165,9 +165,15 @@ 4808030E292EB4FB00C06E2F /* CleverTap+PushPermission.h in Headers */ = {isa = PBXBuildFile; fileRef = 4808030D292EB4FB00C06E2F /* CleverTap+PushPermission.h */; }; 48080311292EB50D00C06E2F /* CTLocalInApp.h in Headers */ = {isa = PBXBuildFile; fileRef = 4808030F292EB50D00C06E2F /* CTLocalInApp.h */; settings = {ATTRIBUTES = (Public, ); }; }; 48080312292EB50D00C06E2F /* CTLocalInApp.m in Sources */ = {isa = PBXBuildFile; fileRef = 48080310292EB50D00C06E2F /* CTLocalInApp.m */; }; - 48BEA4F62AFB868B00690424 /* CTInAppImagePrefetchManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 48BEA4F52AFB868B00690424 /* CTInAppImagePrefetchManager.h */; }; - 48BEA4F82AFB86A300690424 /* CTInAppImagePrefetchManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 48BEA4F72AFB86A300690424 /* CTInAppImagePrefetchManager.m */; }; - 48C31A822B1DC5CF00CA2A90 /* CTInAppImagePrefetchManagerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 48C31A812B1DC5CF00CA2A90 /* CTInAppImagePrefetchManagerTest.m */; }; + 487854032BF4B79F00565685 /* CTFileDownloader.h in Headers */ = {isa = PBXBuildFile; fileRef = 487854022BF4B79F00565685 /* CTFileDownloader.h */; }; + 487854052BF4B7D800565685 /* CTFileDownloader.m in Sources */ = {isa = PBXBuildFile; fileRef = 487854042BF4B7D800565685 /* CTFileDownloader.m */; }; + 487854072BF4BC4E00565685 /* CTFileDownloaderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 487854062BF4BC4E00565685 /* CTFileDownloaderTests.m */; }; + 489A731A2BC51795008D4850 /* CTFileDownloadManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 489A73192BC51795008D4850 /* CTFileDownloadManager.h */; }; + 489A731C2BC517B4008D4850 /* CTFileDownloadManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 489A731B2BC517B4008D4850 /* CTFileDownloadManager.m */; }; + 48A2C4B92BD67DDC006C61BC /* sampleTXTStub.txt in Resources */ = {isa = PBXBuildFile; fileRef = 48A2C4B72BD67DDB006C61BC /* sampleTXTStub.txt */; }; + 48A2C4BA2BD67DDC006C61BC /* samplePDFStub.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 48A2C4B82BD67DDB006C61BC /* samplePDFStub.pdf */; }; + 48C0FD6F2BCD522100E01EA9 /* CTFileDownloadManagerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 48C0FD6E2BCD522100E01EA9 /* CTFileDownloadManagerTests.m */; }; + 48F9FD092C208F7100617770 /* CTInAppStore.m in Sources */ = {isa = PBXBuildFile; fileRef = 6A4427D72ABCE5EB0098866F /* CTInAppStore.m */; }; 4987C665251B5E79003E6BE8 /* CTImageInAppViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = 4987C663251B5E79003E6BE8 /* CTImageInAppViewController.h */; }; 4987C666251B5E79003E6BE8 /* CTImageInAppViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4987C664251B5E79003E6BE8 /* CTImageInAppViewController.m */; }; 4987C668251B5F9E003E6BE8 /* CTImageInAppViewControllerPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4987C667251B5F9E003E6BE8 /* CTImageInAppViewControllerPrivate.h */; }; @@ -313,7 +319,6 @@ 6A4427D02AB9D8C30098866F /* CTInAppFCManager+Legacy.h in Headers */ = {isa = PBXBuildFile; fileRef = 6A4427CE2AB9D8C30098866F /* CTInAppFCManager+Legacy.h */; }; 6A4427D12AB9D8C30098866F /* CTInAppFCManager+Legacy.m in Sources */ = {isa = PBXBuildFile; fileRef = 6A4427CF2AB9D8C30098866F /* CTInAppFCManager+Legacy.m */; }; 6A4427D82ABCE5EB0098866F /* CTInAppStore.h in Headers */ = {isa = PBXBuildFile; fileRef = 6A4427D62ABCE5EB0098866F /* CTInAppStore.h */; }; - 6A4427D92ABCE5EB0098866F /* CTInAppStore.m in Sources */ = {isa = PBXBuildFile; fileRef = 6A4427D72ABCE5EB0098866F /* CTInAppStore.m */; }; 6A59D20D2A334B8500531F9D /* NSDictionaryExtensionsTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 6A59D20C2A334B8500531F9D /* NSDictionaryExtensionsTest.m */; }; 6A59D20F2A3351A800531F9D /* LeanplumCTTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 6A59D20E2A3351A800531F9D /* LeanplumCTTest.m */; }; 6A6591662AC70D07005FDE57 /* CTAttachToBatchHeaderDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 6A6591642AC70D07005FDE57 /* CTAttachToBatchHeaderDelegate.h */; }; @@ -336,6 +341,9 @@ 6B535FB82AD56C60002A2663 /* CTMultiDelegateManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 6B535FB52AD56C60002A2663 /* CTMultiDelegateManager.m */; }; 6B535FB92AD56C60002A2663 /* CTMultiDelegateManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 6B535FB52AD56C60002A2663 /* CTMultiDelegateManager.m */; }; 6B9DEE9F2B4D8A500097EF40 /* clevertap-logo.png in Resources */ = {isa = PBXBuildFile; fileRef = 6B9DEE9E2B4D8A500097EF40 /* clevertap-logo.png */; }; + 6B9E95AC2C27164B0002D557 /* CTFileDownloadTestHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 6B9E95AB2C27164B0002D557 /* CTFileDownloadTestHelper.m */; }; + 6B9E95B12C2864EC0002D557 /* CTFileDownloaderMock.m in Sources */ = {isa = PBXBuildFile; fileRef = 6B9E95B02C2864EC0002D557 /* CTFileDownloaderMock.m */; }; + 6B9E95B52C29C2F40002D557 /* NSFileManagerMock.m in Sources */ = {isa = PBXBuildFile; fileRef = 6B9E95B42C29C2F30002D557 /* NSFileManagerMock.m */; }; 6BA3B2DB2B03E926004E834B /* CTQueueType.h in Headers */ = {isa = PBXBuildFile; fileRef = 6BA3B2DA2B03E926004E834B /* CTQueueType.h */; }; 6BA3B2DC2B03E926004E834B /* CTQueueType.h in Headers */ = {isa = PBXBuildFile; fileRef = 6BA3B2DA2B03E926004E834B /* CTQueueType.h */; }; 6BA3B2E12B05411C004E834B /* InAppHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 6BA3B2E02B05411C004E834B /* InAppHelper.m */; }; @@ -711,9 +719,14 @@ 4808030D292EB4FB00C06E2F /* CleverTap+PushPermission.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "CleverTap+PushPermission.h"; sourceTree = ""; }; 4808030F292EB50D00C06E2F /* CTLocalInApp.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CTLocalInApp.h; sourceTree = ""; }; 48080310292EB50D00C06E2F /* CTLocalInApp.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CTLocalInApp.m; sourceTree = ""; }; - 48BEA4F52AFB868B00690424 /* CTInAppImagePrefetchManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CTInAppImagePrefetchManager.h; sourceTree = ""; }; - 48BEA4F72AFB86A300690424 /* CTInAppImagePrefetchManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTInAppImagePrefetchManager.m; sourceTree = ""; }; - 48C31A812B1DC5CF00CA2A90 /* CTInAppImagePrefetchManagerTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTInAppImagePrefetchManagerTest.m; sourceTree = ""; }; + 487854022BF4B79F00565685 /* CTFileDownloader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CTFileDownloader.h; sourceTree = ""; }; + 487854042BF4B7D800565685 /* CTFileDownloader.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTFileDownloader.m; sourceTree = ""; }; + 487854062BF4BC4E00565685 /* CTFileDownloaderTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTFileDownloaderTests.m; sourceTree = ""; }; + 489A73192BC51795008D4850 /* CTFileDownloadManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CTFileDownloadManager.h; sourceTree = ""; }; + 489A731B2BC517B4008D4850 /* CTFileDownloadManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTFileDownloadManager.m; sourceTree = ""; }; + 48A2C4B72BD67DDB006C61BC /* sampleTXTStub.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = sampleTXTStub.txt; sourceTree = ""; }; + 48A2C4B82BD67DDB006C61BC /* samplePDFStub.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = samplePDFStub.pdf; sourceTree = ""; }; + 48C0FD6E2BCD522100E01EA9 /* CTFileDownloadManagerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTFileDownloadManagerTests.m; sourceTree = ""; }; 4987C663251B5E79003E6BE8 /* CTImageInAppViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CTImageInAppViewController.h; sourceTree = ""; }; 4987C664251B5E79003E6BE8 /* CTImageInAppViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTImageInAppViewController.m; sourceTree = ""; }; 4987C667251B5F9E003E6BE8 /* CTImageInAppViewControllerPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CTImageInAppViewControllerPrivate.h; sourceTree = ""; }; @@ -831,7 +844,14 @@ 6B535FB42AD56C60002A2663 /* CTMultiDelegateManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CTMultiDelegateManager.h; sourceTree = ""; }; 6B535FB52AD56C60002A2663 /* CTMultiDelegateManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTMultiDelegateManager.m; sourceTree = ""; }; 6B9DEE9E2B4D8A500097EF40 /* clevertap-logo.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "clevertap-logo.png"; sourceTree = ""; }; - 6B9DEEA02B4DF1B70097EF40 /* CTInAppImagePrefetchManager+Tests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "CTInAppImagePrefetchManager+Tests.h"; sourceTree = ""; }; + 6B9E95AA2C27164B0002D557 /* CTFileDownloadTestHelper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CTFileDownloadTestHelper.h; sourceTree = ""; }; + 6B9E95AB2C27164B0002D557 /* CTFileDownloadTestHelper.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTFileDownloadTestHelper.m; sourceTree = ""; }; + 6B9E95AE2C2864AF0002D557 /* CTFileDownloader+Tests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "CTFileDownloader+Tests.h"; sourceTree = ""; }; + 6B9E95AF2C2864EC0002D557 /* CTFileDownloaderMock.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CTFileDownloaderMock.h; sourceTree = ""; }; + 6B9E95B02C2864EC0002D557 /* CTFileDownloaderMock.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTFileDownloaderMock.m; sourceTree = ""; }; + 6B9E95B22C2868470002D557 /* CTFileDownloadManager+Tests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "CTFileDownloadManager+Tests.h"; sourceTree = ""; }; + 6B9E95B32C29C2F30002D557 /* NSFileManagerMock.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NSFileManagerMock.h; sourceTree = ""; }; + 6B9E95B42C29C2F30002D557 /* NSFileManagerMock.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NSFileManagerMock.m; sourceTree = ""; }; 6BA3B2DA2B03E926004E834B /* CTQueueType.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CTQueueType.h; sourceTree = ""; }; 6BA3B2DF2B05411C004E834B /* InAppHelper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = InAppHelper.h; sourceTree = ""; }; 6BA3B2E02B05411C004E834B /* InAppHelper.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = InAppHelper.m; sourceTree = ""; }; @@ -1080,8 +1100,6 @@ 6BF5A5902ACC854800CDED20 /* CTInAppDisplayManager.m */, 6BF5A59D2AD4303C00CDED20 /* CleverTap+InAppsResponseHandler.m */, 6BF5A5A02AD4313900CDED20 /* CleverTap+InAppsResponseHandler.h */, - 48BEA4F52AFB868B00690424 /* CTInAppImagePrefetchManager.h */, - 48BEA4F72AFB86A300690424 /* CTInAppImagePrefetchManager.m */, ); path = InApps; sourceTree = ""; @@ -1257,6 +1275,8 @@ 4E1F1560277090D6009387AE /* Stub Responses */ = { isa = PBXGroup; children = ( + 48A2C4B82BD67DDB006C61BC /* samplePDFStub.pdf */, + 48A2C4B72BD67DDB006C61BC /* sampleTXTStub.txt */, 6B9DEE9E2B4D8A500097EF40 /* clevertap-logo.png */, 4E1F156727709848009387AE /* app_inbox.json */, 4E1F1561277090D6009387AE /* inapp_alert.json */, @@ -1335,7 +1355,6 @@ 6BEEC2CF2AF1A3A900BD4EC5 /* CTClockMock.h */, 6BEEC2D02AF1A3A900BD4EC5 /* CTClockMock.m */, 6BD334EF2AF545C70099E33E /* CTInAppStoreTest.m */, - 48C31A812B1DC5CF00CA2A90 /* CTInAppImagePrefetchManagerTest.m */, 6BA3B2DF2B05411C004E834B /* InAppHelper.h */, 6BA3B2E02B05411C004E834B /* InAppHelper.m */, 6BA3B2E22B07E06C004E834B /* CTInAppStore+Tests.h */, @@ -1344,7 +1363,6 @@ 6BA3B2E52B07E1D0004E834B /* CTImpressionManager+Tests.h */, 6BA3B2E62B07E207004E834B /* CTTriggersMatcher+Tests.h */, 6BA3B2E72B07E207004E834B /* CTTriggersMatcher+Tests.m */, - 6B9DEEA02B4DF1B70097EF40 /* CTInAppImagePrefetchManager+Tests.h */, 6B4A0F902B45EF6D00A42C6D /* CTInAppTriggerManagerTest.m */, ); path = InApps; @@ -1366,9 +1384,38 @@ path = ProductExperiences; sourceTree = ""; }; + 6B9E95AD2C285F2F0002D557 /* FileDownload */ = { + isa = PBXGroup; + children = ( + 48C0FD6E2BCD522100E01EA9 /* CTFileDownloadManagerTests.m */, + 487854062BF4BC4E00565685 /* CTFileDownloaderTests.m */, + 6B9E95AA2C27164B0002D557 /* CTFileDownloadTestHelper.h */, + 6B9E95AB2C27164B0002D557 /* CTFileDownloadTestHelper.m */, + 6B9E95AE2C2864AF0002D557 /* CTFileDownloader+Tests.h */, + 6B9E95AF2C2864EC0002D557 /* CTFileDownloaderMock.h */, + 6B9E95B02C2864EC0002D557 /* CTFileDownloaderMock.m */, + 6B9E95B22C2868470002D557 /* CTFileDownloadManager+Tests.h */, + 6B9E95B32C29C2F30002D557 /* NSFileManagerMock.h */, + 6B9E95B42C29C2F30002D557 /* NSFileManagerMock.m */, + ); + path = FileDownload; + sourceTree = ""; + }; + 6B9E95B62C2AE6740002D557 /* FileDownload */ = { + isa = PBXGroup; + children = ( + 489A73192BC51795008D4850 /* CTFileDownloadManager.h */, + 489A731B2BC517B4008D4850 /* CTFileDownloadManager.m */, + 487854022BF4B79F00565685 /* CTFileDownloader.h */, + 487854042BF4B7D800565685 /* CTFileDownloader.m */, + ); + path = FileDownload; + sourceTree = ""; + }; D02AC2D9276044F70031C1BE /* CleverTapSDKTests */ = { isa = PBXGroup; children = ( + 6B9E95AD2C285F2F0002D557 /* FileDownload */, 4E2CF1432AC56D8F00441E8B /* CTEncryptionTests.m */, 6A4427C32AA6513C0098866F /* InApps */, 6A7BB8DE29E60BE900651584 /* ProductExperiences */, @@ -1491,6 +1538,7 @@ D0C7BBBF207D82C0001345EF /* CleverTapSDK */ = { isa = PBXGroup; children = ( + 6B9E95B62C2AE6740002D557 /* FileDownload */, 3242D7DA2B1DDA2E00A5E37A /* PrivacyInfo.xcprivacy */, 4803951A2A7ABAD200C4D254 /* CTAES.h */, 480395192A7ABAD200C4D254 /* CTAES.m */, @@ -1752,6 +1800,7 @@ 07053B7221E653E70085B44A /* UIView+CTToast.h in Headers */, 6A3EBD362AA0705900CE97D4 /* CTLimitAdapter.h in Headers */, 4987C665251B5E79003E6BE8 /* CTImageInAppViewController.h in Headers */, + 487854032BF4B79F00565685 /* CTFileDownloader.h in Headers */, D014B8E220E2F9F9001E0780 /* CleverTapInstanceConfigPrivate.h in Headers */, 071EB4CF217F6427008F0FAB /* CTBaseHeaderFooterViewControllerPrivate.h in Headers */, D0BD759D241760C60006EE55 /* CTProductConfigController.h in Headers */, @@ -1770,7 +1819,6 @@ 4EF0D5452AD84BCA0044C48F /* CTSessionManager.h in Headers */, D0213D4C207D905800FE5740 /* CleverTapSyncDelegate.h in Headers */, 07B94546219EA34300D4C542 /* CTMessageMO+CoreDataProperties.h in Headers */, - 48BEA4F62AFB868B00690424 /* CTInAppImagePrefetchManager.h in Headers */, 6BA3B2DB2B03E926004E834B /* CTQueueType.h in Headers */, D01651B22097B42C00660178 /* CTValidator.h in Headers */, 6A4427D02AB9D8C30098866F /* CTInAppFCManager+Legacy.h in Headers */, @@ -1782,6 +1830,7 @@ 071EB4EE217F6427008F0FAB /* CTAlertViewController.h in Headers */, 4E838C4629A0C94B00ED0875 /* CleverTap+CTVar.h in Headers */, D0CACF9620B8A4F800A02327 /* CTPinnedNSURLSessionDelegate.h in Headers */, + 489A731A2BC51795008D4850 /* CTFileDownloadManager.h in Headers */, 4E41FD92294F46510001FBED /* CTVar-Internal.h in Headers */, 071EB4FE217F6427008F0FAB /* CTDismissButton.h in Headers */, 071EB511217F6427008F0FAB /* CTUIUtils.h in Headers */, @@ -2042,6 +2091,8 @@ 6B9DEE9F2B4D8A500097EF40 /* clevertap-logo.png in Resources */, 4E1F156827709849009387AE /* app_inbox.json in Resources */, 4E1F1562277090D6009387AE /* inapp_alert.json in Resources */, + 48A2C4B92BD67DDC006C61BC /* sampleTXTStub.txt in Resources */, + 48A2C4BA2BD67DDC006C61BC /* samplePDFStub.pdf in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2271,6 +2322,7 @@ 4EFC642B2AB44CF900F01414 /* CTLimitsMatcherTest.m in Sources */, 4EED219B29AF6368006CEA19 /* CTVarCacheTest.m in Sources */, 6BF5A5A42AD45B4D00CDED20 /* CTInAppFCManagerTest.m in Sources */, + 6B9E95B52C29C2F40002D557 /* NSFileManagerMock.m in Sources */, 6A7BB8DC29E47CFF00651584 /* CTVarTest.m in Sources */, 6A2E0B9129CCCC8600FCEA5F /* ContentMergerTest.m in Sources */, 6A2E0B9529D49D0200FCEA5F /* CTVariables+Tests.m in Sources */, @@ -2287,17 +2339,20 @@ 4E2BFB9C2AD69BCA00DEB247 /* XCTestCase+XCTestCase_Tests.m in Sources */, 6A59D20F2A3351A800531F9D /* LeanplumCTTest.m in Sources */, 32790957299CC099001FE140 /* CTUtilsTest.m in Sources */, + 48C0FD6F2BCD522100E01EA9 /* CTFileDownloadManagerTests.m in Sources */, 6A2E4C18291E8A4A00385536 /* CleverTapInstanceConfigTests.m in Sources */, 6BEEC2D12AF1A3A900BD4EC5 /* CTClockMock.m in Sources */, 32790959299F4B29001FE140 /* CTDeviceInfoTest.m in Sources */, - 48C31A822B1DC5CF00CA2A90 /* CTInAppImagePrefetchManagerTest.m in Sources */, 4ECD88312ADC8A05003885CE /* CTSessionManagerTests.m in Sources */, 6A4427C52AA6515A0098866F /* CTTriggersMatcherTest.m in Sources */, 4E2CF1442AC56D8F00441E8B /* CTEncryptionTests.m in Sources */, 32394C2729FA278C00956058 /* CTUriHelperTest.m in Sources */, + 487854072BF4BC4E00565685 /* CTFileDownloaderTests.m in Sources */, 6A59D20D2A334B8500531F9D /* NSDictionaryExtensionsTest.m in Sources */, 4E1F155227692A11009387AE /* CleverTap+Tests.m in Sources */, + 6B9E95AC2C27164B0002D557 /* CTFileDownloadTestHelper.m in Sources */, 6A4427CD2AB8C3B10098866F /* CTInAppEvaluationManagerTest.m in Sources */, + 6B9E95B12C2864EC0002D557 /* CTFileDownloaderMock.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2327,6 +2382,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 48F9FD092C208F7100617770 /* CTInAppStore.m in Sources */, 071EB509217F6427008F0FAB /* CTAlertViewController.m in Sources */, 07B9454B219EA34300D4C542 /* CleverTapInboxMessage.m in Sources */, 4E41FD9C294F46510001FBED /* CTVarCache.m in Sources */, @@ -2336,9 +2392,9 @@ 6A4427C22AA39AC30098866F /* CTLimitsMatcher.m in Sources */, 071EB4F5217F6427008F0FAB /* CTInAppHTMLViewController.m in Sources */, D01651AF2097B38400660178 /* CTEventBuilder.m in Sources */, - 6A4427D92ABCE5EB0098866F /* CTInAppStore.m in Sources */, D020C929209006AD0073F61E /* CTUriHelper.m in Sources */, 07B9454C219EA34300D4C542 /* CTMessageMO.m in Sources */, + 487854052BF4B7D800565685 /* CTFileDownloader.m in Sources */, D0D4C9F32414EE6C0029477E /* CleverTapFeatureFlags.m in Sources */, 4E5A02DE2A4C5FD800DE242A /* LeanplumCT.m in Sources */, 6AA1357B2A2E467800EFF2C1 /* NSDictionary+Extensions.m in Sources */, @@ -2364,6 +2420,7 @@ 071EB513217F6427008F0FAB /* CTInAppNotification.m in Sources */, D0CACF9720B8A4F800A02327 /* CTPinnedNSURLSessionDelegate.m in Sources */, D0047B0F2098E2F00019C6FD /* CTProfileBuilder.m in Sources */, + 489A731C2BC517B4008D4850 /* CTFileDownloadManager.m in Sources */, D0A6626E20801E7F00B403F3 /* CTDeviceInfo.m in Sources */, 6BD334EC2AF2A41F0099E33E /* CTBatchSentDelegateHelper.m in Sources */, 0797133A21A309720011C9A3 /* CTCarouselImageView.m in Sources */, @@ -2413,7 +2470,6 @@ 6A11D83B2A8FC71F007F5D21 /* CTImpressionManager.m in Sources */, 071EB506217F6427008F0FAB /* CTInAppFCManager.m in Sources */, 4E25E3C3278887A70008C888 /* CTFlexibleIdentityRepo.m in Sources */, - 48BEA4F82AFB86A300690424 /* CTInAppImagePrefetchManager.m in Sources */, 48080312292EB50D00C06E2F /* CTLocalInApp.m in Sources */, D01651B72097B81400660178 /* CTKnownProfileFields.m in Sources */, 07B94545219EA34300D4C542 /* CTUserMO+CoreDataProperties.m in Sources */, @@ -2567,6 +2623,7 @@ "$(inherited)", "$(PROJECT_DIR)/Vendors", ); + GCC_NO_COMMON_BLOCKS = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = CleverTapSDKTests/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = ""; @@ -2600,6 +2657,7 @@ "$(inherited)", "$(PROJECT_DIR)/Vendors", ); + GCC_NO_COMMON_BLOCKS = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = CleverTapSDKTests/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = ""; diff --git a/CleverTapSDK.xcworkspace/contents.xcworkspacedata b/CleverTapSDK.xcworkspace/contents.xcworkspacedata index 045267d5..7c48e232 100644 --- a/CleverTapSDK.xcworkspace/contents.xcworkspacedata +++ b/CleverTapSDK.xcworkspace/contents.xcworkspacedata @@ -7,7 +7,4 @@ - - diff --git a/CleverTapSDK/CTConstants.h b/CleverTapSDK/CTConstants.h index c21fd5b2..587229a4 100644 --- a/CleverTapSDK/CTConstants.h +++ b/CleverTapSDK/CTConstants.h @@ -76,6 +76,13 @@ extern NSString *const kSessionId; #define CLTAP_NOTIFICATION_TAG @"W$" #define CLTAP_DATE_FORMAT @"yyyyMMdd" +#pragma mark Constants for File Assets +#define CLTAP_FILE_URLS_EXPIRY_DICT @"file_urls_expiry_dict" +#define CLTAP_FILE_ASSETS_LAST_DELETED_TS @"cs_file_assets_last_deleted_timestamp" +#define CLTAP_FILE_EXPIRY_OFFSET (60 * 60 * 24 * 7 * 2) // 2 weeks +#define CLTAP_FILE_RESOURCE_TIME_OUT_INTERVAL 15 +#define CLTAP_FILES_DIRECTORY_NAME @"CleverTap_Files" + #pragma mark Constants for App fields #define CLTAP_APP_VERSION @"Version" #define CLTAP_LATITUDE @"Latitude" diff --git a/CleverTapSDK/CTInAppNotification.h b/CleverTapSDK/CTInAppNotification.h index e52275ee..b2bb507a 100644 --- a/CleverTapSDK/CTInAppNotification.h +++ b/CleverTapSDK/CTInAppNotification.h @@ -3,7 +3,7 @@ #import "CTInAppUtils.h" #import "CTNotificationButton.h" #if !CLEVERTAP_NO_INAPP_SUPPORT -#import "CTInAppImagePrefetchManager.h" +#import "CTFileDownloader.h" #endif @interface CTInAppNotification : NSObject @@ -66,7 +66,7 @@ - (instancetype)init __unavailable; #if !CLEVERTAP_NO_INAPP_SUPPORT - (instancetype)initWithJSON:(NSDictionary*)json - imagePrefetchManager:(CTInAppImagePrefetchManager *)imagePrefetchManager; + fileDownloader:(CTFileDownloader *)fileDownloader; #endif - (void)prepareWithCompletionHandler: (void (^)(void))completionHandler; diff --git a/CleverTapSDK/CTInAppNotification.m b/CleverTapSDK/CTInAppNotification.m index 46d31566..166f2fdd 100644 --- a/CleverTapSDK/CTInAppNotification.m +++ b/CleverTapSDK/CTInAppNotification.m @@ -66,7 +66,7 @@ @interface CTInAppNotification() { @property (nonatomic, readwrite) NSString *error; -@property (nonatomic, strong) CTInAppImagePrefetchManager *imagePrefetchManager; +@property (nonatomic, strong) CTFileDownloader *fileDownloader; @end @@ -78,10 +78,10 @@ @implementation CTInAppNotification: NSObject @synthesize mediaIsVideo=_mediaIsVideo; - (instancetype)initWithJSON:(NSDictionary *)jsonObject - imagePrefetchManager:(CTInAppImagePrefetchManager *)imagePrefetchManager { + fileDownloader:(CTFileDownloader *)fileDownloader { if (self = [super init]) { @try { - self.imagePrefetchManager = imagePrefetchManager; + self.fileDownloader = fileDownloader; self.inAppType = CTInAppTypeUnknown; self.jsonDescription = jsonObject; self.campaignId = (NSString*) jsonObject[CLTAP_NOTIFICATION_ID_TAG]; @@ -434,7 +434,7 @@ - (BOOL)isKeyValidInDictionary:(NSDictionary *)d forKey:(NSString *)key ofClass: - (UIImage *)loadImageIfPresentInDiskCache:(NSURL *)imageURL { NSString *imageURLString = [imageURL absoluteString]; - UIImage *image = [self.imagePrefetchManager loadImageFromDisk:imageURLString]; + UIImage *image = [self.fileDownloader loadImageFromDisk:imageURLString]; if (image) return image; return nil; } diff --git a/CleverTapSDK/CleverTap.m b/CleverTapSDK/CleverTap.m index cf86d25f..baea71c5 100644 --- a/CleverTapSDK/CleverTap.m +++ b/CleverTapSDK/CleverTap.m @@ -31,6 +31,7 @@ #import "CTDispatchQueueManager.h" #import "CTMultiDelegateManager.h" #import "CTSessionManager.h" +#import "CTFileDownloader.h" #if !CLEVERTAP_NO_INAPP_SUPPORT #import "CTInAppFCManager.h" @@ -56,7 +57,6 @@ #import "CleverTap+InAppsResponseHandler.h" #import "CTInAppEvaluationManager.h" #import "CTInAppTriggerManager.h" -#import "CTInAppImagePrefetchManager.h" #endif #if !CLEVERTAP_NO_INBOX_SUPPORT @@ -242,7 +242,7 @@ @interface CleverTap () { @property (nonatomic, strong, readwrite) CTInAppEvaluationManager *inAppEvaluationManager; @property (nonatomic, strong, readwrite) CTInAppDisplayManager *inAppDisplayManager; @property (nonatomic, strong, readwrite) CTImpressionManager *impressionManager; -@property (nonatomic, strong, readwrite) CTInAppImagePrefetchManager *imagePrefetchManager; +@property (nonatomic, strong, readwrite) CTFileDownloader *fileDownloader; @property (nonatomic, strong, readwrite) CTInAppStore * _Nullable inAppStore; #endif @@ -477,6 +477,8 @@ - (instancetype)initWithConfig:(CleverTapInstanceConfig*)config andCleverTapID:( [self initNetworking]; [self inflateQueuesAsync]; [self addObservers]; + + self.fileDownloader = [[CTFileDownloader alloc] initWithConfig:self.config]; #if !CLEVERTAP_NO_INAPP_SUPPORT if (!_config.analyticsOnly && ![CTUIUtils runningInsideAppExtension]) { [self initializeInAppSupport]; @@ -506,12 +508,9 @@ - (instancetype)initWithConfig:(CleverTapInstanceConfig*)config andCleverTapID:( #if !CLEVERTAP_NO_INAPP_SUPPORT - (void)initializeInAppSupport { - CTInAppImagePrefetchManager *imagePrefetchManager = [[CTInAppImagePrefetchManager alloc] initWithConfig:self.config]; - self.imagePrefetchManager = imagePrefetchManager; - CTInAppStore *inAppStore = [[CTInAppStore alloc] initWithConfig:self.config delegateManager:self.delegateManager - imagePrefetchManager:self.imagePrefetchManager + fileDownloader:self.fileDownloader deviceId:self.deviceInfo.deviceId]; self.inAppStore = inAppStore; @@ -525,7 +524,7 @@ - (void)initializeInAppSupport { inAppFCManager:inAppFCManager impressionManager:impressionManager inAppStore:inAppStore - imagePrefetchManager:self.imagePrefetchManager]; + fileDownloader:self.fileDownloader]; CTInAppEvaluationManager *evaluationManager = [[CTInAppEvaluationManager alloc] initWithAccountId:self.config.accountId deviceId:self.deviceInfo.deviceId delegateManager:self.delegateManager impressionManager:impressionManager inAppDisplayManager:displayManager inAppStore:inAppStore inAppTriggerManager:triggerManager]; @@ -1563,7 +1562,7 @@ - (void)resumeInAppNotifications { } - (void)clearInAppResources:(BOOL)expiredOnly { - [self.imagePrefetchManager _clearImageAssets:expiredOnly]; + [self.fileDownloader clearFileAssets:expiredOnly]; } #endif diff --git a/CleverTapSDK/FileDownload/CTFileDownloadManager.h b/CleverTapSDK/FileDownload/CTFileDownloadManager.h new file mode 100644 index 00000000..6df6e703 --- /dev/null +++ b/CleverTapSDK/FileDownload/CTFileDownloadManager.h @@ -0,0 +1,68 @@ +#import + +@class CleverTapInstanceConfig; + +NS_ASSUME_NONNULL_BEGIN + +typedef void(^CTFilesDownloadCompletedBlock)(NSDictionary *status); +typedef void(^CTFilesDeleteCompletedBlock)(NSDictionary *status); +typedef void(^CTFilesRemoveCompletedBlock)(NSDictionary *status); +typedef void (^DownloadCompletionHandler)(NSURL *url, BOOL success); + +@interface CTFileDownloadManager : NSObject + ++ (instancetype)sharedInstanceWithConfig:(CleverTapInstanceConfig *)config; +- (instancetype)init NS_UNAVAILABLE; + +/*! + @method + + @discussion + This method accepts file urls as NSArray of NSURLs and downloads each file to directory inside NSDocumentDirectory directory. + + @param completion the completion block to be executed when all files are downloaded. `status` dictionary will + contain file download status of each file as {url,success}. The completion block is executed on background queue. + */ +- (void)downloadFiles:(NSArray *)urls withCompletionBlock:(CTFilesDownloadCompletedBlock)completion; + +/*! + @method + + @discussion + This method checks if file is already present in the directory or not. + */ +- (BOOL)isFileAlreadyPresent:(NSURL *)url; + +/*! + @method + + @discussion + This method returns the file path to the file. This method does *not* check if the file exists. + */ +- (NSString *)filePath:(NSURL *)url; + +/*! + @method + + @discussion + This method deletes the files from the directory if present. + + @param completion the completion block to be executed when all files are deleted. `status` dictionary will + contain file delete status of each file as {url,success}. The completion block is executed on background queue. + */ +- (void)deleteFiles:(NSArray *)urls withCompletionBlock:(CTFilesDeleteCompletedBlock)completion; + +/*! + @method + + @discussion + This method deletes all files from the directory. + + @param completion the completion block to be executed when all files are deleted. `status` dictionary will + contain file download status of each file as {file path,success}. The completion block is executed on background queue. + */ +- (void)removeAllFilesWithCompletionBlock:(CTFilesRemoveCompletedBlock)completion; + +@end + +NS_ASSUME_NONNULL_END diff --git a/CleverTapSDK/FileDownload/CTFileDownloadManager.m b/CleverTapSDK/FileDownload/CTFileDownloadManager.m new file mode 100644 index 00000000..1ea4f4e5 --- /dev/null +++ b/CleverTapSDK/FileDownload/CTFileDownloadManager.m @@ -0,0 +1,258 @@ +#import "CTFileDownloadManager.h" +#import "CTConstants.h" +#import "CleverTapInstanceConfig.h" + +@interface CTFileDownloadManager() + +@property (nonatomic, strong) CleverTapInstanceConfig *config; +@property (nonatomic, strong) NSString *documentsDirectory; +@property (nonatomic, strong) NSMutableSet *downloadInProgressUrls; +@property (nonatomic, strong) NSMutableDictionary *> *downloadInProgressHandlers; +@property (nonatomic, strong) NSURLSession *session; +@property (nonatomic, strong) NSFileManager* fileManager; + +@end + +@implementation CTFileDownloadManager + ++ (instancetype)sharedInstanceWithConfig:(CleverTapInstanceConfig *)config { + static CTFileDownloadManager *sharedInstance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedInstance = [[self alloc] initWithConfig:config]; + }); + return sharedInstance; +} + +- (instancetype)initWithConfig:(CleverTapInstanceConfig *)config { + self = [super init]; + if (self) { + self.config = config; + NSString *documents = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject]; + self.documentsDirectory = [documents stringByAppendingPathComponent:CLTAP_FILES_DIRECTORY_NAME]; + + _downloadInProgressUrls = [NSMutableSet new]; + _downloadInProgressHandlers = [NSMutableDictionary new]; + + NSURLSessionConfiguration *sc = [NSURLSessionConfiguration defaultSessionConfiguration]; + sc.timeoutIntervalForRequest = CLTAP_REQUEST_TIME_OUT_INTERVAL; + sc.timeoutIntervalForResource = CLTAP_FILE_RESOURCE_TIME_OUT_INTERVAL; + + self.session = [NSURLSession sessionWithConfiguration:sc]; + self.fileManager = [NSFileManager defaultManager]; + } + + return self; +} + +#pragma mark - Public methods +- (void)downloadFiles:(nonnull NSArray *)urls + withCompletionBlock:(nonnull CTFilesDownloadCompletedBlock)completion { + dispatch_group_t group = dispatch_group_create(); + dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + NSMutableDictionary *filesDownloadStatus = [NSMutableDictionary new]; + for (NSURL *url in urls) { + dispatch_group_enter(group); + @synchronized (self) { + BOOL isAlreadyDownloading = [_downloadInProgressUrls containsObject:url]; + if (isAlreadyDownloading) { + // If the url download is already in Progress, add the completion handler to + // fileDownloadProgressHandlers dictionary which is called when file is downloaded. + if (!_downloadInProgressHandlers[url]) { + _downloadInProgressHandlers[url] = [NSMutableArray array]; + } + [_downloadInProgressHandlers[url] addObject:^(NSURL *completedURL, BOOL success) { + [filesDownloadStatus setObject:[NSNumber numberWithBool:success] forKey:[completedURL absoluteString]]; + dispatch_group_leave(group); + }]; + continue; + } + } + + // Download file only when it is not already present. + if (![self isFileAlreadyPresent:url]) { + @synchronized (self) { + [_downloadInProgressUrls addObject:url]; + } + dispatch_async(concurrentQueue, ^{ + [self downloadSingleFile:url completed:^(BOOL success) { + [filesDownloadStatus setObject:[NSNumber numberWithBool:success] forKey:[url absoluteString]]; + + // Call the other completion handlers for this file url if present + NSArray *handlers; + @synchronized (self) { + handlers = [self->_downloadInProgressHandlers[url] copy]; + [self->_downloadInProgressHandlers removeObjectForKey:url]; + [self->_downloadInProgressUrls removeObject:url]; + } + for (DownloadCompletionHandler handler in handlers) { + handler(url, success); + } + dispatch_group_leave(group); + }]; + }); + } else { + // Add the file url to callback as success true as it is already present + [filesDownloadStatus setObject:@1 forKey:[url absoluteString]]; + dispatch_group_leave(group); + } + } + dispatch_group_notify(group, concurrentQueue, ^{ + // Callback when all files are downloaded with their success status + completion(filesDownloadStatus); + }); +} + +- (BOOL)isFileAlreadyPresent:(NSURL *)url { + NSString* filePath = [self.documentsDirectory stringByAppendingPathComponent:[self hashedFileNameForURL:url]]; + BOOL fileExists = [self.fileManager fileExistsAtPath:filePath]; + return fileExists; +} + +- (NSString *)filePath:(NSURL *)url { + return [self.documentsDirectory stringByAppendingPathComponent:[self hashedFileNameForURL:url]]; +} + +- (void)deleteFiles:(NSArray *)urls withCompletionBlock:(CTFilesDeleteCompletedBlock)completion { + NSMutableDictionary *filesDeleteStatus = [NSMutableDictionary new]; + + if (urls.count == 0) { + completion(filesDeleteStatus); + return; + } + + dispatch_group_t deleteGroup = dispatch_group_create(); + dispatch_queue_t deleteConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + for (NSString *urlString in urls) { + NSURL *url = [NSURL URLWithString:urlString]; + // Delete file only when it is present. + if ([self isFileAlreadyPresent:url]) { + dispatch_group_enter(deleteGroup); + dispatch_async(deleteConcurrentQueue, ^{ + [self deleteSingleFile:url completed:^(BOOL success) { + [filesDeleteStatus setObject:[NSNumber numberWithBool:success] forKey:urlString]; + dispatch_group_leave(deleteGroup); + }]; + }); + } else { + // Add the file url to callback as success true as it is already not present + [filesDeleteStatus setObject:@1 forKey:[url absoluteString]]; + } + } + dispatch_group_notify(deleteGroup, deleteConcurrentQueue, ^{ + // Callback when all files are deleted with their success status + completion(filesDeleteStatus); + }); +} + +- (void)removeAllFilesWithCompletionBlock:(CTFilesRemoveCompletedBlock)completion { + dispatch_group_t deleteGroup = dispatch_group_create(); + dispatch_queue_t deleteConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + NSMutableDictionary *filesDeleteStatus = [NSMutableDictionary new]; + NSError *error; + NSURL *path = [NSURL fileURLWithPath:self.documentsDirectory]; + NSArray *files = [self.fileManager contentsOfDirectoryAtURL:path + includingPropertiesForKeys:nil + options:NSDirectoryEnumerationSkipsHiddenFiles + error:&error]; + if (error) { + CleverTapLogInternal(self.config.logLevel, @"%@ Failed to get contents of directory %@ - %@", self, path, error); + completion(filesDeleteStatus); + } + for (NSURL *file in files) { + dispatch_group_enter(deleteGroup); + dispatch_async(deleteConcurrentQueue, ^{ + NSError *error; + BOOL success = [self.fileManager removeItemAtURL:file error:&error]; + if (!success) { + CleverTapLogInternal(self.config.logLevel, @"%@ Failed to remove file %@ - %@", self, [file absoluteString], error); + } + [filesDeleteStatus setObject:@(success) forKey:[file path]]; + dispatch_group_leave(deleteGroup); + }); + } + dispatch_group_notify(deleteGroup, deleteConcurrentQueue, ^{ + // Callback when all files are deleted with their success status + completion(filesDeleteStatus); + }); +} + +#pragma mark - Private methods + +- (void)downloadSingleFile:(NSURL *)url + completed:(void(^)(BOOL success))completedBlock { + NSURLSessionDownloadTask *downloadTask = [self.session downloadTaskWithURL:url + completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) { + if (error) { + CleverTapLogInternal(self.config.logLevel, @"%@ Error downloading file: %@ - %@", self, url, error); + completedBlock(NO); + return; + } + + NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; + if (httpResponse.statusCode != 200) { + CleverTapLogInternal(self.config.logLevel, @"HTTP Error: %ld for file: %@", (long)httpResponse.statusCode, url); + completedBlock(NO); + return; + } + + NSFileManager *fileManager = self.fileManager; + NSError *createDirectoryError; + if (![fileManager fileExistsAtPath:self.documentsDirectory]) { + [fileManager createDirectoryAtURL:[NSURL fileURLWithPath:self.documentsDirectory] + withIntermediateDirectories:YES + attributes:nil + error:&createDirectoryError]; + if (createDirectoryError) { + CleverTapLogInternal(self.config.logLevel, @"Error creating documents folder: %@", createDirectoryError); + completedBlock(NO); + return; + } + } + + NSString *destinationPath = [self.documentsDirectory stringByAppendingPathComponent:[self hashedFileNameForURL:url]]; + NSURL *destinationURL = [NSURL fileURLWithPath:destinationPath]; + NSError *fileError; + // Check if the file exists at the destination + if ([fileManager fileExistsAtPath:[destinationURL path]]) { + // Remove the existing file + if (![fileManager removeItemAtURL:destinationURL error:&fileError]) { + CleverTapLogInternal(self.config.logLevel, @"Error removing file at destination: %@", fileError); + completedBlock(NO); + return; + } + } + // Move the file from the temporary location to the documents directory + [fileManager moveItemAtURL:location toURL:destinationURL error:&fileError]; + if (fileError) { + CleverTapLogInternal(self.config.logLevel, @"File Error: %@ for file: %@", fileError.localizedDescription, url); + completedBlock(NO); + return; + } + completedBlock(YES); + }]; + [downloadTask resume]; +} + +- (NSString *)hashedFileNameForURL:(NSURL *)url { + NSUInteger hashValue = [url.absoluteString hash]; + return [NSString stringWithFormat:@"%lu_%@", (unsigned long)hashValue, [url lastPathComponent]]; +} + +- (void)deleteSingleFile:(NSURL *)url + completed:(void(^)(BOOL success))completedBlock { + if (![url lastPathComponent] || [[url lastPathComponent] isEqualToString:@""]) { + completedBlock(NO); + return; + } + + NSString *filePath = [self.documentsDirectory stringByAppendingPathComponent:[self hashedFileNameForURL:url]]; + NSError *error; + BOOL success = [self.fileManager removeItemAtPath:filePath error:&error]; + if (!success) { + CleverTapLogInternal(self.config.logLevel, @"%@ Failed to remove file %@ - %@", self, url, error); + } + completedBlock(success); +} + +@end diff --git a/CleverTapSDK/FileDownload/CTFileDownloader.h b/CleverTapSDK/FileDownload/CTFileDownloader.h new file mode 100644 index 00000000..4a1d1466 --- /dev/null +++ b/CleverTapSDK/FileDownload/CTFileDownloader.h @@ -0,0 +1,20 @@ +#import +#import + +@class CleverTapInstanceConfig; + +NS_ASSUME_NONNULL_BEGIN + +@interface CTFileDownloader : NSObject + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithConfig:(CleverTapInstanceConfig *)config; +- (void)downloadFiles:(NSArray *)fileURLs withCompletionBlock:(void (^ _Nullable)(NSDictionary *status))completion; +- (BOOL)isFileAlreadyPresent:(NSString *)url; +- (void)clearFileAssets:(BOOL)expiredOnly; +- (nullable NSString *)fileDownloadPath:(NSString *)url; +- (nullable UIImage *)loadImageFromDisk:(NSString *)imageURL; + +@end + +NS_ASSUME_NONNULL_END diff --git a/CleverTapSDK/FileDownload/CTFileDownloader.m b/CleverTapSDK/FileDownload/CTFileDownloader.m new file mode 100644 index 00000000..f6259b4c --- /dev/null +++ b/CleverTapSDK/FileDownload/CTFileDownloader.m @@ -0,0 +1,233 @@ +#import "CTFileDownloader.h" +#import "CTConstants.h" +#import "CTPreferences.h" +#import "CTFileDownloadManager.h" + +@interface CTFileDownloader() + +@property (nonatomic, strong) CleverTapInstanceConfig *config; +@property (nonatomic, strong) CTFileDownloadManager *fileDownloadManager; +@property (nonatomic, strong) NSMutableDictionary *urlsExpiry; +@property (nonatomic) NSTimeInterval fileExpiryTime; + +@end + +@implementation CTFileDownloader + +- (nonnull instancetype)initWithConfig:(nonnull CleverTapInstanceConfig *)config { + self = [super init]; + if (self) { + self.config = config; + [self setup]; + } + return self; +} + +#pragma mark - Public + +- (void)downloadFiles:(NSArray *)fileURLs withCompletionBlock:(void (^ _Nullable)(NSDictionary *status))completion { + if (fileURLs.count == 0) { + if (completion) { + completion(@{}); + } + return; + } + + NSArray *urls = [self fileURLs:fileURLs]; + [self.fileDownloadManager downloadFiles:urls withCompletionBlock:^(NSDictionary *status) { + [self updateFilesExpiry:status]; + [self updateFilesExpiryInPreference]; + + long lastDeletedTime = [self lastDeletedTimestamp]; + [self removeInactiveExpiredAssets:lastDeletedTime]; + + if (completion) { + completion(status); + } + }]; +} + +- (BOOL)isFileAlreadyPresent:(NSString *)url { + NSURL *fileUrl = [NSURL URLWithString:url]; + BOOL fileExists = [self.fileDownloadManager isFileAlreadyPresent:fileUrl]; + return fileExists; +} + +- (void)clearFileAssets:(BOOL)expiredOnly { + if (expiredOnly) { + // Disregard the last deleted timestamp to force delete the expired files + // currentTime - lastDeletedTime > self.fileExpiryTime + long forceLastDeleted = ([self currentTimeInterval] - self.fileExpiryTime) - 1; + [self removeInactiveExpiredAssets:forceLastDeleted]; + } else { + [self removeAllAssetsWithCompletion:nil]; + } +} + +- (nullable NSString *)fileDownloadPath:(NSString *)url { + NSString *filePath = nil; + if ([self isFileAlreadyPresent:url]) { + NSURL *fileURL = [NSURL URLWithString:url]; + return [self.fileDownloadManager filePath:fileURL]; + } else { + CleverTapLogInternal(self.config.logLevel, @"%@ File %@ is not present.", self, url); + } + return filePath; +} + +- (nullable UIImage *)loadImageFromDisk:(NSString *)imageURL { + NSURL *URL = [NSURL URLWithString:imageURL]; + NSString *imagePath = [self.fileDownloadManager filePath:URL]; + UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfFile:imagePath]]; + if (image) { + return image; + } + + CleverTapLogInternal(self.config.logLevel, @"%@ Failed to load image from path %@", self, imagePath); + return nil; +} + +#pragma mark - Private + +- (void)setup { + self.fileDownloadManager = [CTFileDownloadManager sharedInstanceWithConfig:self.config]; + self.fileExpiryTime = CLTAP_FILE_EXPIRY_OFFSET; + + [self migrateActiveAndInactiveUrls]; + + @synchronized (self) { + NSDictionary *cachedUrlsExpiry = [CTPreferences getObjectForKey:[self storageKeyWithSuffix:CLTAP_FILE_URLS_EXPIRY_DICT]]; + if (cachedUrlsExpiry) { + self.urlsExpiry = [cachedUrlsExpiry mutableCopy]; + } else { + self.urlsExpiry = [NSMutableDictionary new]; + } + } +} + +- (void)removeInactiveExpiredAssets:(long)lastDeletedTime { + if (lastDeletedTime > 0) { + long currentTime = [self currentTimeInterval]; + if (currentTime - lastDeletedTime > self.fileExpiryTime) { + NSMutableArray *inactiveUrls = [NSMutableArray new]; + for (NSString *key in self.urlsExpiry) { + long expiry = [self.urlsExpiry[key] longValue]; + if (currentTime > expiry) { + [inactiveUrls addObject:key]; + } + } + + [self deleteFiles:inactiveUrls withCompletionBlock:nil]; + } + } +} + +- (void)deleteFiles:(NSArray *)urls withCompletionBlock:(CTFilesDeleteCompletedBlock)completion { + [self.fileDownloadManager deleteFiles:urls withCompletionBlock:^(NSDictionary *status) { + [self removeDeletedFilesFromExpiry:status]; + [self updateFilesExpiryInPreference]; + [self updateLastDeletedTimestamp]; + if (completion) { + completion(status); + } + }]; +} + +- (void)removeAllAssetsWithCompletion:(void(^)(NSDictionary *status))completion { + [self.fileDownloadManager removeAllFilesWithCompletionBlock:^(NSDictionary * _Nonnull status) { + [self.urlsExpiry removeAllObjects]; + [self updateFilesExpiryInPreference]; + [self updateLastDeletedTimestamp]; + if (completion) { + completion(status); + } + CleverTapLogInternal(self.config.logLevel, @"%@ Remove all files completed with status: %@", self, status); + }]; +} + +- (void)removeDeletedFilesFromExpiry:(NSDictionary *)status { + @synchronized (self) { + for (NSString *key in status) { + if ([status[key] boolValue]) { + [self.urlsExpiry removeObjectForKey:key]; + } + } + } +} + +- (NSArray *)fileURLs:(NSArray *)fileURLs { + NSMutableSet *urls = [NSMutableSet new]; + for (NSString *urlString in fileURLs) { + NSURL *url = [NSURL URLWithString:urlString]; + [urls addObject:url]; + } + return [urls allObjects]; +} + +- (void)updateFilesExpiry:(NSDictionary *)status { + @synchronized (self) { + NSNumber *expiry = @([self currentTimeInterval] + self.fileExpiryTime); + for (NSString *key in status) { + // Update the expiry for urls with success status + if ([status[key] boolValue]) { + self.urlsExpiry[key] = expiry; + } + } + } +} + +- (void)updateFilesExpiryInPreference { + @synchronized (self) { + [CTPreferences putObject:self.urlsExpiry forKey:[self storageKeyWithSuffix:CLTAP_FILE_URLS_EXPIRY_DICT]]; + } +} + +- (long)lastDeletedTimestamp { + long lastDeletedTime = [CTPreferences getIntForKey:[self storageKeyWithSuffix:CLTAP_FILE_ASSETS_LAST_DELETED_TS] + withResetValue:[self currentTimeInterval]]; + return lastDeletedTime; +} + +- (void)updateLastDeletedTimestamp { + [CTPreferences putInt:[self currentTimeInterval] + forKey:[self storageKeyWithSuffix:CLTAP_FILE_ASSETS_LAST_DELETED_TS]]; +} + +- (NSString *)storageKeyWithSuffix:(NSString *)suffix { + return [NSString stringWithFormat:@"%@:%@", self.config.accountId, suffix]; +} + +- (long)currentTimeInterval { + return [[NSDate date] timeIntervalSince1970]; +} + +- (void)migrateActiveAndInactiveUrls { + NSArray *activeAssetsArray = [CTPreferences getObjectForKey:[self storageKeyWithSuffix:CLTAP_PREFS_CS_INAPP_ACTIVE_ASSETS]]; + NSArray *inactiveAssetsArray = [CTPreferences getObjectForKey:[self storageKeyWithSuffix:CLTAP_PREFS_CS_INAPP_INACTIVE_ASSETS]]; + NSMutableSet *urls = [NSMutableSet new]; + if (activeAssetsArray && activeAssetsArray.count > 0) { + [urls addObjectsFromArray:activeAssetsArray]; + } + if (inactiveAssetsArray && inactiveAssetsArray.count > 0) { + [urls addObjectsFromArray:inactiveAssetsArray]; + } + NSMutableDictionary *urlsExpiry = [NSMutableDictionary new]; + NSNumber *expiry = @([self currentTimeInterval] + self.fileExpiryTime); + for (NSString *url in urls) { + urlsExpiry[url] = expiry; + } + + if (urlsExpiry.count > 0) { + [CTPreferences putObject:urlsExpiry forKey:[self storageKeyWithSuffix:CLTAP_FILE_URLS_EXPIRY_DICT]]; + [CTPreferences removeObjectForKey:[self storageKeyWithSuffix:CLTAP_PREFS_CS_INAPP_ACTIVE_ASSETS]]; + [CTPreferences removeObjectForKey:[self storageKeyWithSuffix:CLTAP_PREFS_CS_INAPP_INACTIVE_ASSETS]]; + } + id inAppAssetsDeletedTs = [CTPreferences getObjectForKey:[self storageKeyWithSuffix:CLTAP_PREFS_CS_INAPP_ASSETS_LAST_DELETED_TS]]; + if ([inAppAssetsDeletedTs isKindOfClass:[NSNumber class]]) { + long ts = [inAppAssetsDeletedTs longLongValue]; + [CTPreferences putInt:ts forKey:[self storageKeyWithSuffix:CLTAP_FILE_ASSETS_LAST_DELETED_TS]]; + [CTPreferences removeObjectForKey:[self storageKeyWithSuffix:CLTAP_PREFS_CS_INAPP_ASSETS_LAST_DELETED_TS]]; + } +} + +@end diff --git a/CleverTapSDK/InApps/CTInAppDisplayManager.h b/CleverTapSDK/InApps/CTInAppDisplayManager.h index e64e9c9d..8c3ad4a7 100644 --- a/CleverTapSDK/InApps/CTInAppDisplayManager.h +++ b/CleverTapSDK/InApps/CTInAppDisplayManager.h @@ -13,7 +13,6 @@ #import "CleverTap.h" #import "CTPushPrimerManager.h" #import "CTInAppStore.h" -#import "CTInAppImagePrefetchManager.h" NS_ASSUME_NONNULL_BEGIN @@ -36,7 +35,7 @@ typedef NS_ENUM(NSInteger, CleverTapInAppRenderingStatus) { inAppFCManager:(CTInAppFCManager *)inAppFCManager impressionManager:(CTImpressionManager *)impressionManager inAppStore:(CTInAppStore *)inAppStore - imagePrefetchManager:(CTInAppImagePrefetchManager *)imagePrefetchManager; + fileDownloader:(CTFileDownloader *)fileDownloader; - (void)setPushPrimerManager:(CTPushPrimerManager* _Nonnull)pushPrimerManagerObj; - (void)prepareNotificationForDisplay:(NSDictionary* _Nonnull)jsonObj; diff --git a/CleverTapSDK/InApps/CTInAppDisplayManager.m b/CleverTapSDK/InApps/CTInAppDisplayManager.m index a09db86d..72d3223b 100644 --- a/CleverTapSDK/InApps/CTInAppDisplayManager.m +++ b/CleverTapSDK/InApps/CTInAppDisplayManager.m @@ -35,7 +35,6 @@ #import "CTLocalInApp.h" #import "CleverTap+PushPermission.h" #import "CleverTapJSInterfacePrivate.h" -#import "CTInAppImagePrefetchManager.h" #endif static const void *const kNotificationQueueKey = &kNotificationQueueKey; @@ -57,7 +56,7 @@ @interface CTInAppDisplayManager() { @property (nonatomic, weak) CleverTap* instance; -@property (nonatomic, strong) CTInAppImagePrefetchManager *imagePrefetchManager; +@property (nonatomic, strong) CTFileDownloader *fileDownloader; @property (nonatomic, strong, readonly) NSString *imageInterstitialHtml; @@ -79,14 +78,14 @@ - (instancetype _Nonnull)initWithCleverTap:(CleverTap * _Nonnull)instance inAppFCManager:(CTInAppFCManager *)inAppFCManager impressionManager:(CTImpressionManager *)impressionManager inAppStore:(CTInAppStore *)inAppStore - imagePrefetchManager:(CTInAppImagePrefetchManager *)imagePrefetchManager { + fileDownloader:(CTFileDownloader *)fileDownloader { if ((self = [super init])) { self.dispatchQueueManager = dispatchQueueManager; self.instance = instance; self.config = instance.config; self.inAppFCManager = inAppFCManager; self.inAppStore = inAppStore; - self.imagePrefetchManager = imagePrefetchManager; + self.fileDownloader = fileDownloader; } return self; } @@ -208,7 +207,7 @@ - (void)prepareNotificationForDisplay:(NSDictionary*)jsonObj { [self.dispatchQueueManager runOnNotificationQueue:^{ CleverTapLogInternal(self.config.logLevel, @"%@: processing inapp notification: %@", self, jsonObj); - __block CTInAppNotification *notification = [[CTInAppNotification alloc] initWithJSON:jsonObj imagePrefetchManager:self.imagePrefetchManager]; + __block CTInAppNotification *notification = [[CTInAppNotification alloc] initWithJSON:jsonObj fileDownloader:self.fileDownloader]; if (notification.error) { CleverTapLogInternal(self.config.logLevel, @"%@: unable to parse inapp notification: %@ error: %@", self, jsonObj, notification.error); return; diff --git a/CleverTapSDK/InApps/CTInAppImagePrefetchManager.h b/CleverTapSDK/InApps/CTInAppImagePrefetchManager.h deleted file mode 100644 index 00a99be4..00000000 --- a/CleverTapSDK/InApps/CTInAppImagePrefetchManager.h +++ /dev/null @@ -1,19 +0,0 @@ -#import -#import "CleverTapInstanceConfig.h" - -@class CTMultiDelegateManager; - -NS_ASSUME_NONNULL_BEGIN - -@interface CTInAppImagePrefetchManager : NSObject - -- (instancetype)init NS_UNAVAILABLE; -- (instancetype)initWithConfig:(CleverTapInstanceConfig *)config; -- (void)preloadClientSideInAppImages:(NSArray *)csInAppNotifs; -- (nullable UIImage *)loadImageFromDisk:(NSString *)imageURL; -- (void)setImageAssetsInactiveAndClearExpired; -- (void)_clearImageAssets:(BOOL)expiredOnly; - -@end - -NS_ASSUME_NONNULL_END diff --git a/CleverTapSDK/InApps/CTInAppImagePrefetchManager.m b/CleverTapSDK/InApps/CTInAppImagePrefetchManager.m deleted file mode 100644 index 8837866c..00000000 --- a/CleverTapSDK/InApps/CTInAppImagePrefetchManager.m +++ /dev/null @@ -1,254 +0,0 @@ -#import "CTConstants.h" -#import "CTPreferences.h" -#import "CTInAppImagePrefetchManager.h" -#import "CTMultiDelegateManager.h" -#import -#import - -static const NSInteger kDefaultInAppExpiryTime = 60 * 60 * 24 * 7 * 2; // 2 weeks - -@interface CTInAppImagePrefetchManager() - -@property (nonatomic, strong) CleverTapInstanceConfig *config; -@property (nonatomic, assign) SDWebImageOptions sdWebImageOptions; -@property (nonatomic, strong) SDWebImageContext *sdWebImageContext; -@property (nonatomic, strong) SDWebImageManager *sdWebImageManager; -@property (nonatomic, strong) SDImageCache *sdImageCache; -@property (nonatomic, strong) NSMutableSet *activeImageSet; -@property (nonatomic, strong) NSMutableSet *inactiveImageSet; -@property (nonatomic) NSTimeInterval inAppExpiryTime; - -@end - -@implementation CTInAppImagePrefetchManager - -- (instancetype)initWithConfig:(CleverTapInstanceConfig *)config { - self = [super init]; - if (self) { - self.config = config; - - [self setup]; - } - - return self; -} - -#pragma mark - Public - -- (void)preloadClientSideInAppImages:(NSArray *)csInAppNotifs { - if (csInAppNotifs.count == 0) return; - - NSArray *mediaURLs = [self getImageURLs:csInAppNotifs]; - [self prefetchURLs:mediaURLs]; -} - -- (nullable UIImage *)loadImageFromDisk:(NSString *)imageURL { - UIImage *image = [self.sdImageCache imageFromDiskCacheForKey:imageURL]; - if (image) return image; - - CleverTapLogInternal(self.config.logLevel, @"%@: Image not found in Disk Cache for URL: %@", self, imageURL); - return nil; -} - -- (void)setImageAssetsInactiveAndClearExpired { - // Move all active image url to inactive and store it in preference. - // Images will be deleted from Disk cache when expiration check is done. - // Check for expired images, if any delete them from disk cache. - [self moveAssetsToInactive]; - - if ([self.inactiveImageSet allObjects].count > 0) { - long lastDeletedTime = [self getLastDeletedTimestamp]; - [self removeInactiveExpiredAssets:lastDeletedTime]; - } -} - -- (void)_clearImageAssets:(BOOL)expiredOnly { - // When expiredOnly is true, delete inapp images from disk cache which are present in inactive set. - // When expiredOnly is false, delete all inapp images from disk cache for current user. - long lastDeletedTime = [self getLastDeletedTimestamp]; - if (!expiredOnly) { - [self moveAssetsToInactive]; - lastDeletedTime = ([self getLastDeletedTimestamp] - (kDefaultInAppExpiryTime + 1)); - } - - if ([self.inactiveImageSet allObjects].count > 0) { - [self removeInactiveExpiredAssets:lastDeletedTime]; - } -} - -#pragma mark - Private - -- (void)setup { - self.inAppExpiryTime = kDefaultInAppExpiryTime; - self.activeImageSet = [NSMutableSet new]; - self.inactiveImageSet = [NSMutableSet new]; - - [self addActiveImageAssets]; - [self addInactiveImageAssets]; - - self.sdWebImageOptions = (SDWebImageRetryFailed); - self.sdWebImageContext = @{SDWebImageContextStoreCacheType : @(SDImageCacheTypeDisk)}; - self.sdWebImageManager = [SDWebImageManager sharedManager]; - self.sdImageCache = [SDImageCache sharedImageCache]; - // Setting this to a negative value means no expiring. We will handle expired images at our side. - [[self.sdImageCache config] setMaxDiskAge:-1]; -} - -- (void)prefetchURLs:(NSArray *)mediaURLs { - if (mediaURLs.count == 0) return; - - // Download the images which are not present in Disk cache. - // Steps: - // 1. Check if new image url is present in inactiveImageSet - // 2. If present move image url to activeImageSet, else download and add it to activeImageSet - // 3. Check for expired images in inactiveImageSet when all images are downloaded - dispatch_group_t group = dispatch_group_create(); - dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); - for (NSString *url in mediaURLs) { - // Check if image is present in disk cache or not. - // If present, add the url in `activeImageSet` and remove from `inactiveImageSet` if present. - UIImage *image = [self loadImageFromDisk:url]; - if (image) { - [self.activeImageSet addObject:url]; - if ([self.inactiveImageSet containsObject:url]) { - [self.inactiveImageSet removeObject:url]; - } - } else { - dispatch_group_enter(group); - dispatch_async(concurrentQueue, ^{ - [self.sdWebImageManager loadImageWithURL:[NSURL URLWithString:url] - options:self.sdWebImageOptions - context:self.sdWebImageContext - progress:nil - completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL * _Nullable imageURL) { - if (image) { - [self.activeImageSet addObject:imageURL.absoluteString]; - } - dispatch_group_leave(group); - }]; - }); - } - } - dispatch_group_notify(group, concurrentQueue, ^{ - // This block will be executed when all images are prefetched. - [self updateImageAssetsInPreference]; - long lastDeletedTime = [self getLastDeletedTimestamp]; - [self removeInactiveExpiredAssets:lastDeletedTime]; - }); -} - -- (void)removeInactiveExpiredAssets:(long)lastDeletedTime { - // Remove the images which are in inactive asset set and over default expiration period. - // Steps: - // 1. Initially set the lastDeletedTime as current time(first time) - // 2. Check if lastDeletedTime + default expiry time has passed - // 3. Delete all images from inactive asset - // 4. Update lastDeletedTime as currentTime and image assets in preference - dispatch_group_t deleteGroup = dispatch_group_create(); - dispatch_queue_t deleteConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); - if (lastDeletedTime > 0) { - long currentTime = (long) [[NSDate date] timeIntervalSince1970]; - if (currentTime - lastDeletedTime > self.inAppExpiryTime) { - // Delete all inactive expired images. - NSArray *inactiveAsset = [self.inactiveImageSet allObjects]; - for (NSString *url in inactiveAsset) { - dispatch_group_enter(deleteGroup); - dispatch_async(deleteConcurrentQueue, ^{ - [self.sdImageCache removeImageForKey:url - fromDisk:YES - withCompletion:^{ - [self.inactiveImageSet removeObject:url]; - dispatch_group_leave(deleteGroup); - }]; - }); - } - CleverTapLogInternal(self.config.logLevel, @"%@: Expired Images are removed from disk cache", self); - dispatch_group_notify(deleteGroup, deleteConcurrentQueue, ^{ - // This block will be executed when all images are removed. - if ([self.inactiveImageSet allObjects].count == 0) { - // Move all images to inactive for the new expiration window - [self moveAssetsToInactive]; - // Update last deleted time only when all inactive images are deleted - [self updateLastDeletedTimestamp]; - } - }); - } - } -} - -- (NSArray *)getImageURLs:(NSArray *)csInAppNotifs { - NSMutableSet *mediaURLs = [NSMutableSet new]; - for (NSDictionary *jsonInApp in csInAppNotifs) { - NSDictionary *media = (NSDictionary*) jsonInApp[@"media"]; - if (media) { - NSString *imageURL = [self getURLFromDictionary:media]; - if (imageURL) { - [mediaURLs addObject:imageURL]; - } - } - NSDictionary *mediaLandscape = (NSDictionary*) jsonInApp[@"mediaLandscape"]; - if (mediaLandscape) { - NSString *imageURL = [self getURLFromDictionary:mediaLandscape]; - if (imageURL) { - [mediaURLs addObject:imageURL]; - } - } - } - return [mediaURLs allObjects]; -} - -- (NSString *)getURLFromDictionary:(NSDictionary *)media { - NSString *contentType = media[@"content_type"]; - NSString *mediaUrl = media[@"url"]; - if (mediaUrl && mediaUrl.length > 0) { - // Preload contentType with image/jpeg or image/gif - if ([contentType hasPrefix:@"image"]) { - return mediaUrl; - } - } - return nil; -} - -- (NSString *)storageKeyWithSuffix:(NSString *)suffix { - return [NSString stringWithFormat:@"%@:%@", self.config.accountId, suffix]; -} - -- (long)getLastDeletedTimestamp { - long now = (long) [[NSDate date] timeIntervalSince1970]; - long lastDeletedTime = [CTPreferences getIntForKey:[self storageKeyWithSuffix:CLTAP_PREFS_CS_INAPP_ASSETS_LAST_DELETED_TS] - withResetValue:now]; - return lastDeletedTime; -} - -- (void)updateLastDeletedTimestamp { - long now = (long) [[NSDate date] timeIntervalSince1970]; - [CTPreferences putInt:now - forKey:[self storageKeyWithSuffix:CLTAP_PREFS_CS_INAPP_ASSETS_LAST_DELETED_TS]]; -} - -- (void)moveAssetsToInactive { - [self.inactiveImageSet unionSet:self.activeImageSet]; - self.activeImageSet = [NSMutableSet new]; - [self updateImageAssetsInPreference]; -} - -- (void)addInactiveImageAssets { - // Add only inactive array from preferences in `inactiveImageSet` - NSArray *inactiveAssetsArray = [CTPreferences getObjectForKey:[self storageKeyWithSuffix:CLTAP_PREFS_CS_INAPP_INACTIVE_ASSETS]]; - [self.inactiveImageSet addObjectsFromArray:inactiveAssetsArray]; -} - -- (void)addActiveImageAssets { - // Add only active array from preferences in `activeImageSet` - NSArray *activeAssetsArray = [CTPreferences getObjectForKey:[self storageKeyWithSuffix:CLTAP_PREFS_CS_INAPP_ACTIVE_ASSETS]]; - [self.activeImageSet addObjectsFromArray:activeAssetsArray]; -} - -- (void)updateImageAssetsInPreference { - [CTPreferences putObject:[self.activeImageSet allObjects] - forKey:[self storageKeyWithSuffix:CLTAP_PREFS_CS_INAPP_ACTIVE_ASSETS]]; - [CTPreferences putObject:[self.inactiveImageSet allObjects] - forKey:[self storageKeyWithSuffix:CLTAP_PREFS_CS_INAPP_INACTIVE_ASSETS]]; -} - -@end diff --git a/CleverTapSDK/InApps/CTInAppStore.h b/CleverTapSDK/InApps/CTInAppStore.h index d5517554..253e8bcf 100644 --- a/CleverTapSDK/InApps/CTInAppStore.h +++ b/CleverTapSDK/InApps/CTInAppStore.h @@ -8,10 +8,10 @@ #import #import "CTSwitchUserDelegate.h" -#import "CTInAppImagePrefetchManager.h" @class CleverTapInstanceConfig; @class CTMultiDelegateManager; +@class CTFileDownloader; @interface CTInAppStore : NSObject @@ -20,7 +20,7 @@ - (instancetype _Nonnull)init NS_UNAVAILABLE; - (instancetype _Nonnull)initWithConfig:(CleverTapInstanceConfig * _Nonnull)config delegateManager:(CTMultiDelegateManager * _Nonnull)delegateManager - imagePrefetchManager:(CTInAppImagePrefetchManager * _Nonnull)imagePrefetchManager + fileDownloader:(CTFileDownloader * _Nonnull)fileDownloader deviceId:(NSString * _Nonnull)deviceId; - (NSArray * _Nonnull)clientSideInApps; diff --git a/CleverTapSDK/InApps/CTInAppStore.m b/CleverTapSDK/InApps/CTInAppStore.m index 30da70ff..741a79d9 100644 --- a/CleverTapSDK/InApps/CTInAppStore.m +++ b/CleverTapSDK/InApps/CTInAppStore.m @@ -12,8 +12,8 @@ #import "CTAES.h" #import "CleverTapInstanceConfig.h" #import "CleverTapInstanceConfigPrivate.h" -#import "CTInAppImagePrefetchManager.h" #import "CTMultiDelegateManager.h" +#import "CTFileDownloader.h" NSString* const kCLIENT_SIDE_MODE = @"CS"; NSString* const kSERVER_SIDE_MODE = @"SS"; @@ -23,7 +23,7 @@ @interface CTInAppStore() @property (nonatomic, strong) CleverTapInstanceConfig *config; @property (nonatomic, strong) NSString *accountId; @property (nonatomic, strong) NSString *deviceId; -@property (nonatomic, strong) CTInAppImagePrefetchManager *imagePrefetchManager; +@property (nonatomic, strong) CTFileDownloader *fileDownloader; @property (nonatomic, strong) CTAES *ctAES; @property (nonatomic, strong) NSArray *inAppsQueue; @@ -38,7 +38,7 @@ @implementation CTInAppStore - (instancetype)initWithConfig:(CleverTapInstanceConfig *)config delegateManager:(CTMultiDelegateManager *)delegateManager - imagePrefetchManager:(CTInAppImagePrefetchManager *)imagePrefetchManager + fileDownloader:(CTFileDownloader *)fileDownloader deviceId:(NSString *)deviceId { self = [super init]; if (self) { @@ -46,7 +46,7 @@ - (instancetype)initWithConfig:(CleverTapInstanceConfig *)config self.accountId = config.accountId; self.deviceId = deviceId; self.ctAES = [[CTAES alloc] initWithAccountID:config.accountId]; - self.imagePrefetchManager = imagePrefetchManager; + self.fileDownloader = fileDownloader; [delegateManager addSwitchUserDelegate:self]; [self migrateInAppQueueKeys]; @@ -180,9 +180,6 @@ - (void)setMode:(nullable NSString *)mode { #pragma mark Client-Side In-Apps - (void)removeClientSideInApps { @synchronized (self) { - // Clear the CS images stored in disk cache - [self.imagePrefetchManager setImageAssetsInactiveAndClearExpired]; - _clientSideInApps = [NSArray new]; NSString *storageKey = [self storageKeyWithSuffix:CLTAP_PREFS_INAPP_KEY_CS]; [CTPreferences removeObjectForKey:storageKey]; @@ -196,7 +193,8 @@ - (void)storeClientSideInApps:(NSArray *)clientSideInApps { _clientSideInApps = clientSideInApps; // Preload CS inApp images to disk cache - [self.imagePrefetchManager preloadClientSideInAppImages:_clientSideInApps]; + NSArray *imageURLs = [self imageURLs:_clientSideInApps]; + [self.fileDownloader downloadFiles:imageURLs withCompletionBlock:nil]; NSString *encryptedString = [self.ctAES getEncryptedBase64String:clientSideInApps]; NSString *storageKey = [self storageKeyWithSuffix:CLTAP_PREFS_INAPP_KEY_CS]; @@ -260,6 +258,39 @@ - (NSString *)storageKeyWithSuffix:(NSString *)suffix { return [NSString stringWithFormat:@"%@:%@:%@", self.accountId, self.deviceId, suffix]; } +- (NSArray *)imageURLs:(NSArray *)csInAppNotifs { + NSMutableSet *mediaURLs = [NSMutableSet new]; + for (NSDictionary *jsonInApp in csInAppNotifs) { + NSDictionary *media = (NSDictionary*) jsonInApp[@"media"]; + if (media) { + NSString *imageURL = [self URLFromDictionary:media]; + if (imageURL) { + [mediaURLs addObject:imageURL]; + } + } + NSDictionary *mediaLandscape = (NSDictionary*) jsonInApp[@"mediaLandscape"]; + if (mediaLandscape) { + NSString *imageURL = [self URLFromDictionary:mediaLandscape]; + if (imageURL) { + [mediaURLs addObject:imageURL]; + } + } + } + return [mediaURLs allObjects]; +} + +- (NSString *)URLFromDictionary:(NSDictionary *)media { + NSString *contentType = media[@"content_type"]; + NSString *mediaUrl = media[@"url"]; + if (mediaUrl && mediaUrl.length > 0) { + // Preload contentType with image/jpeg or image/gif + if ([contentType hasPrefix:@"image"]) { + return mediaUrl; + } + } + return nil; +} + #pragma mark CTSwitchUserDelegate - (void)deviceIdDidChange:(NSString *)newDeviceId { self.deviceId = newDeviceId; diff --git a/CleverTapSDKTests/CTEventBuilderTest.m b/CleverTapSDKTests/CTEventBuilderTest.m index c043eb81..9c7cd0b4 100644 --- a/CleverTapSDKTests/CTEventBuilderTest.m +++ b/CleverTapSDKTests/CTEventBuilderTest.m @@ -13,14 +13,14 @@ #import "InAppHelper.h" @interface CTEventBuilderTest : XCTestCase -@property (nonatomic, strong) CTInAppImagePrefetchManager *prefetchManager; +@property (nonatomic, strong) CTFileDownloader *fileDownloader; @end @implementation CTEventBuilderTest - (void)setUp { InAppHelper *helper = [InAppHelper new]; - self.prefetchManager = helper.imagePrefetchManager; + self.fileDownloader = helper.fileDownloader; } - (void)tearDown { @@ -283,7 +283,7 @@ - (void)test_buildPushNotificationEvent_withClickedFalse { - (void)test_buildInAppNotificationStateEvent_withClickedTrueAndInvalidKey { NSDictionary *notification = @{@"notiKey": @"notiValue"}; - CTInAppNotification *inAppNotification = [[CTInAppNotification alloc] initWithJSON:notification imagePrefetchManager:self.prefetchManager]; + CTInAppNotification *inAppNotification = [[CTInAppNotification alloc] initWithJSON:notification fileDownloader:self.fileDownloader]; NSDictionary *queryParam = @{@"key1": @"value1"}; [CTEventBuilder buildInAppNotificationStateEvent:true forNotification:inAppNotification andQueryParameters:queryParam completionHandler:^(NSDictionary * _Nullable event, NSArray * _Nullable errors) { @@ -296,7 +296,7 @@ - (void)test_buildInAppNotificationStateEvent_withClickedTrueAndInvalidKey { - (void)test_buildInAppNotificationStateEvent_withClickedFalseAndInvalidKey { NSDictionary *notification = @{@"notiKey": @"notiValue"}; - CTInAppNotification *inAppNotification = [[CTInAppNotification alloc] initWithJSON:notification imagePrefetchManager:self.prefetchManager]; + CTInAppNotification *inAppNotification = [[CTInAppNotification alloc] initWithJSON:notification fileDownloader:self.fileDownloader]; NSDictionary *queryParam = @{@"key1": @"value1"}; [CTEventBuilder buildInAppNotificationStateEvent:false forNotification:inAppNotification andQueryParameters:queryParam completionHandler:^(NSDictionary * _Nullable event, NSArray * _Nullable errors) { @@ -309,7 +309,7 @@ - (void)test_buildInAppNotificationStateEvent_withClickedFalseAndInvalidKey { - (void)test_buildInAppNotificationStateEvent_withValidKey { NSDictionary *notification = @{@"wzrk_notiKey": @"notiValue"}; - CTInAppNotification *inAppNotification = [[CTInAppNotification alloc] initWithJSON:notification imagePrefetchManager:self.prefetchManager]; + CTInAppNotification *inAppNotification = [[CTInAppNotification alloc] initWithJSON:notification fileDownloader:self.fileDownloader]; NSDictionary *queryParam = @{@"key1": @"value1"}; [CTEventBuilder buildInAppNotificationStateEvent:false forNotification:inAppNotification andQueryParameters:queryParam completionHandler:^(NSDictionary * _Nullable event, NSArray * _Nullable errors) { diff --git a/CleverTapSDKTests/FileDownload/CTFileDownloadManager+Tests.h b/CleverTapSDKTests/FileDownload/CTFileDownloadManager+Tests.h new file mode 100644 index 00000000..860aceef --- /dev/null +++ b/CleverTapSDKTests/FileDownload/CTFileDownloadManager+Tests.h @@ -0,0 +1,27 @@ +// +// CTFileDownloadManager+Tests.h +// CleverTapSDK +// +// Created by Nikola Zagorchev on 23.06.24. +// Copyright © 2024 CleverTap. All rights reserved. +// + +#ifndef CTFileDownloadManager_Tests_h +#define CTFileDownloadManager_Tests_h + +#import "CTFileDownloadManager.h" + +@interface CTFileDownloadManager(Tests) + +@property (nonatomic, strong) NSURLSession *session; +@property (nonatomic, strong) NSFileManager* fileManager; + +- (void)downloadSingleFile:(NSURL *)url +completed:(void(^)(BOOL success))completedBlock; + +- (void)deleteSingleFile:(NSURL *)url + completed:(void(^)(BOOL success))completedBlock; + +@end + +#endif /* CTFileDownloadManager_Tests_h */ diff --git a/CleverTapSDKTests/FileDownload/CTFileDownloadManagerTests.m b/CleverTapSDKTests/FileDownload/CTFileDownloadManagerTests.m new file mode 100644 index 00000000..4e21b690 --- /dev/null +++ b/CleverTapSDKTests/FileDownload/CTFileDownloadManagerTests.m @@ -0,0 +1,579 @@ +#import +#import +#import "CleverTapInstanceConfig.h" +#import "CTFileDownloadManager+Tests.h" +#import "CTConstants.h" +#import "CTFileDownloadTestHelper.h" +#import "NSFileManagerMock.h" + +@interface CTFileDownloadManagerTests : XCTestCase + +@property (nonatomic, strong) CleverTapInstanceConfig *config; +@property (nonatomic, strong) CTFileDownloadManager *fileDownloadManager; +@property (nonatomic, strong) NSArray *fileURLs; +@property (nonatomic, strong) CTFileDownloadTestHelper *helper; + +@end + +@implementation CTFileDownloadManagerTests + +- (void)setUp { + [super setUp]; + + self.helper = [CTFileDownloadTestHelper new]; + [self.helper addHTTPStub]; + self.config = [[CleverTapInstanceConfig alloc] initWithAccountId:@"testAccountId" accountToken:@"testAccountToken"]; + self.fileDownloadManager = [CTFileDownloadManager sharedInstanceWithConfig:self.config]; +} + +- (void)tearDown { + [super tearDown]; + + [self.helper removeStub]; + [self deleteFiles:self.fileURLs]; +} + +- (void)testFilesExist { + [self downloadFiles]; + + for (NSURL *url in self.fileURLs) { + NSString *filePath = [NSString stringWithFormat:@"%lu_%@", [url.absoluteString hash], [url lastPathComponent]]; + NSString *documentsDirectory = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject]; + NSString *ctFiles = [documentsDirectory stringByAppendingPathComponent:CLTAP_FILES_DIRECTORY_NAME]; + NSString *path = [ctFiles stringByAppendingPathComponent:filePath]; + + XCTAssertTrue([[NSFileManager defaultManager] fileExistsAtPath:path]); + } +} + +- (void)testFilePath { + NSURL *url = [NSURL URLWithString:@"https://clevertap.com/ct_test_url_0.png"]; + NSString *filePath = [self.fileDownloadManager filePath:url]; + NSString *documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject]; + documentsPath = [documentsPath stringByAppendingPathComponent:CLTAP_FILES_DIRECTORY_NAME]; + XCTAssertEqualObjects(filePath, [documentsPath stringByAppendingPathComponent:@"1176188917138815486_ct_test_url_0.png"]); +} + +- (void)testIsFileAlreadyPresent { + [self downloadFiles]; + + for(int i = 0; i < [self.fileURLs count]; i++) { + XCTAssertTrue([self.fileDownloadManager isFileAlreadyPresent:self.fileURLs[i]]); + } +} + +- (void)testDeleteFiles { + [self downloadFiles]; + + NSArray *urls = [self.helper generateFileURLs:2]; + [self deleteFiles:urls]; + + // Deleted 1st and 2nd file url. + XCTAssertFalse([self.fileDownloadManager isFileAlreadyPresent:self.fileURLs[0]]); + XCTAssertFalse([self.fileDownloadManager isFileAlreadyPresent:self.fileURLs[1]]); + XCTAssertTrue([self.fileDownloadManager isFileAlreadyPresent:self.fileURLs[2]]); +} + +- (void)testDeleteFilesStatus { + [self downloadFiles]; + + NSMutableArray *deleteFileURLs = [NSMutableArray new]; + for(int i = 0; i < self.fileURLs.count; i++) { + // Ensure files are downloaded and saved to disk + XCTAssertTrue([self.fileDownloadManager isFileAlreadyPresent:self.fileURLs[i]]); + NSString *fileURL = [self.fileURLs[i] absoluteString]; + [deleteFileURLs addObject:fileURL]; + } + XCTestExpectation *expectation = [self expectationWithDescription:@"Delete files"]; + [self.fileDownloadManager deleteFiles:deleteFileURLs withCompletionBlock:^(NSDictionary * _Nullable status) { + for (NSString *url in deleteFileURLs) { + // Assert delete status is success and file is removed from disk + XCTAssertEqual(YES, [status[url] boolValue]); + XCTAssertFalse([self.fileDownloadManager isFileAlreadyPresent:self.fileURLs[0]]); + } + [expectation fulfill]; + }]; + [self waitForExpectations:@[expectation] timeout:2.0]; +} + +- (void)testNotExistDeleteFiles { + NSArray *urls = [self.helper generateFileURLStrings:2]; + XCTestExpectation *expectation = [self expectationWithDescription:@"Delete files"]; + [self.fileDownloadManager deleteFiles:urls withCompletionBlock:^(NSDictionary * _Nullable status) { + for (NSString *url in urls) { + XCTAssertEqual(YES, [status[url] boolValue]); + } + [expectation fulfill]; + }]; + [self waitForExpectations:@[expectation] timeout:2.0]; +} + +- (void)testDeleteSingleFile { + [self downloadFiles:1]; + XCTAssertTrue([self.fileDownloadManager isFileAlreadyPresent:self.fileURLs[0]]); + + XCTestExpectation *expectation = [self expectationWithDescription:@"Delete single file"]; + [self.fileDownloadManager deleteSingleFile:self.fileURLs[0] completed:^(BOOL success) { + XCTAssertEqual(YES, success); + XCTAssertFalse([self.fileDownloadManager isFileAlreadyPresent:self.fileURLs[0]]); + [expectation fulfill]; + }]; + [self waitForExpectations:@[expectation] timeout:2.0]; +} + +- (void)testDeleteSingleFileIncorrectURL { + XCTestExpectation *expectationNil = [self expectationWithDescription:@"Delete single file nil url"]; + NSURL *urlNil = nil; + [self.fileDownloadManager deleteSingleFile:urlNil completed:^(BOOL success) { + XCTAssertEqual(NO, success); + [expectationNil fulfill]; + }]; + + XCTestExpectation *expectationEmpty = [self expectationWithDescription:@"Delete single file nil url"]; + NSURL *urlEmpty = [NSURL URLWithString:@""]; + [self.fileDownloadManager deleteSingleFile:urlEmpty completed:^(BOOL success) { + XCTAssertEqual(NO, success); + [expectationEmpty fulfill]; + }]; + + XCTestExpectation *expectationNoLastComponent = [self expectationWithDescription:@"Delete single file nil url"]; + NSURL *urlNoLastComponent = [NSURL URLWithString:@"https://no-url-component.png"]; + [self.fileDownloadManager deleteSingleFile:urlNoLastComponent completed:^(BOOL success) { + XCTAssertEqual(NO, success); + [expectationNoLastComponent fulfill]; + }]; + + [self waitForExpectations:@[expectationNil, expectationEmpty, expectationNoLastComponent] timeout:2.0]; +} + +#pragma mark CTFileDownload callback test + +- (void)testAllFilesDownloadedCallback { + self.fileURLs = [self.helper generateFileURLs:2]; + + NSMutableDictionary *expectedStatus = [NSMutableDictionary new]; + NSString *urlString1 = [self.fileURLs[0] absoluteString]; + NSString *urlString2 = [self.fileURLs[1] absoluteString]; + expectedStatus[urlString1] = @1; + expectedStatus[urlString2] = @1; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Download files callback"]; + + // Assert + void (^completionBlock)(NSDictionary * _Nullable) = ^(NSDictionary * _Nullable status) { + XCTAssertEqualObjects(status, expectedStatus); + [expectation fulfill]; + }; + + // Download files + [self.fileDownloadManager downloadFiles:self.fileURLs withCompletionBlock:completionBlock]; + [self waitForExpectations:@[expectation] timeout:2.0]; +} + +- (void)testFileAlreadyDownloaded { + self.fileURLs = [self.helper generateFileURLs:5]; + NSArray *urls = @[self.fileURLs[0], self.fileURLs[1]]; + XCTestExpectation *expectation1 = [self expectationWithDescription:@"Download files callback"]; + XCTestExpectation *expectation2 = [self expectationWithDescription:@"Download already present files callback"]; + + // Download 5 files + [self.fileDownloadManager downloadFiles:self.fileURLs withCompletionBlock:^(NSDictionary * _Nullable status) { + [expectation1 fulfill]; + // Assert files are already downloaded + XCTAssertTrue([self.fileDownloadManager isFileAlreadyPresent:urls[0]]); + XCTAssertTrue([self.fileDownloadManager isFileAlreadyPresent:urls[1]]); + // Call download for 1st and 2nd files again + [self.fileDownloadManager downloadFiles:urls withCompletionBlock:^(NSDictionary * _Nullable status) { + [expectation2 fulfill]; + }]; + }]; + + // Enforce the order + [self waitForExpectations:@[expectation1, expectation2] timeout:2.0 enforceOrder:YES]; + // Ensure total files downloaded are only 5 (one request for each unique file) + XCTAssertEqual(5, self.helper.filesDownloaded.count); + // Ensure 1st and 2nd files are downloaded only once + XCTAssertTrue([self.helper fileDownloadedCount:urls[0]] == 1); + XCTAssertTrue([self.helper fileDownloadedCount:urls[1]] == 1); +} + +- (void)testDownloadsPending { + // Generate 5 file URLs + self.fileURLs = [self.helper generateFileURLs:5]; + NSArray *urls = @[self.fileURLs[0], self.fileURLs[1]]; + XCTestExpectation *expectation1 = [self expectationWithDescription:@"Download 1st and 2nd files callback"]; + XCTestExpectation *expectation2 = [self expectationWithDescription:@"Download all files callback"]; + + // Download all 5 files + [self.fileDownloadManager downloadFiles:self.fileURLs withCompletionBlock:^(NSDictionary * _Nullable status) { + [expectation2 fulfill]; + }]; + + // Ensure files are not present yet since download is not yet completed + XCTAssertFalse([self.fileDownloadManager isFileAlreadyPresent:urls[0]]); + XCTAssertFalse([self.fileDownloadManager isFileAlreadyPresent:urls[1]]); + // Download the 1st and 2nd files + [self.fileDownloadManager downloadFiles:urls withCompletionBlock:^(NSDictionary * _Nullable status) { + [expectation1 fulfill]; + }]; + + // Ensure the expecation for Download 1st and 2nd files is fulfilled first + [self waitForExpectations:@[expectation1, expectation2] timeout:2.0 enforceOrder:YES]; + // Ensure total files downloaded are only 5 (one request for each unique file) + XCTAssertEqual(5, self.helper.filesDownloaded.count); + // Ensure 1st and 2nd files are downloaded only once + XCTAssertTrue([self.helper fileDownloadedCount:urls[0]] == 1); + XCTAssertTrue([self.helper fileDownloadedCount:urls[1]] == 1); +} + +- (void)testRequestFailure { + id stub = [HTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { + return [request.URL.absoluteString containsString:@"non-existent"]; + } withStubResponse:^HTTPStubsResponse*(NSURLRequest *request) { + return [HTTPStubsResponse responseWithData:[NSData data] + statusCode:404 + headers:@{@"Content-Type":@"text/plain"}]; + }]; + + NSArray *fileURLs = [self.helper generateFileURLs:2]; + NSMutableArray *urls = [fileURLs mutableCopy]; + NSString *nonexistentURLString = @"https://non-existent.com/non-existent.png"; + NSURL *nonexistentURL = [NSURL URLWithString:nonexistentURLString]; + [urls insertObject:nonexistentURL atIndex:0]; + self.fileURLs = urls; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Download files callback"]; + [self.fileDownloadManager downloadFiles:self.fileURLs withCompletionBlock:^(NSDictionary * _Nonnull status) { + // Assert the file that returns 404 has error status + XCTAssertEqualObjects(@0, status[nonexistentURLString]); + // Assert the files that return 200 has success status + XCTAssertEqualObjects(@1, status[[urls[1] absoluteString]]); + XCTAssertEqualObjects(@1, status[[urls[2] absoluteString]]); + + // Assert error file not written to disk + XCTAssertFalse([self.fileDownloadManager isFileAlreadyPresent:nonexistentURL]); + // Assert files written to disk + XCTAssertTrue([self.fileDownloadManager isFileAlreadyPresent:urls[1]]); + XCTAssertTrue([self.fileDownloadManager isFileAlreadyPresent:urls[2]]); + + [expectation fulfill]; + }]; + + [self waitForExpectations:@[expectation] timeout:2.0]; + [HTTPStubs removeStub:stub]; +} + +- (void)testDownloadFilesOneUrlTimeOut { + // Stub the network request for timeout file to simulate a Timed Out Error + id stub = [HTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { + return [request.URL.absoluteString containsString:@"timeout"]; + } withStubResponse:^HTTPStubsResponse*(NSURLRequest *request) { + // Return Timed Out Error and delay the response time with 0.1s + return [[HTTPStubsResponse responseWithError:[NSError errorWithDomain:NSURLErrorDomain + code:NSURLErrorTimedOut + userInfo:nil]] + responseTime:0.1]; + }]; + + XCTestExpectation *expectation1 = [self expectationWithDescription:@"Download files callback"]; + XCTestExpectation *expectation2 = [self expectationWithDescription:@"Download files with 1 file timed out callback"]; + + NSArray *fileURLs = [self.helper generateFileURLs:2]; + NSMutableArray *fileURLsWithTimedOutURL = [fileURLs mutableCopy]; + [fileURLsWithTimedOutURL insertObject:[NSURL URLWithString:@"https://timeout.com/timeout.png"] atIndex:0]; + self.fileURLs = fileURLsWithTimedOutURL; + + // Download files where 1st file will time out + [self.fileDownloadManager downloadFiles:fileURLsWithTimedOutURL withCompletionBlock:^(NSDictionary * _Nonnull status) { + XCTAssertEqualObjects(@0, status[[fileURLsWithTimedOutURL[0] absoluteString]]); + XCTAssertEqualObjects(@1, status[[fileURLsWithTimedOutURL[1] absoluteString]]); + XCTAssertEqualObjects(@1, status[[fileURLsWithTimedOutURL[2] absoluteString]]); + [expectation2 fulfill]; + }]; + + // Download files + // Ensure the downloadFiles does not wait for the first call to complete + [self.fileDownloadManager downloadFiles:fileURLs withCompletionBlock:^(NSDictionary * _Nonnull status) { + XCTAssertEqualObjects(@1, status[[fileURLs[0] absoluteString]]); + XCTAssertEqualObjects(@1, status[[fileURLs[1] absoluteString]]); + [expectation1 fulfill]; + }]; + + // Ensure 2nd downloadFiles callback does not wait on the 1st + // Ensure the expecation for successful download is called first + [self waitForExpectations:@[expectation1, expectation2] timeout:2.0 enforceOrder:YES]; + [HTTPStubs removeStub:stub]; +} + +- (void)testDownloadSingle { + self.fileURLs = [self.helper generateFileURLs:1]; + XCTestExpectation *expectation1 = [self expectationWithDescription:@"Download files callback 1"]; + XCTestExpectation *expectation2 = [self expectationWithDescription:@"Download files callback 2"]; + + // downloadSingleFile directly downloads the file + [self.fileDownloadManager downloadSingleFile:self.fileURLs[0] completed:^(BOOL success) { + XCTAssertTrue(success); + [expectation1 fulfill]; + }]; + [self.fileDownloadManager downloadSingleFile:self.fileURLs[0] completed:^(BOOL success) { + XCTAssertTrue(success); + [expectation2 fulfill]; + }]; + + [self waitForExpectations:@[expectation1, expectation2] timeout:2.0]; + // Expected file requests to equal the calls to downloadSingleFile + XCTAssertTrue([self.helper fileDownloadedCount:self.fileURLs[0]] == 2); +} + +- (void)testDownloadSingleSameURLComponentDifferentHost { + NSURL *url1 = [NSURL URLWithString:[NSString stringWithFormat:@"https://clevertap.com/%@.png", self.helper.fileURL]]; + NSURL *url2 = [NSURL URLWithString:[NSString stringWithFormat:@"https://clevertap-1.com/%@.png", self.helper.fileURL]]; + self.fileURLs = @[url1, url2]; + + XCTestExpectation *expectation1 = [self expectationWithDescription:@"Download files callback 1"]; + XCTestExpectation *expectation2 = [self expectationWithDescription:@"Download files callback 2"]; + + // Expect to download and save two different files + [self.fileDownloadManager downloadSingleFile:url1 completed:^(BOOL success) { + XCTAssertTrue(success); + XCTAssertTrue([self.fileDownloadManager isFileAlreadyPresent:url1]); + [expectation1 fulfill]; + }]; + [self.fileDownloadManager downloadSingleFile:url2 completed:^(BOOL success) { + XCTAssertTrue(success); + XCTAssertTrue([self.fileDownloadManager isFileAlreadyPresent:url2]); + [expectation2 fulfill]; + }]; + + [self waitForExpectations:@[expectation1, expectation2] timeout:2.0]; + // Expected file requests to equal the calls to downloadSingleFile + XCTAssertEqual(2, self.helper.filesDownloaded.count); + XCTAssertNotEqualObjects([self.fileDownloadManager filePath:url1], [self.fileDownloadManager filePath:url2]); +} + +- (void)testDownloadSingleOverwriteFile { + NSURL *url = [self.helper generateFileURL]; + self.fileURLs = @[url]; + + __block NSDate *firstFileCreationDate; + XCTestExpectation *expectation1 = [self expectationWithDescription:@"Download file callback"]; + // Download file + [self.fileDownloadManager downloadSingleFile:url completed:^(BOOL success) { + XCTAssertTrue(success); + XCTAssertTrue([self.fileDownloadManager isFileAlreadyPresent:url]); + + // Set the 1st file creation date + NSDictionary* fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:[self.fileDownloadManager filePath:url] error:nil]; + firstFileCreationDate = [fileAttributes objectForKey:NSFileCreationDate]; + [expectation1 fulfill]; + }]; + + __block NSDate *secondFileCreationDate; + XCTestExpectation *expectation2 = [self expectationWithDescription:@"Download file again callback"]; + // Download the file again. Since the file exists, it should be deleted and then saved. + [self.fileDownloadManager downloadSingleFile:url completed:^(BOOL success) { + XCTAssertTrue(success); + XCTAssertTrue([self.fileDownloadManager isFileAlreadyPresent:url]); + + // Set the 2nd file creation date + NSDictionary* fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:[self.fileDownloadManager filePath:url] error:nil]; + secondFileCreationDate = [fileAttributes objectForKey:NSFileCreationDate]; + [expectation2 fulfill]; + }]; + + [self waitForExpectations:@[expectation1, expectation2] timeout:2.0]; + // Ensure the file is overwritten by comparing the created dates + XCTAssertNotEqualObjects(firstFileCreationDate, secondFileCreationDate); +} + +- (void)testDownloadSingleFileWithCreateDirectoryError { + NSFileManager *originalFileManager = self.fileDownloadManager.fileManager; + NSFileManagerMock *fileManagerMock = [[NSFileManagerMock alloc] init]; + self.fileDownloadManager.fileManager = fileManagerMock; + XCTestExpectation *expectation = [self expectationWithDescription:@"Completion block called"]; + + NSURL *URL = [self.helper generateFileURL]; + fileManagerMock.createDirectoryError = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileWriteNoPermissionError userInfo:nil]; + + [self.fileDownloadManager downloadSingleFile:URL completed:^(BOOL success) { + XCTAssertFalse(success); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:1.0 handler:nil]; + self.fileDownloadManager.fileManager = originalFileManager; +} + +- (void)testDownloadSingleFileWithFileRemoveError { + NSFileManager *originalFileManager = self.fileDownloadManager.fileManager; + NSFileManagerMock *fileManagerMock = [[NSFileManagerMock alloc] init]; + self.fileDownloadManager.fileManager = fileManagerMock; + XCTestExpectation *expectation = [self expectationWithDescription:@"Completion block called"]; + + NSURL *URL = [self.helper generateFileURL]; + fileManagerMock.fileExists = YES; + fileManagerMock.removeItemError = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileWriteNoPermissionError userInfo:nil]; + + [self.fileDownloadManager downloadSingleFile:URL completed:^(BOOL success) { + XCTAssertFalse(success); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:1.0 handler:nil]; + self.fileDownloadManager.fileManager = originalFileManager; +} + +- (void)testDownloadSingleFileWithFileMoveError { + NSFileManager *originalFileManager = self.fileDownloadManager.fileManager; + NSFileManagerMock *fileManagerMock = [[NSFileManagerMock alloc] init]; + self.fileDownloadManager.fileManager = fileManagerMock; + XCTestExpectation *expectation = [self expectationWithDescription:@"Completion block called"]; + + NSURL *URL = [self.helper generateFileURL]; + fileManagerMock.moveItemError = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileWriteNoPermissionError userInfo:nil]; + + [self.fileDownloadManager downloadSingleFile:URL completed:^(BOOL success) { + XCTAssertFalse(success); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:1.0 handler:nil]; + self.fileDownloadManager.fileManager = originalFileManager; +} + +- (void)testTimeoutConfiguration { + NSURLSessionConfiguration *configuration = self.fileDownloadManager.session.configuration; + XCTAssertEqual(configuration.timeoutIntervalForRequest, CLTAP_REQUEST_TIME_OUT_INTERVAL); + XCTAssertEqual(configuration.timeoutIntervalForResource, CLTAP_FILE_RESOURCE_TIME_OUT_INTERVAL); +} + +- (void)testDownloadSingle404 { + // Stub the network request to simulate a 404 Not Found error + id stub = [HTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { + return [request.URL.absoluteString containsString:self.helper.fileURL]; + } withStubResponse:^HTTPStubsResponse*(NSURLRequest *request) { + return [HTTPStubsResponse responseWithData:[NSData data] + statusCode:404 + headers:@{@"Content-Type":@"text/plain"}]; + }]; + + NSURL *url = [self.helper generateFileURL]; + XCTestExpectation *expectation = [self expectationWithDescription:@"Download files callback"]; + [self.fileDownloadManager downloadSingleFile:url completed:^(BOOL success) { + XCTAssertFalse(success); + XCTAssertFalse([self.fileDownloadManager isFileAlreadyPresent:url]); + [expectation fulfill]; + }]; + + [self waitForExpectations:@[expectation] timeout:2.0]; + [HTTPStubs removeStub:stub]; +} + +- (void)testDownloadSingleHostNotFound { + id stub = [HTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { + return [request.URL.absoluteString containsString:self.helper.fileURL]; + } withStubResponse:^HTTPStubsResponse*(NSURLRequest *request) { + return [HTTPStubsResponse responseWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorCannotFindHost userInfo:nil]]; + }]; + + NSURL *url = [self.helper generateFileURL]; + XCTestExpectation *expectation = [self expectationWithDescription:@"Download files callback"]; + [self.fileDownloadManager downloadSingleFile:url completed:^(BOOL success) { + XCTAssertFalse(success); + XCTAssertFalse([self.fileDownloadManager isFileAlreadyPresent:url]); + [expectation fulfill]; + }]; + + [self waitForExpectations:@[expectation] timeout:2.0]; + [HTTPStubs removeStub:stub]; +} + +- (void)testRemoveAllFiles { + [self downloadFiles:3]; + NSMutableArray *paths = [NSMutableArray array]; + for (int i = 0; i < 3; i++) { + XCTAssertTrue([self.fileDownloadManager isFileAlreadyPresent:self.fileURLs[i]]); + [paths addObject:[self.fileDownloadManager filePath:self.fileURLs[i]]]; + } + XCTestExpectation *expectation = [self expectationWithDescription:@"Remove all files callback"]; + [self.fileDownloadManager removeAllFilesWithCompletionBlock:^(NSDictionary * _Nonnull status) { + for (int i = 0; i < 3; i++) { + XCTAssertTrue(status[paths[i]]); + XCTAssertFalse([self.fileDownloadManager isFileAlreadyPresent:self.fileURLs[i]]); + } + [expectation fulfill]; + }]; + [self waitForExpectations:@[expectation] timeout:2.0]; +} + +- (void)testRemoveAllFilesContentsOfDirectoryError { + NSFileManager *originalFileManager = self.fileDownloadManager.fileManager; + NSFileManagerMock *fileManagerMock = [[NSFileManagerMock alloc] init]; + self.fileDownloadManager.fileManager = fileManagerMock; + XCTestExpectation *expectation = [self expectationWithDescription:@"Completion block called"]; + + fileManagerMock.contentsOfDirectoryError = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileWriteNoPermissionError userInfo:nil]; + [self.fileDownloadManager removeAllFilesWithCompletionBlock:^(NSDictionary * _Nonnull status) { + XCTAssertTrue(status.count == 0); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:1.0 handler:nil]; + self.fileDownloadManager.fileManager = originalFileManager; +} + +- (void)testRemoveAllFilesRemoveFileError { + [self downloadFiles:3]; + NSMutableArray *paths = [NSMutableArray array]; + for (int i = 0; i < 3; i++) { + XCTAssertTrue([self.fileDownloadManager isFileAlreadyPresent:self.fileURLs[i]]); + [paths addObject:[self.fileDownloadManager filePath:self.fileURLs[i]]]; + } + + NSFileManager *originalFileManager = self.fileDownloadManager.fileManager; + NSFileManagerMock *fileManagerMock = [[NSFileManagerMock alloc] init]; + self.fileDownloadManager.fileManager = fileManagerMock; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Remove all files callback"]; + fileManagerMock.removeItemError = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileWriteNoPermissionError userInfo:nil]; + [self.fileDownloadManager removeAllFilesWithCompletionBlock:^(NSDictionary * _Nonnull status) { + self.fileDownloadManager.fileManager = originalFileManager; + for (int i = 0; i < 3; i++) { + XCTAssertEqualObjects(@0, status[paths[i]]); + XCTAssertTrue([self.fileDownloadManager isFileAlreadyPresent:self.fileURLs[i]]); + } + [expectation fulfill]; + }]; + [self waitForExpectations:@[expectation] timeout:2.0]; +} + +#pragma mark Private methods + +- (void)downloadFiles { + [self downloadFiles:3]; +} + +- (void)downloadFiles:(int)count { + self.fileURLs = [self.helper generateFileURLs:count]; + XCTestExpectation *expectation = [self expectationWithDescription:@"Download files"]; + + [self.fileDownloadManager downloadFiles:self.fileURLs withCompletionBlock:^(NSDictionary * _Nullable status) { + [expectation fulfill]; + }]; + [self waitForExpectations:@[expectation] timeout:2.0]; +} + +- (void)deleteFiles:(NSArray *)urls { + NSMutableArray *deleteFileURLs = [NSMutableArray new]; + for(int i = 0; i < urls.count; i++) { + NSString *fileURL = [urls[i] absoluteString]; + [deleteFileURLs addObject:fileURL]; + } + XCTestExpectation *expectation = [self expectationWithDescription:@"Delete files"]; + [self.fileDownloadManager deleteFiles:deleteFileURLs withCompletionBlock:^(NSDictionary * _Nullable status) { + [expectation fulfill]; + }]; + [self waitForExpectations:@[expectation] timeout:2.0]; +} + +@end diff --git a/CleverTapSDKTests/FileDownload/CTFileDownloadTestHelper.h b/CleverTapSDKTests/FileDownload/CTFileDownloadTestHelper.h new file mode 100644 index 00000000..d8f5af09 --- /dev/null +++ b/CleverTapSDKTests/FileDownload/CTFileDownloadTestHelper.h @@ -0,0 +1,28 @@ +// +// CTFileDownloadTestHelper.h +// CleverTapSDKTests +// +// Created by Nikola Zagorchev on 22.06.24. +// Copyright © 2024 CleverTap. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface CTFileDownloadTestHelper : NSObject + +@property (nonatomic, strong) NSMutableDictionary *filesDownloaded; + +- (void)addHTTPStub; +- (int)fileDownloadedCount:(NSURL *)url; +- (void)removeStub; + +- (NSString *)fileURL; +- (NSURL *)generateFileURL; +- (NSArray *)generateFileURLs:(int)count; +- (NSArray *)generateFileURLStrings:(int)count; + +@end + +NS_ASSUME_NONNULL_END diff --git a/CleverTapSDKTests/FileDownload/CTFileDownloadTestHelper.m b/CleverTapSDKTests/FileDownload/CTFileDownloadTestHelper.m new file mode 100644 index 00000000..ca7dccc8 --- /dev/null +++ b/CleverTapSDKTests/FileDownload/CTFileDownloadTestHelper.m @@ -0,0 +1,108 @@ +// +// CTFileDownloadTestHelper.m +// CleverTapSDKTests +// +// Created by Nikola Zagorchev on 22.06.24. +// Copyright © 2024 CleverTap. All rights reserved. +// + +#import "CTFileDownloadTestHelper.h" +#import + +@interface CTFileDownloadTestHelper() + +@property (nonatomic, strong) id HTTPStub; +@property (nonatomic, strong) NSArray *fileURLTypes; + +@end + +@implementation CTFileDownloadTestHelper + +- (instancetype)init { + self = [super init]; + if (self) { + self.filesDownloaded = [NSMutableDictionary new]; + self.fileURLTypes = @[@"txt", @"pdf", @"png"]; + } + return self; +} + +- (NSString *)fileURL { + return @"ct_test_url"; +} + +- (int)fileDownloadedCount:(NSURL *)url { + NSString *key = [url absoluteString]; + if (self.filesDownloaded[key]) { + return [self.filesDownloaded[key] intValue]; + } + return -1; +} + +- (void)addHTTPStub { + self.HTTPStub = [HTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { + return [request.URL.absoluteString containsString:self.fileURL]; + } withStubResponse:^HTTPStubsResponse*(NSURLRequest *request) { + NSString *fileString = [request.URL absoluteString]; + NSString *fileType = [fileString pathExtension]; + NSBundle *bundle = [NSBundle bundleForClass:[self class]]; + + NSNumber *count = self.filesDownloaded[fileString]; + if (count) { + int value = [count intValue]; + count = [NSNumber numberWithInt:value + 1]; + self.filesDownloaded[fileString] = count; + } else { + self.filesDownloaded[fileString] = @1; + } + + if ([fileType isEqualToString:@"txt"]) { + return [HTTPStubsResponse responseWithFileAtPath:[bundle pathForResource:@"sampleTXTStub" ofType:@"txt"] + statusCode:200 + headers:nil]; + } else if ([fileType isEqualToString:@"pdf"]) { + return [HTTPStubsResponse responseWithFileAtPath:[bundle pathForResource:@"samplePDFStub" ofType:@"pdf"] + statusCode:200 + headers:nil]; + } else { + return [HTTPStubsResponse responseWithFileAtPath:[bundle pathForResource:@"clevertap-logo" ofType:@"png"] + statusCode:200 + headers:nil]; + } + }]; +} + +- (void)removeStub { + if (self.HTTPStub) { + [HTTPStubs removeStub:self.HTTPStub]; + } +} + +- (NSURL *)generateFileURL { + return [NSURL URLWithString:[self generateFileURLStringAtIndex:0]]; +} + +- (NSArray *)generateFileURLs:(int)count { + NSMutableArray *arr = [NSMutableArray arrayWithCapacity:count]; + for (int i = 0; i < count; i++) { + NSString *urlString = [self generateFileURLStringAtIndex:i]; + [arr addObject:[NSURL URLWithString:urlString]]; + } + return arr; +} + +- (NSArray *)generateFileURLStrings:(int)count { + NSMutableArray *arr = [NSMutableArray arrayWithCapacity:count]; + for (int i = 0; i < count; i++) { + NSString *urlString = [self generateFileURLStringAtIndex:i]; + [arr addObject:urlString]; + } + return arr; +} + +- (NSString *)generateFileURLStringAtIndex:(int)index { + int type = index >= 3 ? index % 3 : index; + return [NSString stringWithFormat:@"https://clevertap.com/%@_%d.%@", self.fileURL, index, self.fileURLTypes[type]]; +} + +@end diff --git a/CleverTapSDKTests/FileDownload/CTFileDownloader+Tests.h b/CleverTapSDKTests/FileDownload/CTFileDownloader+Tests.h new file mode 100644 index 00000000..3584998e --- /dev/null +++ b/CleverTapSDKTests/FileDownload/CTFileDownloader+Tests.h @@ -0,0 +1,33 @@ +// +// CTFileDownloader+Tests.h +// CleverTapSDK +// +// Created by Nikola Zagorchev on 23.06.24. +// Copyright © 2024 CleverTap. All rights reserved. +// + +#ifndef CTFileDownloader_Tests_h +#define CTFileDownloader_Tests_h + +#import "CTFileDownloader.h" + +@interface CTFileDownloader(Tests) + +@property (nonatomic, strong) CTFileDownloadManager *fileDownloadManager; +@property (nonatomic, strong) NSMutableDictionary *urlsExpiry; +@property (nonatomic) NSTimeInterval fileExpiryTime; +- (long)currentTimeInterval; +- (void)removeInactiveExpiredAssets:(long)lastDeletedTime; +- (void)removeDeletedFilesFromExpiry:(NSDictionary *)status; +- (void)updateFilesExpiryInPreference; +- (void)updateLastDeletedTimestamp; +- (long)lastDeletedTimestamp; +- (void)deleteFiles:(NSArray *)urls withCompletionBlock:(CTFilesDeleteCompletedBlock)completion; +- (void)migrateActiveAndInactiveUrls; +- (NSString *)storageKeyWithSuffix:(NSString *)suffix; +- (void)updateFilesExpiry:(NSDictionary *)status; +- (void)removeAllAssetsWithCompletion:(void(^)(NSDictionary *status))completion; + +@end + +#endif /* CTFileDownloader_Tests_h */ diff --git a/CleverTapSDKTests/FileDownload/CTFileDownloaderMock.h b/CleverTapSDKTests/FileDownload/CTFileDownloaderMock.h new file mode 100644 index 00000000..b09ab0f0 --- /dev/null +++ b/CleverTapSDKTests/FileDownload/CTFileDownloaderMock.h @@ -0,0 +1,27 @@ +// +// CTFileDownloaderMock.h +// CleverTapSDKTests +// +// Created by Nikola Zagorchev on 23.06.24. +// Copyright © 2024 CleverTap. All rights reserved. +// + +#import +#import "CTFileDownloader.h" +#import "CTFileDownloadManager.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface CTFileDownloaderMock : CTFileDownloader + +@property (nonatomic) long mockCurrentTimeInterval; +@property (nonatomic) void(^removeInactiveExpiredAssetsBlock)(long); + +@property (nonatomic) CTFilesDeleteCompletedBlock deleteCompletion; +@property (nonatomic, nullable) void(^deleteFilesInvokedBlock)(NSArray *); + +@property (nonatomic, nullable) CTFilesDeleteCompletedBlock removeAllAssetsCompletion; + +@end + +NS_ASSUME_NONNULL_END diff --git a/CleverTapSDKTests/FileDownload/CTFileDownloaderMock.m b/CleverTapSDKTests/FileDownload/CTFileDownloaderMock.m new file mode 100644 index 00000000..b6bbc95b --- /dev/null +++ b/CleverTapSDKTests/FileDownload/CTFileDownloaderMock.m @@ -0,0 +1,55 @@ +// +// CTFileDownloaderMock.m +// CleverTapSDKTests +// +// Created by Nikola Zagorchev on 23.06.24. +// Copyright © 2024 CleverTap. All rights reserved. +// + +#import "CTFileDownloaderMock.h" +#import "CTFileDownloader+Tests.h" + +@implementation CTFileDownloaderMock + +- (long)currentTimeInterval { + if (self.mockCurrentTimeInterval) { + return self.mockCurrentTimeInterval; + } + return [super currentTimeInterval]; +} + +- (void)removeInactiveExpiredAssets:(long)lastDeletedTime { + if (self.removeInactiveExpiredAssetsBlock) { + self.removeInactiveExpiredAssetsBlock(lastDeletedTime); + } + [super removeInactiveExpiredAssets:lastDeletedTime]; +} + +- (void)deleteFiles:(NSArray *)urls withCompletionBlock:(CTFilesDeleteCompletedBlock)completion { + if (self.deleteFilesInvokedBlock) { + self.deleteFilesInvokedBlock(urls); + } + CTFilesDeleteCompletedBlock completionBlock = ^(NSDictionary *status) { + if (completion) { + completion(status); + } + if (self.deleteCompletion) { + self.deleteCompletion(status); + } + }; + [super deleteFiles:urls withCompletionBlock:completionBlock]; +} + +- (void)removeAllAssetsWithCompletion:(void(^)(NSDictionary *status))completion { + CTFilesDeleteCompletedBlock completionBlock = ^(NSDictionary *status) { + if (completion) { + completion(status); + } + if (self.removeAllAssetsCompletion) { + self.removeAllAssetsCompletion(status); + } + }; + [super removeAllAssetsWithCompletion:completionBlock]; +} + +@end diff --git a/CleverTapSDKTests/FileDownload/CTFileDownloaderTests.m b/CleverTapSDKTests/FileDownload/CTFileDownloaderTests.m new file mode 100644 index 00000000..57757866 --- /dev/null +++ b/CleverTapSDKTests/FileDownload/CTFileDownloaderTests.m @@ -0,0 +1,459 @@ +#import +#import +#import "CTPreferences.h" +#import "CTConstants.h" +#import "CTFileDownloadManager.h" +#import "CTFileDownloadTestHelper.h" +#import "CTFileDownloader+Tests.h" +#import "CTFileDownloaderMock.h" + +@interface CTFileDownloaderTests : XCTestCase + +@property (nonatomic, strong) CleverTapInstanceConfig *config; +@property (nonatomic, strong) CTFileDownloaderMock *fileDownloader; +@property (nonatomic, strong) NSArray *fileURLs; +@property (nonatomic, strong) CTFileDownloadTestHelper *helper; + +@end + +@implementation CTFileDownloaderTests + +- (void)setUp { + [super setUp]; + + self.helper = [CTFileDownloadTestHelper new]; + [self.helper addHTTPStub]; + + self.config = [[CleverTapInstanceConfig alloc] initWithAccountId:@"testAccountId" accountToken:@"testAccountToken"]; + self.fileDownloader = [[CTFileDownloaderMock alloc] initWithConfig:self.config]; +} + +- (void)tearDown { + [super tearDown]; + + [self.helper removeStub]; + + [CTPreferences removeObjectForKey:[self.fileDownloader storageKeyWithSuffix:CLTAP_FILE_URLS_EXPIRY_DICT]]; + [CTPreferences removeObjectForKey:[self.fileDownloader storageKeyWithSuffix:CLTAP_FILE_ASSETS_LAST_DELETED_TS]]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Wait for cleanup"]; + self.fileDownloader.removeAllAssetsCompletion = ^(NSDictionary * _Nonnull status) { + [expectation fulfill]; + }; + // Clear all files + [self.fileDownloader clearFileAssets:NO]; + [self waitForExpectations:@[expectation] timeout:2.0]; +} + +- (void)testMigration { + NSArray *activeAssetsArray = @[@"url0", @"url1"]; + NSArray *inactiveAssetsArray = @[@"url2", @"url3"]; + [CTPreferences putObject:activeAssetsArray forKey:[self.fileDownloader storageKeyWithSuffix:CLTAP_PREFS_CS_INAPP_ACTIVE_ASSETS]]; + [CTPreferences putObject:inactiveAssetsArray forKey:[self.fileDownloader storageKeyWithSuffix:CLTAP_PREFS_CS_INAPP_INACTIVE_ASSETS]]; + + long ts = (long)[[NSDate date] timeIntervalSince1970]; + self.fileDownloader.mockCurrentTimeInterval = ts; + [CTPreferences putInt:ts forKey:[self.fileDownloader storageKeyWithSuffix:CLTAP_PREFS_CS_INAPP_ASSETS_LAST_DELETED_TS]]; + + [self.fileDownloader migrateActiveAndInactiveUrls]; + + XCTAssertNil([CTPreferences getObjectForKey:[self.fileDownloader storageKeyWithSuffix:CLTAP_PREFS_CS_INAPP_ACTIVE_ASSETS]]); + XCTAssertNil([CTPreferences getObjectForKey:[self.fileDownloader storageKeyWithSuffix:CLTAP_PREFS_CS_INAPP_INACTIVE_ASSETS]]); + NSDictionary *urlsExpiry = [CTPreferences getObjectForKey:[self.fileDownloader storageKeyWithSuffix:CLTAP_FILE_URLS_EXPIRY_DICT]]; + NSDictionary *urlsExpiryExpected = @{ + @"url0": @(ts + self.fileDownloader.fileExpiryTime), + @"url1": @(ts + self.fileDownloader.fileExpiryTime), + @"url2": @(ts + self.fileDownloader.fileExpiryTime), + @"url3": @(ts + self.fileDownloader.fileExpiryTime) + }; + XCTAssertTrue([urlsExpiryExpected isEqualToDictionary:urlsExpiry]); + + XCTAssertNil([CTPreferences getObjectForKey:[self.fileDownloader storageKeyWithSuffix:CLTAP_PREFS_CS_INAPP_ASSETS_LAST_DELETED_TS]]); + id filesLastDeletedTs = [CTPreferences getObjectForKey:[self.fileDownloader storageKeyWithSuffix:CLTAP_FILE_ASSETS_LAST_DELETED_TS]]; + XCTAssertEqual(ts, [filesLastDeletedTs longValue]); +} + +- (void)testSetup { + // Test setup initializes the FileDownloader with the urlsExpiry from cache + NSDictionary *urlsExpiry= @{ + @"url0": @(1), + @"url1": @(1) + }; + [CTPreferences putObject:urlsExpiry forKey:[self.fileDownloader storageKeyWithSuffix:CLTAP_FILE_URLS_EXPIRY_DICT]]; + + self.fileDownloader = [[CTFileDownloaderMock alloc] initWithConfig:self.config]; + XCTAssertTrue([urlsExpiry isEqualToDictionary:self.fileDownloader.urlsExpiry]); + XCTAssertTrue([self.fileDownloader.urlsExpiry isKindOfClass:[NSMutableDictionary class]]); + + // Test setup initializes the FileDownloader with empty dictionary if no cached value + [CTPreferences removeObjectForKey:[self.fileDownloader storageKeyWithSuffix:CLTAP_FILE_URLS_EXPIRY_DICT]]; + self.fileDownloader = [[CTFileDownloaderMock alloc] initWithConfig:self.config]; + XCTAssertNotNil(self.fileDownloader.urlsExpiry); + XCTAssertEqual(0, self.fileDownloader.urlsExpiry.count); + XCTAssertTrue([self.fileDownloader.urlsExpiry isKindOfClass:[NSMutableDictionary class]]); +} + +- (void)testDefaultExpiryTime { + XCTAssertEqual(self.fileDownloader.fileExpiryTime, CLTAP_FILE_EXPIRY_OFFSET); +} + +- (void)testFileAlreadyPresent { + NSArray *urls = [self.helper generateFileURLStrings:2]; + XCTAssertFalse([self.fileDownloader isFileAlreadyPresent:urls[0]]); + XCTAssertFalse([self.fileDownloader isFileAlreadyPresent:urls[1]]); + + [self downloadFiles:@[urls[0]]]; + + XCTAssertTrue([self.fileDownloader isFileAlreadyPresent:urls[0]]); + XCTAssertFalse([self.fileDownloader isFileAlreadyPresent:urls[1]]); +} + +- (void)testImageLoadedFromDisk { + // Download files + NSArray *urls = [self.helper generateFileURLStrings:3]; + // Download files, urls[2] is of type txt + [self downloadFiles:@[urls[2]]]; + + // Check image is present in disk cache + UIImage *image = [self.fileDownloader loadImageFromDisk:urls[2]]; + XCTAssertNotNil(image); +} + +- (void)testImageNotLoadedFromDisk { + NSArray *urls = [self.helper generateFileURLStrings:3]; + // Download files, urls[0] is of type txt + [self downloadFiles:@[urls[0]]]; + + // Check image is present in disk cache + UIImage *image = [self.fileDownloader loadImageFromDisk:urls[0]]; + XCTAssertNil(image); +} + +- (void)testFileDownloadPath { + NSArray *urls = [self.helper generateFileURLStrings:1]; + [self downloadFiles:urls]; + NSString *filePath = [self.fileDownloader fileDownloadPath:urls[0]]; + + NSURL *URL = [NSURL URLWithString:urls[0]]; + NSString *documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject]; + NSString *pathComponent = [URL lastPathComponent]; + long hash = [urls[0] hash]; + NSString *fileName = [NSString stringWithFormat:@"%@/%ld_%@", CLTAP_FILES_DIRECTORY_NAME, hash, pathComponent]; + NSString *expectedFilePath = [documentsPath stringByAppendingPathComponent:fileName]; + XCTAssertNotNil(filePath); + XCTAssertEqualObjects(filePath, expectedFilePath); +} + +- (void)testFileDownloadPathNotFound { + NSArray *urls = [self.helper generateFileURLStrings:1]; + NSString *filePath = [self.fileDownloader fileDownloadPath:urls[0]]; + XCTAssertNil(filePath); +} + +- (void)testDownloadEmptyUrls { + XCTestExpectation *expectation = [self expectationWithDescription:@"Download files"]; + [self.fileDownloader downloadFiles:@[] withCompletionBlock:^(NSDictionary * _Nullable status) { + XCTAssertNotNil(status); + XCTAssertTrue(status.count == 0); + [expectation fulfill]; + }]; + + [self waitForExpectations:@[expectation] timeout:2.0]; +} + +- (void)testDownloadUpdatesFileExpiryTs { + // Mock currentTimeInterval + long ts = (long)[[NSDate date] timeIntervalSince1970]; + self.fileDownloader.mockCurrentTimeInterval = ts; + + NSString *url = [self.helper generateFileURLStrings:1][0]; + // Ensure not yet present + XCTAssertNil(self.fileDownloader.urlsExpiry[url]); + + [self downloadFiles:@[url]]; + long expiryDate = ts + self.fileDownloader.fileExpiryTime; + // Ensure url has correct expiry set + XCTAssertEqualObjects(@(expiryDate), self.fileDownloader.urlsExpiry[url]); + + self.fileDownloader.mockCurrentTimeInterval = ts + 100; + [self downloadFiles:@[url]]; + // Ensure url expiry is updated + XCTAssertEqualObjects(@(expiryDate + 100), self.fileDownloader.urlsExpiry[url]); +} + +- (void)testDownloadUpdatesFileExpiryCache { + NSArray *urls = [self.helper generateFileURLStrings:2]; + XCTAssertEqual(0, self.fileDownloader.urlsExpiry.count); + + long ts = (long)[[NSDate date] timeIntervalSince1970]; + self.fileDownloader.mockCurrentTimeInterval = ts; + [self downloadFiles:urls]; + NSDictionary *urlsExpiry = [NSDictionary dictionaryWithDictionary:self.fileDownloader.urlsExpiry]; + XCTAssertEqual(2, self.fileDownloader.urlsExpiry.count); + XCTAssertEqualObjects(self.fileDownloader.urlsExpiry, [CTPreferences getObjectForKey:[self.fileDownloader storageKeyWithSuffix:CLTAP_FILE_URLS_EXPIRY_DICT]]); + + self.fileDownloader.mockCurrentTimeInterval = ts + 100; + [self downloadFiles:urls]; + XCTAssertNotEqualObjects(urlsExpiry, self.fileDownloader.urlsExpiry); + XCTAssertEqualObjects(self.fileDownloader.urlsExpiry, [CTPreferences getObjectForKey:[self.fileDownloader storageKeyWithSuffix:CLTAP_FILE_URLS_EXPIRY_DICT]]); +} + +- (void)testDownloadTriggersRemoveExpired { + NSArray *urls = [self.helper generateFileURLStrings:2]; + XCTestExpectation *expectation = [self expectationWithDescription:@"Download triggers remove expired files"]; + long lastDeletedTs = [self.fileDownloader lastDeletedTimestamp]; + // Download files should trigger removeInactiveExpiredAssets with the lastDeletedTimestamp + self.fileDownloader.removeInactiveExpiredAssetsBlock = ^(long lastDeleted) { + XCTAssertEqual(lastDeletedTs, lastDeleted); + [expectation fulfill]; + }; + [self downloadFiles:urls]; + [self waitForExpectations:@[expectation] timeout:1.0]; +} + +- (void)testRemoveExpiredAssetsNoDeletedFiles { + // This block is synchronous + self.fileDownloader.deleteFilesInvokedBlock = ^(NSArray *urls) { + // Delete files should not be invoked if the last deleted time is within the expiry time + XCTFail(); + }; + + long ts = (long)[[NSDate date] timeIntervalSince1970]; + long lastDeleted = ts - 1; + self.fileDownloader.mockCurrentTimeInterval = ts; + self.fileDownloader.urlsExpiry = [@{ + @"url0": @(4) + } mutableCopy]; + [self.fileDownloader removeInactiveExpiredAssets:lastDeleted]; + self.fileDownloader.deleteFilesInvokedBlock = nil; +} + +- (void)testRemoveExpiredAssets { + // Mock the current time + long ts = (long)[[NSDate date] timeIntervalSince1970]; + self.fileDownloader.mockCurrentTimeInterval = ts; + + NSString *expiredUrl1 = @"url0-expired"; + NSString *expiredUrl2 = @"url2-expired"; + + self.fileDownloader.deleteFilesInvokedBlock = ^(NSArray *urls) { + // Delete files is invoked with the expired urls only + NSSet *urlsSet = [NSSet setWithArray:urls]; + NSSet *expected = [NSSet setWithArray:@[expiredUrl1, expiredUrl2]]; + XCTAssertEqualObjects(expected, urlsSet); + }; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Wait for delete completion"]; + __weak CTFileDownloaderTests *weakSelf = self; + // Expired files are deleted + self.fileDownloader.deleteCompletion = ^(NSDictionary * _Nonnull status) { + XCTAssertNotNil([status objectForKey:expiredUrl1]); + XCTAssertNotNil([status objectForKey:expiredUrl2]); + + NSDictionary *urlsExpiry = [@{ + @"url1": @(ts), + @"url3": @(ts + 1) + } mutableCopy]; + + XCTAssertTrue([weakSelf.fileDownloader.urlsExpiry isEqualToDictionary:urlsExpiry]); + [expectation fulfill]; + }; + + // Calculate last deleted to force remove assets + long lastDeleted = ts - self.fileDownloader.fileExpiryTime - 1; + + // Set the urls expiry to have both expired and valid assets + self.fileDownloader.urlsExpiry = [@{ + expiredUrl1: @(ts - 1), + @"url1": @(ts), + expiredUrl2: @(ts - 60), + @"url3": @(ts + 1), + } mutableCopy]; + + [self.fileDownloader removeInactiveExpiredAssets:lastDeleted]; + [self waitForExpectations:@[expectation] timeout:2.0]; + self.fileDownloader.deleteFilesInvokedBlock = nil; +} + +- (void)testUpdateFilesExpiry { + long ts = (long)[[NSDate date] timeIntervalSince1970]; + self.fileDownloader.mockCurrentTimeInterval = ts; + long expiry = ts + self.fileDownloader.fileExpiryTime; + + // url0 and url3 are successful + NSDictionary *status = @{ + @"url0": @(1), + @"url1": @(0), + @"url2": @(0), + @"url3": @(1) + }; + + long previousExpiry = (ts - 100) + self.fileDownloader.fileExpiryTime; + // url3 is not in the expiry dictionary + self.fileDownloader.urlsExpiry = [@{ + @"url0": @(previousExpiry), + @"url2": @(previousExpiry), + @"url4": @(previousExpiry) + } mutableCopy]; + + [self.fileDownloader updateFilesExpiry:status]; + + // Expect url0 to be updated and url3 to be added + NSMutableDictionary *expected = [@{ + @"url0": @(expiry), + @"url2": @(previousExpiry), + @"url3": @(expiry), + @"url4": @(previousExpiry) + } mutableCopy]; + + XCTAssertEqualObjects(expected, self.fileDownloader.urlsExpiry); +} + +- (void)testDeleteFiles { + long ts = (long)[[NSDate date] timeIntervalSince1970]; + self.fileDownloader.mockCurrentTimeInterval = ts; + + // Set 3 files in urlsExpiry + NSArray *urls = [self.helper generateFileURLStrings:3]; + for (NSString *url in urls) { + self.fileDownloader.urlsExpiry[url] = @(ts); + } + + // Assert lastDeletedTimestamp returns current timestamp + XCTAssertEqual(ts, [self.fileDownloader lastDeletedTimestamp]); + // Change the current timestamp + self.fileDownloader.mockCurrentTimeInterval = ts + 100; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Delete files."]; + // Delete 1st and 2nd file (3rd file is not deleted) + [self.fileDownloader deleteFiles:@[urls[0], urls[1]] withCompletionBlock:^(NSDictionary * _Nonnull status) { + // Assert files are deleted + XCTAssertEqualObjects(status[urls[0]], @1); + XCTAssertEqualObjects(status[urls[1]], @1); + + // Assert 1st and 2nd files are removed from urlsExpiry + // Assert 3rd file is still in the urlsExpiry + NSDictionary *expectedExpiry = [@{ + urls[2]: @(ts) + } mutableCopy]; + XCTAssertTrue([expectedExpiry isEqualToDictionary:self.fileDownloader.urlsExpiry]); + + // Assert expiry is updated in preferences + XCTAssertEqualObjects(self.fileDownloader.urlsExpiry, [CTPreferences getObjectForKey:[self.fileDownloader storageKeyWithSuffix:CLTAP_FILE_URLS_EXPIRY_DICT]]); + + // Assert last deleted timestamp is updated + XCTAssertEqual(ts + 100, [self.fileDownloader lastDeletedTimestamp]); + [expectation fulfill]; + }]; + [self waitForExpectations:@[expectation] timeout:2.0]; +} + +- (void)testClearExpiredFiles { + long ts = (long)[[NSDate date] timeIntervalSince1970]; + self.fileDownloader.mockCurrentTimeInterval = ts; + NSArray *urlsExpiry = [self.helper generateFileURLStrings:3]; + for (int i = 1; i < urlsExpiry.count; i++) { + // Set to expired + self.fileDownloader.urlsExpiry[urlsExpiry[i]] = @(ts - 1); + } + // Set non expired + self.fileDownloader.urlsExpiry[urlsExpiry[0]] = @(ts); + XCTestExpectation *expectation = [self expectationWithDescription:@"ClearAllFiles expired only triggers remove expired files"]; + self.fileDownloader.removeInactiveExpiredAssetsBlock = ^(long lastDeleted) { + long expectedForceLastDeleted = (ts - self.fileDownloader.fileExpiryTime) - 1; + XCTAssertEqual(expectedForceLastDeleted, lastDeleted); + XCTAssertTrue(ts - expectedForceLastDeleted > self.fileDownloader.fileExpiryTime); + [expectation fulfill]; + }; + XCTestExpectation *expectation2 = [self expectationWithDescription:@"ClearAllFiles trigger delete files"]; + self.fileDownloader.deleteFilesInvokedBlock = ^(NSArray *urls) { + NSSet *urlsSet = [NSSet setWithArray:urls]; + // Expired URLs are to be deleted + NSSet *expected = [NSSet setWithObjects:urlsExpiry[1], urlsExpiry[2], nil]; + XCTAssertEqualObjects(expected, urlsSet); + [expectation2 fulfill]; + }; + [self.fileDownloader clearFileAssets:YES]; + [self waitForExpectations:@[expectation, expectation2] timeout:2.0]; + self.fileDownloader.deleteFilesInvokedBlock = nil; +} + +- (void)testDownloadAndClearAllFileAssets { + // Mock the current timestamp + long ts = (long)[[NSDate date] timeIntervalSince1970]; + self.fileDownloader.mockCurrentTimeInterval = ts; + + // Download files + NSArray *urls = [self.helper generateFileURLStrings:3]; + [self downloadFiles:urls]; + // Assert expiry is updated + XCTAssertEqual(3, self.fileDownloader.urlsExpiry.count); + + NSMutableArray *paths = [NSMutableArray array]; + for (NSString *url in urls) { + // Assert the files are downloaded + XCTAssertTrue([self.fileDownloader isFileAlreadyPresent:url]); + [paths addObject:[self.fileDownloader fileDownloadPath:url]]; + } + + long lastDeleted = self.fileDownloader.lastDeletedTimestamp; + self.fileDownloader.mockCurrentTimeInterval = lastDeleted + 100; + + // Clear all file assets + XCTestExpectation *expectation = [self expectationWithDescription:@"Clear all assets"]; + __weak CTFileDownloaderTests *weakSelf = self; + self.fileDownloader.removeAllAssetsCompletion = ^(NSDictionary * _Nonnull status) { + // Assert all files status is success + for (NSString *path in paths) { + XCTAssertTrue(status[path]); + } + // Assert the files no longer exist + for (NSString *url in urls) { + XCTAssertFalse([weakSelf.fileDownloader isFileAlreadyPresent:url]); + } + // Assert urlsExpiry is cleared + XCTAssertEqual(0, weakSelf.fileDownloader.urlsExpiry.count); + // Assert the last deleted ts is updated + XCTAssertEqual(lastDeleted + 100, weakSelf.fileDownloader.lastDeletedTimestamp); + [expectation fulfill]; + }; + + [self.fileDownloader clearFileAssets:NO]; + [self waitForExpectations:@[expectation] timeout:2.0]; + self.fileDownloader.removeAllAssetsCompletion = nil; +} + +- (void)testFileDownloadCallbacksWhenFileIsAlreadyDownloading { + // This test checks the file download callbacks when same url is already downloading. + // Verified from adding logs that same url is not downloaded twice if download is in + // progress for that url. Also callbacks are triggered when download is completed. + XCTestExpectation *expectation1 = [self expectationWithDescription:@"Wait for first download callbacks"]; + XCTestExpectation *expectation2 = [self expectationWithDescription:@"Wait for second download callbacks"]; + + NSArray *urls = [self.helper generateFileURLStrings:3]; + [self.fileDownloader downloadFiles:@[urls[0], urls[1], urls[2]] withCompletionBlock:^(NSDictionary * _Nullable status) { + XCTAssertTrue([self.fileDownloader isFileAlreadyPresent:urls[0]]); + XCTAssertTrue([self.fileDownloader isFileAlreadyPresent:urls[1]]); + XCTAssertTrue([self.fileDownloader isFileAlreadyPresent:urls[2]]); + [expectation1 fulfill]; + }]; + [self.fileDownloader downloadFiles:@[urls[0], urls[1]] withCompletionBlock:^(NSDictionary * _Nullable status) { + XCTAssertTrue([self.fileDownloader isFileAlreadyPresent:urls[0]]); + XCTAssertTrue([self.fileDownloader isFileAlreadyPresent:urls[1]]); + [expectation2 fulfill]; + }]; + [self waitForExpectations:@[expectation2, expectation1] timeout:2.0 enforceOrder:YES]; +} + +#pragma mark Private methods + +- (void)downloadFiles:(NSArray *)urls { + XCTestExpectation *expectation = [self expectationWithDescription:@"Download files"]; + [self.fileDownloader downloadFiles:urls withCompletionBlock:^(NSDictionary * _Nullable status) { + [expectation fulfill]; + }]; + + [self waitForExpectations:@[expectation] timeout:2.0]; +} + +@end diff --git a/CleverTapSDKTests/FileDownload/NSFileManagerMock.h b/CleverTapSDKTests/FileDownload/NSFileManagerMock.h new file mode 100644 index 00000000..54b6a713 --- /dev/null +++ b/CleverTapSDKTests/FileDownload/NSFileManagerMock.h @@ -0,0 +1,23 @@ +// +// NSFileManagerMock.h +// CleverTapSDKTests +// +// Created by Nikola Zagorchev on 24.06.24. +// Copyright © 2024 CleverTap. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface NSFileManagerMock : NSFileManager + +@property (nonatomic, strong) NSError *createDirectoryError; +@property (nonatomic, strong) NSError *removeItemError; +@property (nonatomic, strong) NSError *moveItemError; +@property (nonatomic, assign) BOOL fileExists; +@property (nonatomic, strong) NSError *contentsOfDirectoryError; + +@end + +NS_ASSUME_NONNULL_END diff --git a/CleverTapSDKTests/FileDownload/NSFileManagerMock.m b/CleverTapSDKTests/FileDownload/NSFileManagerMock.m new file mode 100644 index 00000000..072e13a2 --- /dev/null +++ b/CleverTapSDKTests/FileDownload/NSFileManagerMock.m @@ -0,0 +1,50 @@ +// +// NSFileManagerMock.m +// CleverTapSDKTests +// +// Created by Nikola Zagorchev on 24.06.24. +// Copyright © 2024 CleverTap. All rights reserved. +// + +#import "NSFileManagerMock.h" + +@implementation NSFileManagerMock + +- (BOOL)fileExistsAtPath:(NSString *)path { + return self.fileExists; +} + +- (BOOL)createDirectoryAtURL:(NSURL *)url withIntermediateDirectories:(BOOL)createIntermediates attributes:(NSDictionary *)attributes error:(NSError **)error { + if (self.createDirectoryError) { + *error = self.createDirectoryError; + return NO; + } + return YES; +} + +- (BOOL)removeItemAtURL:(NSURL *)URL error:(NSError **)error { + if (self.removeItemError) { + *error = self.removeItemError; + return NO; + } + return YES; +} + +- (BOOL)moveItemAtURL:(NSURL *)srcURL toURL:(NSURL *)dstURL error:(NSError **)error { + if (self.moveItemError) { + *error = self.moveItemError; + return NO; + } + return YES; +} + +- (NSArray *)contentsOfDirectoryAtURL:(NSURL *)url includingPropertiesForKeys:(NSArray *)keys options:(NSDirectoryEnumerationOptions)mask error:(NSError *__autoreleasing _Nullable *)error { + if (self.contentsOfDirectoryError) { + *error = self.contentsOfDirectoryError; + return nil; + } + + return [super contentsOfDirectoryAtURL:url includingPropertiesForKeys:keys options:mask error:error]; +} + +@end diff --git a/CleverTapSDKTests/InApps/CTInAppEvaluationManagerTest.m b/CleverTapSDKTests/InApps/CTInAppEvaluationManagerTest.m index cea7a903..eb8c7cd3 100644 --- a/CleverTapSDKTests/InApps/CTInAppEvaluationManagerTest.m +++ b/CleverTapSDKTests/InApps/CTInAppEvaluationManagerTest.m @@ -36,7 +36,7 @@ - (instancetype)initWithNil { inAppFCManager:nil impressionManager:nil inAppStore:nil - imagePrefetchManager:nil]) { + fileDownloader:nil]) { self.inappNotifs = [NSMutableArray new]; } return self; diff --git a/CleverTapSDKTests/InApps/CTInAppFCManagerTest.m b/CleverTapSDKTests/InApps/CTInAppFCManagerTest.m index 56160a59..c2a3aa71 100644 --- a/CleverTapSDKTests/InApps/CTInAppFCManagerTest.m +++ b/CleverTapSDKTests/InApps/CTInAppFCManagerTest.m @@ -33,7 +33,7 @@ @implementation CTInAppFCManagerMock @interface CTInAppFCManagerTest : XCTestCase @property (nonatomic, strong) CTInAppFCManagerMock *inAppFCManager; -@property (nonatomic, strong) CTInAppImagePrefetchManager *prefetchManager; +@property (nonatomic, strong) CTFileDownloader *fileDownloader; @property (nonatomic, strong) InAppHelper *helper; @end @@ -47,7 +47,7 @@ - (void)setUp { // Set to the reset values self.inAppFCManager.globalSessionMax = 1; self.inAppFCManager.maxPerDayCount = 1; - self.prefetchManager = helper.imagePrefetchManager; + self.fileDownloader = helper.fileDownloader; } - (void)tearDown { @@ -75,7 +75,7 @@ - (void)testDidShow { NSDictionary *inApp = @{ @"ti": @1 }; - CTInAppNotification *notif = [[CTInAppNotification alloc] initWithJSON:inApp imagePrefetchManager:self.prefetchManager]; + CTInAppNotification *notif = [[CTInAppNotification alloc] initWithJSON:inApp fileDownloader:self.fileDownloader]; [self.inAppFCManager didShow:notif]; XCTAssertEqual(1, [[self.inAppFCManager.impressionManager getImpressions:@"1"] count]); XCTAssertEqual(1, [self.inAppFCManager shownTodayCount]); @@ -109,7 +109,7 @@ - (void)testRemoveStaleInAppCounts { NSDictionary *inApp = @{ @"ti": @1 }; - CTInAppNotification *notif = [[CTInAppNotification alloc] initWithJSON:inApp imagePrefetchManager:self.prefetchManager]; + CTInAppNotification *notif = [[CTInAppNotification alloc] initWithJSON:inApp fileDownloader:self.fileDownloader]; [self.inAppFCManager didShow:notif]; XCTAssertNotNil(self.inAppFCManager.inAppCounts[@"1"]); @@ -121,7 +121,7 @@ - (void)testRemoveStaleInAppCountsRemovesTriggersAndImpressions { NSDictionary *inApp = @{ @"ti": @1 }; - CTInAppNotification *notif = [[CTInAppNotification alloc] initWithJSON:inApp imagePrefetchManager:self.helper.imagePrefetchManager]; + CTInAppNotification *notif = [[CTInAppNotification alloc] initWithJSON:inApp fileDownloader:self.helper.fileDownloader]; [self.helper.inAppTriggerManager incrementTrigger:@"1"]; [self.inAppFCManager didShow:notif]; XCTAssertNotNil(self.inAppFCManager.inAppCounts[@"1"]); @@ -193,7 +193,7 @@ - (void)testSessionCapacityMaxedOutGlobal { }; self.inAppFCManager.globalSessionMax = 5; - CTInAppNotification *notif = [[CTInAppNotification alloc] initWithJSON:inApp imagePrefetchManager:self.prefetchManager]; + CTInAppNotification *notif = [[CTInAppNotification alloc] initWithJSON:inApp fileDownloader:self.fileDownloader]; XCTAssertEqual(-1, notif.maxPerSession); // Record 4 impressions [self recordImpressions:4]; @@ -221,7 +221,7 @@ - (void)testSessionCapacityMaxedOutGlobalLegacy { }; self.inAppFCManager.globalSessionMax = 5; - CTInAppNotification *notif = [[CTInAppNotification alloc] initWithJSON:inApp imagePrefetchManager:self.prefetchManager]; + CTInAppNotification *notif = [[CTInAppNotification alloc] initWithJSON:inApp fileDownloader:self.fileDownloader]; XCTAssertEqual(1000, notif.maxPerSession); // Record 4 impressions [self recordImpressions:4]; @@ -241,7 +241,7 @@ - (void)testSessionCapacityMaxedOutInApp { NSDictionary *inApp = @{ @"ti": @1 }; - CTInAppNotification *notif = [[CTInAppNotification alloc] initWithJSON:inApp imagePrefetchManager:self.prefetchManager]; + CTInAppNotification *notif = [[CTInAppNotification alloc] initWithJSON:inApp fileDownloader:self.fileDownloader]; self.inAppFCManager.impressionManager.sessionImpressions = [@{} mutableCopy]; // InApp session max will default to -1 on notification level XCTAssertEqual(-1, notif.maxPerSession); @@ -262,7 +262,7 @@ - (void)testSessionCapacityMaxedOutInApp { @"ti": @1, @"mdc": @1 }; - notif = [[CTInAppNotification alloc] initWithJSON:inAppMdc1000 imagePrefetchManager:self.prefetchManager]; + notif = [[CTInAppNotification alloc] initWithJSON:inAppMdc1000 fileDownloader:self.fileDownloader]; XCTAssertEqual(1, notif.maxPerSession); self.inAppFCManager.impressionManager.sessionImpressions = [@{} mutableCopy]; @@ -277,7 +277,7 @@ - (void)testSessionCapacityMaxedOutInApp { @"ti": @1, @"mdc": @3 }; - notif = [[CTInAppNotification alloc] initWithJSON:inAppMdc3 imagePrefetchManager:self.prefetchManager]; + notif = [[CTInAppNotification alloc] initWithJSON:inAppMdc3 fileDownloader:self.fileDownloader]; XCTAssertEqual(3, notif.maxPerSession); self.inAppFCManager.impressionManager.sessionImpressions = [@{ @"1": @2 @@ -310,7 +310,7 @@ - (void)testSessionCapacityMaxedOutInAppLegacy { @"html": @"" } }; - CTInAppNotification *notif = [[CTInAppNotification alloc] initWithJSON:inAppMdc3 imagePrefetchManager:self.prefetchManager]; + CTInAppNotification *notif = [[CTInAppNotification alloc] initWithJSON:inAppMdc3 fileDownloader:self.fileDownloader]; XCTAssertEqual(3, notif.maxPerSession); self.inAppFCManager.impressionManager.sessionImpressions = [@{ @"1": @2 @@ -329,7 +329,7 @@ - (void)testLifetimeCapacityMaxedOut { @"ti": @1, @"tlc": @5 }; - CTInAppNotification *notif = [[CTInAppNotification alloc] initWithJSON:inApp imagePrefetchManager:self.prefetchManager]; + CTInAppNotification *notif = [[CTInAppNotification alloc] initWithJSON:inApp fileDownloader:self.fileDownloader]; XCTAssertEqual(5, notif.totalLifetimeCount); XCTAssertFalse([self.inAppFCManager hasLifetimeCapacityMaxedOut:notif]); @@ -347,7 +347,7 @@ - (void)testDailyCapacityMaxedOutGlobalDefault { NSDictionary *inApp = @{ @"ti": @1 }; - CTInAppNotification *notif = [[CTInAppNotification alloc] initWithJSON:inApp imagePrefetchManager:self.prefetchManager]; + CTInAppNotification *notif = [[CTInAppNotification alloc] initWithJSON:inApp fileDownloader:self.fileDownloader]; XCTAssertEqual(-1, notif.totalDailyCount); XCTAssertFalse([self.inAppFCManager hasDailyCapacityMaxedOut:notif]); // Max Default is 1 @@ -361,7 +361,7 @@ - (void)testDailyCapacityMaxedOutGlobal { @"ti": @1, @"tdc": @10 }; - CTInAppNotification *notif = [[CTInAppNotification alloc] initWithJSON:inApp imagePrefetchManager:self.prefetchManager]; + CTInAppNotification *notif = [[CTInAppNotification alloc] initWithJSON:inApp fileDownloader:self.fileDownloader]; XCTAssertEqual(10, notif.totalDailyCount); XCTAssertFalse([self.inAppFCManager hasDailyCapacityMaxedOut:notif]); @@ -380,7 +380,7 @@ - (void)testDailyCapacityMaxedOutInApp { @"ti": @1, @"tdc": @5 }; - CTInAppNotification *notif = [[CTInAppNotification alloc] initWithJSON:inApp imagePrefetchManager:self.prefetchManager]; + CTInAppNotification *notif = [[CTInAppNotification alloc] initWithJSON:inApp fileDownloader:self.fileDownloader]; XCTAssertEqual(5, notif.totalDailyCount); // Record 4 impressions @@ -403,7 +403,7 @@ - (void)testHasInAppFrequencyLimitsMaxedOut { } ] }; - CTInAppNotification *notif = [[CTInAppNotification alloc] initWithJSON:inApp imagePrefetchManager:self.prefetchManager]; + CTInAppNotification *notif = [[CTInAppNotification alloc] initWithJSON:inApp fileDownloader:self.fileDownloader]; // 1 impression is in limit [self.inAppFCManager.impressionManager recordImpression:notif.Id]; @@ -421,7 +421,7 @@ - (void)testCanShowExcludeCaps { @"efc": @1, @"tdc": @1 }; - CTInAppNotification *notif = [[CTInAppNotification alloc] initWithJSON:inApp imagePrefetchManager:self.prefetchManager]; + CTInAppNotification *notif = [[CTInAppNotification alloc] initWithJSON:inApp fileDownloader:self.fileDownloader]; [self.inAppFCManager recordImpression:notif.Id]; XCTAssertTrue([self.inAppFCManager canShow:notif]); @@ -433,7 +433,7 @@ - (void)testCanShowExcludeGlobalCaps { @"excludeGlobalFCaps": @1, @"tdc": @1 }; - CTInAppNotification *notif = [[CTInAppNotification alloc] initWithJSON:inApp imagePrefetchManager:self.prefetchManager]; + CTInAppNotification *notif = [[CTInAppNotification alloc] initWithJSON:inApp fileDownloader:self.fileDownloader]; [self.inAppFCManager recordImpression:notif.Id]; XCTAssertTrue([self.inAppFCManager canShow:notif]); @@ -444,7 +444,7 @@ - (void)testCanShowMaxedOut { @"ti": @1, @"tdc": @1 }; - CTInAppNotification *notif = [[CTInAppNotification alloc] initWithJSON:inApp imagePrefetchManager:self.prefetchManager]; + CTInAppNotification *notif = [[CTInAppNotification alloc] initWithJSON:inApp fileDownloader:self.fileDownloader]; [self.inAppFCManager recordImpression:notif.Id]; XCTAssertFalse([self.inAppFCManager canShow:notif]); diff --git a/CleverTapSDKTests/InApps/CTInAppImagePrefetchManager+Tests.h b/CleverTapSDKTests/InApps/CTInAppImagePrefetchManager+Tests.h deleted file mode 100644 index a9925444..00000000 --- a/CleverTapSDKTests/InApps/CTInAppImagePrefetchManager+Tests.h +++ /dev/null @@ -1,20 +0,0 @@ -// -// CTInAppImagePrefetchManager+Tests.h -// CleverTapSDKTests -// -// Created by Nikola Zagorchev on 9.01.24. -// Copyright © 2024 CleverTap. All rights reserved. -// - -@interface CTInAppImagePrefetchManager (Tests) - -@property (nonatomic, strong) NSMutableSet *activeImageSet; -@property (nonatomic, strong) NSMutableSet *inactiveImageSet; - -- (void)prefetchURLs:(NSArray *)mediaURLs; -- (NSString *)storageKeyWithSuffix:(NSString *)suffix; -- (NSArray *)getImageURLs:(NSArray *)csInAppNotifs; -- (void)removeInactiveExpiredAssets:(long)lastDeletedTime; -- (long)getLastDeletedTimestamp; - -@end diff --git a/CleverTapSDKTests/InApps/CTInAppImagePrefetchManagerTest.m b/CleverTapSDKTests/InApps/CTInAppImagePrefetchManagerTest.m deleted file mode 100644 index 495268a1..00000000 --- a/CleverTapSDKTests/InApps/CTInAppImagePrefetchManagerTest.m +++ /dev/null @@ -1,295 +0,0 @@ -#import -#import -#import "CTInAppImagePrefetchManager.h" -#import "InAppHelper.h" -#import "CTPreferences.h" -#import "CTConstants.h" -#import -#import "CTInAppImagePrefetchManager+Tests.h" - -NSString * const imageURLMatch = @"ct_test_image"; - -NSString * const imageResourcePath = @"clevertap-logo"; -NSString * const imageResourceType = @"png"; - -@interface CTInAppImagePrefetchManagerTest : XCTestCase -@property (nonatomic, strong) CTInAppImagePrefetchManager *prefetchManager; -@end - -@implementation CTInAppImagePrefetchManagerTest - -- (void)setUp { - [super setUp]; - InAppHelper *helper = [InAppHelper new]; - self.prefetchManager = helper.imagePrefetchManager; - // Stub the image download request - [HTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { - // Match requests with ct_test_image - return [request.URL.absoluteString containsString:imageURLMatch]; - } withStubResponse:^HTTPStubsResponse*(NSURLRequest *request) { - // Load a local image instead of making the actual request - NSString *imagePath = [[NSBundle bundleForClass:[self class]] pathForResource:imageResourcePath ofType:imageResourceType]; - NSData *imageData = [NSData dataWithContentsOfFile:imagePath]; - - return [HTTPStubsResponse responseWithData:imageData statusCode:200 headers:nil]; - }]; -} - -- (void)tearDown { - [super tearDown]; - [HTTPStubs removeAllStubs]; - [CTPreferences removeObjectForKey:[self.prefetchManager storageKeyWithSuffix:CLTAP_PREFS_CS_INAPP_ASSETS_LAST_DELETED_TS]]; - - XCTestExpectation *expectation = [self expectationWithDescription:@"Wait for cleanup"]; - [self.prefetchManager _clearImageAssets:NO]; - dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC); - dispatch_after(delay, dispatch_get_main_queue(), ^(void){ - [expectation fulfill]; - }); - - [self waitForExpectations:@[expectation] timeout:2.5]; -} - -- (void)preloadImagesToDisk:(NSArray *)urls { - XCTestExpectation *expectation = [self expectationWithDescription:@"Preload images"]; - // Preload Images - [self.prefetchManager prefetchURLs:urls]; - - dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC); - dispatch_after(delay, dispatch_get_main_queue(), ^(void){ - [expectation fulfill]; - }); - - [self waitForExpectations:@[expectation] timeout:2.5]; -} - -- (NSArray *)generateImageURLs:(int)count { - NSMutableArray *arr = [NSMutableArray new]; - for (int i = 0; i < count; i++) { - [arr addObject:[[NSString alloc] initWithFormat:@"https://clevertap.com/%@%d.%@", - imageURLMatch, i, imageResourceType]]; - } - return arr; -} - -- (void)setLastDeletedPastExpiry { - // Expiration is 2 weeks - // Set the last deleted timestamp to 15 days ago - long ts = (long) [[NSDate date] timeIntervalSince1970] - (60 * 60 * 24 * (2 * 7 + 1)); - [CTPreferences putInt:ts - forKey:[self.prefetchManager storageKeyWithSuffix:CLTAP_PREFS_CS_INAPP_ASSETS_LAST_DELETED_TS]]; -} - -- (void)testGetImageURLs { - NSArray *urls = [self generateImageURLs:4]; - NSArray *csInAppNotifs = @[ - @{ - @"media": @{ - @"content_type": @"image/jpeg", - @"url": urls[0], - } - }, - @{ - @"media": @{ - @"content_type": @"image/gif", - @"url": urls[1], - } - }, - @{ - @"mediaLandscape": @{ - @"content_type": @"image/jpeg", - @"url": urls[2] - } - }, - @{ - @"mediaLandscape": @{ - @"content_type": @"image/gif", - @"url": urls[3] - } - } - ]; - - NSArray *imageUrls = [self.prefetchManager getImageURLs:csInAppNotifs]; - XCTAssertEqual([imageUrls count], [urls count]); -} - -- (void)testImagePresentInDiskCache { - // Check image is present in disk cache - NSArray *urls = [self generateImageURLs:1]; - [self preloadImagesToDisk:urls]; - UIImage *image = [self.prefetchManager loadImageFromDisk:urls[0]]; - XCTAssertNotNil(image); -} - -- (void)testPreloadClientSideInAppImages { - NSArray *urls = [self generateImageURLs:2]; - NSArray *csInAppNotifs = @[ - @{ - @"media": @{ - @"content_type": @"image/jpeg", - @"url": urls[0], - } - }, - @{ - @"media": @{ - @"content_type": @"image/jpeg", - @"url": urls[1] - } - } - ]; - XCTestExpectation *expectation = [self expectationWithDescription:@"Image preload to disk cache"]; - // Preload Images - [self.prefetchManager preloadClientSideInAppImages:csInAppNotifs]; - - dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC); - dispatch_after(delay, dispatch_get_main_queue(), ^(void) { - XCTAssertEqual([[self.prefetchManager activeImageSet] count], 2); - XCTAssertNotNil([self.prefetchManager loadImageFromDisk:urls[0]]); - XCTAssertNotNil([self.prefetchManager loadImageFromDisk:urls[1]]); - [expectation fulfill]; - }); - - [self waitForExpectations:@[expectation] timeout:2.5]; -} - -- (void)testPreloadImages { - // 1. Test preloading images adds to active assets - NSArray *urls = [self generateImageURLs:3]; - // Load the images to disk, new images are directly added to the active set - [self preloadImagesToDisk:@[urls[0], urls[1]]]; - XCTAssertEqual([[self.prefetchManager activeImageSet] count], 2); - - // 2. Test preloading already present images removes them from inactive assets - // Set the inactive URLs set - NSMutableSet *urlsSet = [[NSMutableSet alloc] initWithArray:urls]; - [self.prefetchManager setInactiveImageSet:urlsSet]; - - // Load the images to disk again, so the already saved ones are removed from the inactive set - [self preloadImagesToDisk:@[urls[0], urls[1]]]; - - XCTAssertEqual([[self.prefetchManager activeImageSet] count], 2); - XCTAssertEqual([[self.prefetchManager inactiveImageSet] count], 1); - - // 3. Test preloading images triggers remove of expired assets when expiration time has come - [self setLastDeletedPastExpiry]; - [self preloadImagesToDisk:@[urls[0]]]; - // Active assets are moved to inactive to start expiration again - XCTAssertEqual([[self.prefetchManager activeImageSet] count], 0); - XCTAssertEqual([[self.prefetchManager inactiveImageSet] count], 2); -} - -- (void)testClearAllImageAssets { - // Save the image - NSArray *urls = [self generateImageURLs:1]; - [self preloadImagesToDisk:urls]; - UIImage *image = [self.prefetchManager loadImageFromDisk:urls[0]]; - XCTAssertNotNil(image); - - [self.prefetchManager _clearImageAssets:NO]; - XCTestExpectation *expectation = [self expectationWithDescription:@"Clear all assets"]; - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC), dispatch_get_main_queue(), ^(void) { - UIImage *image = [self.prefetchManager loadImageFromDisk:urls[0]]; - XCTAssertNil(image); - [expectation fulfill]; - }); - - [self waitForExpectations:@[expectation] timeout:2.5]; -} - -- (void)testClearInactiveImageAssets { - // Save the image - NSArray *urls = [self generateImageURLs:2]; - [self preloadImagesToDisk:urls]; - - NSMutableSet *urlsSet = [[NSMutableSet alloc] initWithArray:@[urls[0]]]; - [self.prefetchManager setInactiveImageSet:urlsSet]; - - [self setLastDeletedPastExpiry]; - [self.prefetchManager _clearImageAssets:YES]; - XCTestExpectation *expectation = [self expectationWithDescription:@"Clear inactive assets"]; - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC), dispatch_get_main_queue(), ^(void) { - UIImage *inactiveImage = [self.prefetchManager loadImageFromDisk:urls[0]]; - XCTAssertNil(inactiveImage); - UIImage *activeImage = [self.prefetchManager loadImageFromDisk:urls[1]]; - XCTAssertNotNil(activeImage); - [expectation fulfill]; - }); - - [self waitForExpectations:@[expectation] timeout:2.5]; -} - -- (void)testSetImageAssetsInactiveAndClearExpired { - // Save the image - NSArray *urls = [self generateImageURLs:2]; - [self preloadImagesToDisk:urls]; - XCTAssertEqual([[self.prefetchManager inactiveImageSet] count], 0); - - [self.prefetchManager setImageAssetsInactiveAndClearExpired]; - // Last deleted date has not passed, images are moved to inactive assets only - XCTAssertEqual([[self.prefetchManager inactiveImageSet] count], 2); - - [self setLastDeletedPastExpiry]; - [self.prefetchManager setImageAssetsInactiveAndClearExpired]; - XCTestExpectation *expectation = [self expectationWithDescription:@"Clear disk cache"]; - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC), dispatch_get_main_queue(), ^(void) { - XCTAssertEqual([[self.prefetchManager inactiveImageSet] count], 0); - XCTAssertEqual([[self.prefetchManager activeImageSet] count], 0); - XCTAssertNil([self.prefetchManager loadImageFromDisk:urls[0]]); - XCTAssertNil([self.prefetchManager loadImageFromDisk:urls[1]]); - [expectation fulfill]; - }); - - [self waitForExpectations:@[expectation] timeout:2.5]; -} - -- (void)testRemoveInactiveExpiredAssetsUpdatesDeletedTs { - long ts = (long) [[NSDate date] timeIntervalSince1970] - (60 * 60 * 24 * (2 * 7 + 1)); - [CTPreferences putInt:ts - forKey:[self.prefetchManager storageKeyWithSuffix:CLTAP_PREFS_CS_INAPP_ASSETS_LAST_DELETED_TS]]; - NSMutableSet *urlsSet = [[NSMutableSet alloc] initWithArray:[self generateImageURLs:4]]; - [self.prefetchManager setActiveImageSet:urlsSet]; - // Timestamp is expired, will trigger delete of expired assets - // There are no inactive assets to be removed - // Last deleted timestamp must be updated - // Active assets must be moved to inactive - XCTAssertEqual([[self.prefetchManager inactiveImageSet] count], 0); - [self.prefetchManager removeInactiveExpiredAssets:ts]; - XCTestExpectation *expectation = [self expectationWithDescription:@"Remove expired assets"]; - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC), dispatch_get_main_queue(), ^(void) { - XCTAssertEqual([urlsSet count], [[self.prefetchManager inactiveImageSet] count]); - XCTAssertEqual(0, [[self.prefetchManager activeImageSet] count]); - XCTAssertNotEqual(ts, [self.prefetchManager getLastDeletedTimestamp]); - // Must be set to current timestamp (now and set timestamp might differ slightly) - XCTAssertTrue([[NSDate date] timeIntervalSince1970] - [self.prefetchManager getLastDeletedTimestamp] < 3); - [expectation fulfill]; - }); - - [self waitForExpectations:@[expectation] timeout:1.5]; -} - -- (void)testRemoveInactiveExpiredAssetsNoop { - long ts = (long) [[NSDate date] timeIntervalSince1970] - (60 * 60 * 24 * 12); // 12 days - [CTPreferences putInt:ts - forKey:[self.prefetchManager storageKeyWithSuffix:CLTAP_PREFS_CS_INAPP_ASSETS_LAST_DELETED_TS]]; - NSArray *urls = [self generateImageURLs:5]; - NSMutableSet *urlsSetActive = [[NSMutableSet alloc] initWithArray:@[urls[0], urls[1]]]; - [self.prefetchManager setActiveImageSet:urlsSetActive]; - NSMutableSet *urlsSetInactive = [[NSMutableSet alloc] initWithArray:@[urls[2], urls[3], urls[4]]]; - [self.prefetchManager setInactiveImageSet:urlsSetInactive]; - // Timestamp is not expired, will not trigger delete of expired assets - // No assets must be removed - // Active and Inactive assets must remain the same - // Delete timestamp must not change - [self.prefetchManager removeInactiveExpiredAssets:ts]; - XCTestExpectation *expectation = [self expectationWithDescription:@"Remove expired assets"]; - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC), dispatch_get_main_queue(), ^(void) { - XCTAssertEqual(ts, [self.prefetchManager getLastDeletedTimestamp]); - XCTAssertEqual([urlsSetActive count], [[self.prefetchManager activeImageSet] count]); - XCTAssertEqual([urlsSetInactive count], [[self.prefetchManager inactiveImageSet] count]); - [expectation fulfill]; - }); - - [self waitForExpectations:@[expectation] timeout:1.5]; -} - -@end diff --git a/CleverTapSDKTests/InApps/CTInAppStoreTest.m b/CleverTapSDKTests/InApps/CTInAppStoreTest.m index 70d5f10f..3da66752 100644 --- a/CleverTapSDKTests/InApps/CTInAppStoreTest.m +++ b/CleverTapSDKTests/InApps/CTInAppStoreTest.m @@ -330,7 +330,7 @@ - (void)testInAppsQueue { - (void)testSwitchUserDelegateAdded { CTMultiDelegateManager *delegateManager = [[CTMultiDelegateManager alloc] init]; NSUInteger count = [[delegateManager switchUserDelegates] count]; - __unused CTInAppStore *store = [[CTInAppStore alloc] initWithConfig:self.helper.config delegateManager:delegateManager imagePrefetchManager:self.helper.imagePrefetchManager deviceId:self.helper.deviceId]; + __unused CTInAppStore *store = [[CTInAppStore alloc] initWithConfig:self.helper.config delegateManager:delegateManager fileDownloader:self.helper.fileDownloader deviceId:self.helper.deviceId]; XCTAssertEqual([[delegateManager switchUserDelegates] count], count + 1); } diff --git a/CleverTapSDKTests/InApps/InAppHelper.h b/CleverTapSDKTests/InApps/InAppHelper.h index 196f71d1..0a782204 100644 --- a/CleverTapSDKTests/InApps/InAppHelper.h +++ b/CleverTapSDKTests/InApps/InAppHelper.h @@ -15,7 +15,7 @@ @class CleverTapInstanceConfig; @class CTMultiDelegateManager; @class CTInAppTriggerManager; -@class CTInAppImagePrefetchManager; +@class CTFileDownloader; NS_ASSUME_NONNULL_BEGIN @@ -34,7 +34,7 @@ extern NSString *const CLTAP_TEST_CAMPAIGN_ID; @property (nonatomic, strong) CTImpressionManager *impressionManager; @property (nonatomic, strong) CTInAppStore *inAppStore; @property (nonatomic, strong) CTInAppTriggerManager *inAppTriggerManager; -@property (nonatomic, strong) CTInAppImagePrefetchManager *imagePrefetchManager; +@property (nonatomic, strong) CTFileDownloader *fileDownloader; - (NSString *)accountId; - (NSString *)accountToken; diff --git a/CleverTapSDKTests/InApps/InAppHelper.m b/CleverTapSDKTests/InApps/InAppHelper.m index 1b38c358..30745d62 100644 --- a/CleverTapSDKTests/InApps/InAppHelper.m +++ b/CleverTapSDKTests/InApps/InAppHelper.m @@ -15,7 +15,7 @@ #import "CleverTapInstanceConfig.h" #import "CTInAppFCManager.h" #import "CTInAppTriggerManager.h" -#import "CTInAppImagePrefetchManager.h" +#import "CTFileDownloader.h" NSString *const CLTAP_TEST_ACCOUNT_ID = @"testAccountId"; NSString *const CLTAP_TEST_ACCOUNT_TOKEN = @"testAccountToken"; @@ -47,7 +47,7 @@ - (instancetype)init { self.config = [[CleverTapInstanceConfig alloc] initWithAccountId:self.accountId accountToken:self.accountToken]; - self.imagePrefetchManager = [[CTInAppImagePrefetchManager alloc] initWithConfig:self.config]; + self.fileDownloader = [[CTFileDownloader alloc] initWithConfig:self.config]; self.impressionManager = [[CTImpressionManager alloc] initWithAccountId:self.accountId deviceId:self.deviceId @@ -55,7 +55,7 @@ - (instancetype)init { self.inAppStore = [[CTInAppStore alloc] initWithConfig:self.config delegateManager:self.delegateManager - imagePrefetchManager:self.imagePrefetchManager + fileDownloader:self.fileDownloader deviceId:self.deviceId]; self.inAppTriggerManager = [[CTInAppTriggerManager alloc] initWithAccountId:self.accountId diff --git a/CleverTapSDKTests/Stub Responses/samplePDFStub.pdf b/CleverTapSDKTests/Stub Responses/samplePDFStub.pdf new file mode 100644 index 0000000000000000000000000000000000000000..774c2ea70c55104973794121eae56bcad918da97 GIT binary patch literal 13264 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 literal 0 HcmV?d00001 diff --git a/CleverTapSDKTests/Stub Responses/sampleTXTStub.txt b/CleverTapSDKTests/Stub Responses/sampleTXTStub.txt new file mode 100644 index 00000000..552c384a --- /dev/null +++ b/CleverTapSDKTests/Stub Responses/sampleTXTStub.txt @@ -0,0 +1 @@ +This is sample text file