diff --git a/examples/audience/Assets/SampleApp/Scripts/AudienceSample.Events.cs b/examples/audience/Assets/SampleApp/Scripts/AudienceSample.Events.cs index 455b3fa2a..80265e0e0 100644 --- a/examples/audience/Assets/SampleApp/Scripts/AudienceSample.Events.cs +++ b/examples/audience/Assets/SampleApp/Scripts/AudienceSample.Events.cs @@ -55,15 +55,15 @@ internal readonly struct EventSpec internal static readonly EventSpec[] Catalogue = { - new EventSpec("sign_up", new[] { EventField.Text("method", optional: true) }), - new EventSpec("sign_in", new[] { EventField.Text("method", optional: true) }), - new EventSpec("email_acquired", new[] { EventField.Text("source", optional: true) }), - new EventSpec("wishlist_add", new[] { - EventField.Text("gameId"), - EventField.Text("source", optional: true), - EventField.Text("platform", optional: true), + new EventSpec(SampleAppCustomEvents.SignUp, new[] { EventField.Text(SampleAppCustomEventPropertyKeys.Method, optional: true) }), + new EventSpec(SampleAppCustomEvents.SignIn, new[] { EventField.Text(SampleAppCustomEventPropertyKeys.Method, optional: true) }), + new EventSpec(SampleAppCustomEvents.EmailAcquired, new[] { EventField.Text(SampleAppCustomEventPropertyKeys.Source, optional: true) }), + new EventSpec(SampleAppCustomEvents.WishlistAdd, new[] { + EventField.Text(SampleAppCustomEventPropertyKeys.GameId), + EventField.Text(SampleAppCustomEventPropertyKeys.Source, optional: true), + EventField.Text(SampleAppCustomEventPropertyKeys.Platform, optional: true), }), - new EventSpec("wishlist_remove", new[] { EventField.Text("gameId") }), + new EventSpec(SampleAppCustomEvents.WishlistRemove, new[] { EventField.Text(SampleAppCustomEventPropertyKeys.GameId) }), new EventSpec(EventNames.Purchase, new[] { EventField.Text(EventPropertyKeys.Currency), EventField.Number(EventPropertyKeys.Value), @@ -91,16 +91,16 @@ internal readonly struct EventSpec EventField.Text(EventPropertyKeys.ItemId, optional: true), }), new EventSpec(EventNames.MilestoneReached, new[] { EventField.Text(EventPropertyKeys.Name) }), - new EventSpec("game_page_viewed", new[] { - EventField.Text("gameId"), - EventField.Text("gameName", optional: true), - EventField.Text("slug", optional: true), + new EventSpec(SampleAppCustomEvents.GamePageViewed, new[] { + EventField.Text(SampleAppCustomEventPropertyKeys.GameId), + EventField.Text(SampleAppCustomEventPropertyKeys.GameName, optional: true), + EventField.Text(SampleAppCustomEventPropertyKeys.Slug, optional: true), }), - new EventSpec("link_clicked", new[] { - EventField.Text("url"), - EventField.Text("label", optional: true), - EventField.Text("source", optional: true), - EventField.Text("gameId", optional: true), + new EventSpec(SampleAppCustomEvents.LinkClicked, new[] { + EventField.Text(SampleAppCustomEventPropertyKeys.Url), + EventField.Text(SampleAppCustomEventPropertyKeys.Label, optional: true), + EventField.Text(SampleAppCustomEventPropertyKeys.Source, optional: true), + EventField.Text(SampleAppCustomEventPropertyKeys.GameId, optional: true), }), }; diff --git a/examples/audience/Assets/SampleApp/Scripts/AudienceSample.cs b/examples/audience/Assets/SampleApp/Scripts/AudienceSample.cs index 4f6564bb2..6a16d7626 100644 --- a/examples/audience/Assets/SampleApp/Scripts/AudienceSample.cs +++ b/examples/audience/Assets/SampleApp/Scripts/AudienceSample.cs @@ -110,8 +110,8 @@ private void OnPage() => RunAndLog(SampleAppUi.LogLabels.Page, () => { GuardConsentForTrack(); var screen = SceneManager.GetActiveScene().name; - var props = new Dictionary { ["path"] = screen }; - ImmutableAudience.Track("screen_viewed", props); + var props = new Dictionary { [SampleAppCustomEventPropertyKeys.Path] = screen }; + ImmutableAudience.Track(SampleAppCustomEvents.ScreenViewed, props); return Json.Serialize(props, 2); }); diff --git a/examples/audience/Assets/SampleApp/Scripts/SampleAppCustomEvents.cs b/examples/audience/Assets/SampleApp/Scripts/SampleAppCustomEvents.cs new file mode 100644 index 000000000..9dd3c5182 --- /dev/null +++ b/examples/audience/Assets/SampleApp/Scripts/SampleAppCustomEvents.cs @@ -0,0 +1,29 @@ +namespace Immutable.Audience.Samples.SampleApp +{ + // Sample-only event names that demonstrate the custom Track pattern. + internal static class SampleAppCustomEvents + { + internal const string SignUp = "sign_up"; + internal const string SignIn = "sign_in"; + internal const string EmailAcquired = "email_acquired"; + internal const string WishlistAdd = "wishlist_add"; + internal const string WishlistRemove = "wishlist_remove"; + internal const string GamePageViewed = "game_page_viewed"; + internal const string LinkClicked = "link_clicked"; + internal const string ScreenViewed = "screen_viewed"; + } + + // Property keys for the sample-app demo events. + internal static class SampleAppCustomEventPropertyKeys + { + internal const string Method = "method"; + internal const string Source = "source"; + internal const string GameId = "gameId"; + internal const string Platform = "platform"; + internal const string GameName = "gameName"; + internal const string Slug = "slug"; + internal const string Url = "url"; + internal const string Label = "label"; + internal const string Path = "path"; + } +} diff --git a/examples/audience/Assets/SampleApp/Scripts/SampleAppCustomEvents.cs.meta b/examples/audience/Assets/SampleApp/Scripts/SampleAppCustomEvents.cs.meta new file mode 100644 index 000000000..96534b02e --- /dev/null +++ b/examples/audience/Assets/SampleApp/Scripts/SampleAppCustomEvents.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6dec0a319b2b488a9c964a046bb410b2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/examples/audience/Assets/SampleApp/Tests/Runtime/SampleAppLiveFireTests.cs b/examples/audience/Assets/SampleApp/Tests/Runtime/SampleAppLiveFireTests.cs index 5a8cd50d7..2682fb149 100644 --- a/examples/audience/Assets/SampleApp/Tests/Runtime/SampleAppLiveFireTests.cs +++ b/examples/audience/Assets/SampleApp/Tests/Runtime/SampleAppLiveFireTests.cs @@ -152,8 +152,8 @@ public IEnumerator TypedEvent_Resource_FlushReportsOk() { yield return DriveTypedEventAndFlush(SampleAppUi.Buttons.TypedEvent(EventNames.Resource), root => { - root.Q(SampleAppUi.TypedEventField(EventNames.Resource, "currency")).value = "GOLD"; - root.Q(SampleAppUi.TypedEventField(EventNames.Resource, "amount")).value = "100"; + root.Q(SampleAppUi.TypedEventField(EventNames.Resource, EventPropertyKeys.Currency)).value = "GOLD"; + root.Q(SampleAppUi.TypedEventField(EventNames.Resource, EventPropertyKeys.Amount)).value = "100"; }); } @@ -162,8 +162,8 @@ public IEnumerator TypedEvent_Purchase_FlushReportsOk() { yield return DriveTypedEventAndFlush(SampleAppUi.Buttons.TypedEvent(EventNames.Purchase), root => { - root.Q(SampleAppUi.TypedEventField(EventNames.Purchase, "currency")).value = "USD"; - root.Q(SampleAppUi.TypedEventField(EventNames.Purchase, "value")).value = "9.99"; + root.Q(SampleAppUi.TypedEventField(EventNames.Purchase, EventPropertyKeys.Currency)).value = "USD"; + root.Q(SampleAppUi.TypedEventField(EventNames.Purchase, EventPropertyKeys.Value)).value = "9.99"; }); } @@ -172,7 +172,7 @@ public IEnumerator TypedEvent_MilestoneReached_FlushReportsOk() { yield return DriveTypedEventAndFlush(SampleAppUi.Buttons.TypedEvent(EventNames.MilestoneReached), root => { - root.Q(SampleAppUi.TypedEventField(EventNames.MilestoneReached, "name")).value = "il2cpp_smoke"; + root.Q(SampleAppUi.TypedEventField(EventNames.MilestoneReached, EventPropertyKeys.Name)).value = "il2cpp_smoke"; }); } @@ -403,54 +403,54 @@ public IEnumerator IdentifyTraits_AfterIdentify_FlushReportsOk() [UnityTest] public IEnumerator TypedEvent_SignUp_FlushReportsOk() { - yield return DriveTypedEventAndFlush(SampleAppUi.Buttons.TypedEvent("sign_up")); + yield return DriveTypedEventAndFlush(SampleAppUi.Buttons.TypedEvent(SampleAppCustomEvents.SignUp)); } [UnityTest] public IEnumerator TypedEvent_SignIn_FlushReportsOk() { - yield return DriveTypedEventAndFlush(SampleAppUi.Buttons.TypedEvent("sign_in")); + yield return DriveTypedEventAndFlush(SampleAppUi.Buttons.TypedEvent(SampleAppCustomEvents.SignIn)); } [UnityTest] public IEnumerator TypedEvent_EmailAcquired_FlushReportsOk() { - yield return DriveTypedEventAndFlush(SampleAppUi.Buttons.TypedEvent("email_acquired")); + yield return DriveTypedEventAndFlush(SampleAppUi.Buttons.TypedEvent(SampleAppCustomEvents.EmailAcquired)); } [UnityTest] public IEnumerator TypedEvent_WishlistAdd_FlushReportsOk() { - yield return DriveTypedEventAndFlush(SampleAppUi.Buttons.TypedEvent("wishlist_add"), root => + yield return DriveTypedEventAndFlush(SampleAppUi.Buttons.TypedEvent(SampleAppCustomEvents.WishlistAdd), root => { - root.Q(SampleAppUi.TypedEventField("wishlist_add", "gameId")).value = "il2cpp_game_1"; + root.Q(SampleAppUi.TypedEventField(SampleAppCustomEvents.WishlistAdd, SampleAppCustomEventPropertyKeys.GameId)).value = "il2cpp_game_1"; }); } [UnityTest] public IEnumerator TypedEvent_WishlistRemove_FlushReportsOk() { - yield return DriveTypedEventAndFlush(SampleAppUi.Buttons.TypedEvent("wishlist_remove"), root => + yield return DriveTypedEventAndFlush(SampleAppUi.Buttons.TypedEvent(SampleAppCustomEvents.WishlistRemove), root => { - root.Q(SampleAppUi.TypedEventField("wishlist_remove", "gameId")).value = "il2cpp_game_1"; + root.Q(SampleAppUi.TypedEventField(SampleAppCustomEvents.WishlistRemove, SampleAppCustomEventPropertyKeys.GameId)).value = "il2cpp_game_1"; }); } [UnityTest] public IEnumerator TypedEvent_GamePageViewed_FlushReportsOk() { - yield return DriveTypedEventAndFlush(SampleAppUi.Buttons.TypedEvent("game_page_viewed"), root => + yield return DriveTypedEventAndFlush(SampleAppUi.Buttons.TypedEvent(SampleAppCustomEvents.GamePageViewed), root => { - root.Q(SampleAppUi.TypedEventField("game_page_viewed", "gameId")).value = "il2cpp_game_1"; + root.Q(SampleAppUi.TypedEventField(SampleAppCustomEvents.GamePageViewed, SampleAppCustomEventPropertyKeys.GameId)).value = "il2cpp_game_1"; }); } [UnityTest] public IEnumerator TypedEvent_LinkClicked_FlushReportsOk() { - yield return DriveTypedEventAndFlush(SampleAppUi.Buttons.TypedEvent("link_clicked"), root => + yield return DriveTypedEventAndFlush(SampleAppUi.Buttons.TypedEvent(SampleAppCustomEvents.LinkClicked), root => { - root.Q(SampleAppUi.TypedEventField("link_clicked", "url")).value = "https://example.com/il2cpp"; + root.Q(SampleAppUi.TypedEventField(SampleAppCustomEvents.LinkClicked, SampleAppCustomEventPropertyKeys.Url)).value = "https://example.com/il2cpp"; }); } diff --git a/src/Packages/Audience/Runtime/AssemblyInfo.cs b/src/Packages/Audience/Runtime/AssemblyInfo.cs index 565986f27..00cb69340 100644 --- a/src/Packages/Audience/Runtime/AssemblyInfo.cs +++ b/src/Packages/Audience/Runtime/AssemblyInfo.cs @@ -1,6 +1,7 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("Immutable.Audience.Runtime.Tests")] +[assembly: InternalsVisibleTo("Immutable.Audience.Editor.Tests")] [assembly: InternalsVisibleTo("Immutable.Audience.Unity")] // First-party SampleApp reaches Json.Serialize and diff --git a/src/Packages/Audience/Runtime/Unity/AssemblyInfo.cs b/src/Packages/Audience/Runtime/Unity/AssemblyInfo.cs new file mode 100644 index 000000000..8cb318847 --- /dev/null +++ b/src/Packages/Audience/Runtime/Unity/AssemblyInfo.cs @@ -0,0 +1,4 @@ +using System.Runtime.CompilerServices; + +// Editor-only test fixture for DeviceCollector reads internals of this assembly. +[assembly: InternalsVisibleTo("Immutable.Audience.Editor.Tests")] diff --git a/src/Packages/Audience/Runtime/Unity/AssemblyInfo.cs.meta b/src/Packages/Audience/Runtime/Unity/AssemblyInfo.cs.meta new file mode 100644 index 000000000..b41abd572 --- /dev/null +++ b/src/Packages/Audience/Runtime/Unity/AssemblyInfo.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 42c73f606a344d9d8fcda6cd3cf6fa5c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/src/Packages/Audience/Tests/Editor/DeviceCollectorTests.cs b/src/Packages/Audience/Tests/Editor/DeviceCollectorTests.cs new file mode 100644 index 000000000..83ee856d3 --- /dev/null +++ b/src/Packages/Audience/Tests/Editor/DeviceCollectorTests.cs @@ -0,0 +1,113 @@ +#nullable enable + +using System.Collections.Generic; +using NUnit.Framework; +using Immutable.Audience.Unity; + +namespace Immutable.Audience.Tests.Editor +{ + // Editor-only (DeviceCollector needs a Unity domain; skipped by the headless dotnet build). + // Pins emitted payload keys against GameLaunchPropertyKeys and ContextKeys. + [TestFixture] + internal class DeviceCollectorTests + { + [Test] + public void CollectGameLaunchProperties_EmitsTheExpectedKeySet() + { + var props = DeviceCollector.CollectGameLaunchProperties(); + + // Always-present keys. ScreenDpi is conditional (0 on some Linux WMs). + CollectionAssert.Contains(props.Keys, GameLaunchPropertyKeys.Platform); + CollectionAssert.Contains(props.Keys, GameLaunchPropertyKeys.Version); + CollectionAssert.Contains(props.Keys, GameLaunchPropertyKeys.BuildGuid); + CollectionAssert.Contains(props.Keys, GameLaunchPropertyKeys.UnityVersion); + CollectionAssert.Contains(props.Keys, GameLaunchPropertyKeys.OsFamily); + CollectionAssert.Contains(props.Keys, GameLaunchPropertyKeys.DeviceModel); + CollectionAssert.Contains(props.Keys, GameLaunchPropertyKeys.Gpu); + CollectionAssert.Contains(props.Keys, GameLaunchPropertyKeys.GpuVendor); + CollectionAssert.Contains(props.Keys, GameLaunchPropertyKeys.Cpu); + CollectionAssert.Contains(props.Keys, GameLaunchPropertyKeys.CpuCores); + CollectionAssert.Contains(props.Keys, GameLaunchPropertyKeys.RamMb); + } + + [Test] + public void CollectGameLaunchProperties_EmitsNoUnknownKeys() + { + // Confirms the payload only carries known GameLaunchPropertyKeys entries. + var allowed = new HashSet + { + GameLaunchPropertyKeys.Platform, + GameLaunchPropertyKeys.Version, + GameLaunchPropertyKeys.BuildGuid, + GameLaunchPropertyKeys.UnityVersion, + GameLaunchPropertyKeys.OsFamily, + GameLaunchPropertyKeys.DeviceModel, + GameLaunchPropertyKeys.Gpu, + GameLaunchPropertyKeys.GpuVendor, + GameLaunchPropertyKeys.Cpu, + GameLaunchPropertyKeys.CpuCores, + GameLaunchPropertyKeys.RamMb, + GameLaunchPropertyKeys.ScreenDpi, + }; + + var props = DeviceCollector.CollectGameLaunchProperties(); + foreach (var key in props.Keys) + Assert.IsTrue(allowed.Contains(key), + $"DeviceCollector.CollectGameLaunchProperties emitted unknown key '{key}' " + + "with no matching GameLaunchPropertyKeys constant"); + } + + [Test] + public void CollectGameLaunchProperties_TruncatesStringValuesToMaxFieldLength() + { + // Every string value respects MaxFieldLength; an untruncated .ToString() would fail here. + var props = DeviceCollector.CollectGameLaunchProperties(); + foreach (var kv in props) + { + if (kv.Value is string s) + Assert.LessOrEqual(s.Length, Constants.MaxFieldLength, + $"GameLaunchPropertyKeys.{kv.Key} value exceeds Constants.MaxFieldLength"); + } + } + + [Test] + public void CollectContext_EmitsTheExpectedKeySet() + { + var ctx = DeviceCollector.CollectContext(); + + // UserAgent is unconditional. Timezone / Locale / Screen are + // best-effort and may be absent under unusual hosts. + CollectionAssert.Contains(ctx.Keys, ContextKeys.UserAgent); + } + + [Test] + public void CollectContext_EmitsNoUnknownKeys() + { + var allowed = new HashSet + { + ContextKeys.UserAgent, + ContextKeys.Timezone, + ContextKeys.Locale, + ContextKeys.Screen, + }; + + var ctx = DeviceCollector.CollectContext(); + foreach (var key in ctx.Keys) + Assert.IsTrue(allowed.Contains(key), + $"DeviceCollector.CollectContext emitted unknown key '{key}' " + + "with no matching ContextKeys constant"); + } + + [Test] + public void CollectContext_TruncatesStringValuesToMaxFieldLength() + { + var ctx = DeviceCollector.CollectContext(); + foreach (var kv in ctx) + { + if (kv.Value is string s) + Assert.LessOrEqual(s.Length, Constants.MaxFieldLength, + $"ContextKeys.{kv.Key} value exceeds Constants.MaxFieldLength"); + } + } + } +} diff --git a/src/Packages/Audience/Tests/Editor/com.immutable.audience.tests.editor.asmdef b/src/Packages/Audience/Tests/Editor/com.immutable.audience.tests.editor.asmdef new file mode 100644 index 000000000..84dfd7a33 --- /dev/null +++ b/src/Packages/Audience/Tests/Editor/com.immutable.audience.tests.editor.asmdef @@ -0,0 +1,19 @@ +{ + "name": "Immutable.Audience.Editor.Tests", + "rootNamespace": "Immutable.Audience.Tests.Editor", + "references": [ + "Immutable.Audience.Runtime", + "Immutable.Audience.Unity", + "UnityEditor.TestRunner", + "UnityEngine.TestRunner" + ], + "includePlatforms": ["Editor"], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": true, + "precompiledReferences": ["nunit.framework.dll"], + "autoReferenced": false, + "defineConstraints": ["UNITY_INCLUDE_TESTS"], + "versionDefines": [], + "noEngineReferences": false +} diff --git a/src/Packages/Audience/Tests/Editor/com.immutable.audience.tests.editor.asmdef.meta b/src/Packages/Audience/Tests/Editor/com.immutable.audience.tests.editor.asmdef.meta new file mode 100644 index 000000000..c98de3689 --- /dev/null +++ b/src/Packages/Audience/Tests/Editor/com.immutable.audience.tests.editor.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: a0de2d71dd564e69b03eb055285e697d +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/src/Packages/Audience/Tests/Runtime/ConstantsTests.cs b/src/Packages/Audience/Tests/Runtime/ConstantsTests.cs index fd7258eae..223b88057 100644 --- a/src/Packages/Audience/Tests/Runtime/ConstantsTests.cs +++ b/src/Packages/Audience/Tests/Runtime/ConstantsTests.cs @@ -69,6 +69,20 @@ public void LibraryVersion_MatchesPackageJson() "Constants.LibraryVersion must match package.json version"); } + [Test] + public void LibraryName_MatchesPackageJson() + { + // Same idea as LibraryVersion: fails the build if + // Constants.LibraryName drifts from package.json "name". + var packageJson = ReadPackageJson(); + var parsed = JsonReader.DeserializeObject(packageJson); + + Assert.IsTrue(parsed.TryGetValue("name", out var nameObj), + "package.json is missing a \"name\" field"); + Assert.AreEqual(Constants.LibraryName, nameObj, + "Constants.LibraryName must match package.json name"); + } + private static string ReadPackageJson() { // Walk up from the test binary location looking for the Audience diff --git a/src/Packages/Audience/Tests/Runtime/Events/MessageBuilderTests.cs b/src/Packages/Audience/Tests/Runtime/Events/MessageBuilderTests.cs index e8805c9e4..593f28d0a 100644 --- a/src/Packages/Audience/Tests/Runtime/Events/MessageBuilderTests.cs +++ b/src/Packages/Audience/Tests/Runtime/Events/MessageBuilderTests.cs @@ -13,14 +13,14 @@ public class MessageBuilderTests [Test] public void Track_RequiredFieldsPresent() { - var result = MessageBuilder.Track("level_complete", "anon-1", null, PackageVersion); + var result = MessageBuilder.Track(TestEventNames.LevelComplete, "anon-1", null, PackageVersion); Assert.AreEqual(MessageTypes.Track, result[MessageFields.Type]); Assert.IsTrue(result.ContainsKey(MessageFields.MessageId)); Assert.IsTrue(result.ContainsKey(MessageFields.EventTimestamp)); Assert.IsTrue(result.ContainsKey(MessageFields.Context)); Assert.IsTrue(result.ContainsKey(MessageFields.Surface)); - Assert.AreEqual("level_complete", result[MessageFields.EventName]); + Assert.AreEqual(TestEventNames.LevelComplete, result[MessageFields.EventName]); } [Test] diff --git a/src/Packages/Audience/Tests/Runtime/IdentityTypeTests.cs b/src/Packages/Audience/Tests/Runtime/IdentityTypeTests.cs index d20939613..4050e73fe 100644 --- a/src/Packages/Audience/Tests/Runtime/IdentityTypeTests.cs +++ b/src/Packages/Audience/Tests/Runtime/IdentityTypeTests.cs @@ -19,6 +19,37 @@ public void ToLowercaseString_MapsEachEnumValueToLowercaseBackendString(Identity Assert.AreEqual(expected, type.ToLowercaseString()); } + [TestCase("passport", IdentityType.Passport)] + [TestCase("steam", IdentityType.Steam)] + [TestCase("epic", IdentityType.Epic)] + [TestCase("google", IdentityType.Google)] + [TestCase("apple", IdentityType.Apple)] + [TestCase("discord", IdentityType.Discord)] + [TestCase("email", IdentityType.Email)] + [TestCase("custom", IdentityType.Custom)] + public void ParseLowercaseString_MapsKnownStringToEnum(string wire, IdentityType expected) + { + Assert.AreEqual(expected, IdentityTypeExtensions.ParseLowercaseString(wire)); + } + + [TestCase("Steam", IdentityType.Steam)] + [TestCase("STEAM", IdentityType.Steam)] + [TestCase("Passport", IdentityType.Passport)] + public void ParseLowercaseString_AcceptsMixedCase(string wire, IdentityType expected) + { + Assert.AreEqual(expected, IdentityTypeExtensions.ParseLowercaseString(wire)); + } + + [TestCase(null)] + [TestCase("")] + [TestCase("unknown_provider")] + [TestCase("steamX")] + public void ParseLowercaseString_FallsBackToCustomForUnknownOrEmpty(string? wire) + { + // ParseLowercaseString never throws; unknown values map to Custom. + Assert.AreEqual(IdentityType.Custom, IdentityTypeExtensions.ParseLowercaseString(wire)); + } + [Test] public void ToLowercaseString_UnknownValue_Throws() { diff --git a/src/Packages/Audience/Tests/Runtime/ImmutableAudienceTests.cs b/src/Packages/Audience/Tests/Runtime/ImmutableAudienceTests.cs index efdff3643..07861159d 100644 --- a/src/Packages/Audience/Tests/Runtime/ImmutableAudienceTests.cs +++ b/src/Packages/Audience/Tests/Runtime/ImmutableAudienceTests.cs @@ -129,7 +129,7 @@ public void AnonymousId_ConsentAnonymous_ReturnsPersistedId() { ImmutableAudience.Init(MakeConfig(ConsentLevel.Anonymous)); // Track once so Identity.GetOrCreate runs and writes the id file. - ImmutableAudience.Track("warmup_event"); + ImmutableAudience.Track(TestEventNames.WarmupEvent); var id = ImmutableAudience.AnonymousId; Assert.IsFalse(string.IsNullOrEmpty(id), @@ -165,7 +165,7 @@ public void QueueSize_ZeroBeforeInit_GrowsWithEnqueue() Assert.Greater(afterInit, 0, "QueueSize should include session_start and game_launch after Init"); - ImmutableAudience.Track("explicit_track_event"); + ImmutableAudience.Track(TestEventNames.ExplicitTrackEvent); Assert.Greater(ImmutableAudience.QueueSize, afterInit, "QueueSize should grow when a new event is enqueued"); } @@ -186,7 +186,7 @@ public void ContextProvider_Set_MergesFieldsIntoEveryMessageContext() }; ImmutableAudience.Init(MakeConfig()); - ImmutableAudience.Track("unit_test_event"); + ImmutableAudience.Track(TestEventNames.UnitTestEvent); ImmutableAudience.Shutdown(); var queueDir = AudiencePaths.QueueDir(_testDir); @@ -234,7 +234,7 @@ public void ContextProvider_ThrowingDelegate_SwallowsAndShipsBaseContext() ImmutableAudience.ContextProvider = () => throw new InvalidOperationException("boom"); ImmutableAudience.Init(MakeConfig()); - ImmutableAudience.Track("unit_test_event"); + ImmutableAudience.Track(TestEventNames.UnitTestEvent); ImmutableAudience.Shutdown(); var queueDir = AudiencePaths.QueueDir(_testDir); @@ -250,7 +250,7 @@ public void ContextProvider_ReturnsNull_ShipsBaseContext() ImmutableAudience.ContextProvider = () => null; ImmutableAudience.Init(MakeConfig()); - ImmutableAudience.Track("unit_test_event"); + ImmutableAudience.Track(TestEventNames.UnitTestEvent); ImmutableAudience.Shutdown(); var queueDir = AudiencePaths.QueueDir(_testDir); @@ -488,7 +488,7 @@ public void Track_CustomEvent_WritesEventToDisk() { ImmutableAudience.Init(MakeConfig()); - ImmutableAudience.Track("crafting_started", new Dictionary + ImmutableAudience.Track(TestEventNames.CraftingStarted, new Dictionary { { "recipe_id", "iron_sword" } }); @@ -511,7 +511,7 @@ public void Track_NoProperties_WritesEvent() { ImmutableAudience.Init(MakeConfig()); - ImmutableAudience.Track("main_menu_opened"); + ImmutableAudience.Track(TestEventNames.MainMenuOpened); ImmutableAudience.Shutdown(); var queueDir = AudiencePaths.QueueDir(_testDir); @@ -525,7 +525,7 @@ public void Track_ConsentNone_DoesNotEnqueue() { ImmutableAudience.Init(MakeConfig(ConsentLevel.None)); - ImmutableAudience.Track("should_not_appear"); + ImmutableAudience.Track(TestEventNames.ShouldNotAppear); ImmutableAudience.Shutdown(); var queueDir = AudiencePaths.QueueDir(_testDir); @@ -669,12 +669,12 @@ public void Reset_GeneratesNewAnonymousId() { ImmutableAudience.Init(MakeConfig()); - ImmutableAudience.Track("before_reset"); + ImmutableAudience.Track(TestEventNames.BeforeReset); var id1 = Identity.GetOrCreate(_testDir, ConsentLevel.Anonymous); ImmutableAudience.Reset(); - ImmutableAudience.Track("after_reset"); + ImmutableAudience.Track(TestEventNames.AfterReset); var id2 = Identity.GetOrCreate(_testDir, ConsentLevel.Anonymous); Assert.AreNotEqual(id1, id2, "Reset should generate a new anonymousId"); @@ -685,7 +685,7 @@ public void Reset_DiscardsQueuedEventsOnDisk() { ImmutableAudience.Init(MakeConfig()); - ImmutableAudience.Track("before_reset"); + ImmutableAudience.Track(TestEventNames.BeforeReset); ImmutableAudience.FlushQueueToDiskForTesting(); var queueDir = AudiencePaths.QueueDir(_testDir); @@ -707,7 +707,7 @@ public void SetConsent_DowngradeToNone_PurgesQueueOnDiskAndInMemory() { ImmutableAudience.Init(MakeConfig()); - ImmutableAudience.Track("event_under_old_consent"); + ImmutableAudience.Track(TestEventNames.EventUnderOldConsent); var queueDir = AudiencePaths.QueueDir(_testDir); // Force memory → disk so we can verify the purge wipes both layers. @@ -746,7 +746,7 @@ public void SetConsent_DowngradeToNone_DropsInFlightTrack_ThatRacesThePurge() var trackTask = Task.Run(() => { trackStarted.Set(); - ImmutableAudience.Track("racing_event"); + ImmutableAudience.Track(TestEventNames.RacingEvent); }); trackStarted.Wait(); @@ -799,7 +799,7 @@ public void SetConsent_DowngradeToNone_StressTest_NoLeak() trackers[t] = Task.Run(() => { barrier.SignalAndWait(); - ImmutableAudience.Track("race_stress"); + ImmutableAudience.Track(TestEventNames.RaceStress); }); } @@ -988,7 +988,7 @@ public void SetConsent_DowngradeToAnonymous_StressTest_NoUserIdLeak() trackers[t] = Task.Run(() => { barrier.SignalAndWait(); - ImmutableAudience.Track("race_stress"); + ImmutableAudience.Track(TestEventNames.RaceStress); }); } @@ -1151,6 +1151,21 @@ public void Init_LeavesDistributionPlatformUnchanged_WhenAlreadyLowercase() Assert.AreEqual(DistributionPlatforms.Steam, config.DistributionPlatform); } + // Lowercase normalisation must apply to every DistributionPlatforms value. + [TestCase(DistributionPlatforms.Steam)] + [TestCase(DistributionPlatforms.Epic)] + [TestCase(DistributionPlatforms.GOG)] + [TestCase(DistributionPlatforms.Itch)] + [TestCase(DistributionPlatforms.Standalone)] + public void Init_LowercasesDistributionPlatform_AcrossAllPublicValues(string canonical) + { + var config = MakeConfig(); + config.DistributionPlatform = canonical.ToUpperInvariant(); + ImmutableAudience.Init(config); + + Assert.AreEqual(canonical, config.DistributionPlatform); + } + [Test] public void Init_LeavesDistributionPlatformNull_WhenNotSet() { @@ -1260,7 +1275,7 @@ public void Track_AfterShutdown_IsIgnored() ImmutableAudience.Init(MakeConfig()); ImmutableAudience.Shutdown(); - Assert.DoesNotThrow(() => ImmutableAudience.Track("should_not_crash")); + Assert.DoesNotThrow(() => ImmutableAudience.Track(TestEventNames.ShouldNotCrash)); } [Test] @@ -1276,7 +1291,7 @@ public void Shutdown_ReleasesInitLock_BeforeBlockingTeardown() config.ShutdownFlushTimeoutMs = 10_000; ImmutableAudience.Init(config); - ImmutableAudience.Track("ensure_nonempty_queue"); + ImmutableAudience.Track(TestEventNames.EnsureNonemptyQueue); ImmutableAudience.FlushQueueToDiskForTesting(); // Phase 1 flips _initialized and releases the lock; Phase 2 enters @@ -1312,7 +1327,7 @@ public void FullToAnonymous_StripsUserIdFromQueuedTrackAndDropsIdentifyAlias() ImmutableAudience.Identify("player_steam", IdentityType.Steam); ImmutableAudience.Alias("player_steam", IdentityType.Steam, "player_passport", IdentityType.Passport); - ImmutableAudience.Track("tracked_before_downgrade"); + ImmutableAudience.Track(TestEventNames.TrackedBeforeDowngrade); ImmutableAudience.FlushQueueToDiskForTesting(); @@ -1339,7 +1354,7 @@ public void FullToAnonymous_FutureTracksOmitUserId() ImmutableAudience.Identify("player_steam", IdentityType.Steam); ImmutableAudience.SetConsent(ConsentLevel.Anonymous); - ImmutableAudience.Track("tracked_after_downgrade"); + ImmutableAudience.Track(TestEventNames.TrackedAfterDowngrade); ImmutableAudience.FlushQueueToDiskForTesting(); var queueDir = AudiencePaths.QueueDir(_testDir); @@ -1347,7 +1362,7 @@ public void FullToAnonymous_FutureTracksOmitUserId() .Select(f => JsonReader.DeserializeObject(File.ReadAllText(f))) .Where(m => (string)m[MessageFields.Type] == MessageTypes.Track && m.ContainsKey(MessageFields.EventName) - && (string)m[MessageFields.EventName] == "tracked_after_downgrade") + && (string)m[MessageFields.EventName] == TestEventNames.TrackedAfterDowngrade) .ToList(); Assert.AreEqual(1, trackFiles.Count); @@ -1367,7 +1382,7 @@ public void SendBatch_ConcurrentTicks_OnlyOneReachesTransport() config.HttpHandler = handler; ImmutableAudience.Init(config); - ImmutableAudience.Track("event_to_send"); + ImmutableAudience.Track(TestEventNames.EventToSend); ImmutableAudience.FlushQueueToDiskForTesting(); // Kick off one SendBatch on a worker. It will block inside the @@ -1402,7 +1417,7 @@ public void FlushAsync_ConcurrentCallers_OnlyOneReachesTransport() config.HttpHandler = handler; ImmutableAudience.Init(config); - ImmutableAudience.Track("event_to_send"); + ImmutableAudience.Track(TestEventNames.EventToSend); ImmutableAudience.FlushQueueToDiskForTesting(); // First caller enters SendAsync and blocks on handler.Release. @@ -1438,7 +1453,7 @@ public async Task FlushAsync_CancelledToken_Terminates_DoesNotHotLoop() config.HttpHandler = handler; ImmutableAudience.Init(config); - ImmutableAudience.Track("event_to_send"); + ImmutableAudience.Track(TestEventNames.EventToSend); ImmutableAudience.FlushQueueToDiskForTesting(); using var cts = new CancellationTokenSource(); diff --git a/src/Packages/Audience/Tests/Runtime/MessagesTests.cs b/src/Packages/Audience/Tests/Runtime/MessagesTests.cs new file mode 100644 index 000000000..5bd126980 --- /dev/null +++ b/src/Packages/Audience/Tests/Runtime/MessagesTests.cs @@ -0,0 +1,133 @@ +using System; +using NUnit.Framework; + +namespace Immutable.Audience.Tests +{ + // Pins each message constant to its exact wording so a reword breaks the build. + [TestFixture] + internal class AudienceErrorMessagesTests + { + [Test] + public void LocalStorageReadFailed_PrefixesAndAppendsExceptionMessage() + { + var ex = new InvalidOperationException("disk full"); + Assert.AreEqual( + "Local storage read failed: disk full", + AudienceErrorMessages.LocalStorageReadFailed(ex)); + } + + [Test] + public void BatchPartiallyRejected_FormatsRejectedAndTotalCounts() + { + Assert.AreEqual( + "Batch partially rejected: 3 of 20 events dropped", + AudienceErrorMessages.BatchPartiallyRejected(3, 20)); + } + + [Test] + public void BatchRejectedPrefix_IsExactWording() + { + Assert.AreEqual("Batch rejected", AudienceErrorMessages.BatchRejectedPrefix); + } + + [Test] + public void ServerErrorWillRetryPrefix_IsExactWording() + { + Assert.AreEqual("Server error, will retry", AudienceErrorMessages.ServerErrorWillRetryPrefix); + } + + [Test] + public void ConsentSyncFailedWithStatus_FormatsStatusCode() + { + Assert.AreEqual( + "Consent sync failed with status 503", + AudienceErrorMessages.ConsentSyncFailedWithStatus(503)); + } + + [Test] + public void ConsentSyncThrew_PrefixesAndAppendsExceptionMessage() + { + var ex = new TimeoutException("timed out"); + Assert.AreEqual( + "Consent sync threw: timed out", + AudienceErrorMessages.ConsentSyncThrew(ex)); + } + } + + [TestFixture] + internal class AudienceArgumentMessagesTests + { + [Test] + public void PublishableKeyRequired_IsExactWording() + { + Assert.AreEqual("PublishableKey is required", + AudienceArgumentMessages.PublishableKeyRequired); + } + + [Test] + public void PersistentDataPathRequired_IsExactWording() + { + Assert.AreEqual("PersistentDataPath is required", + AudienceArgumentMessages.PersistentDataPathRequired); + } + + [Test] + public void ProgressionStatusRequired_IsExactWording() + { + Assert.AreEqual( + "Progression.Status is required. Set it before calling Track(IEvent).", + AudienceArgumentMessages.ProgressionStatusRequired); + } + + [Test] + public void ResourceFlowRequired_IsExactWording() + { + Assert.AreEqual( + "Resource.Flow is required. Set it before calling Track(IEvent).", + AudienceArgumentMessages.ResourceFlowRequired); + } + + [Test] + public void ResourceCurrencyRequired_IsExactWording() + { + Assert.AreEqual( + "Resource.Currency is required. Set a non-empty string before calling Track(IEvent).", + AudienceArgumentMessages.ResourceCurrencyRequired); + } + + [Test] + public void ResourceAmountRequired_IsExactWording() + { + Assert.AreEqual( + "Resource.Amount is required. Set it before calling Track(IEvent).", + AudienceArgumentMessages.ResourceAmountRequired); + } + + [Test] + public void PurchaseValueRequired_IsExactWording() + { + Assert.AreEqual( + "Purchase.Value is required. Set it before calling Track(IEvent).", + AudienceArgumentMessages.PurchaseValueRequired); + } + + [Test] + public void MilestoneReachedNameRequired_IsExactWording() + { + Assert.AreEqual( + "MilestoneReached.Name must not be null or empty", + AudienceArgumentMessages.MilestoneReachedNameRequired); + } + + [TestCase("USD", + "Purchase.Currency 'USD' must be a three-letter uppercase ISO 4217 code")] + [TestCase(null, + "Purchase.Currency '' must be a three-letter uppercase ISO 4217 code")] + [TestCase("usd", + "Purchase.Currency 'usd' must be a three-letter uppercase ISO 4217 code")] + public void PurchaseCurrencyInvalid_FormatsCurrencyValue(string? currency, string expected) + { + Assert.AreEqual(expected, AudienceArgumentMessages.PurchaseCurrencyInvalid(currency)); + } + } +} diff --git a/src/Packages/Audience/Tests/Runtime/OfflineResilienceTests.cs b/src/Packages/Audience/Tests/Runtime/OfflineResilienceTests.cs index b7db80763..7da31a416 100644 --- a/src/Packages/Audience/Tests/Runtime/OfflineResilienceTests.cs +++ b/src/Packages/Audience/Tests/Runtime/OfflineResilienceTests.cs @@ -93,7 +93,7 @@ public void Shutdown_DiskWritesBlocked_DoesNotThrow() // Shutdown is invoked from app-quit handlers; an exception would // crash the process. ImmutableAudience.Init(MakeConfig()); - ImmutableAudience.Track("event_pre_block"); + ImmutableAudience.Track(TestEventNames.EventPreBlock); BlockDiskWrites(); for (int i = 0; i < 20; i++) ImmutableAudience.Track($"blocked_{i}"); diff --git a/src/Packages/Audience/Tests/Runtime/PublishableKeyPrefixTests.cs b/src/Packages/Audience/Tests/Runtime/PublishableKeyPrefixTests.cs index 8c45c879d..c59b1ad56 100644 --- a/src/Packages/Audience/Tests/Runtime/PublishableKeyPrefixTests.cs +++ b/src/Packages/Audience/Tests/Runtime/PublishableKeyPrefixTests.cs @@ -180,7 +180,7 @@ public async Task Track_BackendReturns401_SurfacesValidationRejected() config.OnError = errors.Add; ImmutableAudience.Init(config); - ImmutableAudience.Track("event_against_prod_with_test_key"); + ImmutableAudience.Track(TestEventNames.EventAgainstProdWithTestKey); await ImmutableAudience.FlushAsync(); Assert.IsTrue(errors.Any(e => e.Code == AudienceErrorCode.ValidationRejected), diff --git a/src/Packages/Audience/Tests/Runtime/TestEventNames.cs b/src/Packages/Audience/Tests/Runtime/TestEventNames.cs new file mode 100644 index 000000000..657c18926 --- /dev/null +++ b/src/Packages/Audience/Tests/Runtime/TestEventNames.cs @@ -0,0 +1,30 @@ +namespace Immutable.Audience.Tests +{ + // Per-test event names. Each names what the test measures. + internal static class TestEventNames + { + internal const string Warmup = "warmup"; + internal const string WarmupEvent = "warmup_event"; + internal const string ExplicitTrackEvent = "explicit_track_event"; + internal const string UnitTestEvent = "unit_test_event"; + internal const string CraftingStarted = "crafting_started"; + internal const string MainMenuOpened = "main_menu_opened"; + internal const string ShouldNotAppear = "should_not_appear"; + internal const string BeforeReset = "before_reset"; + internal const string AfterReset = "after_reset"; + internal const string EventUnderOldConsent = "event_under_old_consent"; + internal const string RacingEvent = "racing_event"; + internal const string RaceStress = "race_stress"; + internal const string ShouldNotCrash = "should_not_crash"; + internal const string EnsureNonemptyQueue = "ensure_nonempty_queue"; + internal const string TrackedBeforeDowngrade = "tracked_before_downgrade"; + internal const string TrackedAfterDowngrade = "tracked_after_downgrade"; + internal const string EventToSend = "event_to_send"; + internal const string EventPreBlock = "event_pre_block"; + internal const string EventAgainstProdWithTestKey = "event_against_prod_with_test_key"; + internal const string StressTrack = "stress_track"; + internal const string MixedLoadTrack = "mixed_load_track"; + internal const string SteadyState = "steady_state"; + internal const string LevelComplete = "level_complete"; + } +} diff --git a/src/Packages/Audience/Tests/Runtime/ThreadSafetyStressTests.cs b/src/Packages/Audience/Tests/Runtime/ThreadSafetyStressTests.cs index 2a205706f..984a905a7 100644 --- a/src/Packages/Audience/Tests/Runtime/ThreadSafetyStressTests.cs +++ b/src/Packages/Audience/Tests/Runtime/ThreadSafetyStressTests.cs @@ -92,7 +92,7 @@ private void RunSustainedTrackLoad(int threadCount, int durationSeconds) int count = 0; while (DateTime.UtcNow < deadline) { - ImmutableAudience.Track("stress_track"); + ImmutableAudience.Track(TestEventNames.StressTrack); count++; } firedPerThread[idx] = count; @@ -155,7 +155,7 @@ public void TrackIdentifySetConsent_ConcurrentLoad_NoRaceExceptions() { barrier.SignalAndWait(); while (DateTime.UtcNow < deadline) - ImmutableAudience.Track("mixed_load_track"); + ImmutableAudience.Track(TestEventNames.MixedLoadTrack); } catch (Exception ex) { exceptions.Add(ex); } }); @@ -216,7 +216,7 @@ public void Track_SteadyState_BoundedMainThreadAllocation() ImmutableAudience.Init(MakeConfig()); // Warm up so JIT and one-time allocations are out of the measured window. - for (int i = 0; i < 200; i++) ImmutableAudience.Track("warmup"); + for (int i = 0; i < 200; i++) ImmutableAudience.Track(TestEventNames.Warmup); ImmutableAudience.FlushQueueToDiskForTesting(); GC.Collect(); GC.WaitForPendingFinalizers(); @@ -226,7 +226,7 @@ public void Track_SteadyState_BoundedMainThreadAllocation() const int iterations = 10_000; for (int i = 0; i < iterations; i++) - ImmutableAudience.Track("steady_state"); + ImmutableAudience.Track(TestEventNames.SteadyState); long allocDelta = GC.GetAllocatedBytesForCurrentThread() - allocBefore; double bytesPerCall = (double)allocDelta / iterations; diff --git a/src/Packages/Audience/Tests/Runtime/Transport/HttpTransportTests.cs b/src/Packages/Audience/Tests/Runtime/Transport/HttpTransportTests.cs index fa094515c..b7f41f040 100644 --- a/src/Packages/Audience/Tests/Runtime/Transport/HttpTransportTests.cs +++ b/src/Packages/Audience/Tests/Runtime/Transport/HttpTransportTests.cs @@ -217,7 +217,7 @@ public async Task SendBatchAsync_429_NoRetryAfter_KeepsFilesAndUsesExpoBackoff_N Assert.AreEqual(1, _store.Count(), "429 must keep files for retry"); Assert.IsTrue(transport.IsInBackoffWindow); - Assert.AreEqual(5_000, transport.BackoffMs); + Assert.AreEqual(Constants.HttpBackoff1stMs, transport.BackoffMs); Assert.IsNull(reportedError, "429 is transient; must not fire onError"); } @@ -282,7 +282,7 @@ public async Task SendBatchAsync_429_PastRetryAfterDate_FallsBackToExpoBackoff() await transport.SendBatchAsync(); - Assert.AreEqual(5_000, transport.BackoffMs); + Assert.AreEqual(Constants.HttpBackoff1stMs, transport.BackoffMs); Assert.IsTrue(transport.IsInBackoffWindow); } @@ -306,7 +306,7 @@ public async Task SendBatchAsync_429ThenSuccess_DeliversBatchAndClearsBackoff() await transport.SendBatchAsync(); Assert.AreEqual(1, _store.Count(), "429 keeps the batch"); - Assert.AreEqual(5_000, transport.BackoffMs); + Assert.AreEqual(Constants.HttpBackoff1stMs, transport.BackoffMs); Advance(5_001); await transport.SendBatchAsync(); @@ -384,7 +384,7 @@ public async Task SendBatchAsync_5xx_KeepsFilesAndIncreasesBackoff() Assert.AreEqual(1, _store.Count(), "5xx should keep files for retry"); Assert.IsTrue(transport.IsInBackoffWindow); - Assert.AreEqual(5000, transport.BackoffMs, "first failure = 5s backoff"); + Assert.AreEqual(Constants.HttpBackoff1stMs, transport.BackoffMs, "first failure = HttpBackoff1stMs"); Assert.IsNotNull(reportedError); Assert.AreEqual(AudienceErrorCode.FlushFailed, reportedError!.Code); } @@ -400,27 +400,27 @@ public async Task BackoffMs_EscalatesOnlyAfterWindowElapsed() // Schedule: 5s → 10s → 20s → 40s → 60s cap. // Each escalation requires the previous window to have elapsed. await transport.SendBatchAsync(); - Assert.AreEqual(5_000, transport.BackoffMs); + Assert.AreEqual(Constants.HttpBackoff1stMs, transport.BackoffMs); Advance(5_001); await transport.SendBatchAsync(); - Assert.AreEqual(10_000, transport.BackoffMs); + Assert.AreEqual(Constants.HttpBackoff2ndMs, transport.BackoffMs); Advance(10_001); await transport.SendBatchAsync(); - Assert.AreEqual(20_000, transport.BackoffMs); + Assert.AreEqual(Constants.HttpBackoff3rdMs, transport.BackoffMs); Advance(20_001); await transport.SendBatchAsync(); - Assert.AreEqual(40_000, transport.BackoffMs); + Assert.AreEqual(Constants.HttpBackoff4thMs, transport.BackoffMs); Advance(40_001); await transport.SendBatchAsync(); - Assert.AreEqual(60_000, transport.BackoffMs, "reaches 60s cap after 40s step"); + Assert.AreEqual(Constants.HttpBackoffCapMs, transport.BackoffMs, "reaches cap after 4th step"); Advance(60_001); await transport.SendBatchAsync(); - Assert.AreEqual(60_000, transport.BackoffMs, "stays at cap"); + Assert.AreEqual(Constants.HttpBackoffCapMs, transport.BackoffMs, "stays at cap"); } [Test] @@ -432,7 +432,7 @@ public async Task BackoffMs_DoesNotEscalateWhileInsidePreviousWindow() handler: handler, getUtcNow: _getUtcNow); await transport.SendBatchAsync(); - Assert.AreEqual(5_000, transport.BackoffMs); + Assert.AreEqual(Constants.HttpBackoff1stMs, transport.BackoffMs); var firstDeadline = transport.NextAttemptAt; Assert.IsNotNull(firstDeadline); @@ -447,12 +447,12 @@ public async Task BackoffMs_DoesNotEscalateWhileInsidePreviousWindow() // Another premature retry: still no escalation. Advance(3_000); await transport.SendBatchAsync(); - Assert.AreEqual(5_000, transport.BackoffMs); + Assert.AreEqual(Constants.HttpBackoff1stMs, transport.BackoffMs); // Wait out the window, fail again → now we escalate. _utcNow = firstDeadline.Value.AddMilliseconds(1); await transport.SendBatchAsync(); - Assert.AreEqual(10_000, transport.BackoffMs); + Assert.AreEqual(Constants.HttpBackoff2ndMs, transport.BackoffMs); } [Test] @@ -474,11 +474,11 @@ public async Task BackoffMs_ResetsAfterSuccess() handler: handler, getUtcNow: _getUtcNow); await transport.SendBatchAsync(); - Assert.AreEqual(5_000, transport.BackoffMs); + Assert.AreEqual(Constants.HttpBackoff1stMs, transport.BackoffMs); Advance(5_001); await transport.SendBatchAsync(); - Assert.AreEqual(10_000, transport.BackoffMs); + Assert.AreEqual(Constants.HttpBackoff2ndMs, transport.BackoffMs); Advance(10_001); await transport.SendBatchAsync(); @@ -580,7 +580,7 @@ public async Task IsInBackoffWindow_ClearsAfterNextAttemptAtElapses() await transport.SendBatchAsync(); Assert.IsTrue(transport.IsInBackoffWindow, "within window immediately after failure"); - Assert.AreEqual(now.AddMilliseconds(5_000), transport.NextAttemptAt); + Assert.AreEqual(now.AddMilliseconds(Constants.HttpBackoff1stMs), transport.NextAttemptAt); // Advance the clock just before NextAttemptAt: still backing off. now = now.AddMilliseconds(4_999); diff --git a/src/Packages/Audience/Tests/Runtime/Utility/GzipTests.cs b/src/Packages/Audience/Tests/Runtime/Utility/GzipTests.cs index 28430646f..efc40110a 100644 --- a/src/Packages/Audience/Tests/Runtime/Utility/GzipTests.cs +++ b/src/Packages/Audience/Tests/Runtime/Utility/GzipTests.cs @@ -34,7 +34,7 @@ public void Compress_OutputIsSmallerThanInput_ForRealisticPayload() { if (i > 0) sb.Append(','); sb.Append(WireFixture.Track( - (MessageFields.EventName, "level_complete"), + (MessageFields.EventName, TestEventNames.LevelComplete), (MessageFields.AnonymousId, $"anon-{i}"))); } diff --git a/src/Packages/Audience/Tests/Runtime/Utility/JsonTests.cs b/src/Packages/Audience/Tests/Runtime/Utility/JsonTests.cs index a3abe358e..b8f9c4524 100644 --- a/src/Packages/Audience/Tests/Runtime/Utility/JsonTests.cs +++ b/src/Packages/Audience/Tests/Runtime/Utility/JsonTests.cs @@ -182,7 +182,7 @@ public void Serialize_RealisticEventPayload_ProducesCorrectJson() var data = new Dictionary { { MessageFields.Type, MessageTypes.Track }, - { MessageFields.EventName, "level_complete" }, + { MessageFields.EventName, TestEventNames.LevelComplete }, { MessageFields.AnonymousId, "anon-123" }, { MessageFields.UserId, null }, { MessageFields.Properties, new Dictionary