Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
233 changes: 233 additions & 0 deletions IntegrationTests/Assets/Tests/EditMode/JsonModelTests.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,30 @@
using System;
using NUnit.Framework;
using RevenueCat.SimpleJSON;

namespace RevenueCat.Tests
{
public class JsonModelTests
{
/// CustomerInfo.map() / CustomerInfo.dictionary in purchases-hybrid-common 18.29.0 for a
Comment thread
vegaro marked this conversation as resolved.
Outdated
/// 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\":{}}";
Comment thread
vegaro marked this conversation as resolved.
Outdated

[Test]
public void VirtualCurrenciesParsesCurrenciesByCode()
{
Expand Down Expand Up @@ -54,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\"," +
Expand All @@ -67,5 +90,215 @@ 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}}," +
"\"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 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()
{
// "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 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<Purchases.WebPurchaseRedemptionResult.Success>());
Assert.That(((Purchases.WebPurchaseRedemptionResult.Success)result).CustomerInfo, Is.Not.Null);
}

[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\"}}"
);

var result = Purchases.WebPurchaseRedemptionResult.FromJson(response);

Assert.That(result, Is.InstanceOf<Purchases.WebPurchaseRedemptionResult.RedemptionError>());
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<Purchases.WebPurchaseRedemptionResult.Expired>());
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<ArgumentException>(() => Purchases.WebPurchaseRedemptionResult.FromJson(response));
}
}
}
Loading