From d9de068bcaff80b7cff999d34af3af6c333e88ce Mon Sep 17 00:00:00 2001 From: Alex Baker Date: Mon, 15 Jul 2024 22:30:08 -0700 Subject: [PATCH] Resolve issue where Dates passed as input that contain a microsecond portion would cause API exceptions --- Sources/AppStoreServerLibrary/Utility.swift | 10 +++++++--- .../AppStoreServerAPIClientTests.swift | 19 +++++++++++++++++++ .../XcodeSignedDataVerifierTests.swift | 15 ++++++++++++--- 3 files changed, 38 insertions(+), 6 deletions(-) diff --git a/Sources/AppStoreServerLibrary/Utility.swift b/Sources/AppStoreServerLibrary/Utility.swift index 6d612ef..df60cfd 100644 --- a/Sources/AppStoreServerLibrary/Utility.swift +++ b/Sources/AppStoreServerLibrary/Utility.swift @@ -19,7 +19,11 @@ internal func getJsonDecoder() -> JSONDecoder { } internal func getJsonEncoder() -> JSONEncoder { - let decoder = JSONEncoder() - decoder.dateEncodingStrategy = .millisecondsSince1970 - return decoder + let encoder = JSONEncoder() + encoder.dateEncodingStrategy = .custom({ date, e in + // To encode the same as millisecondsSince1970, however truncating the decimal part + var container = e.singleValueContainer() + try container.encode((date.timeIntervalSince1970 * 1000.0).rounded(.towardZero)) + }) + return encoder } diff --git a/Tests/AppStoreServerLibraryTests/AppStoreServerAPIClientTests.swift b/Tests/AppStoreServerLibraryTests/AppStoreServerAPIClientTests.swift index 5c7b384..4513f71 100644 --- a/Tests/AppStoreServerLibraryTests/AppStoreServerAPIClientTests.swift +++ b/Tests/AppStoreServerLibraryTests/AppStoreServerAPIClientTests.swift @@ -246,6 +246,25 @@ final class AppStoreServerAPIClientTests: XCTestCase { TestingUtility.confirmCodableInternallyConsistent(notificationHistoryResponse) } + public func testGetNotificationHistoryWithMicrosecondValues() async throws { + let client = try getClientWithBody("resources/models/getNotificationHistoryResponse.json") { request, body in + let decodedJson = try! JSONSerialization.jsonObject(with: body!) as! [String: Any] + XCTAssertEqual(1698148900000, decodedJson["startDate"] as! Int) + XCTAssertEqual(1698148950000, decodedJson["endDate"] as! Int) + } + + let notificationHistoryRequest = NotificationHistoryRequest( + startDate: Date(timeIntervalSince1970: 1698148900).advanced(by: 0.000_9), // 900 microseconds + endDate: Date(timeIntervalSince1970: 1698148950).advanced(by: 0.000_001), // 1 microsecond + notificationType: NotificationTypeV2.subscribed, + notificationSubtype: Subtype.initialBuy, + transactionId: "999733843", + onlyFailures: true + ) + + let _ = await client.getNotificationHistory(paginationToken: "a036bc0e-52b8-4bee-82fc-8c24cb6715d6", notificationHistoryRequest: notificationHistoryRequest) + } + public func testGetTransactionHistoryV1() async throws { let client = try getClientWithBody("resources/models/transactionHistoryResponse.json") { request, body in XCTAssertEqual(.GET, request.method) diff --git a/Tests/AppStoreServerLibraryTests/XcodeSignedDataVerifierTests.swift b/Tests/AppStoreServerLibraryTests/XcodeSignedDataVerifierTests.swift index 2de6a64..67c711b 100644 --- a/Tests/AppStoreServerLibraryTests/XcodeSignedDataVerifierTests.swift +++ b/Tests/AppStoreServerLibraryTests/XcodeSignedDataVerifierTests.swift @@ -30,7 +30,7 @@ final class XcodeSignedDataVerifierTests: XCTestCase { XCTAssertNil(appTransaction.preorderDate) XCTAssertEqual(.xcode, appTransaction.receiptType) XCTAssertEqual("Xcode", appTransaction.rawReceiptType) - TestingUtility.confirmCodableInternallyConsistent(appTransaction) + confirmCodableInternallyConsistentForXcode(appTransaction) } public func testXcodeSignedTransaction() async throws { @@ -72,7 +72,7 @@ final class XcodeSignedDataVerifierTests: XCTestCase { XCTAssertEqual("143441", transaction.storefrontId) XCTAssertEqual(TransactionReason.purchase, transaction.transactionReason) XCTAssertEqual("PURCHASE", transaction.rawTransactionReason) - TestingUtility.confirmCodableInternallyConsistent(transaction) + confirmCodableInternallyConsistentForXcode(transaction) } public func testXcodeSignedRenewalInfo() async throws { @@ -102,7 +102,7 @@ final class XcodeSignedDataVerifierTests: XCTestCase { XCTAssertEqual("Xcode", renewalInfo.rawEnvironment) compareXcodeDates(Date(timeIntervalSince1970: 1697679936.049), renewalInfo.recentSubscriptionStartDate) compareXcodeDates(Date(timeIntervalSince1970: 1700358336.049), renewalInfo.renewalDate) - TestingUtility.confirmCodableInternallyConsistent(renewalInfo) + confirmCodableInternallyConsistentForXcode(renewalInfo) } public func testXcodeSignedAppTransactionWithProductionEnvironment() async throws { @@ -122,4 +122,13 @@ final class XcodeSignedDataVerifierTests: XCTestCase { private func compareXcodeDates(_ first: Date, _ second: Date?) { XCTAssertEqual(floor((first.timeIntervalSince1970 * 1000)), floor(((second?.timeIntervalSince1970 ?? 0.0) * 1000))) } + + private func confirmCodableInternallyConsistentForXcode(_ codable: T) where T : Codable, T : Equatable { + let type = type(of: codable) + let encoder = JSONEncoder() + // Xcode receipts contain a decimal value, we encode the value as encoded in those receipts + encoder.dateEncodingStrategy = .millisecondsSince1970 + let parsedValue = try! getJsonDecoder().decode(type, from: encoder.encode(codable)) + XCTAssertEqual(parsedValue, codable) + } }