using DeepDrftModels.Enums; using DeepDrftPublic.Client.Services; using Microsoft.AspNetCore.Components; using Microsoft.JSInterop; using Microsoft.JSInterop.Infrastructure; namespace DeepDrftTests; /// /// Unit tests for the Phase 16 share tracker (): the per-(target,channel) /// debounce (§1b — at most one event per target+channel per 60s window per session). The tracker fires /// through a beacon that wraps ; the tests use a no-op JS runtime (the send is /// fire-and-forget and its outcome is irrelevant) and assert on the debounce decision via the bool the /// recorder returns — true when an event fired, false when debounced. /// [TestFixture] public class ShareTrackerTests { // sendBeacon interop is fire-and-forget; the tracker never reads the result, so a no-op runtime that // returns default for any invocation is sufficient to exercise the debounce logic. private sealed class NoopJsRuntime : IJSRuntime { public ValueTask InvokeAsync(string identifier, object?[]? args) => ValueTask.FromResult(default!); public ValueTask InvokeAsync(string identifier, CancellationToken cancellationToken, object?[]? args) => ValueTask.FromResult(default!); } // Minimal NavigationManager so the tracker can compose the (unused-in-test) beacon URL. private sealed class TestNavigationManager : NavigationManager { public TestNavigationManager() => Initialize("https://deepdrft.test/", "https://deepdrft.test/"); protected override void NavigateToCore(string uri, bool forceLoad) { } } // Fixed-value anon-id provider so the tracker can attach a token without JS interop. Treated as // already warmed (Current returns the value); EnsureLoadedAsync is a no-op here. private sealed class StubAnonIdProvider : IAnonIdProvider { public StubAnonIdProvider(string? current) => Current = current; public string? Current { get; } public ValueTask EnsureLoadedAsync() => ValueTask.CompletedTask; } private ShareTracker _tracker = null!; private readonly DateTimeOffset _t0 = new(2026, 6, 19, 12, 0, 0, TimeSpan.Zero); [SetUp] public void SetUp() => _tracker = new ShareTracker( new BeaconInterop(new NoopJsRuntime()), new StubAnonIdProvider("anon-1"), new TestNavigationManager()); // A copy-link records one share with channel = link. [Test] public void RecordShare_CopyLink_FiresOnce() => Assert.That(_tracker.RecordShare(ShareTargetType.Track, "k", ShareChannel.Link, _t0), Is.True); // A copy-embed records one share with channel = embed — distinct (target,channel) from the link copy. [Test] public void RecordShare_CopyEmbedAfterLink_FiresSeparately() { Assert.That(_tracker.RecordShare(ShareTargetType.Track, "k", ShareChannel.Link, _t0), Is.True); Assert.That(_tracker.RecordShare(ShareTargetType.Track, "k", ShareChannel.Embed, _t0), Is.True, "embed is a different channel from link — not debounced against it"); } // An immediate repeat copy of the same (target, channel) within the window is debounced. [Test] public void RecordShare_ImmediateRepeat_IsDebounced() { Assert.That(_tracker.RecordShare(ShareTargetType.Track, "k", ShareChannel.Link, _t0), Is.True); Assert.That(_tracker.RecordShare(ShareTargetType.Track, "k", ShareChannel.Link, _t0.AddSeconds(5)), Is.False); } // After the 60s window elapses, the same (target, channel) fires again. [Test] public void RecordShare_AfterWindow_FiresAgain() { Assert.That(_tracker.RecordShare(ShareTargetType.Track, "k", ShareChannel.Link, _t0), Is.True); Assert.That(_tracker.RecordShare(ShareTargetType.Track, "k", ShareChannel.Link, _t0.AddSeconds(61)), Is.True); } // Different targets debounce independently — sharing track A then track B both fire. [Test] public void RecordShare_DifferentTargets_FireIndependently() { Assert.That(_tracker.RecordShare(ShareTargetType.Track, "a", ShareChannel.Link, _t0), Is.True); Assert.That(_tracker.RecordShare(ShareTargetType.Track, "b", ShareChannel.Link, _t0), Is.True); } // A track key and a release key are distinct targets even if the key string collides. [Test] public void RecordShare_TrackVsRelease_AreDistinctTargets() { Assert.That(_tracker.RecordShare(ShareTargetType.Track, "x", ShareChannel.Link, _t0), Is.True); Assert.That(_tracker.RecordShare(ShareTargetType.Release, "x", ShareChannel.Link, _t0), Is.True); } // A blank target key never fires (defensive — the popover guards too). [Test] public void RecordShare_BlankKey_DoesNotFire() => Assert.That(_tracker.RecordShare(ShareTargetType.Track, " ", ShareChannel.Link, _t0), Is.False); }