From 7fa63ca8ea1b11399d8e613bbd4db4962238ab26 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega <664544+vegaro@users.noreply.github.com> Date: Fri, 24 Jul 2026 17:38:10 +0200 Subject: [PATCH 1/4] test: cover custom paywall tracking, ad tracker, and model parsing Adds TrackingTests.cs for custom paywall parameter resolution and ad-tracker forwarding/JSON serialization. Expands JsonModelTests.cs with full/minimal CustomerInfo and SubscriptionOption payloads, the epoch-vs-null date quirk, unrecognized-enum fallback, and WebPurchaseRedemption / WebPurchaseRedemptionResult variants. --- .../Assets/Tests/EditMode/JsonModelTests.cs | 209 ++++++++++++++ .../Assets/Tests/EditMode/TrackingTests.cs | 261 ++++++++++++++++++ .../Tests/EditMode/TrackingTests.cs.meta | 11 + 3 files changed, 481 insertions(+) create mode 100644 IntegrationTests/Assets/Tests/EditMode/TrackingTests.cs create mode 100644 IntegrationTests/Assets/Tests/EditMode/TrackingTests.cs.meta diff --git a/IntegrationTests/Assets/Tests/EditMode/JsonModelTests.cs b/IntegrationTests/Assets/Tests/EditMode/JsonModelTests.cs index 2dca5455..2486f03c 100644 --- a/IntegrationTests/Assets/Tests/EditMode/JsonModelTests.cs +++ b/IntegrationTests/Assets/Tests/EditMode/JsonModelTests.cs @@ -1,3 +1,4 @@ +using System; using NUnit.Framework; using RevenueCat.SimpleJSON; @@ -5,6 +6,14 @@ namespace RevenueCat.Tests { public class JsonModelTests { + private const string MinimalCustomerInfoJson = + "{\"entitlements\":{\"all\":{},\"active\":{}}," + + "\"activeSubscriptions\":[],\"allPurchasedProductIdentifiers\":[]," + + "\"firstSeenMillis\":1700000000000,\"originalAppUserId\":\"user_1\"," + + "\"requestDateMillis\":1700000000001,\"allExpirationDatesMillis\":{}," + + "\"allPurchaseDatesMillis\":{},\"nonSubscriptionTransactions\":[]," + + "\"subscriptionsByProductIdentifier\":{}}"; + [Test] public void VirtualCurrenciesParsesCurrenciesByCode() { @@ -67,5 +76,205 @@ public void StoreProductFallsBackToUnknownProductCategory() Assert.That(product.DefaultOption, Is.Null); Assert.That(product.Discounts, Is.Null); } + + [Test] + public void CustomerInfoParsesFullPayload() + { + var response = JSONNode.Parse( + "{\"entitlements\":{" + + "\"all\":{\"premium\":{\"identifier\":\"premium\",\"isActive\":true,\"willRenew\":true," + + "\"periodType\":\"NORMAL\",\"latestPurchaseDateMillis\":1700000000000," + + "\"originalPurchaseDateMillis\":1690000000000,\"store\":\"APP_STORE\"," + + "\"productIdentifier\":\"monthly\",\"isSandbox\":false}}," + + "\"active\":{\"premium\":{\"identifier\":\"premium\",\"isActive\":true,\"willRenew\":true," + + "\"periodType\":\"NORMAL\",\"latestPurchaseDateMillis\":1700000000000," + + "\"originalPurchaseDateMillis\":1690000000000,\"store\":\"APP_STORE\"," + + "\"productIdentifier\":\"monthly\",\"isSandbox\":false}}}," + + "\"activeSubscriptions\":[\"monthly\"]," + + "\"allPurchasedProductIdentifiers\":[\"monthly\",\"lifetime\"]," + + "\"firstSeenMillis\":1700000000000,\"originalAppUserId\":\"user_1\"," + + "\"requestDateMillis\":1700000000001," + + "\"originalPurchaseDateMillis\":1690000000000," + + "\"latestExpirationDateMillis\":1720000000000," + + "\"managementURL\":\"https://mgmt\"," + + "\"allExpirationDatesMillis\":{\"monthly\":1720000000000,\"lifetime\":0}," + + "\"allPurchaseDatesMillis\":{\"monthly\":1700000000000,\"lifetime\":0}," + + "\"originalApplicationVersion\":\"1.0\"," + + "\"nonSubscriptionTransactions\":[{\"transactionIdentifier\":\"txn_1\"," + + "\"productIdentifier\":\"lifetime\",\"purchaseDateMillis\":1700000000000}]," + + "\"subscriptionsByProductIdentifier\":{\"monthly\":{\"productIdentifier\":\"monthly\"," + + "\"purchaseDate\":\"2023-01-01T00:00:00Z\",\"store\":\"APP_STORE\",\"isSandbox\":false," + + "\"periodType\":\"NORMAL\",\"isActive\":true,\"willRenew\":true}}}" + ); + + var customerInfo = new Purchases.CustomerInfo(response); + + Assert.That(customerInfo.OriginalAppUserId, Is.EqualTo("user_1")); + Assert.That(customerInfo.Entitlements.All["premium"].IsActive, Is.True); + Assert.That(customerInfo.Entitlements.Active.ContainsKey("premium"), Is.True); + Assert.That(customerInfo.ActiveSubscriptions, Is.EquivalentTo(new[] { "monthly" })); + Assert.That(customerInfo.AllPurchasedProductIdentifiers, Has.Count.EqualTo(2)); + Assert.That(customerInfo.OriginalPurchaseDate, Is.Not.Null); + Assert.That(customerInfo.LatestExpirationDate, Is.Not.Null); + Assert.That(customerInfo.ManagementURL, Is.EqualTo("https://mgmt")); + Assert.That(customerInfo.OriginalApplicationVersion, Is.EqualTo("1.0")); + Assert.That(customerInfo.NonSubscriptionTransactions, Has.Count.EqualTo(1)); + Assert.That(customerInfo.NonSubscriptionTransactions[0].ProductIdentifier, Is.EqualTo("lifetime")); + Assert.That(customerInfo.SubscriptionsByProductIdentifier["monthly"].ProductIdentifier, + Is.EqualTo("monthly")); + + // A millis value of exactly 0 is indistinguishable from an absent date and is parsed as null. + Assert.That(customerInfo.AllExpirationDates["monthly"], Is.Not.Null); + Assert.That(customerInfo.AllExpirationDates["lifetime"], Is.Null); + Assert.That(customerInfo.AllPurchaseDates["monthly"], Is.Not.Null); + Assert.That(customerInfo.AllPurchaseDates["lifetime"], Is.Null); + } + + [Test] + public void SubscriptionOptionParsesFullPayloadWithPricingPhases() + { + const string pricingPhaseJson = + "{\"billingPeriod\":{\"unit\":\"MONTH\",\"value\":1,\"iso8601\":\"P1M\"}," + + "\"recurrenceMode\":\"INFINITE_RECURRING\",\"billingCycleCount\":0," + + "\"price\":{\"formatted\":\"$9.99\",\"amountMicros\":9990000,\"currencyCode\":\"USD\"}," + + "\"offerPaymentMode\":\"SINGLE_PAYMENT\"}"; + + var response = JSONNode.Parse( + "{\"id\":\"monthly:base\",\"storeProductId\":\"monthly\",\"productId\":\"monthly\"," + + "\"tags\":[\"tag1\"],\"isBasePlan\":true," + + "\"billingPeriod\":{\"unit\":\"MONTH\",\"value\":1,\"iso8601\":\"P1M\"},\"isPrepaid\":false," + + "\"pricingPhases\":[" + pricingPhaseJson + "]," + + "\"fullPricePhase\":" + pricingPhaseJson + "," + + "\"presentedOfferingContext\":{\"offeringIdentifier\":\"default\"}," + + "\"installmentsInfo\":{\"commitmentPaymentsCount\":3,\"renewalCommitmentPaymentsCount\":1}}" + ); + + var option = new Purchases.SubscriptionOption(response); + + Assert.That(option.Tags, Is.EquivalentTo(new[] { "tag1" })); + Assert.That(option.PricingPhases, Has.Length.EqualTo(1)); + Assert.That(option.FullPricePhase, Is.Not.Null); + Assert.That(option.FullPricePhase.RecurrenceMode, + Is.EqualTo(Purchases.SubscriptionOption.RecurrenceMode.INFINITE_RECURRING)); + Assert.That(option.PresentedOfferingContext, Is.Not.Null); + Assert.That(option.PresentedOfferingContext.OfferingIdentifier, Is.EqualTo("default")); + Assert.That(option.OptionInstallmentsInfo, Is.Not.Null); + Assert.That(option.OptionInstallmentsInfo.CommitmentPaymentsCount, Is.EqualTo(3)); + } + + [Test] + public void SubscriptionOptionParsesMinimalPayloadWithoutOptionalFields() + { + var response = JSONNode.Parse( + "{\"id\":\"monthly:base\",\"storeProductId\":\"monthly\",\"productId\":\"monthly\"," + + "\"tags\":[],\"isBasePlan\":true," + + "\"billingPeriod\":{\"unit\":\"MONTH\",\"value\":1,\"iso8601\":\"P1M\"},\"isPrepaid\":false}" + ); + + var option = new Purchases.SubscriptionOption(response); + + Assert.That(option.PricingPhases, Is.Null); + Assert.That(option.FullPricePhase, Is.Null); + Assert.That(option.FreePhase, Is.Null); + Assert.That(option.IntroPhase, Is.Null); + Assert.That(option.PresentedOfferingContext, Is.Null); + Assert.That(option.OptionInstallmentsInfo, Is.Null); + } + + [Test] + public void SubscriptionOptionPricingPhaseFallsBackToUnknownForUnrecognizedEnums() + { + var response = JSONNode.Parse( + "{\"id\":\"monthly:base\",\"storeProductId\":\"monthly\",\"productId\":\"monthly\"," + + "\"tags\":[],\"isBasePlan\":true," + + "\"billingPeriod\":{\"unit\":\"MONTH\",\"value\":1,\"iso8601\":\"P1M\"},\"isPrepaid\":false," + + "\"fullPricePhase\":{\"billingPeriod\":{\"unit\":\"MONTH\",\"value\":1,\"iso8601\":\"P1M\"}," + + "\"recurrenceMode\":\"BOGUS\",\"billingCycleCount\":0," + + "\"price\":{\"formatted\":\"$9.99\",\"amountMicros\":9990000,\"currencyCode\":\"USD\"}," + + "\"offerPaymentMode\":\"BOGUS\"}}" + ); + + var option = new Purchases.SubscriptionOption(response); + + Assert.That(option.FullPricePhase.RecurrenceMode, + Is.EqualTo(Purchases.SubscriptionOption.RecurrenceMode.UNKNOWN)); + Assert.That(option.FullPricePhase.OfferPaymentMode, + Is.EqualTo(Purchases.SubscriptionOption.OfferPaymentMode.UNKNOWN)); + } + + [Test] + public void WebPurchaseRedemptionWrapsRedemptionLink() + { + var redemption = new Purchases.WebPurchaseRedemption("https://rev.cat/redeem/abc"); + + Assert.That(redemption.RedemptionLink, Is.EqualTo("https://rev.cat/redeem/abc")); + } + + [Test] + public void WebPurchaseRedemptionResultParsesSuccessVariant() + { + var response = JSONNode.Parse( + "{\"result\":\"SUCCESS\",\"customerInfo\":" + MinimalCustomerInfoJson + "}" + ); + + var result = Purchases.WebPurchaseRedemptionResult.FromJson(response); + + Assert.That(result, Is.InstanceOf()); + Assert.That(((Purchases.WebPurchaseRedemptionResult.Success)result).CustomerInfo, Is.Not.Null); + } + + [Test] + public void WebPurchaseRedemptionResultParsesErrorVariant() + { + var response = JSONNode.Parse( + "{\"result\":\"ERROR\",\"error\":{\"message\":\"Redemption failed\",\"code\":11," + + "\"underlyingErrorMessage\":\"Backend rejected\",\"readableErrorCode\":\"UNKNOWN_ERROR\"}}" + ); + + var result = Purchases.WebPurchaseRedemptionResult.FromJson(response); + + Assert.That(result, Is.InstanceOf()); + Assert.That(((Purchases.WebPurchaseRedemptionResult.RedemptionError)result).Error.Code, Is.EqualTo(11)); + } + + [Test] + public void WebPurchaseRedemptionResultParsesInvalidTokenVariant() + { + var response = JSONNode.Parse("{\"result\":\"INVALID_TOKEN\"}"); + + var result = Purchases.WebPurchaseRedemptionResult.FromJson(response); + + Assert.That(result, Is.SameAs(Purchases.WebPurchaseRedemptionResult.InvalidToken.Instance)); + } + + [Test] + public void WebPurchaseRedemptionResultParsesExpiredVariant() + { + var response = JSONNode.Parse("{\"result\":\"EXPIRED\",\"obfuscatedEmail\":\"a***@b.com\"}"); + + var result = Purchases.WebPurchaseRedemptionResult.FromJson(response); + + Assert.That(result, Is.InstanceOf()); + Assert.That(((Purchases.WebPurchaseRedemptionResult.Expired)result).ObfuscatedEmail, + Is.EqualTo("a***@b.com")); + } + + [Test] + public void WebPurchaseRedemptionResultParsesPurchaseBelongsToOtherUserVariant() + { + var response = JSONNode.Parse("{\"result\":\"PURCHASE_BELONGS_TO_OTHER_USER\"}"); + + var result = Purchases.WebPurchaseRedemptionResult.FromJson(response); + + Assert.That(result, Is.SameAs(Purchases.WebPurchaseRedemptionResult.PurchaseBelongsToOtherUser.Instance)); + } + + [Test] + public void WebPurchaseRedemptionResultThrowsForUnrecognizedResultType() + { + var response = JSONNode.Parse("{\"result\":\"SOMETHING_ELSE\"}"); + + Assert.Throws(() => Purchases.WebPurchaseRedemptionResult.FromJson(response)); + } } } diff --git a/IntegrationTests/Assets/Tests/EditMode/TrackingTests.cs b/IntegrationTests/Assets/Tests/EditMode/TrackingTests.cs new file mode 100644 index 00000000..1ba8fa2a --- /dev/null +++ b/IntegrationTests/Assets/Tests/EditMode/TrackingTests.cs @@ -0,0 +1,261 @@ +using NUnit.Framework; +using RevenueCat; +using RevenueCat.SimpleJSON; +using UnityEngine; + +namespace RevenueCat.Tests +{ + public class TrackingTests + { + private GameObject _gameObject; + private Purchases _purchases; + private PurchasesWrapperSpy _wrapper; + + [SetUp] + public void SetUp() + { + _gameObject = new GameObject("RevenueCatTests"); + _purchases = _gameObject.AddComponent(); + _wrapper = new PurchasesWrapperSpy(); + _purchases.SetWrapper(_wrapper); + } + + [TearDown] + public void TearDown() + { + Object.DestroyImmediate(_gameObject); + } + + [Test] + public void TrackCustomPaywallImpressionWithoutParametersCreatesEmptyParameters() + { + _purchases.TrackCustomPaywallImpression(); + + var invocation = AssertLastInvocation(nameof(IPurchasesWrapper.TrackCustomPaywallImpression), 1); + var parameters = (Purchases.CustomPaywallImpressionParams)invocation.Arguments[0]; + Assert.That(parameters.PaywallId, Is.Null); + Assert.That(parameters.OfferingId, Is.Null); + Assert.That(parameters.Offering, Is.Null); + } + + [Test] + public void TrackCustomPaywallImpressionWithPaywallIdOnlySetsPaywallIdOnly() + { + _purchases.TrackCustomPaywallImpression(new Purchases.CustomPaywallImpressionParams("paywall_1")); + + var invocation = AssertLastInvocation(nameof(IPurchasesWrapper.TrackCustomPaywallImpression), 1); + var parameters = (Purchases.CustomPaywallImpressionParams)invocation.Arguments[0]; + Assert.That(parameters.PaywallId, Is.EqualTo("paywall_1")); + Assert.That(parameters.OfferingId, Is.Null); + Assert.That(parameters.Offering, Is.Null); + } + + [Test] + public void TrackCustomPaywallImpressionWithOfferingResolvesOfferingIdAndContext() + { + var offering = CreateOfferingWithPackage(); + + _purchases.TrackCustomPaywallImpression(new Purchases.CustomPaywallImpressionParams(offering)); + + var invocation = AssertLastInvocation(nameof(IPurchasesWrapper.TrackCustomPaywallImpression), 1); + var parameters = (Purchases.CustomPaywallImpressionParams)invocation.Arguments[0]; + Assert.That(parameters.PaywallId, Is.Null); + Assert.That(parameters.Offering, Is.SameAs(offering)); + Assert.That(parameters.OfferingId, Is.EqualTo(offering.Identifier)); + } + + [Test] + public void CustomPaywallImpressionParamsResolvesPresentedOfferingContextFromFirstPackage() + { + var offering = CreateOfferingWithPackage(); + + var parameters = new Purchases.CustomPaywallImpressionParams(offering); + + Assert.That(parameters.PresentedOfferingContext, Is.Not.Null); + Assert.That(parameters.PresentedOfferingContext.OfferingIdentifier, Is.EqualTo("default")); + } + + [Test] + public void CustomPaywallImpressionParamsHasNullPresentedOfferingContextWhenOfferingHasNoPackages() + { + var offering = new Purchases.Offering(JSONNode.Parse( + "{\"identifier\":\"default\",\"serverDescription\":\"desc\",\"availablePackages\":[]}")); + + var parameters = new Purchases.CustomPaywallImpressionParams(offering); + + Assert.That(parameters.PresentedOfferingContext, Is.Null); + } + + [Test] + public void TrackCustomPaywallImpressionWithPaywallIdAndOfferingSetsBoth() + { + var offering = CreateOfferingWithPackage(); + + _purchases.TrackCustomPaywallImpression(new Purchases.CustomPaywallImpressionParams("paywall_1", offering)); + + var invocation = AssertLastInvocation(nameof(IPurchasesWrapper.TrackCustomPaywallImpression), 1); + var parameters = (Purchases.CustomPaywallImpressionParams)invocation.Arguments[0]; + Assert.That(parameters.PaywallId, Is.EqualTo("paywall_1")); + Assert.That(parameters.OfferingId, Is.EqualTo(offering.Identifier)); + } + + [Test] + [System.Obsolete] + public void TrackCustomPaywallImpressionDeprecatedCtorUsesExplicitOfferingId() + { + _purchases.TrackCustomPaywallImpression( + new Purchases.CustomPaywallImpressionParams("paywall_1", "offering_1")); + + var invocation = AssertLastInvocation(nameof(IPurchasesWrapper.TrackCustomPaywallImpression), 1); + var parameters = (Purchases.CustomPaywallImpressionParams)invocation.Arguments[0]; + Assert.That(parameters.PaywallId, Is.EqualTo("paywall_1")); + Assert.That(parameters.OfferingId, Is.EqualTo("offering_1")); + Assert.That(parameters.Offering, Is.Null); + } + + [Test] + public void TrackAdDisplayedForwardsDataToWrapper() + { + var data = new AdDisplayedData(AdTracker.MediatorName.AdMob, AdTracker.Format.Banner, "unit_1", "impression_1"); + + _purchases.AdTracker.TrackAdDisplayed(data); + + var invocation = AssertLastInvocation(nameof(IPurchasesWrapper.TrackAdDisplayed), 1); + Assert.That(invocation.Arguments[0], Is.SameAs(data)); + } + + [Test] + public void TrackAdOpenedForwardsDataToWrapper() + { + var data = new AdOpenedData(AdTracker.MediatorName.AppLovin, AdTracker.Format.Interstitial, "unit_1", "impression_1"); + + _purchases.AdTracker.TrackAdOpened(data); + + var invocation = AssertLastInvocation(nameof(IPurchasesWrapper.TrackAdOpened), 1); + Assert.That(invocation.Arguments[0], Is.SameAs(data)); + } + + [Test] + public void TrackAdRevenueForwardsDataToWrapper() + { + var data = new AdRevenueData(AdTracker.MediatorName.AdMob, AdTracker.Format.Rewarded, "unit_1", + "impression_1", 1500000L, "USD", AdTracker.Precision.Exact); + + _purchases.AdTracker.TrackAdRevenue(data); + + var invocation = AssertLastInvocation(nameof(IPurchasesWrapper.TrackAdRevenue), 1); + Assert.That(invocation.Arguments[0], Is.SameAs(data)); + } + + [Test] + public void TrackAdLoadedForwardsDataToWrapper() + { + var data = new AdLoadedData(AdTracker.MediatorName.AdMob, AdTracker.Format.Native, "unit_1", "impression_1"); + + _purchases.AdTracker.TrackAdLoaded(data); + + var invocation = AssertLastInvocation(nameof(IPurchasesWrapper.TrackAdLoaded), 1); + Assert.That(invocation.Arguments[0], Is.SameAs(data)); + } + + [Test] + public void TrackAdFailedToLoadForwardsDataToWrapper() + { + var data = new AdFailedToLoadData(AdTracker.MediatorName.AdMob, AdTracker.Format.Banner, "unit_1"); + + _purchases.AdTracker.TrackAdFailedToLoad(data); + + var invocation = AssertLastInvocation(nameof(IPurchasesWrapper.TrackAdFailedToLoad), 1); + Assert.That(invocation.Arguments[0], Is.SameAs(data)); + } + + [Test] + public void AdDisplayedDataToJsonStringOmitsOptionalFieldsWhenNull() + { + var data = new AdDisplayedData(AdTracker.MediatorName.AdMob, AdTracker.Format.Banner, "unit_1", "impression_1"); + + var json = JSONNode.Parse(data.ToJsonString()); + + Assert.That(json["mediatorName"].Value, Is.EqualTo("AdMob")); + Assert.That(json["adFormat"].Value, Is.EqualTo("banner")); + Assert.That(json["adUnitId"].Value, Is.EqualTo("unit_1")); + Assert.That(json["impressionId"].Value, Is.EqualTo("impression_1")); + Assert.That(json.HasKey("networkName"), Is.False); + Assert.That(json.HasKey("placement"), Is.False); + } + + [Test] + public void AdDisplayedDataToJsonStringIncludesOptionalFieldsWhenPresent() + { + var data = new AdDisplayedData(AdTracker.MediatorName.AppLovin, AdTracker.Format.Interstitial, "unit_1", + "impression_1", "network_1", "placement_1"); + + var json = JSONNode.Parse(data.ToJsonString()); + + Assert.That(json["networkName"].Value, Is.EqualTo("network_1")); + Assert.That(json["placement"].Value, Is.EqualTo("placement_1")); + } + + [Test] + public void AdRevenueDataToJsonStringIncludesRevenueFields() + { + var data = new AdRevenueData(AdTracker.MediatorName.AdMob, AdTracker.Format.Rewarded, "unit_1", + "impression_1", 1500000L, "USD", AdTracker.Precision.Exact); + + var json = JSONNode.Parse(data.ToJsonString()); + + Assert.That(json["revenueMicros"].AsLong, Is.EqualTo(1500000L)); + Assert.That(json["currency"].Value, Is.EqualTo("USD")); + Assert.That(json["precision"].Value, Is.EqualTo("exact")); + Assert.That(json.HasKey("networkName"), Is.False); + } + + [Test] + public void AdFailedToLoadDataToJsonStringOmitsOptionalFieldsWhenAbsent() + { + var data = new AdFailedToLoadData(AdTracker.MediatorName.AdMob, AdTracker.Format.Banner, "unit_1"); + + var json = JSONNode.Parse(data.ToJsonString()); + + Assert.That(json["mediatorName"].Value, Is.EqualTo("AdMob")); + Assert.That(json["adFormat"].Value, Is.EqualTo("banner")); + Assert.That(json["adUnitId"].Value, Is.EqualTo("unit_1")); + Assert.That(json.HasKey("impressionId"), Is.False); + Assert.That(json.HasKey("placement"), Is.False); + Assert.That(json.HasKey("mediatorErrorCode"), Is.False); + } + + [Test] + public void AdFailedToLoadDataToJsonStringIncludesOptionalFieldsWhenPresent() + { + var data = new AdFailedToLoadData(AdTracker.MediatorName.AdMob, AdTracker.Format.Banner, "unit_1", + "placement_1", 42); + + var json = JSONNode.Parse(data.ToJsonString()); + + Assert.That(json["placement"].Value, Is.EqualTo("placement_1")); + Assert.That(json["mediatorErrorCode"].AsInt, Is.EqualTo(42)); + } + + private PurchasesWrapperSpy.Invocation AssertLastInvocation(string method, int argumentCount) + { + Assert.That(_wrapper.Invocations, Has.Count.EqualTo(1)); + Assert.That(_wrapper.LastInvocation.Method, Is.EqualTo(method)); + Assert.That(_wrapper.LastInvocation.Arguments, Has.Length.EqualTo(argumentCount)); + return _wrapper.LastInvocation; + } + + private static Purchases.Offering CreateOfferingWithPackage() + { + return new Purchases.Offering(JSONNode.Parse( + "{\"identifier\":\"default\",\"serverDescription\":\"desc\",\"availablePackages\":[" + + "{\"identifier\":\"$rc_monthly\",\"packageType\":\"MONTHLY\"," + + "\"product\":{\"title\":\"Monthly\",\"identifier\":\"monthly\"," + + "\"description\":\"Monthly access\",\"price\":9.99,\"priceString\":\"$9.99\"," + + "\"currencyCode\":\"USD\",\"productCategory\":\"SUBSCRIPTION\"}," + + "\"presentedOfferingContext\":{\"offeringIdentifier\":\"default\"}}" + + "]}" + )); + } + } +} diff --git a/IntegrationTests/Assets/Tests/EditMode/TrackingTests.cs.meta b/IntegrationTests/Assets/Tests/EditMode/TrackingTests.cs.meta new file mode 100644 index 00000000..ad1eb1ad --- /dev/null +++ b/IntegrationTests/Assets/Tests/EditMode/TrackingTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 31362b5f5071458e9ba438e105bbbdee +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: From b4971786ca2dccc32c306e84c770d0610b426af9 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega <664544+vegaro@users.noreply.github.com> Date: Thu, 6 Aug 2026 13:41:59 +0200 Subject: [PATCH 2/4] test: align JsonModelTests fixtures with purchases-hybrid-common payloads MinimalCustomerInfoJson omitted every field the mappers always send as an explicit null, plus entitlements.verification: Android's convertToJson maps Kotlin null to JSONObject.NULL and the iOS mappers use NSNull(), so those keys are present on the wire rather than absent. Spelled them out. The web redemption error fixture paired code 11 with readableErrorCode "UNKNOWN_ERROR", which neither platform produces and which contradicts the code. Code 11 is InvalidCredentialsError, so it now carries the iOS values for that code and the deprecated readable_error_code key that ships alongside. Left the deliberately synthetic values alone and documented why: "UNRECOGNIZED" and "BOGUS" stand in for enum members added natively before Unity knows the name, which is what those two fallback tests exist to pin down. Co-Authored-By: Claude --- .../Assets/Tests/EditMode/JsonModelTests.cs | 38 +++++++++++++++---- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/IntegrationTests/Assets/Tests/EditMode/JsonModelTests.cs b/IntegrationTests/Assets/Tests/EditMode/JsonModelTests.cs index 2486f03c..70b22bf5 100644 --- a/IntegrationTests/Assets/Tests/EditMode/JsonModelTests.cs +++ b/IntegrationTests/Assets/Tests/EditMode/JsonModelTests.cs @@ -6,12 +6,23 @@ namespace RevenueCat.Tests { public class JsonModelTests { + /// CustomerInfo.map() / CustomerInfo.dictionary in purchases-hybrid-common 18.29.0 for a + /// user with no purchases. Every optional field is present as an explicit null rather than + /// absent: Android's Map.convertToJson() maps Kotlin null to JSONObject.NULL and the iOS + /// mappers use NSNull(). SimpleJSON treats a null node and an absent node alike, but the + /// fixture spells them out so it matches what actually ships. private const string MinimalCustomerInfoJson = - "{\"entitlements\":{\"all\":{},\"active\":{}}," + + "{\"entitlements\":{\"all\":{},\"active\":{},\"verification\":\"NOT_REQUESTED\"}," + "\"activeSubscriptions\":[],\"allPurchasedProductIdentifiers\":[]," + - "\"firstSeenMillis\":1700000000000,\"originalAppUserId\":\"user_1\"," + - "\"requestDateMillis\":1700000000001,\"allExpirationDatesMillis\":{}," + - "\"allPurchaseDatesMillis\":{},\"nonSubscriptionTransactions\":[]," + + "\"latestExpirationDate\":null,\"latestExpirationDateMillis\":null," + + "\"firstSeen\":\"2023-11-14T22:13:20.000Z\",\"firstSeenMillis\":1700000000000," + + "\"originalAppUserId\":\"user_1\"," + + "\"requestDate\":\"2023-11-14T22:13:20.001Z\",\"requestDateMillis\":1700000000001," + + "\"allExpirationDates\":{},\"allExpirationDatesMillis\":{}," + + "\"allPurchaseDates\":{},\"allPurchaseDatesMillis\":{}," + + "\"originalApplicationVersion\":null," + + "\"originalPurchaseDate\":null,\"originalPurchaseDateMillis\":null," + + "\"managementURL\":null,\"nonSubscriptionTransactions\":[]," + "\"subscriptionsByProductIdentifier\":{}}"; [Test] @@ -63,6 +74,9 @@ public void StoreProductParsesKnownProductCategory() [Test] public void StoreProductFallsBackToUnknownProductCategory() { + // "UNRECOGNIZED" is deliberately synthetic: StoreProductMapper can only send + // SUBSCRIPTION, NON_SUBSCRIPTION or UNKNOWN. This pins down the parser's fallback for + // a category added natively before Unity knows about it. var response = JSONNode.Parse( "{\"title\":\"Lifetime\",\"identifier\":\"lifetime\",\"description\":\"Lifetime access\"," + "\"price\":99.99,\"priceString\":\"$99.99\",\"currencyCode\":\"USD\"," + @@ -89,7 +103,8 @@ public void CustomerInfoParsesFullPayload() "\"active\":{\"premium\":{\"identifier\":\"premium\",\"isActive\":true,\"willRenew\":true," + "\"periodType\":\"NORMAL\",\"latestPurchaseDateMillis\":1700000000000," + "\"originalPurchaseDateMillis\":1690000000000,\"store\":\"APP_STORE\"," + - "\"productIdentifier\":\"monthly\",\"isSandbox\":false}}}," + + "\"productIdentifier\":\"monthly\",\"isSandbox\":false}}," + + "\"verification\":\"NOT_REQUESTED\"}," + "\"activeSubscriptions\":[\"monthly\"]," + "\"allPurchasedProductIdentifiers\":[\"monthly\",\"lifetime\"]," + "\"firstSeenMillis\":1700000000000,\"originalAppUserId\":\"user_1\"," + @@ -184,6 +199,9 @@ public void SubscriptionOptionParsesMinimalPayloadWithoutOptionalFields() [Test] public void SubscriptionOptionPricingPhaseFallsBackToUnknownForUnrecognizedEnums() { + // "BOGUS" is deliberately synthetic, like the UNRECOGNIZED category above: it stands in + // for a recurrenceMode or offerPaymentMode that Android starts sending before Unity + // knows the name, and pins down the UNKNOWN fallback. var response = JSONNode.Parse( "{\"id\":\"monthly:base\",\"storeProductId\":\"monthly\",\"productId\":\"monthly\"," + "\"tags\":[],\"isBasePlan\":true," + @@ -227,8 +245,14 @@ public void WebPurchaseRedemptionResultParsesSuccessVariant() public void WebPurchaseRedemptionResultParsesErrorVariant() { var response = JSONNode.Parse( - "{\"result\":\"ERROR\",\"error\":{\"message\":\"Redemption failed\",\"code\":11," + - "\"underlyingErrorMessage\":\"Backend rejected\",\"readableErrorCode\":\"UNKNOWN_ERROR\"}}" + // readableErrorCode is ErrorCode.codeName on iOS and PurchasesErrorCode.name on + // Android; both duplicate it under the deprecated readable_error_code. Code 11 is + // InvalidCredentialsError, so these are the iOS values for that code. + "{\"result\":\"ERROR\",\"error\":{\"message\":\"There was a credentials issue. " + + "Check the underlying error for more details.\",\"code\":11," + + "\"underlyingErrorMessage\":\"Backend rejected\"," + + "\"readableErrorCode\":\"INVALID_CREDENTIALS\"," + + "\"readable_error_code\":\"INVALID_CREDENTIALS\"}}" ); var result = Purchases.WebPurchaseRedemptionResult.FromJson(response); From 2fe4374f3f386f42a51c7479a1c6f8b8f891ca40 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega <664544+vegaro@users.noreply.github.com> Date: Thu, 6 Aug 2026 14:47:44 +0200 Subject: [PATCH 3/4] test: drop the hybrid-common version pin from the fixture comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pinned version went stale within a day of being written — it was bumped from 18.25.0 to 18.29.0 while this branch was in review. The mapper names alone are enough to find the source. Co-Authored-By: Claude --- .../Assets/Tests/EditMode/JsonModelTests.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/IntegrationTests/Assets/Tests/EditMode/JsonModelTests.cs b/IntegrationTests/Assets/Tests/EditMode/JsonModelTests.cs index 70b22bf5..cdd703a2 100644 --- a/IntegrationTests/Assets/Tests/EditMode/JsonModelTests.cs +++ b/IntegrationTests/Assets/Tests/EditMode/JsonModelTests.cs @@ -6,11 +6,11 @@ namespace RevenueCat.Tests { public class JsonModelTests { - /// CustomerInfo.map() / CustomerInfo.dictionary in purchases-hybrid-common 18.29.0 for a - /// user with no purchases. Every optional field is present as an explicit null rather than - /// absent: Android's Map.convertToJson() maps Kotlin null to JSONObject.NULL and the iOS - /// mappers use NSNull(). SimpleJSON treats a null node and an absent node alike, but the - /// fixture spells them out so it matches what actually ships. + /// purchases-hybrid-common's CustomerInfo.map() (Android) / CustomerInfo.dictionary (iOS) + /// for a user with no purchases. Every optional field is present as an explicit null rather + /// than absent: Android's Map.convertToJson() maps Kotlin null to JSONObject.NULL and the + /// iOS mappers use NSNull(). SimpleJSON treats a null node and an absent node alike, but + /// the fixture spells them out so it matches what actually ships. private const string MinimalCustomerInfoJson = "{\"entitlements\":{\"all\":{},\"active\":{},\"verification\":\"NOT_REQUESTED\"}," + "\"activeSubscriptions\":[],\"allPurchasedProductIdentifiers\":[]," + From 7c1a88a68dae90feabc668491d5db3c7c6b70bfb Mon Sep 17 00:00:00 2001 From: Cesar de la Vega <664544+vegaro@users.noreply.github.com> Date: Thu, 6 Aug 2026 15:48:15 +0200 Subject: [PATCH 4/4] test: extract JSON model fixtures into files --- .../Tests/EditMode/JsonModelFixtures.meta | 8 + .../JsonModelFixtures/customer-info-full.json | 71 +++++++++ .../customer-info-full.json.meta | 7 + .../store-product-known-category.json | 9 ++ .../store-product-known-category.json.meta | 7 + .../store-product-unrecognized-category.json | 9 ++ ...re-product-unrecognized-category.json.meta | 7 + .../subscription-option-full.json | 54 +++++++ .../subscription-option-full.json.meta | 7 + .../subscription-option-minimal.json | 13 ++ .../subscription-option-minimal.json.meta | 7 + ...ubscription-option-unrecognized-enums.json | 28 ++++ ...iption-option-unrecognized-enums.json.meta | 7 + .../subscription-price-large-amount.json | 5 + .../subscription-price-large-amount.json.meta | 7 + .../JsonModelFixtures/virtual-currencies.json | 16 ++ .../virtual-currencies.json.meta | 7 + .../web-purchase-redemption-error.json | 10 ++ .../web-purchase-redemption-error.json.meta | 7 + .../web-purchase-redemption-expired.json | 4 + .../web-purchase-redemption-expired.json.meta | 7 + ...web-purchase-redemption-invalid-token.json | 3 + ...urchase-redemption-invalid-token.json.meta | 7 + .../web-purchase-redemption-other-user.json | 3 + ...b-purchase-redemption-other-user.json.meta | 7 + .../web-purchase-redemption-success.json | 29 ++++ .../web-purchase-redemption-success.json.meta | 7 + .../web-purchase-redemption-unrecognized.json | 3 + ...purchase-redemption-unrecognized.json.meta | 7 + .../Assets/Tests/EditMode/JsonModelTests.cs | 137 ++++-------------- 30 files changed, 390 insertions(+), 110 deletions(-) create mode 100644 IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures.meta create mode 100644 IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/customer-info-full.json create mode 100644 IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/customer-info-full.json.meta create mode 100644 IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/store-product-known-category.json create mode 100644 IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/store-product-known-category.json.meta create mode 100644 IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/store-product-unrecognized-category.json create mode 100644 IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/store-product-unrecognized-category.json.meta create mode 100644 IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/subscription-option-full.json create mode 100644 IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/subscription-option-full.json.meta create mode 100644 IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/subscription-option-minimal.json create mode 100644 IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/subscription-option-minimal.json.meta create mode 100644 IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/subscription-option-unrecognized-enums.json create mode 100644 IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/subscription-option-unrecognized-enums.json.meta create mode 100644 IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/subscription-price-large-amount.json create mode 100644 IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/subscription-price-large-amount.json.meta create mode 100644 IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/virtual-currencies.json create mode 100644 IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/virtual-currencies.json.meta create mode 100644 IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/web-purchase-redemption-error.json create mode 100644 IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/web-purchase-redemption-error.json.meta create mode 100644 IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/web-purchase-redemption-expired.json create mode 100644 IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/web-purchase-redemption-expired.json.meta create mode 100644 IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/web-purchase-redemption-invalid-token.json create mode 100644 IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/web-purchase-redemption-invalid-token.json.meta create mode 100644 IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/web-purchase-redemption-other-user.json create mode 100644 IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/web-purchase-redemption-other-user.json.meta create mode 100644 IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/web-purchase-redemption-success.json create mode 100644 IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/web-purchase-redemption-success.json.meta create mode 100644 IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/web-purchase-redemption-unrecognized.json create mode 100644 IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/web-purchase-redemption-unrecognized.json.meta diff --git a/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures.meta b/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures.meta new file mode 100644 index 00000000..d223f1c6 --- /dev/null +++ b/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 5982e1cdbdb7404faef3d912f9ace126 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/customer-info-full.json b/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/customer-info-full.json new file mode 100644 index 00000000..451e4fb0 --- /dev/null +++ b/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/customer-info-full.json @@ -0,0 +1,71 @@ +{ + "entitlements": { + "all": { + "premium": { + "identifier": "premium", + "isActive": true, + "willRenew": true, + "periodType": "NORMAL", + "latestPurchaseDateMillis": 1700000000000, + "originalPurchaseDateMillis": 1690000000000, + "store": "APP_STORE", + "productIdentifier": "monthly", + "isSandbox": false + } + }, + "active": { + "premium": { + "identifier": "premium", + "isActive": true, + "willRenew": true, + "periodType": "NORMAL", + "latestPurchaseDateMillis": 1700000000000, + "originalPurchaseDateMillis": 1690000000000, + "store": "APP_STORE", + "productIdentifier": "monthly", + "isSandbox": false + } + }, + "verification": "NOT_REQUESTED" + }, + "activeSubscriptions": [ + "monthly" + ], + "allPurchasedProductIdentifiers": [ + "monthly", + "lifetime" + ], + "firstSeenMillis": 1700000000000, + "originalAppUserId": "user_1", + "requestDateMillis": 1700000000001, + "originalPurchaseDateMillis": 1690000000000, + "latestExpirationDateMillis": 1720000000000, + "managementURL": "https://mgmt", + "allExpirationDatesMillis": { + "monthly": 1720000000000, + "lifetime": 0 + }, + "allPurchaseDatesMillis": { + "monthly": 1700000000000, + "lifetime": 0 + }, + "originalApplicationVersion": "1.0", + "nonSubscriptionTransactions": [ + { + "transactionIdentifier": "txn_1", + "productIdentifier": "lifetime", + "purchaseDateMillis": 1700000000000 + } + ], + "subscriptionsByProductIdentifier": { + "monthly": { + "productIdentifier": "monthly", + "purchaseDate": "2023-01-01T00:00:00Z", + "store": "APP_STORE", + "isSandbox": false, + "periodType": "NORMAL", + "isActive": true, + "willRenew": true + } + } +} diff --git a/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/customer-info-full.json.meta b/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/customer-info-full.json.meta new file mode 100644 index 00000000..f6119312 --- /dev/null +++ b/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/customer-info-full.json.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: a9256b23db244a3fb510dbb51cee33ce +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/store-product-known-category.json b/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/store-product-known-category.json new file mode 100644 index 00000000..cf290428 --- /dev/null +++ b/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/store-product-known-category.json @@ -0,0 +1,9 @@ +{ + "title": "Monthly", + "identifier": "monthly", + "description": "Monthly access", + "price": 9.99, + "priceString": "$9.99", + "currencyCode": "USD", + "productCategory": "SUBSCRIPTION" +} diff --git a/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/store-product-known-category.json.meta b/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/store-product-known-category.json.meta new file mode 100644 index 00000000..e73c791d --- /dev/null +++ b/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/store-product-known-category.json.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: dc667e3ab7be4e87b8431fa93f8c1019 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/store-product-unrecognized-category.json b/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/store-product-unrecognized-category.json new file mode 100644 index 00000000..d70bf29e --- /dev/null +++ b/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/store-product-unrecognized-category.json @@ -0,0 +1,9 @@ +{ + "title": "Lifetime", + "identifier": "lifetime", + "description": "Lifetime access", + "price": 99.99, + "priceString": "$99.99", + "currencyCode": "USD", + "productCategory": "UNRECOGNIZED" +} diff --git a/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/store-product-unrecognized-category.json.meta b/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/store-product-unrecognized-category.json.meta new file mode 100644 index 00000000..8125ebd5 --- /dev/null +++ b/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/store-product-unrecognized-category.json.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 9a4e14f71acc4c1ca8ea41983fb43af1 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/subscription-option-full.json b/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/subscription-option-full.json new file mode 100644 index 00000000..0faba4e4 --- /dev/null +++ b/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/subscription-option-full.json @@ -0,0 +1,54 @@ +{ + "id": "monthly:base", + "storeProductId": "monthly", + "productId": "monthly", + "tags": [ + "tag1" + ], + "isBasePlan": true, + "billingPeriod": { + "unit": "MONTH", + "value": 1, + "iso8601": "P1M" + }, + "isPrepaid": false, + "pricingPhases": [ + { + "billingPeriod": { + "unit": "MONTH", + "value": 1, + "iso8601": "P1M" + }, + "recurrenceMode": "INFINITE_RECURRING", + "billingCycleCount": 0, + "price": { + "formatted": "$9.99", + "amountMicros": 9990000, + "currencyCode": "USD" + }, + "offerPaymentMode": "SINGLE_PAYMENT" + } + ], + "fullPricePhase": { + "billingPeriod": { + "unit": "MONTH", + "value": 1, + "iso8601": "P1M" + }, + "recurrenceMode": "INFINITE_RECURRING", + "billingCycleCount": 0, + "price": { + "formatted": "$9.99", + "amountMicros": 9990000, + "currencyCode": "USD" + }, + "offerPaymentMode": "SINGLE_PAYMENT" + }, + "presentedOfferingContext": { + "offeringIdentifier": "default" + }, + "installmentsInfo": { + "commitmentPaymentsCount": 3, + "renewalCommitmentPaymentsCount": 1 + } +} diff --git a/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/subscription-option-full.json.meta b/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/subscription-option-full.json.meta new file mode 100644 index 00000000..528d08ad --- /dev/null +++ b/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/subscription-option-full.json.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 668c4bb7b993434287d13be22bbd0b63 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/subscription-option-minimal.json b/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/subscription-option-minimal.json new file mode 100644 index 00000000..73a41e3e --- /dev/null +++ b/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/subscription-option-minimal.json @@ -0,0 +1,13 @@ +{ + "id": "monthly:base", + "storeProductId": "monthly", + "productId": "monthly", + "tags": [], + "isBasePlan": true, + "billingPeriod": { + "unit": "MONTH", + "value": 1, + "iso8601": "P1M" + }, + "isPrepaid": false +} diff --git a/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/subscription-option-minimal.json.meta b/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/subscription-option-minimal.json.meta new file mode 100644 index 00000000..0f2559b4 --- /dev/null +++ b/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/subscription-option-minimal.json.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 17f04d4eab744eba86e08a38c55607ac +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/subscription-option-unrecognized-enums.json b/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/subscription-option-unrecognized-enums.json new file mode 100644 index 00000000..f45459c9 --- /dev/null +++ b/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/subscription-option-unrecognized-enums.json @@ -0,0 +1,28 @@ +{ + "id": "monthly:base", + "storeProductId": "monthly", + "productId": "monthly", + "tags": [], + "isBasePlan": true, + "billingPeriod": { + "unit": "MONTH", + "value": 1, + "iso8601": "P1M" + }, + "isPrepaid": false, + "fullPricePhase": { + "billingPeriod": { + "unit": "MONTH", + "value": 1, + "iso8601": "P1M" + }, + "recurrenceMode": "BOGUS", + "billingCycleCount": 0, + "price": { + "formatted": "$9.99", + "amountMicros": 9990000, + "currencyCode": "USD" + }, + "offerPaymentMode": "BOGUS" + } +} diff --git a/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/subscription-option-unrecognized-enums.json.meta b/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/subscription-option-unrecognized-enums.json.meta new file mode 100644 index 00000000..b3d05e03 --- /dev/null +++ b/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/subscription-option-unrecognized-enums.json.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 69529197d68743fc9763f3ef380fd215 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/subscription-price-large-amount.json b/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/subscription-price-large-amount.json new file mode 100644 index 00000000..01681b53 --- /dev/null +++ b/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/subscription-price-large-amount.json @@ -0,0 +1,5 @@ +{ + "formatted": "$4,294.97", + "amountMicros": 4294970000, + "currencyCode": "USD" +} diff --git a/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/subscription-price-large-amount.json.meta b/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/subscription-price-large-amount.json.meta new file mode 100644 index 00000000..c4f74311 --- /dev/null +++ b/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/subscription-price-large-amount.json.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: ee385f49a7ac492a81c9a02bd913d50a +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/virtual-currencies.json b/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/virtual-currencies.json new file mode 100644 index 00000000..ad76ddc5 --- /dev/null +++ b/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/virtual-currencies.json @@ -0,0 +1,16 @@ +{ + "all": { + "COIN": { + "balance": 120, + "name": "Coins", + "code": "COIN", + "serverDescription": "Earned in game" + }, + "GEM": { + "balance": 4, + "name": "Gems", + "code": "GEM", + "serverDescription": null + } + } +} diff --git a/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/virtual-currencies.json.meta b/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/virtual-currencies.json.meta new file mode 100644 index 00000000..382dd69d --- /dev/null +++ b/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/virtual-currencies.json.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: f86a94c2dcbd4186a352cf0ff8ea74d8 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/web-purchase-redemption-error.json b/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/web-purchase-redemption-error.json new file mode 100644 index 00000000..46df23e4 --- /dev/null +++ b/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/web-purchase-redemption-error.json @@ -0,0 +1,10 @@ +{ + "result": "ERROR", + "error": { + "message": "There was a credentials issue. Check the underlying error for more details.", + "code": 11, + "underlyingErrorMessage": "Backend rejected", + "readableErrorCode": "INVALID_CREDENTIALS", + "readable_error_code": "INVALID_CREDENTIALS" + } +} diff --git a/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/web-purchase-redemption-error.json.meta b/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/web-purchase-redemption-error.json.meta new file mode 100644 index 00000000..ca69c247 --- /dev/null +++ b/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/web-purchase-redemption-error.json.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 76d06d1bacaf42f4b267121f81f9e88b +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/web-purchase-redemption-expired.json b/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/web-purchase-redemption-expired.json new file mode 100644 index 00000000..2b7e6be0 --- /dev/null +++ b/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/web-purchase-redemption-expired.json @@ -0,0 +1,4 @@ +{ + "result": "EXPIRED", + "obfuscatedEmail": "a***@b.com" +} diff --git a/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/web-purchase-redemption-expired.json.meta b/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/web-purchase-redemption-expired.json.meta new file mode 100644 index 00000000..b2f87c72 --- /dev/null +++ b/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/web-purchase-redemption-expired.json.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 4b69d0b1fe2647b4a714d47dcc595492 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/web-purchase-redemption-invalid-token.json b/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/web-purchase-redemption-invalid-token.json new file mode 100644 index 00000000..ea38798b --- /dev/null +++ b/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/web-purchase-redemption-invalid-token.json @@ -0,0 +1,3 @@ +{ + "result": "INVALID_TOKEN" +} diff --git a/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/web-purchase-redemption-invalid-token.json.meta b/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/web-purchase-redemption-invalid-token.json.meta new file mode 100644 index 00000000..afdcd9f5 --- /dev/null +++ b/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/web-purchase-redemption-invalid-token.json.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 37288a8309d94e188d26a7e8d288aa38 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/web-purchase-redemption-other-user.json b/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/web-purchase-redemption-other-user.json new file mode 100644 index 00000000..fbe21343 --- /dev/null +++ b/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/web-purchase-redemption-other-user.json @@ -0,0 +1,3 @@ +{ + "result": "PURCHASE_BELONGS_TO_OTHER_USER" +} diff --git a/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/web-purchase-redemption-other-user.json.meta b/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/web-purchase-redemption-other-user.json.meta new file mode 100644 index 00000000..9897fe75 --- /dev/null +++ b/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/web-purchase-redemption-other-user.json.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 514b31eb0ed34b2ab2c4360491d85a1a +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/web-purchase-redemption-success.json b/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/web-purchase-redemption-success.json new file mode 100644 index 00000000..e125f8d1 --- /dev/null +++ b/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/web-purchase-redemption-success.json @@ -0,0 +1,29 @@ +{ + "result": "SUCCESS", + "customerInfo": { + "entitlements": { + "all": {}, + "active": {}, + "verification": "NOT_REQUESTED" + }, + "activeSubscriptions": [], + "allPurchasedProductIdentifiers": [], + "latestExpirationDate": null, + "latestExpirationDateMillis": null, + "firstSeen": "2023-11-14T22:13:20.000Z", + "firstSeenMillis": 1700000000000, + "originalAppUserId": "user_1", + "requestDate": "2023-11-14T22:13:20.001Z", + "requestDateMillis": 1700000000001, + "allExpirationDates": {}, + "allExpirationDatesMillis": {}, + "allPurchaseDates": {}, + "allPurchaseDatesMillis": {}, + "originalApplicationVersion": null, + "originalPurchaseDate": null, + "originalPurchaseDateMillis": null, + "managementURL": null, + "nonSubscriptionTransactions": [], + "subscriptionsByProductIdentifier": {} + } +} diff --git a/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/web-purchase-redemption-success.json.meta b/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/web-purchase-redemption-success.json.meta new file mode 100644 index 00000000..0dbad4c9 --- /dev/null +++ b/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/web-purchase-redemption-success.json.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 64fb48f417904c56990f465635dfbb98 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/web-purchase-redemption-unrecognized.json b/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/web-purchase-redemption-unrecognized.json new file mode 100644 index 00000000..bcfb1ca8 --- /dev/null +++ b/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/web-purchase-redemption-unrecognized.json @@ -0,0 +1,3 @@ +{ + "result": "SOMETHING_ELSE" +} diff --git a/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/web-purchase-redemption-unrecognized.json.meta b/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/web-purchase-redemption-unrecognized.json.meta new file mode 100644 index 00000000..6675a303 --- /dev/null +++ b/IntegrationTests/Assets/Tests/EditMode/JsonModelFixtures/web-purchase-redemption-unrecognized.json.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 8aecb3d59a7b4eda9af887a2fda16e51 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/IntegrationTests/Assets/Tests/EditMode/JsonModelTests.cs b/IntegrationTests/Assets/Tests/EditMode/JsonModelTests.cs index cdd703a2..71481396 100644 --- a/IntegrationTests/Assets/Tests/EditMode/JsonModelTests.cs +++ b/IntegrationTests/Assets/Tests/EditMode/JsonModelTests.cs @@ -1,39 +1,23 @@ using System; +using System.IO; using NUnit.Framework; using RevenueCat.SimpleJSON; +using UnityEngine; namespace RevenueCat.Tests { public class JsonModelTests { - /// purchases-hybrid-common's CustomerInfo.map() (Android) / CustomerInfo.dictionary (iOS) - /// for a user with no purchases. Every optional field is present as an explicit null rather - /// than absent: Android's Map.convertToJson() maps Kotlin null to JSONObject.NULL and the - /// iOS mappers use NSNull(). SimpleJSON treats a null node and an absent node alike, but - /// the fixture spells them out so it matches what actually ships. - private const string MinimalCustomerInfoJson = - "{\"entitlements\":{\"all\":{},\"active\":{},\"verification\":\"NOT_REQUESTED\"}," + - "\"activeSubscriptions\":[],\"allPurchasedProductIdentifiers\":[]," + - "\"latestExpirationDate\":null,\"latestExpirationDateMillis\":null," + - "\"firstSeen\":\"2023-11-14T22:13:20.000Z\",\"firstSeenMillis\":1700000000000," + - "\"originalAppUserId\":\"user_1\"," + - "\"requestDate\":\"2023-11-14T22:13:20.001Z\",\"requestDateMillis\":1700000000001," + - "\"allExpirationDates\":{},\"allExpirationDatesMillis\":{}," + - "\"allPurchaseDates\":{},\"allPurchaseDatesMillis\":{}," + - "\"originalApplicationVersion\":null," + - "\"originalPurchaseDate\":null,\"originalPurchaseDateMillis\":null," + - "\"managementURL\":null,\"nonSubscriptionTransactions\":[]," + - "\"subscriptionsByProductIdentifier\":{}}"; + private static JSONNode LoadFixture(string filename) + { + var path = Path.Combine(Application.dataPath, "Tests", "EditMode", "JsonModelFixtures", filename); + return JSONNode.Parse(File.ReadAllText(path)); + } [Test] public void VirtualCurrenciesParsesCurrenciesByCode() { - var response = JSONNode.Parse( - "{\"all\":{" + - "\"COIN\":{\"balance\":120,\"name\":\"Coins\",\"code\":\"COIN\",\"serverDescription\":\"Earned in game\"}," + - "\"GEM\":{\"balance\":4,\"name\":\"Gems\",\"code\":\"GEM\",\"serverDescription\":null}" + - "}}" - ); + var response = LoadFixture("virtual-currencies.json"); var virtualCurrencies = new Purchases.VirtualCurrencies(response); @@ -48,9 +32,7 @@ public void VirtualCurrenciesParsesCurrenciesByCode() [Test] public void SubscriptionPriceSupportsValuesAboveInt32Range() { - var response = JSONNode.Parse( - "{\"formatted\":\"$4,294.97\",\"amountMicros\":4294970000,\"currencyCode\":\"USD\"}" - ); + var response = LoadFixture("subscription-price-large-amount.json"); var price = new Purchases.SubscriptionOption.Price(response); @@ -60,11 +42,7 @@ public void SubscriptionPriceSupportsValuesAboveInt32Range() [Test] public void StoreProductParsesKnownProductCategory() { - var response = JSONNode.Parse( - "{\"title\":\"Monthly\",\"identifier\":\"monthly\",\"description\":\"Monthly access\"," + - "\"price\":9.99,\"priceString\":\"$9.99\",\"currencyCode\":\"USD\"," + - "\"productCategory\":\"SUBSCRIPTION\"}" - ); + var response = LoadFixture("store-product-known-category.json"); var product = new Purchases.StoreProduct(response); @@ -77,11 +55,7 @@ public void StoreProductFallsBackToUnknownProductCategory() // "UNRECOGNIZED" is deliberately synthetic: StoreProductMapper can only send // SUBSCRIPTION, NON_SUBSCRIPTION or UNKNOWN. This pins down the parser's fallback for // a category added natively before Unity knows about it. - var response = JSONNode.Parse( - "{\"title\":\"Lifetime\",\"identifier\":\"lifetime\",\"description\":\"Lifetime access\"," + - "\"price\":99.99,\"priceString\":\"$99.99\",\"currencyCode\":\"USD\"," + - "\"productCategory\":\"UNRECOGNIZED\"}" - ); + var response = LoadFixture("store-product-unrecognized-category.json"); var product = new Purchases.StoreProduct(response); @@ -94,33 +68,7 @@ public void StoreProductFallsBackToUnknownProductCategory() [Test] public void CustomerInfoParsesFullPayload() { - var response = JSONNode.Parse( - "{\"entitlements\":{" + - "\"all\":{\"premium\":{\"identifier\":\"premium\",\"isActive\":true,\"willRenew\":true," + - "\"periodType\":\"NORMAL\",\"latestPurchaseDateMillis\":1700000000000," + - "\"originalPurchaseDateMillis\":1690000000000,\"store\":\"APP_STORE\"," + - "\"productIdentifier\":\"monthly\",\"isSandbox\":false}}," + - "\"active\":{\"premium\":{\"identifier\":\"premium\",\"isActive\":true,\"willRenew\":true," + - "\"periodType\":\"NORMAL\",\"latestPurchaseDateMillis\":1700000000000," + - "\"originalPurchaseDateMillis\":1690000000000,\"store\":\"APP_STORE\"," + - "\"productIdentifier\":\"monthly\",\"isSandbox\":false}}," + - "\"verification\":\"NOT_REQUESTED\"}," + - "\"activeSubscriptions\":[\"monthly\"]," + - "\"allPurchasedProductIdentifiers\":[\"monthly\",\"lifetime\"]," + - "\"firstSeenMillis\":1700000000000,\"originalAppUserId\":\"user_1\"," + - "\"requestDateMillis\":1700000000001," + - "\"originalPurchaseDateMillis\":1690000000000," + - "\"latestExpirationDateMillis\":1720000000000," + - "\"managementURL\":\"https://mgmt\"," + - "\"allExpirationDatesMillis\":{\"monthly\":1720000000000,\"lifetime\":0}," + - "\"allPurchaseDatesMillis\":{\"monthly\":1700000000000,\"lifetime\":0}," + - "\"originalApplicationVersion\":\"1.0\"," + - "\"nonSubscriptionTransactions\":[{\"transactionIdentifier\":\"txn_1\"," + - "\"productIdentifier\":\"lifetime\",\"purchaseDateMillis\":1700000000000}]," + - "\"subscriptionsByProductIdentifier\":{\"monthly\":{\"productIdentifier\":\"monthly\"," + - "\"purchaseDate\":\"2023-01-01T00:00:00Z\",\"store\":\"APP_STORE\",\"isSandbox\":false," + - "\"periodType\":\"NORMAL\",\"isActive\":true,\"willRenew\":true}}}" - ); + var response = LoadFixture("customer-info-full.json"); var customerInfo = new Purchases.CustomerInfo(response); @@ -148,21 +96,7 @@ public void CustomerInfoParsesFullPayload() [Test] public void SubscriptionOptionParsesFullPayloadWithPricingPhases() { - const string pricingPhaseJson = - "{\"billingPeriod\":{\"unit\":\"MONTH\",\"value\":1,\"iso8601\":\"P1M\"}," + - "\"recurrenceMode\":\"INFINITE_RECURRING\",\"billingCycleCount\":0," + - "\"price\":{\"formatted\":\"$9.99\",\"amountMicros\":9990000,\"currencyCode\":\"USD\"}," + - "\"offerPaymentMode\":\"SINGLE_PAYMENT\"}"; - - var response = JSONNode.Parse( - "{\"id\":\"monthly:base\",\"storeProductId\":\"monthly\",\"productId\":\"monthly\"," + - "\"tags\":[\"tag1\"],\"isBasePlan\":true," + - "\"billingPeriod\":{\"unit\":\"MONTH\",\"value\":1,\"iso8601\":\"P1M\"},\"isPrepaid\":false," + - "\"pricingPhases\":[" + pricingPhaseJson + "]," + - "\"fullPricePhase\":" + pricingPhaseJson + "," + - "\"presentedOfferingContext\":{\"offeringIdentifier\":\"default\"}," + - "\"installmentsInfo\":{\"commitmentPaymentsCount\":3,\"renewalCommitmentPaymentsCount\":1}}" - ); + var response = LoadFixture("subscription-option-full.json"); var option = new Purchases.SubscriptionOption(response); @@ -180,11 +114,7 @@ public void SubscriptionOptionParsesFullPayloadWithPricingPhases() [Test] public void SubscriptionOptionParsesMinimalPayloadWithoutOptionalFields() { - var response = JSONNode.Parse( - "{\"id\":\"monthly:base\",\"storeProductId\":\"monthly\",\"productId\":\"monthly\"," + - "\"tags\":[],\"isBasePlan\":true," + - "\"billingPeriod\":{\"unit\":\"MONTH\",\"value\":1,\"iso8601\":\"P1M\"},\"isPrepaid\":false}" - ); + var response = LoadFixture("subscription-option-minimal.json"); var option = new Purchases.SubscriptionOption(response); @@ -202,15 +132,7 @@ public void SubscriptionOptionPricingPhaseFallsBackToUnknownForUnrecognizedEnums // "BOGUS" is deliberately synthetic, like the UNRECOGNIZED category above: it stands in // for a recurrenceMode or offerPaymentMode that Android starts sending before Unity // knows the name, and pins down the UNKNOWN fallback. - var response = JSONNode.Parse( - "{\"id\":\"monthly:base\",\"storeProductId\":\"monthly\",\"productId\":\"monthly\"," + - "\"tags\":[],\"isBasePlan\":true," + - "\"billingPeriod\":{\"unit\":\"MONTH\",\"value\":1,\"iso8601\":\"P1M\"},\"isPrepaid\":false," + - "\"fullPricePhase\":{\"billingPeriod\":{\"unit\":\"MONTH\",\"value\":1,\"iso8601\":\"P1M\"}," + - "\"recurrenceMode\":\"BOGUS\",\"billingCycleCount\":0," + - "\"price\":{\"formatted\":\"$9.99\",\"amountMicros\":9990000,\"currencyCode\":\"USD\"}," + - "\"offerPaymentMode\":\"BOGUS\"}}" - ); + var response = LoadFixture("subscription-option-unrecognized-enums.json"); var option = new Purchases.SubscriptionOption(response); @@ -231,9 +153,10 @@ public void WebPurchaseRedemptionWrapsRedemptionLink() [Test] public void WebPurchaseRedemptionResultParsesSuccessVariant() { - var response = JSONNode.Parse( - "{\"result\":\"SUCCESS\",\"customerInfo\":" + MinimalCustomerInfoJson + "}" - ); + // The nested CustomerInfo comes from purchases-hybrid-common's CustomerInfo.map() + // (Android) / CustomerInfo.dictionary (iOS) for a user with no purchases. Every + // optional field is an explicit null, matching the payload that actually ships. + var response = LoadFixture("web-purchase-redemption-success.json"); var result = Purchases.WebPurchaseRedemptionResult.FromJson(response); @@ -244,16 +167,10 @@ public void WebPurchaseRedemptionResultParsesSuccessVariant() [Test] public void WebPurchaseRedemptionResultParsesErrorVariant() { - var response = JSONNode.Parse( - // readableErrorCode is ErrorCode.codeName on iOS and PurchasesErrorCode.name on - // Android; both duplicate it under the deprecated readable_error_code. Code 11 is - // InvalidCredentialsError, so these are the iOS values for that code. - "{\"result\":\"ERROR\",\"error\":{\"message\":\"There was a credentials issue. " + - "Check the underlying error for more details.\",\"code\":11," + - "\"underlyingErrorMessage\":\"Backend rejected\"," + - "\"readableErrorCode\":\"INVALID_CREDENTIALS\"," + - "\"readable_error_code\":\"INVALID_CREDENTIALS\"}}" - ); + // readableErrorCode is ErrorCode.codeName on iOS and PurchasesErrorCode.name on + // Android; both duplicate it under the deprecated readable_error_code. Code 11 is + // InvalidCredentialsError, so these are the iOS values for that code. + var response = LoadFixture("web-purchase-redemption-error.json"); var result = Purchases.WebPurchaseRedemptionResult.FromJson(response); @@ -264,7 +181,7 @@ public void WebPurchaseRedemptionResultParsesErrorVariant() [Test] public void WebPurchaseRedemptionResultParsesInvalidTokenVariant() { - var response = JSONNode.Parse("{\"result\":\"INVALID_TOKEN\"}"); + var response = LoadFixture("web-purchase-redemption-invalid-token.json"); var result = Purchases.WebPurchaseRedemptionResult.FromJson(response); @@ -274,7 +191,7 @@ public void WebPurchaseRedemptionResultParsesInvalidTokenVariant() [Test] public void WebPurchaseRedemptionResultParsesExpiredVariant() { - var response = JSONNode.Parse("{\"result\":\"EXPIRED\",\"obfuscatedEmail\":\"a***@b.com\"}"); + var response = LoadFixture("web-purchase-redemption-expired.json"); var result = Purchases.WebPurchaseRedemptionResult.FromJson(response); @@ -286,7 +203,7 @@ public void WebPurchaseRedemptionResultParsesExpiredVariant() [Test] public void WebPurchaseRedemptionResultParsesPurchaseBelongsToOtherUserVariant() { - var response = JSONNode.Parse("{\"result\":\"PURCHASE_BELONGS_TO_OTHER_USER\"}"); + var response = LoadFixture("web-purchase-redemption-other-user.json"); var result = Purchases.WebPurchaseRedemptionResult.FromJson(response); @@ -296,7 +213,7 @@ public void WebPurchaseRedemptionResultParsesPurchaseBelongsToOtherUserVariant() [Test] public void WebPurchaseRedemptionResultThrowsForUnrecognizedResultType() { - var response = JSONNode.Parse("{\"result\":\"SOMETHING_ELSE\"}"); + var response = LoadFixture("web-purchase-redemption-unrecognized.json"); Assert.Throws(() => Purchases.WebPurchaseRedemptionResult.FromJson(response)); }