Initial commit
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
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";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user