using RudderSdk.Core; using RudderSdk.Core.Abstractions; using RudderSdk.Core.Models.Auth; using Xunit; namespace RudderSdk.Core.Tests; public sealed class RudderClientTests { [Fact] public void Constructor_Composes_All_Runtime_Services() { var client = CreateClient(); Assert.NotNull(client.Auth); Assert.NotNull(client.Player); Assert.NotNull(client.RemoteConfig); Assert.NotNull(client.Storage); Assert.NotNull(client.Stores); Assert.NotNull(client.Leaderboards); Assert.NotNull(client.Inventory); Assert.NotNull(client.BattlePass); Assert.NotNull(client.Quests); Assert.NotNull(client.Ugc); Assert.NotNull(client.Scenario); Assert.NotNull(client.Realtime); } [Fact] public void Constructor_Requires_BaseUrl_And_ProjectKey() { Assert.Throws(() => new RudderClient(new RudderClientOptions { ProjectKey = "key" })); Assert.Throws(() => new RudderClient(new RudderClientOptions { BaseUrl = "http://localhost" })); } [Fact] public void Constructor_Uses_HttpClientTransport_By_Default() { var client = new RudderClient(new RudderClientOptions { BaseUrl = "http://localhost:8082", ProjectKey = "project-key" }); Assert.NotNull(client); } [Fact] public async Task Auth_LoginWithDevice_Saves_Tokens_And_Raises_SignedIn() { var transport = new FakeTransport(); transport.Responses[typeof(LoginViaDeviceResponse)] = new LoginViaDeviceResponse { AccessToken = "access-token", RefreshToken = "refresh-token" }; var tokenStore = new FakeTokenStore(); var client = CreateClient(transport: transport, tokenStore: tokenStore); var states = new List(); client.Auth.AuthStateChanged += states.Add; await client.Auth.LoginWithDeviceAsync("en", "en", "nickname"); var request = Assert.IsType(transport.LastRequest); Assert.Equal("POST", transport.LastMethod); Assert.Equal("/sdk/v1/authorization/device", transport.LastPath); Assert.Equal("nickname", request.Nickname); Assert.Equal("access-token", tokenStore.AccessToken); Assert.Equal("refresh-token", tokenStore.RefreshToken); Assert.Equal(new[] { RudderAuthState.SignedIn }, states); } [Fact] public void Auth_Logout_Clears_Tokens_And_Raises_SignedOut() { var tokenStore = new FakeTokenStore { AccessToken = "a", RefreshToken = "r" }; var client = CreateClient(tokenStore: tokenStore); var states = new List(); client.Auth.AuthStateChanged += states.Add; client.Auth.Logout(); Assert.Null(tokenStore.AccessToken); Assert.Null(tokenStore.RefreshToken); Assert.Equal(new[] { RudderAuthState.SignedOut }, states); } [Fact] public async Task Realtime_Connect_Returns_Session_From_Injected_Transport() { var realtimeTransport = new FakeRealtimeTransport(); var tokenStore = new FakeTokenStore { AccessToken = "access-token" }; var client = CreateClient( tokenStore: tokenStore, realtimeTransportFactory: new FakeRealtimeTransportFactory(realtimeTransport)); var session = await client.Realtime.ConnectAsync(); Assert.NotNull(session); Assert.True(session.IsConnected); Assert.Equal("access-token", session.AccessToken); Assert.Equal(new Uri("ws://localhost:8090/api/realtime/ws"), realtimeTransport.Uri); } private static RudderClient CreateClient( FakeTransport? transport = null, FakeTokenStore? tokenStore = null, IRealtimeTransportFactory? realtimeTransportFactory = null) { return new RudderClient(new RudderClientOptions { BaseUrl = "http://localhost:8082", RealtimeUrl = "ws://localhost:8090/api/realtime/ws", ProjectKey = "project-key", Transport = transport ?? new FakeTransport(), TokenStore = tokenStore ?? new FakeTokenStore(), DeviceIdProvider = new FakeDeviceIdProvider(), RealtimeTransportFactory = realtimeTransportFactory ?? new FakeRealtimeTransportFactory(new FakeRealtimeTransport()) }); } internal sealed class FakeTransport : IRudderTransport { public Dictionary Responses { get; } = new(); public string? LastMethod { get; private set; } public string? LastPath { get; private set; } public object? LastRequest { get; private set; } public string? LastAccessToken { get; private set; } public Task SendAsync( string method, string path, TRequest? request, string? accessToken, CancellationToken cancellationToken = default) { LastMethod = method; LastPath = path; LastRequest = request; LastAccessToken = accessToken; if (Responses.TryGetValue(typeof(TResponse), out var response)) return Task.FromResult((TResponse)response); return Task.FromResult(default(TResponse)!); } } internal 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 FakeRealtimeTransportFactory : IRealtimeTransportFactory { private readonly IRealtimeTransport _transport; public FakeRealtimeTransportFactory(IRealtimeTransport transport) => _transport = transport; public IRealtimeTransport Create() => _transport; } private sealed class FakeRealtimeTransport : IRealtimeTransport { public Uri? Uri { get; private set; } public bool IsConnected { get; private set; } public event Action? Closed; public event Action? Error; public event Action>? Received; public Task ConnectAsync(Uri uri, TimeSpan timeout, CancellationToken cancellationToken = default) { Uri = uri; IsConnected = true; return Task.CompletedTask; } public Task SendAsync(ArraySegment payload, CancellationToken cancellationToken = default) => Task.CompletedTask; public Task CloseAsync(CancellationToken cancellationToken = default) { IsConnected = false; Closed?.Invoke(); return Task.CompletedTask; } public void Update(float deltaTime) { } public void EmitError(Exception ex) => Error?.Invoke(ex); public void EmitReceived(ArraySegment data) => Received?.Invoke(data); } }