6c590ca520
- client.Effects: typed effects + completion methods posting scenario callbacks - client.Scenario reduced to TriggerAsync - pending polling via Update() pump: 30s heartbeat + wait-deadline checks - local engine, plan persistence (IPlanStateStore) and resume removed - RealtimeService and realtime transports removed (relay never deployed) - dead IPlanScheduler leftover removed - models regenerated from openapi (ExecutionPlan/BoundaryNode gone, PendingEffect added)
610 lines
23 KiB
C#
610 lines
23 KiB
C#
using Newtonsoft.Json.Linq;
|
|
using RudderSdk.Core;
|
|
using RudderSdk.Core.Abstractions;
|
|
using RudderSdk.Core.Models;
|
|
using RudderSdk.Core.Models.Auth;
|
|
using RudderSdk.Core.Models.Scenarios;
|
|
|
|
using Xunit;
|
|
|
|
namespace RudderSdk.Core.Tests;
|
|
|
|
public sealed class ScenarioServiceTests
|
|
{
|
|
[Fact]
|
|
public async Task TriggerAsync_Emits_Returned_Effects()
|
|
{
|
|
var transport = new FakeTransport();
|
|
transport.Enqueue(new TriggerScenarioResponse
|
|
{
|
|
Effects = new List<PendingEffect>
|
|
{
|
|
Effect("run-1", "n1", "notification"),
|
|
Effect("run-2", "n2", "notification")
|
|
}
|
|
});
|
|
var client = CreateClient(transport);
|
|
var notifications = new List<NotificationEffect>();
|
|
client.Effects.OnNotification += notifications.Add;
|
|
|
|
await client.Scenario.TriggerAsync("login");
|
|
|
|
Assert.Equal(2, notifications.Count);
|
|
Assert.Equal("n1", notifications[0].NodeId);
|
|
Assert.Equal("n2", notifications[1].NodeId);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TriggerAsync_Dedups_By_RunId_And_NodeId()
|
|
{
|
|
var transport = new FakeTransport();
|
|
var effect = Effect("run-1", "n1", "notification");
|
|
transport.Enqueue(new TriggerScenarioResponse { Effects = new List<PendingEffect> { effect } });
|
|
transport.Enqueue(new TriggerScenarioResponse { Effects = new List<PendingEffect> { Effect("run-1", "n1", "notification") } });
|
|
var client = CreateClient(transport);
|
|
var notifications = 0;
|
|
client.Effects.OnNotification += _ => notifications++;
|
|
|
|
await client.Scenario.TriggerAsync("login");
|
|
await client.Scenario.TriggerAsync("login");
|
|
|
|
Assert.Equal(1, notifications);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Completing_Notification_Posts_Output_And_Ingests_Next_Effect()
|
|
{
|
|
var transport = new FakeTransport();
|
|
transport.Enqueue(new TriggerScenarioResponse
|
|
{
|
|
Effects = new List<PendingEffect> { Effect("run-1", "n1", "notification", new { title = "hi", message = "there" }) }
|
|
});
|
|
transport.Enqueue(new HandleScenarioCallbackResponse
|
|
{
|
|
Effect = JObject.FromObject(Effect("run-1", "store", "store"))
|
|
});
|
|
var client = CreateClient(transport);
|
|
NotificationEffect? notification = null;
|
|
StoreOfferEffect? store = null;
|
|
client.Effects.OnNotification += e => notification = e;
|
|
client.Effects.OnStoreOffer += e => store = e;
|
|
|
|
await client.Scenario.TriggerAsync("login");
|
|
Assert.Equal("hi", notification!.Title);
|
|
Assert.Equal("there", notification.Message);
|
|
await notification.DoneAsync();
|
|
|
|
var callback = Assert.IsType<HandleScenarioCallbackRequest>(
|
|
transport.Calls.Single(call => call.Path == "/sdk/v1/scenarios/callback").Request);
|
|
Assert.Equal("n1", callback.NodeId);
|
|
Assert.Equal("output", callback.Handle);
|
|
Assert.Equal("run-1", callback.RunId);
|
|
Assert.NotNull(store);
|
|
Assert.Equal("store", store!.NodeId);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Completing_Last_Effect_Raises_ScenarioCompleted()
|
|
{
|
|
var transport = new FakeTransport();
|
|
transport.Enqueue(new TriggerScenarioResponse
|
|
{
|
|
Effects = new List<PendingEffect> { Effect("run-1", "n1", "notification") }
|
|
});
|
|
transport.Enqueue(new HandleScenarioCallbackResponse());
|
|
var client = CreateClient(transport);
|
|
NotificationEffect? notification = null;
|
|
ScenarioCompletedEffect? completed = null;
|
|
client.Effects.OnNotification += e => notification = e;
|
|
client.Effects.OnScenarioCompleted += e => completed = e;
|
|
|
|
await client.Scenario.TriggerAsync("login");
|
|
await notification!.DoneAsync();
|
|
|
|
Assert.NotNull(completed);
|
|
Assert.Equal("run-1", completed!.RunId);
|
|
Assert.Equal("scenario-1", completed.ScenarioId);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Emits_Typed_Events_For_All_Supported_Interactive_Node_Types()
|
|
{
|
|
var transport = new FakeTransport();
|
|
transport.Enqueue(new TriggerScenarioResponse
|
|
{
|
|
Effects = new List<PendingEffect>
|
|
{
|
|
Effect("run-1", "n1", "notification"),
|
|
Effect("run-2", "n2", "store"),
|
|
Effect("run-3", "n3", "quest"),
|
|
Effect("run-4", "n4", "leaderboard"),
|
|
Effect("run-5", "n5", "battlepass"),
|
|
Effect("run-6", "n6", "battlepass_level", new { levelNumber = 3 }),
|
|
Effect("run-7", "n7", "wait", waitDeadline: new DateTimeOffset(2026, 5, 30, 10, 30, 0, TimeSpan.Zero))
|
|
}
|
|
});
|
|
var client = CreateClient(transport);
|
|
var notification = 0;
|
|
var store = 0;
|
|
var quest = 0;
|
|
var leaderboard = 0;
|
|
var battlePass = 0;
|
|
var battlePassLevel = 0;
|
|
var wait = 0;
|
|
WaitEffect? waitEffect = null;
|
|
BattlePassLevelEffect? levelEffect = null;
|
|
client.Effects.OnNotification += _ => notification++;
|
|
client.Effects.OnStoreOffer += _ => store++;
|
|
client.Effects.OnQuest += _ => quest++;
|
|
client.Effects.OnLeaderboard += _ => leaderboard++;
|
|
client.Effects.OnBattlePass += _ => battlePass++;
|
|
client.Effects.OnBattlePassLevel += e => { battlePassLevel++; levelEffect = e; };
|
|
client.Effects.OnWait += e => { wait++; waitEffect = e; };
|
|
|
|
await client.Scenario.TriggerAsync("login");
|
|
|
|
Assert.Equal(1, notification);
|
|
Assert.Equal(1, store);
|
|
Assert.Equal(1, quest);
|
|
Assert.Equal(1, leaderboard);
|
|
Assert.Equal(1, battlePass);
|
|
Assert.Equal(1, battlePassLevel);
|
|
Assert.Equal(1, wait);
|
|
Assert.Equal(3, levelEffect!.Level);
|
|
Assert.Equal(new DateTimeOffset(2026, 5, 30, 10, 30, 0, TimeSpan.Zero), waitEffect!.DeadlineUtc);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Store_Purchase_And_Decline_Post_Matching_Handles()
|
|
{
|
|
var transport = new FakeTransport();
|
|
transport.Enqueue(new TriggerScenarioResponse
|
|
{
|
|
Effects = new List<PendingEffect> { Effect("run-1", "store", "store") }
|
|
});
|
|
transport.Enqueue(new HandleScenarioCallbackResponse());
|
|
var client = CreateClient(transport);
|
|
StoreOfferEffect? store = null;
|
|
client.Effects.OnStoreOffer += e => store = e;
|
|
|
|
await client.Scenario.TriggerAsync("login");
|
|
await store!.PurchaseAsync();
|
|
|
|
var callback = Assert.IsType<HandleScenarioCallbackRequest>(
|
|
transport.Calls.Single(call => call.Path == "/sdk/v1/scenarios/callback").Request);
|
|
Assert.Equal("onPurchase", callback.Handle);
|
|
Assert.True(store.IsResolved);
|
|
|
|
store.Decline();
|
|
Assert.Equal(1, transport.Calls.Count(call => call.Path == "/sdk/v1/scenarios/callback"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Leaderboard_End_Posts_OnEnd()
|
|
{
|
|
var transport = new FakeTransport();
|
|
transport.Enqueue(new TriggerScenarioResponse
|
|
{
|
|
Effects = new List<PendingEffect> { Effect("run-1", "lb", "leaderboard") }
|
|
});
|
|
transport.Enqueue(new HandleScenarioCallbackResponse());
|
|
var client = CreateClient(transport);
|
|
LeaderboardEffect? leaderboard = null;
|
|
client.Effects.OnLeaderboard += e => leaderboard = e;
|
|
|
|
await client.Scenario.TriggerAsync("login");
|
|
await leaderboard!.EndAsync();
|
|
|
|
var callback = Assert.IsType<HandleScenarioCallbackRequest>(
|
|
transport.Calls.Single(call => call.Path == "/sdk/v1/scenarios/callback").Request);
|
|
Assert.Equal("onEnd", callback.Handle);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Leaderboard_Claim_Posts_OnClaim()
|
|
{
|
|
var transport = new FakeTransport();
|
|
transport.Enqueue(new TriggerScenarioResponse
|
|
{
|
|
Effects = new List<PendingEffect> { Effect("run-1", "lb", "leaderboard") }
|
|
});
|
|
transport.Enqueue(new HandleScenarioCallbackResponse());
|
|
var client = CreateClient(transport);
|
|
LeaderboardEffect? leaderboard = null;
|
|
client.Effects.OnLeaderboard += e => leaderboard = e;
|
|
|
|
await client.Scenario.TriggerAsync("login");
|
|
await leaderboard!.ClaimAsync();
|
|
|
|
var callback = Assert.IsType<HandleScenarioCallbackRequest>(
|
|
transport.Calls.Single(call => call.Path == "/sdk/v1/scenarios/callback").Request);
|
|
Assert.Equal("onClaim", callback.Handle);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Quest_Progress_Completed_Response_Ingests_Next_Effect()
|
|
{
|
|
var transport = new FakeTransport();
|
|
transport.Enqueue(new TriggerScenarioResponse
|
|
{
|
|
Effects = new List<PendingEffect> { Effect("run-1", "quest", "quest", new { name = "wins" }) }
|
|
});
|
|
transport.Enqueue(new UpdateScenarioCounterResponse
|
|
{
|
|
Completed = true,
|
|
Effect = JObject.FromObject(Effect("run-1", "done", "notification"))
|
|
});
|
|
var client = CreateClient(transport);
|
|
QuestEffect? quest = null;
|
|
NotificationEffect? notification = null;
|
|
client.Effects.OnQuest += e => quest = e;
|
|
client.Effects.OnNotification += e => notification = e;
|
|
|
|
await client.Scenario.TriggerAsync("login");
|
|
Assert.Equal("wins", quest!.Name);
|
|
await quest.ReportProgressAsync("wins", 1);
|
|
|
|
var counter = Assert.IsType<UpdateScenarioCounterRequest>(
|
|
transport.Calls.Single(call => call.Path == "/sdk/v1/scenarios/counter").Request);
|
|
Assert.Equal("quest", counter.NodeId);
|
|
Assert.Equal("wins", counter.CounterKey);
|
|
Assert.Equal(1, counter.Amount);
|
|
Assert.NotNull(notification);
|
|
Assert.Equal("done", notification!.NodeId);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Quest_Progress_Completed_Without_Next_Raises_ScenarioCompleted()
|
|
{
|
|
var transport = new FakeTransport();
|
|
transport.Enqueue(new TriggerScenarioResponse
|
|
{
|
|
Effects = new List<PendingEffect> { Effect("run-1", "quest", "quest") }
|
|
});
|
|
transport.Enqueue(new UpdateScenarioCounterResponse { Completed = true });
|
|
var client = CreateClient(transport);
|
|
QuestEffect? quest = null;
|
|
var completed = 0;
|
|
client.Effects.OnQuest += e => quest = e;
|
|
client.Effects.OnScenarioCompleted += _ => completed++;
|
|
|
|
await client.Scenario.TriggerAsync("login");
|
|
await quest!.ReportProgressAsync("wins", 1);
|
|
|
|
Assert.Equal(1, completed);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Quest_Progress_Failure_Does_Not_Fail_The_Run()
|
|
{
|
|
var transport = new FakeTransport();
|
|
transport.Enqueue(new TriggerScenarioResponse
|
|
{
|
|
Effects = new List<PendingEffect> { Effect("run-1", "quest", "quest") }
|
|
});
|
|
transport.Enqueue(new InvalidOperationException("boom"));
|
|
var logger = new FakeLogger();
|
|
var client = CreateClient(transport, logger: logger);
|
|
QuestEffect? quest = null;
|
|
var failed = 0;
|
|
client.Effects.OnQuest += e => quest = e;
|
|
client.Effects.OnScenarioFailed += _ => failed++;
|
|
|
|
await client.Scenario.TriggerAsync("login");
|
|
await quest!.ReportProgressAsync("wins", 1);
|
|
|
|
Assert.Equal(0, failed);
|
|
Assert.Contains(logger.Messages, m =>
|
|
m.Level == RudderLogLevel.Warning && m.Message.Contains("counter update failed"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Unknown_Run_Drops_Effects_And_Raises_Failed()
|
|
{
|
|
var transport = new FakeTransport();
|
|
transport.Enqueue(new TriggerScenarioResponse
|
|
{
|
|
Effects = new List<PendingEffect> { Effect("run-1", "n1", "notification") }
|
|
});
|
|
transport.Enqueue(new RudderNotFoundException(404, RudderErrorCodes.UnknownRun, "unknown"));
|
|
var client = CreateClient(transport);
|
|
NotificationEffect? notification = null;
|
|
ScenarioFailedEffect? failure = null;
|
|
client.Effects.OnNotification += e => notification = e;
|
|
client.Effects.OnScenarioFailed += e => failure = e;
|
|
|
|
await client.Scenario.TriggerAsync("login");
|
|
await notification!.DoneAsync();
|
|
|
|
Assert.NotNull(failure);
|
|
Assert.Equal("run-1", failure!.RunId);
|
|
Assert.Equal("n1", failure.NodeId);
|
|
Assert.IsType<RudderNotFoundException>(failure.Exception);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Expired_Run_Drops_Effects()
|
|
{
|
|
var transport = new FakeTransport();
|
|
transport.Enqueue(new TriggerScenarioResponse
|
|
{
|
|
Effects = new List<PendingEffect> { Effect("run-1", "n1", "notification") }
|
|
});
|
|
transport.Enqueue(new RudderApiException(400, RudderErrorCodes.RunExpired, "expired"));
|
|
var client = CreateClient(transport);
|
|
NotificationEffect? notification = null;
|
|
ScenarioFailedEffect? failure = null;
|
|
client.Effects.OnNotification += e => notification = e;
|
|
client.Effects.OnScenarioFailed += e => failure = e;
|
|
|
|
await client.Scenario.TriggerAsync("login");
|
|
await notification!.DoneAsync();
|
|
|
|
Assert.NotNull(failure);
|
|
Assert.Equal("run-1", failure!.RunId);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Unknown_Nodes_Fail_The_Run()
|
|
{
|
|
var transport = new FakeTransport();
|
|
transport.Enqueue(new TriggerScenarioResponse
|
|
{
|
|
Effects = new List<PendingEffect> { Effect("run-1", "future", "future_node") }
|
|
});
|
|
var logger = new FakeLogger();
|
|
var client = CreateClient(transport, logger: logger);
|
|
ScenarioFailedEffect? failure = null;
|
|
client.Effects.OnScenarioFailed += e => failure = e;
|
|
|
|
await client.Scenario.TriggerAsync("login");
|
|
|
|
Assert.NotNull(failure);
|
|
Assert.Equal("future", failure!.NodeId);
|
|
Assert.Contains(logger.Messages, m =>
|
|
m.Level == RudderLogLevel.Warning && m.Message.Contains("Unsupported scenario node type 'future_node'"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Wait_Deadline_Refreshes_Pending_Without_Posting_Callback()
|
|
{
|
|
var clock = new FakeClock(new DateTimeOffset(2026, 5, 30, 10, 0, 0, TimeSpan.Zero));
|
|
var transport = new FakeTransport();
|
|
transport.Enqueue(new ListPendingScenarioEffectsResponse { Effects = new List<PendingEffect>() });
|
|
transport.Enqueue(new TriggerScenarioResponse
|
|
{
|
|
Effects = new List<PendingEffect>
|
|
{
|
|
Effect("run-1", "wait", "wait", waitDeadline: clock.UtcNow.AddMinutes(30))
|
|
}
|
|
});
|
|
var client = CreateClient(transport, clock: clock);
|
|
client.Update(0);
|
|
await Task.Delay(50);
|
|
Assert.Equal(1, transport.Calls.Count(call => call.Path == "/sdk/v1/scenarios/pending"));
|
|
|
|
await client.Scenario.TriggerAsync("login");
|
|
client.Update(0);
|
|
await Task.Delay(50);
|
|
Assert.Equal(1, transport.Calls.Count(call => call.Path == "/sdk/v1/scenarios/pending"));
|
|
Assert.DoesNotContain(transport.Calls, call => call.Path == "/sdk/v1/scenarios/callback");
|
|
|
|
transport.Enqueue(new ListPendingScenarioEffectsResponse
|
|
{
|
|
Effects = new List<PendingEffect> { Effect("run-1", "done", "notification") }
|
|
});
|
|
var notifications = 0;
|
|
client.Effects.OnNotification += _ => notifications++;
|
|
clock.UtcNow = clock.UtcNow.AddMinutes(31);
|
|
client.Update(0);
|
|
await Task.Delay(50);
|
|
|
|
Assert.Equal(2, transport.Calls.Count(call => call.Path == "/sdk/v1/scenarios/pending"));
|
|
Assert.Equal(1, notifications);
|
|
Assert.DoesNotContain(transport.Calls, call => call.Path == "/sdk/v1/scenarios/callback");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Heartbeat_Refreshes_Pending_After_30_Seconds()
|
|
{
|
|
var clock = new FakeClock(new DateTimeOffset(2026, 5, 30, 10, 0, 0, TimeSpan.Zero));
|
|
var transport = new FakeTransport();
|
|
transport.Enqueue(new ListPendingScenarioEffectsResponse { Effects = new List<PendingEffect>() });
|
|
var client = CreateClient(transport, clock: clock);
|
|
|
|
client.Update(0);
|
|
await Task.Delay(50);
|
|
Assert.Equal(1, transport.Calls.Count(call => call.Path == "/sdk/v1/scenarios/pending"));
|
|
|
|
clock.UtcNow = clock.UtcNow.AddSeconds(29);
|
|
client.Update(0);
|
|
await Task.Delay(50);
|
|
Assert.Equal(1, transport.Calls.Count(call => call.Path == "/sdk/v1/scenarios/pending"));
|
|
|
|
transport.Enqueue(new ListPendingScenarioEffectsResponse { Effects = new List<PendingEffect>() });
|
|
clock.UtcNow = clock.UtcNow.AddSeconds(1);
|
|
client.Update(0);
|
|
await Task.Delay(50);
|
|
Assert.Equal(2, transport.Calls.Count(call => call.Path == "/sdk/v1/scenarios/pending"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Login_Fetches_Pending_On_Next_Update()
|
|
{
|
|
var transport = new FakeTransport();
|
|
transport.Enqueue(new LoginViaDeviceResponse
|
|
{
|
|
AccessToken = "access-token",
|
|
RefreshToken = "refresh-token"
|
|
});
|
|
transport.Enqueue(new ListPendingScenarioEffectsResponse
|
|
{
|
|
Effects = new List<PendingEffect> { Effect("run-1", "n1", "notification") }
|
|
});
|
|
var tokenStore = new FakeTokenStore();
|
|
var client = CreateClient(transport, tokenStore: tokenStore);
|
|
var notifications = 0;
|
|
client.Effects.OnNotification += _ => notifications++;
|
|
|
|
await client.Auth.LoginWithDeviceAsync("en", "en");
|
|
Assert.Equal(0, notifications);
|
|
|
|
client.Update(0);
|
|
await Task.Delay(50);
|
|
|
|
Assert.Equal(1, notifications);
|
|
Assert.Contains(transport.Calls, call => call.Method == "GET" && call.Path == "/sdk/v1/scenarios/pending");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task BattlePass_LevelUp_Posts_OnLevelUp()
|
|
{
|
|
var transport = new FakeTransport();
|
|
transport.Enqueue(new TriggerScenarioResponse
|
|
{
|
|
Effects = new List<PendingEffect> { Effect("run-1", "bp", "battlepass") }
|
|
});
|
|
transport.Enqueue(new HandleScenarioCallbackResponse());
|
|
var client = CreateClient(transport);
|
|
BattlePassEffect? battlePass = null;
|
|
client.Effects.OnBattlePass += e => battlePass = e;
|
|
|
|
await client.Scenario.TriggerAsync("login");
|
|
await battlePass!.LevelUpAsync();
|
|
|
|
var callback = Assert.IsType<HandleScenarioCallbackRequest>(
|
|
transport.Calls.Single(call => call.Path == "/sdk/v1/scenarios/callback").Request);
|
|
Assert.Equal("onLevelUp", callback.Handle);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task BattlePassLevel_Claim_Posts_OnComplete()
|
|
{
|
|
var transport = new FakeTransport();
|
|
transport.Enqueue(new TriggerScenarioResponse
|
|
{
|
|
Effects = new List<PendingEffect> { Effect("run-1", "bpl", "battlepass_level", new { levelNumber = 2 }) }
|
|
});
|
|
transport.Enqueue(new HandleScenarioCallbackResponse());
|
|
var client = CreateClient(transport);
|
|
BattlePassLevelEffect? level = null;
|
|
client.Effects.OnBattlePassLevel += e => level = e;
|
|
|
|
await client.Scenario.TriggerAsync("login");
|
|
await level!.ClaimAsync();
|
|
|
|
var callback = Assert.IsType<HandleScenarioCallbackRequest>(
|
|
transport.Calls.Single(call => call.Path == "/sdk/v1/scenarios/callback").Request);
|
|
Assert.Equal("onComplete", callback.Handle);
|
|
Assert.Equal(2, level.Level);
|
|
}
|
|
|
|
private static RudderClient CreateClient(
|
|
FakeTransport transport,
|
|
FakeClock? clock = null,
|
|
IRudderLogger? logger = null,
|
|
FakeTokenStore? tokenStore = null)
|
|
{
|
|
return new RudderClient(new RudderClientOptions
|
|
{
|
|
BaseUrl = "http://localhost:8082",
|
|
ProjectKey = "project-key",
|
|
Transport = transport,
|
|
TokenStore = tokenStore ?? new FakeTokenStore { AccessToken = "access-token" },
|
|
DeviceIdProvider = new FakeDeviceIdProvider(),
|
|
Clock = clock ?? new FakeClock(DateTimeOffset.UtcNow),
|
|
Logger = logger
|
|
});
|
|
}
|
|
|
|
private static PendingEffect Effect(string runId, string nodeId, string type, object? data = null, DateTimeOffset? waitDeadline = null)
|
|
{
|
|
return new PendingEffect
|
|
{
|
|
RunId = runId,
|
|
ScenarioId = "scenario-1",
|
|
NodeId = nodeId,
|
|
Type = type,
|
|
Data = data == null ? new JObject() : JObject.FromObject(data),
|
|
WaitDeadline = waitDeadline
|
|
};
|
|
}
|
|
|
|
private sealed class FakeTransport : IRudderTransport
|
|
{
|
|
private readonly Queue<object> _responses = new();
|
|
public List<FakeCall> Calls { get; } = new();
|
|
|
|
public void Enqueue(object response) => _responses.Enqueue(response);
|
|
|
|
public Task<TResponse> SendAsync<TRequest, TResponse>(
|
|
string method,
|
|
string path,
|
|
TRequest? request,
|
|
string? accessToken,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
Calls.Add(new FakeCall(method, path, request, accessToken));
|
|
if (_responses.Count == 0)
|
|
return Task.FromResult(default(TResponse)!);
|
|
|
|
var next = _responses.Dequeue();
|
|
if (next is Exception ex)
|
|
return Task.FromException<TResponse>(ex);
|
|
|
|
return Task.FromResult((TResponse)next);
|
|
}
|
|
}
|
|
|
|
private sealed class FakeCall
|
|
{
|
|
public FakeCall(string method, string path, object? request, string? accessToken)
|
|
{
|
|
Method = method;
|
|
Path = path;
|
|
Request = request;
|
|
AccessToken = accessToken;
|
|
}
|
|
|
|
public string Method { get; }
|
|
public string Path { get; }
|
|
public object? Request { get; }
|
|
public string? AccessToken { get; }
|
|
}
|
|
|
|
private sealed class FakeTokenStore : ITokenStore
|
|
{
|
|
public string? AccessToken { get; set; }
|
|
public string? RefreshToken { get; set; }
|
|
public string? GetAccessToken() => AccessToken;
|
|
public string? GetRefreshToken() => RefreshToken;
|
|
public void SaveTokens(string accessToken, string refreshToken)
|
|
{
|
|
AccessToken = accessToken;
|
|
RefreshToken = refreshToken;
|
|
}
|
|
public void Clear()
|
|
{
|
|
AccessToken = null;
|
|
RefreshToken = null;
|
|
}
|
|
}
|
|
|
|
private sealed class FakeDeviceIdProvider : IDeviceIdProvider
|
|
{
|
|
public string DeviceId => "device-id";
|
|
}
|
|
|
|
private sealed class FakeClock : IClock
|
|
{
|
|
public FakeClock(DateTimeOffset utcNow) => UtcNow = utcNow;
|
|
public DateTimeOffset UtcNow { get; set; }
|
|
}
|
|
|
|
private sealed class FakeLogger : IRudderLogger
|
|
{
|
|
public List<(RudderLogLevel Level, string Message)> Messages { get; } = new();
|
|
public void Log(RudderLogLevel level, string message) => Messages.Add((level, message));
|
|
}
|
|
}
|