Files

198 lines
6.8 KiB
C#
Raw Permalink Normal View History

2026-08-12 14:04:55 +03:00
using RudderSdk.Core;
using RudderSdk.Core.Abstractions;
using RudderSdk.Core.Models.Auth;
using RudderSdk.Core.Models.Player;
using Xunit;
namespace RudderSdk.Core.Tests;
public sealed class AuthSessionTests
{
[Fact]
public async Task Request_401_Refreshes_Tokens_And_Retries_Once()
{
var tokenStore = new FakeTokenStore { AccessToken = "old-access", RefreshToken = "refresh" };
var transport = new FakeTransport();
var profileCalls = 0;
transport.Handler = (method, path, accessToken) =>
{
if (path == "/sdk/v1/authorization/refresh")
{
var request = new RefreshAccessTokenResponse { AccessToken = "new-access", RefreshToken = "new-refresh" };
return Task.FromResult<object?>(request);
}
profileCalls++;
if (profileCalls == 1)
throw new RudderAuthException(401, "unauthorized", "Unauthorized");
Assert.Equal("new-access", accessToken);
return Task.FromResult<object?>(new PlayerProfile());
};
var client = CreateClient(transport, tokenStore);
var profile = await client.Player.GetProfileAsync();
Assert.NotNull(profile);
Assert.Equal(2, profileCalls);
Assert.Equal("new-access", tokenStore.AccessToken);
Assert.Equal("new-refresh", tokenStore.RefreshToken);
}
[Fact]
public async Task Concurrent_401s_Share_One_Refresh_Request()
{
var tokenStore = new FakeTokenStore { AccessToken = "old-access", RefreshToken = "refresh" };
var transport = new FakeTransport();
var refreshCalls = 0;
transport.Handler = async (method, path, accessToken) =>
{
if (path == "/sdk/v1/authorization/refresh")
{
refreshCalls++;
await Task.Delay(50);
return new RefreshAccessTokenResponse { AccessToken = "new-access", RefreshToken = "new-refresh" };
}
if (accessToken != "new-access")
throw new RudderAuthException(401, "unauthorized", "Unauthorized");
return new PlayerProfile();
};
var client = CreateClient(transport, tokenStore);
await Task.WhenAll(
client.Player.GetProfileAsync(),
client.Player.GetProfileAsync(),
client.Player.GetProfileAsync());
Assert.Equal(1, refreshCalls);
}
[Fact]
public async Task Failed_Refresh_Clears_Tokens_Raises_SignedOut_And_Rethrows()
{
var tokenStore = new FakeTokenStore { AccessToken = "old-access", RefreshToken = "refresh" };
var transport = new FakeTransport();
transport.Handler = (method, path, accessToken) =>
throw new RudderAuthException(401, "unauthorized", "Unauthorized");
var client = CreateClient(transport, tokenStore);
var states = new List<RudderAuthState>();
client.Auth.AuthStateChanged += states.Add;
await Assert.ThrowsAsync<RudderAuthException>(() => client.Player.GetProfileAsync());
Assert.Null(tokenStore.AccessToken);
Assert.Null(tokenStore.RefreshToken);
Assert.Equal(new[] { RudderAuthState.SignedOut }, states);
}
[Fact]
public async Task Missing_Refresh_Token_Fails_Without_Refresh_Call()
{
var tokenStore = new FakeTokenStore { AccessToken = "old-access" };
var transport = new FakeTransport();
var refreshCalls = 0;
transport.Handler = (method, path, accessToken) =>
{
if (path == "/sdk/v1/authorization/refresh")
{
refreshCalls++;
return Task.FromResult<object?>(new RefreshAccessTokenResponse());
}
throw new RudderAuthException(401, "unauthorized", "Unauthorized");
};
var client = CreateClient(transport, tokenStore);
var states = new List<RudderAuthState>();
client.Auth.AuthStateChanged += states.Add;
await Assert.ThrowsAsync<RudderAuthException>(() => client.Player.GetProfileAsync());
Assert.Equal(0, refreshCalls);
Assert.Equal(new[] { RudderAuthState.SignedOut }, states);
}
[Fact]
public async Task RefreshAsync_Stores_New_Token_Pair()
{
var tokenStore = new FakeTokenStore { AccessToken = "old-access", RefreshToken = "refresh" };
var transport = new FakeTransport();
transport.Handler = (method, path, accessToken) =>
{
Assert.Equal("/sdk/v1/authorization/refresh", path);
Assert.Null(accessToken);
return Task.FromResult<object?>(new RefreshAccessTokenResponse
{
AccessToken = "new-access",
RefreshToken = "new-refresh"
});
};
var client = CreateClient(transport, tokenStore);
var refreshed = await client.Auth.RefreshAsync();
Assert.True(refreshed);
Assert.Equal("new-access", tokenStore.AccessToken);
Assert.Equal("new-refresh", tokenStore.RefreshToken);
}
private static RudderClient CreateClient(FakeTransport transport, FakeTokenStore tokenStore)
{
return new RudderClient(new RudderClientOptions
{
BaseUrl = "http://localhost:8082",
ProjectKey = "project-key",
Transport = transport,
TokenStore = tokenStore,
DeviceIdProvider = new FakeDeviceIdProvider()
});
}
private sealed class FakeTransport : IRudderTransport
{
public Func<string, string, string?, Task<object?>>? Handler;
public Task<TResponse> SendAsync<TRequest, TResponse>(
string method,
string path,
TRequest? request,
string? accessToken,
CancellationToken cancellationToken = default)
{
return Handle<TResponse>(method, path, accessToken);
}
private async Task<TResponse> Handle<TResponse>(string method, string path, string? accessToken)
{
var result = await Handler!(method, path, accessToken);
return (TResponse)result!;
}
}
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";
}
}