diff --git a/IntegrationTests/Assets/Tests/EditMode/AttributionPassthroughTests.cs b/IntegrationTests/Assets/Tests/EditMode/AttributionPassthroughTests.cs new file mode 100644 index 00000000..d6a6d89f --- /dev/null +++ b/IntegrationTests/Assets/Tests/EditMode/AttributionPassthroughTests.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections; +using NUnit.Framework; +using UnityEngine; + +namespace RevenueCat.Tests +{ + public class AttributionPassthroughTests + { + 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() + { + // Qualified because `using System` above makes a bare `Object` ambiguous. + UnityEngine.Object.DestroyImmediate(_gameObject); + } + + /// + /// Every string attribution setter on . Each case carries the wrapper method it is + /// expected to reach, the call to make, and the value that must arrive unchanged. + /// + private static IEnumerable StringSetters() + { + yield return Setter(nameof(IPurchasesWrapper.SetEmail), + (purchases, value) => purchases.SetEmail(value), "support@revenuecat.com"); + yield return Setter(nameof(IPurchasesWrapper.SetPhoneNumber), + (purchases, value) => purchases.SetPhoneNumber(value), "+15551234567"); + yield return Setter(nameof(IPurchasesWrapper.SetDisplayName), + (purchases, value) => purchases.SetDisplayName(value), "Ada Lovelace"); + yield return Setter(nameof(IPurchasesWrapper.SetPushToken), + (purchases, value) => purchases.SetPushToken(value), "push_token_1"); + yield return Setter(nameof(IPurchasesWrapper.SetAdjustID), + (purchases, value) => purchases.SetAdjustID(value), "adjust_id_1"); + yield return Setter(nameof(IPurchasesWrapper.SetAppsflyerID), + (purchases, value) => purchases.SetAppsflyerID(value), "appsflyer_id_1"); + yield return Setter(nameof(IPurchasesWrapper.SetFBAnonymousID), + (purchases, value) => purchases.SetFBAnonymousID(value), "fb_anonymous_id_1"); + yield return Setter(nameof(IPurchasesWrapper.SetMparticleID), + (purchases, value) => purchases.SetMparticleID(value), "mparticle_id_1"); + yield return Setter(nameof(IPurchasesWrapper.SetOnesignalID), + (purchases, value) => purchases.SetOnesignalID(value), "onesignal_id_1"); + yield return Setter(nameof(IPurchasesWrapper.SetOnesignalUserID), + (purchases, value) => purchases.SetOnesignalUserID(value), "onesignal_user_id_1"); + yield return Setter(nameof(IPurchasesWrapper.SetAirshipChannelID), + (purchases, value) => purchases.SetAirshipChannelID(value), "airship_channel_1"); + yield return Setter(nameof(IPurchasesWrapper.SetCleverTapID), + (purchases, value) => purchases.SetCleverTapID(value), "clever_tap_id_1"); + yield return Setter(nameof(IPurchasesWrapper.SetMixpanelDistinctID), + (purchases, value) => purchases.SetMixpanelDistinctID(value), "mixpanel_distinct_id_1"); + yield return Setter(nameof(IPurchasesWrapper.SetFirebaseAppInstanceID), + (purchases, value) => purchases.SetFirebaseAppInstanceID(value), "firebase_app_instance_id_1"); + yield return Setter(nameof(IPurchasesWrapper.SetMediaSource), + (purchases, value) => purchases.SetMediaSource(value), "media_source_1"); + yield return Setter(nameof(IPurchasesWrapper.SetCampaign), + (purchases, value) => purchases.SetCampaign(value), "campaign_1"); + yield return Setter(nameof(IPurchasesWrapper.SetAdGroup), + (purchases, value) => purchases.SetAdGroup(value), "ad_group_1"); + yield return Setter(nameof(IPurchasesWrapper.SetAd), + (purchases, value) => purchases.SetAd(value), "ad_1"); + yield return Setter(nameof(IPurchasesWrapper.SetKeyword), + (purchases, value) => purchases.SetKeyword(value), "keyword_1"); + yield return Setter(nameof(IPurchasesWrapper.SetCreative), + (purchases, value) => purchases.SetCreative(value), "creative_1"); + } + + private static TestCaseData Setter(string method, Action invoke, string value) + { + return new TestCaseData(method, invoke, value) + .SetName($"{nameof(ForwardsValueToWrapper)}({method})"); + } + + [TestCaseSource(nameof(StringSetters))] + public void ForwardsValueToWrapper(string method, Action invoke, string value) + { + invoke(_purchases, value); + + var invocation = AssertOnlyInvocation(method, 1); + Assert.That(invocation.Arguments[0], Is.EqualTo(value)); + } + + /// + /// A null value means "delete this subscriber attribute", so the facade must not filter it out before it + /// reaches the wrapper. All of the setters above share the same single-statement body, so one representative + /// case is enough to catch a null guard being introduced. + /// + [Test] + public void ForwardsNullToWrapper() + { + _purchases.SetEmail(null); + + var invocation = AssertOnlyInvocation(nameof(IPurchasesWrapper.SetEmail), 1); + Assert.That(invocation.Arguments[0], Is.Null); + } + + private PurchasesWrapperSpy.Invocation AssertOnlyInvocation(string method, int argumentCount) + { + Assert.That(_wrapper.Invocations, Has.Count.EqualTo(1)); + + var invocation = _wrapper.Invocations[0]; + Assert.That(invocation.Method, Is.EqualTo(method)); + Assert.That(invocation.Arguments, Has.Length.EqualTo(argumentCount)); + return invocation; + } + } +} diff --git a/IntegrationTests/Assets/Tests/EditMode/AttributionPassthroughTests.cs.meta b/IntegrationTests/Assets/Tests/EditMode/AttributionPassthroughTests.cs.meta new file mode 100644 index 00000000..702abf5f --- /dev/null +++ b/IntegrationTests/Assets/Tests/EditMode/AttributionPassthroughTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: af4f11ed787c47b4b16d80e0b24648eb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/IntegrationTests/Assets/Tests/EditMode/TrackingTests.cs b/IntegrationTests/Assets/Tests/EditMode/TrackingTests.cs index 1ba8fa2a..78963939 100644 --- a/IntegrationTests/Assets/Tests/EditMode/TrackingTests.cs +++ b/IntegrationTests/Assets/Tests/EditMode/TrackingTests.cs @@ -41,9 +41,12 @@ public void TrackCustomPaywallImpressionWithoutParametersCreatesEmptyParameters( [Test] public void TrackCustomPaywallImpressionWithPaywallIdOnlySetsPaywallIdOnly() { - _purchases.TrackCustomPaywallImpression(new Purchases.CustomPaywallImpressionParams("paywall_1")); + var suppliedParameters = new Purchases.CustomPaywallImpressionParams("paywall_1"); + + _purchases.TrackCustomPaywallImpression(suppliedParameters); var invocation = AssertLastInvocation(nameof(IPurchasesWrapper.TrackCustomPaywallImpression), 1); + Assert.That(invocation.Arguments[0], Is.SameAs(suppliedParameters)); var parameters = (Purchases.CustomPaywallImpressionParams)invocation.Arguments[0]; Assert.That(parameters.PaywallId, Is.EqualTo("paywall_1")); Assert.That(parameters.OfferingId, Is.Null); diff --git a/IntegrationTests/Assets/Tests/EditMode/WrapperPassthroughTests.cs b/IntegrationTests/Assets/Tests/EditMode/WrapperPassthroughTests.cs index 1a3bc7b6..03c8d8e4 100644 --- a/IntegrationTests/Assets/Tests/EditMode/WrapperPassthroughTests.cs +++ b/IntegrationTests/Assets/Tests/EditMode/WrapperPassthroughTests.cs @@ -95,17 +95,40 @@ public void SetAppsFlyerConversionDataSerializesNestedValues() ["media_source"] = null, ["click_count"] = 3, ["nested"] = new Dictionary { ["key"] = "value" }, - ["list"] = new List { "a", "b" } + ["list"] = new List { "a", "b" }, + ["is_first_launch"] = true, + ["is_retargeting"] = false, + ["install_time_ms"] = 1717171717171L, + ["revenue_float"] = 9.99f, + ["revenue_double"] = 12.34d }); var invocation = AssertLastInvocation(nameof(IPurchasesWrapper.SetAppsFlyerConversionData), 1); - var conversionData = JSONNode.Parse((string)invocation.Arguments[0]); + var conversionDataJson = (string)invocation.Arguments[0]; + var conversionData = JSONNode.Parse(conversionDataJson); Assert.That(conversionData["af_status"].Value, Is.EqualTo("Organic")); Assert.That(conversionData["media_source"].IsNull, Is.True); + Assert.That(conversionData["click_count"].IsNumber, Is.True); Assert.That(conversionData["click_count"].AsInt, Is.EqualTo(3)); Assert.That(conversionData["nested"]["key"].Value, Is.EqualTo("value")); Assert.That(conversionData["list"][0].Value, Is.EqualTo("a")); Assert.That(conversionData["list"][1].Value, Is.EqualTo("b")); + // The IsBoolean/IsNumber checks carry the weight here. AsBool and AsFloat fall back to + // parsing the node's string value, so a number or bool that regressed into being + // serialized as a quoted string still satisfies the value assertion on its own. + Assert.That(conversionData["is_first_launch"].IsBoolean, Is.True); + Assert.That(conversionData["is_first_launch"].AsBool, Is.True); + Assert.That(conversionData["is_retargeting"].IsBoolean, Is.True); + Assert.That(conversionData["is_retargeting"].AsBool, Is.False); + Assert.That(conversionData["install_time_ms"].IsNumber, Is.True); + Assert.That(conversionData["install_time_ms"].AsLong, Is.EqualTo(1717171717171L)); + Assert.That(conversionData["revenue_float"].IsNumber, Is.True); + Assert.That(conversionData["revenue_float"].AsFloat, Is.EqualTo(9.99f).Within(0.0001f)); + Assert.That(conversionData["revenue_double"].IsNumber, Is.True); + Assert.That(conversionData["revenue_double"].AsDouble, Is.EqualTo(12.34d).Within(0.0001d)); + Assert.That(conversionDataJson, Does.Contain("\"is_first_launch\":true")); + Assert.That(conversionDataJson, Does.Contain("\"is_retargeting\":false")); + Assert.That(conversionDataJson, Does.Contain("\"install_time_ms\":1717171717171")); } [Test] @@ -139,12 +162,115 @@ public void SetSimulatesAskToBuyInSandboxForwardsFlag() } [Test] - public void SetAdjustIdForwardsValue() + public void SetLogLevelForwardsLevel() { - _purchases.SetAdjustID("adjust_id_1"); + _purchases.SetLogLevel(Purchases.LogLevel.Warn); - var invocation = AssertLastInvocation(nameof(IPurchasesWrapper.SetAdjustID), 1); - Assert.That(invocation.Arguments[0], Is.EqualTo("adjust_id_1")); + var invocation = AssertLastInvocation(nameof(IPurchasesWrapper.SetLogLevel), 1); + Assert.That(invocation.Arguments[0], Is.EqualTo(Purchases.LogLevel.Warn)); + } + + [Test] + public void SetDebugLogsEnabledForwardsFlag() + { +#pragma warning disable 618 + _purchases.SetDebugLogsEnabled(true); +#pragma warning restore 618 + + var invocation = AssertLastInvocation(nameof(IPurchasesWrapper.SetDebugLogsEnabled), 1); + Assert.That(invocation.Arguments[0], Is.True); + } + + [Test] + public void SetAllowSharingStoreAccountForwardsFlag() + { +#pragma warning disable 618 + _purchases.SetAllowSharingStoreAccount(true); +#pragma warning restore 618 + + var invocation = AssertLastInvocation(nameof(IPurchasesWrapper.SetAllowSharingStoreAccount), 1); + Assert.That(invocation.Arguments[0], Is.True); + } + + [Test] + public void SyncPurchasesCallsWrapper() + { + _purchases.SyncPurchases(); + + AssertLastInvocation(nameof(IPurchasesWrapper.SyncPurchases), 0); + } + + [Test] + public void SyncPurchasesWithCallbackCallsWrapper() + { + _purchases.SyncPurchases((customerInfo, error) => { }); + + AssertLastInvocation(nameof(IPurchasesWrapper.SyncPurchases), 0); + } + + [Test] + public void SyncAttributesAndOfferingsIfNeededCallsWrapper() + { + _purchases.SyncAttributesAndOfferingsIfNeeded((offerings, error) => { }); + + AssertLastInvocation(nameof(IPurchasesWrapper.SyncAttributesAndOfferingsIfNeeded), 0); + } + + [Test] + public void SetLogHandlerCallsWrapper() + { + _purchases.SetLogHandler((logLevel, message) => { }); + + AssertLastInvocation(nameof(IPurchasesWrapper.SetLogHandler), 0); + } + + [Test] + public void PresentCodeRedemptionSheetCallsWrapper() + { + _purchases.PresentCodeRedemptionSheet(); + + AssertLastInvocation(nameof(IPurchasesWrapper.PresentCodeRedemptionSheet), 0); + } + + [Test] + public void EnableAdServicesAttributionTokenCollectionCallsWrapper() + { + _purchases.EnableAdServicesAttributionTokenCollection(); + + AssertLastInvocation(nameof(IPurchasesWrapper.EnableAdServicesAttributionTokenCollection), 0); + } + + [Test] + public void GetAppUserIdCallsWrapper() + { + _purchases.GetAppUserId(); + + AssertLastInvocation(nameof(IPurchasesWrapper.GetAppUserId), 0); + } + + [Test] + public void IsAnonymousCallsWrapper() + { + _purchases.IsAnonymous(); + + AssertLastInvocation(nameof(IPurchasesWrapper.IsAnonymous), 0); + } + + [Test] + public void IsConfiguredCallsWrapper() + { + _purchases.IsConfigured(); + + AssertLastInvocation(nameof(IPurchasesWrapper.IsConfigured), 0); + } + + [Test] + public void GetCachedVirtualCurrenciesReturnsNullWhenWrapperHasNothingCached() + { + var virtualCurrencies = _purchases.GetCachedVirtualCurrencies(); + + AssertLastInvocation(nameof(IPurchasesWrapper.GetCachedVirtualCurrencies), 0); + Assert.That(virtualCurrencies, Is.Null); } private PurchasesWrapperSpy.Invocation AssertLastInvocation(string method, int argumentCount)