diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index fedc6de563..23ae0e95d4 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -149,7 +149,7 @@ 351B519F26D4508A00BD2BD7 /* DeviceCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E35D87B7E6F91E27E98F42 /* DeviceCacheTests.swift */; }; 351B51A326D450BC00BD2BD7 /* DictionaryExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DD269162522A20A006AC4BC /* DictionaryExtensionsTests.swift */; }; 351B51A426D450BC00BD2BD7 /* NSError+RCExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E353AF2CAD3CEDE6D9B368 /* NSError+RCExtensionsTests.swift */; }; - 351B51A526D450BC00BD2BD7 /* NSDate+RCExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E3508EC20EEBAB4EAC4C82 /* NSDate+RCExtensionsTests.swift */; }; + 351B51A526D450BC00BD2BD7 /* DateExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E3508EC20EEBAB4EAC4C82 /* DateExtensionsTests.swift */; }; 351B51A726D450D400BD2BD7 /* SystemInfoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E35EEE7783629CDE41B70C /* SystemInfoTests.swift */; }; 351B51B526D450E800BD2BD7 /* ProductsFetcherSK1Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E351F0E21361EAEC078A0D /* ProductsFetcherSK1Tests.swift */; }; 351B51B626D450E800BD2BD7 /* ReceiptFetcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D84458826B9CD270033B5A3 /* ReceiptFetcherTests.swift */; }; @@ -300,6 +300,7 @@ 578DAA482948EEAD001700FD /* Clock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 578DAA472948EEAD001700FD /* Clock.swift */; }; 578DAA4A2948EF4F001700FD /* TestClock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 578DAA492948EF4F001700FD /* TestClock.swift */; }; 578FB10E27ADDA8000F70709 /* AvailabilityChecks.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3BE0263275942D500915B4C /* AvailabilityChecks.swift */; }; + 57910CB329C3889B006209D5 /* DispatchTimeIntervalExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57910CB229C3889B006209D5 /* DispatchTimeIntervalExtensionsTests.swift */; }; 579189B728F4747700BF4963 /* EitherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 579189B628F4747700BF4963 /* EitherTests.swift */; }; 579189E928F47E8D00BF4963 /* PurchasesDiagnosticsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 579189E828F47E8D00BF4963 /* PurchasesDiagnosticsTests.swift */; }; 579189EB28F47F0F00BF4963 /* MockPurchases.swift in Sources */ = {isa = PBXBuildFile; fileRef = 579189EA28F47F0F00BF4963 /* MockPurchases.swift */; }; @@ -779,7 +780,7 @@ 37E350420D54B99BB39448E0 /* AttributionTypeFactoryTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttributionTypeFactoryTests.swift; sourceTree = ""; }; 37E3507939634ED5A9280544 /* Strings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = ""; }; 37E3508E52201122137D4B4A /* PurchasesSubscriberAttributesTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PurchasesSubscriberAttributesTests.swift; sourceTree = ""; }; - 37E3508EC20EEBAB4EAC4C82 /* NSDate+RCExtensionsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSDate+RCExtensionsTests.swift"; sourceTree = ""; }; + 37E3508EC20EEBAB4EAC4C82 /* DateExtensionsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DateExtensionsTests.swift; sourceTree = ""; }; 37E35092F0E41512E0D610BA /* ContainerFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContainerFactory.swift; sourceTree = ""; }; 37E350E57B0A393455A72B40 /* ProductRequestDataTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProductRequestDataTests.swift; sourceTree = ""; }; 37E351D0EBC4698E1D3585A6 /* ReceiptParserTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReceiptParserTests.swift; sourceTree = ""; }; @@ -916,6 +917,7 @@ 578D79932936B0810042E434 /* LoggerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggerTests.swift; sourceTree = ""; }; 578DAA472948EEAD001700FD /* Clock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Clock.swift; sourceTree = ""; }; 578DAA492948EF4F001700FD /* TestClock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestClock.swift; sourceTree = ""; }; + 57910CB229C3889B006209D5 /* DispatchTimeIntervalExtensionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DispatchTimeIntervalExtensionsTests.swift; sourceTree = ""; }; 57910CBD29C393A6006209D5 /* CI-RevenueCat.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; name = "CI-RevenueCat.xctestplan"; path = "Tests/TestPlans/CI-RevenueCat.xctestplan"; sourceTree = ""; }; 579189B628F4747700BF4963 /* EitherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EitherTests.swift; sourceTree = ""; }; 579189E828F47E8D00BF4963 /* PurchasesDiagnosticsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchasesDiagnosticsTests.swift; sourceTree = ""; }; @@ -1837,11 +1839,12 @@ children = ( 572247F627BF1ADF00C524A7 /* ArrayExtensionsTests.swift */, 57069A5D28E398E100B86355 /* AsyncExtensionsTests.swift */, + 37E3508EC20EEBAB4EAC4C82 /* DateExtensionsTests.swift */, 37E35FDA0A44EA03EA12DAA2 /* DateFormatter+ExtensionsTests.swift */, 57ACB13628184CF1000DCC9F /* DecoderExtensionTests.swift */, 2DD269162522A20A006AC4BC /* DictionaryExtensionsTests.swift */, + 57910CB229C3889B006209D5 /* DispatchTimeIntervalExtensionsTests.swift */, 37E35ABEE9FD79CCA64E4F8B /* NSData+RCExtensionsTests.swift */, - 37E3508EC20EEBAB4EAC4C82 /* NSDate+RCExtensionsTests.swift */, 37E353AF2CAD3CEDE6D9B368 /* NSError+RCExtensionsTests.swift */, 5766AA41283C768600FA6091 /* OperatorExtensionsTests.swift */, 5759B321296DEF56002472D5 /* OptionalExtensionsTests.swift */, @@ -3042,6 +3045,7 @@ 573A10D52800A7C800F976E5 /* SKErrorTests.swift in Sources */, B31C8BEC285BD58F001017B7 /* MockIdentityAPI.swift in Sources */, 351B513D26D4491E00BD2BD7 /* MockDeviceCache.swift in Sources */, + 57910CB329C3889B006209D5 /* DispatchTimeIntervalExtensionsTests.swift in Sources */, 351B515C26D44B7900BD2BD7 /* MockIntroEligibilityCalculator.swift in Sources */, 5733D01128D00354008638D8 /* PromotionalOfferTests.swift in Sources */, 57544C28285FA94B004E54D5 /* MockAttributeSyncing.swift in Sources */, @@ -3114,7 +3118,7 @@ 573A10D92800ADCD00F976E5 /* StoreKitErrorTests.swift in Sources */, 5766AABF283E80B500FA6091 /* BasePurchasesTests.swift in Sources */, 351B515826D44B3E00BD2BD7 /* MockOfferingsFactory.swift in Sources */, - 351B51A526D450BC00BD2BD7 /* NSDate+RCExtensionsTests.swift in Sources */, + 351B51A526D450BC00BD2BD7 /* DateExtensionsTests.swift in Sources */, B390F5BA271DDC7400B64D65 /* PurchasesDeprecation.swift in Sources */, 57FDAABE28493A29009A48F1 /* SandboxEnvironmentDetectorTests.swift in Sources */, 57BA943128330ACA00CD5FC5 /* ConfigurationTests.swift in Sources */, diff --git a/Sources/FoundationExtensions/Date+Extensions.swift b/Sources/FoundationExtensions/Date+Extensions.swift index 6af695f08e..fcea8581b4 100644 --- a/Sources/FoundationExtensions/Date+Extensions.swift +++ b/Sources/FoundationExtensions/Date+Extensions.swift @@ -14,26 +14,15 @@ import Foundation -extension NSDate { - - func millisecondsSince1970AsUInt64() -> UInt64 { - return (self as Date).millisecondsSince1970AsUInt64() - } - -} - extension Date { - init(millisecondsSince1970: Int) { + init(millisecondsSince1970: UInt64) { self.init(timeIntervalSince1970: TimeInterval(millisecondsSince1970) / 1000) } - var millisecondsSince1970: Int { - return Int(self.timeIntervalSince1970 * 1000.0) - } - - func millisecondsSince1970AsUInt64() -> UInt64 { - return UInt64(self.millisecondsSince1970) + /// - Important: this needs to be 64 bits because `Int` is 32 bits in watchOS + var millisecondsSince1970: UInt64 { + return UInt64(self.timeIntervalSince1970 * 1000) } } diff --git a/Sources/FoundationExtensions/DispatchTimeInterval+Extensions.swift b/Sources/FoundationExtensions/DispatchTimeInterval+Extensions.swift index c6f8f41e84..1f6e574807 100644 --- a/Sources/FoundationExtensions/DispatchTimeInterval+Extensions.swift +++ b/Sources/FoundationExtensions/DispatchTimeInterval+Extensions.swift @@ -29,12 +29,25 @@ extension DispatchTimeInterval { /// `DispatchTimeInterval` can only be used by specifying a unit of time. /// This allows us to easily convert any `DispatchTimeInterval` into nanoseconds. - var nanoseconds: Int { + /// - Important: It's likely that `x * 1_000_000_000` can't be represented in 32 bits. + var nanoseconds: UInt64 { switch self { - case let .seconds(s): return s * 1_000_000_000 - case let .milliseconds(ms): return ms * 1_000_000 - case let .microseconds(ms): return ms * 1000 - case let .nanoseconds(ns): return ns + case let .seconds(s): return UInt64(s) * UInt64(1_000_000_000) + case let .milliseconds(ms): return UInt64(ms) * UInt64(1_000_000) + case let .microseconds(ms): return UInt64(ms) * UInt64(1000) + case let .nanoseconds(ns): return UInt64(ns) + case .never: return 0 + @unknown default: fatalError("Unknown value: \(self)") + } + } + + /// - Note: this returns `Int`, so it might lose precision for `.milliseconds` and `.microseconds`. + var milliseconds: Int { + switch self { + case let .seconds(s): return s * 1_000 + case let .milliseconds(ms): return ms + case let .microseconds(ms): return Int((Double(ms) / 1_000).rounded()) + case let .nanoseconds(ns): return Int((Double(ns) / 1_000_000).rounded()) case .never: return 0 @unknown default: fatalError("Unknown value: \(self)") } @@ -45,7 +58,7 @@ extension DispatchTimeInterval { var seconds: Double { switch self { case let .seconds(seconds): return Double(seconds) - case let .milliseconds(ms): return Double(ms) / 1000 + case let .milliseconds(ms): return Double(ms) / 1_000 case let .microseconds(ms): return Double(ms) / 1_000_000 case let .nanoseconds(ns): return Double(ns) / 1_000_000_000 case .never: return 0 @@ -62,11 +75,15 @@ extension DispatchTimeInterval { // swiftlint:enable identifier_name func + (lhs: DispatchTimeInterval, rhs: DispatchTimeInterval) -> DispatchTimeInterval { - return .nanoseconds(lhs.nanoseconds + rhs.nanoseconds) + // Note: `DispatchTimeInterval` uses `Int` for nanoseconds, which might overflow in 32 bits + // This loses some precision by using milliseconds, but avoids potential overflows. + return .milliseconds(lhs.milliseconds + rhs.milliseconds) } func - (lhs: DispatchTimeInterval, rhs: DispatchTimeInterval) -> DispatchTimeInterval { - return .nanoseconds(lhs.nanoseconds - rhs.nanoseconds) + // Note: `DispatchTimeInterval` uses `Int` for nanoseconds, which might overflow in 32 bits + // This loses some precision by using milliseconds, but avoids potential overflows. + return .milliseconds(lhs.milliseconds - rhs.milliseconds) } extension DispatchTimeInterval: Comparable { diff --git a/Sources/Security/Signing+ResponseVerification.swift b/Sources/Security/Signing+ResponseVerification.swift index 715be1f755..9b7ec3c358 100644 --- a/Sources/Security/Signing+ResponseVerification.swift +++ b/Sources/Security/Signing+ResponseVerification.swift @@ -156,7 +156,7 @@ extension HTTPResponse { forCaseInsensitiveHeaderField: HTTPClient.ResponseHeader.requestDate.rawValue, in: headers ), - let intValue = Int(stringValue) else { return nil } + let intValue = UInt64(stringValue) else { return nil } return .init(millisecondsSince1970: intValue) } diff --git a/Sources/Security/Signing.swift b/Sources/Security/Signing.swift index 8631ca5e8d..56cd1fad6b 100644 --- a/Sources/Security/Signing.swift +++ b/Sources/Security/Signing.swift @@ -36,7 +36,7 @@ enum Signing: SigningType { let message: Data let nonce: Data - let requestDate: Int + let requestDate: UInt64 } diff --git a/Sources/SubscriberAttributes/SubscriberAttribute.swift b/Sources/SubscriberAttributes/SubscriberAttribute.swift index 4acad40f89..3fcc18dd51 100644 --- a/Sources/SubscriberAttributes/SubscriberAttribute.swift +++ b/Sources/SubscriberAttributes/SubscriberAttribute.swift @@ -57,7 +57,7 @@ extension SubscriberAttribute { func asBackendDictionary() -> [String: Any] { return [BackendKey.value.rawValue: self.value, - BackendKey.timestamp.rawValue: self.setTime.millisecondsSince1970AsUInt64()] + BackendKey.timestamp.rawValue: self.setTime.millisecondsSince1970] } } diff --git a/Tests/UnitTests/FoundationExtensions/NSDate+RCExtensionsTests.swift b/Tests/UnitTests/FoundationExtensions/DateExtensionsTests.swift similarity index 65% rename from Tests/UnitTests/FoundationExtensions/NSDate+RCExtensionsTests.swift rename to Tests/UnitTests/FoundationExtensions/DateExtensionsTests.swift index ee5e171437..665f0ae255 100644 --- a/Tests/UnitTests/FoundationExtensions/NSDate+RCExtensionsTests.swift +++ b/Tests/UnitTests/FoundationExtensions/DateExtensionsTests.swift @@ -8,18 +8,18 @@ import XCTest @testable import RevenueCat -class NSDateExtensionsTests: TestCase { +class DateExtensionsTests: TestCase { func testMillisecondsSince1970ConvertsCorrectlyWithCurrentTime() { - let date = NSDate() - expect(date.millisecondsSince1970AsUInt64()) == (UInt64)(date.timeIntervalSince1970 * 1000) + let date = Date() + expect(date.millisecondsSince1970) == UInt64(date.timeIntervalSince1970 * 1000) } func testMillisecondsSince1970ConvertsCorrectlyWithFixedTime() { let secondsSince1970: TimeInterval = 1619555571.0 let millisecondsSince1970UInt64: UInt64 = 1619555571000 - let date = NSDate(timeIntervalSince1970: secondsSince1970) - expect(date.millisecondsSince1970AsUInt64()) == millisecondsSince1970UInt64 + let date = Date(timeIntervalSince1970: secondsSince1970) + expect(date.millisecondsSince1970) == millisecondsSince1970UInt64 } func testMillisecondsSince1970() { diff --git a/Tests/UnitTests/FoundationExtensions/DispatchTimeIntervalExtensionsTests.swift b/Tests/UnitTests/FoundationExtensions/DispatchTimeIntervalExtensionsTests.swift new file mode 100644 index 0000000000..d18e8281ed --- /dev/null +++ b/Tests/UnitTests/FoundationExtensions/DispatchTimeIntervalExtensionsTests.swift @@ -0,0 +1,87 @@ +// +// Copyright RevenueCat Inc. All Rights Reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// DispatchTimeIntervalExtensionsTests.swift +// +// Created by Nacho Soto on 3/16/23. + +import Nimble +import XCTest + +@testable import RevenueCat + +// swiftlint:disable identifier_name + +class DispatchTimeIntervalExtensionsTests: TestCase { + + func testIsLessThan() { + let a: DispatchTimeInterval = .seconds(1) + let b: DispatchTimeInterval = .seconds(2) + + expect(a) < b + } + + func testIsGreaterThan() { + let a: DispatchTimeInterval = .seconds(2) + let b: DispatchTimeInterval = .seconds(1) + + expect(a) > b + } + + func testSecondsToMilliseconds() { + expect(DispatchTimeInterval.seconds(5).milliseconds) == 5_000 + } + + func testSecondsToNanoseconds() { + expect(DispatchTimeInterval.seconds(5).nanoseconds) == 5_000_000_000 + } + + func testAddingSeconds() { + let a: DispatchTimeInterval = .seconds(2) + let b: DispatchTimeInterval = .seconds(1) + + expect(a + b) == .seconds(3) + } + + func testAddingMilliseconds() { + let a: DispatchTimeInterval = .milliseconds(2) + let b: DispatchTimeInterval = .milliseconds(1) + + expect(a + b) == .milliseconds(3) + } + + func testAddingNanoseconds() { + let a: DispatchTimeInterval = .nanoseconds(2_000_000) + let b: DispatchTimeInterval = .nanoseconds(1_000_000) + + expect(a + b) == .milliseconds(3) + } + + func testSubstractingSeconds() { + let a: DispatchTimeInterval = .seconds(3) + let b: DispatchTimeInterval = .seconds(1) + + expect(a - b) == .seconds(2) + } + + func testSubstractingMilliseconds() { + let a: DispatchTimeInterval = .milliseconds(3) + let b: DispatchTimeInterval = .milliseconds(1) + + expect(a - b) == .milliseconds(2) + } + + func testSubstractingNanoseconds() { + let a: DispatchTimeInterval = .nanoseconds(3_000_000) + let b: DispatchTimeInterval = .nanoseconds(1_000_000) + + expect(a - b) == .milliseconds(2) + } + +} diff --git a/Tests/UnitTests/Security/SigningTests.swift b/Tests/UnitTests/Security/SigningTests.swift index c1b8eca750..fe758366c8 100644 --- a/Tests/UnitTests/Security/SigningTests.swift +++ b/Tests/UnitTests/Security/SigningTests.swift @@ -68,7 +68,7 @@ class SigningTests: TestCase { let message = "Hello World" let nonce = "nonce" - let requestDate = 1677005916012 + let requestDate: UInt64 = 1677005916012 let signature = "this is not a signature" expect(Signing.verify( @@ -113,7 +113,7 @@ class SigningTests: TestCase { func testVerifySignatureWithValidSignature() throws { let message = "Hello World" let nonce = "nonce" - let requestDate = 1677005916012 + let requestDate: UInt64 = 1677005916012 let salt = Self.createSalt() let signature = try self.sign( @@ -155,7 +155,7 @@ class SigningTests: TestCase { // swiftlint:enable line_length let nonce = try XCTUnwrap(Data(base64Encoded: "MTIzNDU2Nzg5MGFi")) - let requestDate = 1677005916012 + let requestDate: UInt64 = 1677005916012 expect( Signing.verify( @@ -182,7 +182,7 @@ class SigningTests: TestCase { */ let nonce = try XCTUnwrap(Data(base64Encoded: "MTIzNDU2Nzg5MGFi")) - let requestDate = 1677013582768 + let requestDate: UInt64 = 1677013582768 let eTag = "b7bd9a697c7fd1a2" // swiftlint:disable:next line_length diff --git a/Tests/UnitTests/SubscriberAttributes/SubscriberAttributeTests.swift b/Tests/UnitTests/SubscriberAttributes/SubscriberAttributeTests.swift index 75a6d8b472..431d61cf32 100644 --- a/Tests/UnitTests/SubscriberAttributes/SubscriberAttributeTests.swift +++ b/Tests/UnitTests/SubscriberAttributes/SubscriberAttributeTests.swift @@ -40,7 +40,7 @@ class SubscriberAttributeTests: TestCase { expect(receivedDictionary["value"] as? String) == Self.mockAttribute.value expect((receivedDictionary["updated_at_ms"] as? NSNumber)?.uint64Value) - == Self.mockAttribute.setTime.millisecondsSince1970AsUInt64() + == Self.mockAttribute.setTime.millisecondsSince1970 } func testInitWithDictionarySetsRightValues() throws {