Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
- Implemented comprehensive unit tests for RabbitMqTransportServer, covering constructor, disposal, connection management, event handlers, and exception handling. - Added configuration tests for RabbitMqTransportServer to validate SSL, durable queues, auto-recovery, and custom virtual host options. - Created unit tests for UdpFrameProtocol, including frame parsing and serialization, header size validation, and round-trip data preservation. - Developed tests for UdpTransportClient, focusing on connection handling, event subscriptions, and exception scenarios. - Established tests for UdpTransportServer, ensuring proper start/stop behavior, connection state management, and event handling. - Included tests for UdpTransportOptions to verify default values and modification capabilities. - Enhanced service registration tests for Udp transport services in the dependency injection container.
271 lines
8.3 KiB
C#
271 lines
8.3 KiB
C#
using FluentAssertions;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using Microsoft.Extensions.Options;
|
|
using Moq;
|
|
using StellaOps.Gateway.WebService.Authorization;
|
|
using StellaOps.Router.Common.Models;
|
|
using Xunit;
|
|
|
|
namespace StellaOps.Gateway.WebService.Tests;
|
|
|
|
/// <summary>
|
|
/// Unit tests for <see cref="AuthorityClaimsRefreshService"/>.
|
|
/// </summary>
|
|
public sealed class AuthorityClaimsRefreshServiceTests
|
|
{
|
|
private readonly Mock<IAuthorityClaimsProvider> _claimsProviderMock;
|
|
private readonly Mock<IEffectiveClaimsStore> _claimsStoreMock;
|
|
private readonly AuthorityConnectionOptions _options;
|
|
|
|
public AuthorityClaimsRefreshServiceTests()
|
|
{
|
|
_claimsProviderMock = new Mock<IAuthorityClaimsProvider>();
|
|
_claimsStoreMock = new Mock<IEffectiveClaimsStore>();
|
|
_options = new AuthorityConnectionOptions
|
|
{
|
|
AuthorityUrl = "http://authority.local",
|
|
Enabled = true,
|
|
RefreshInterval = TimeSpan.FromMilliseconds(100),
|
|
WaitForAuthorityOnStartup = false,
|
|
StartupTimeout = TimeSpan.FromSeconds(1)
|
|
};
|
|
|
|
_claimsProviderMock.Setup(p => p.GetOverridesAsync(It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(new Dictionary<EndpointKey, IReadOnlyList<ClaimRequirement>>());
|
|
}
|
|
|
|
private AuthorityClaimsRefreshService CreateService()
|
|
{
|
|
return new AuthorityClaimsRefreshService(
|
|
_claimsProviderMock.Object,
|
|
_claimsStoreMock.Object,
|
|
Options.Create(_options),
|
|
NullLogger<AuthorityClaimsRefreshService>.Instance);
|
|
}
|
|
|
|
#region ExecuteAsync Tests - Disabled
|
|
|
|
[Fact]
|
|
public async Task ExecuteAsync_WhenDisabled_DoesNotFetchClaims()
|
|
{
|
|
// Arrange
|
|
_options.Enabled = false;
|
|
var service = CreateService();
|
|
using var cts = new CancellationTokenSource();
|
|
|
|
// Act
|
|
await service.StartAsync(cts.Token);
|
|
await Task.Delay(50);
|
|
await service.StopAsync(cts.Token);
|
|
|
|
// Assert
|
|
_claimsProviderMock.Verify(
|
|
p => p.GetOverridesAsync(It.IsAny<CancellationToken>()),
|
|
Times.Never);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ExecuteAsync_WhenNoAuthorityUrl_DoesNotFetchClaims()
|
|
{
|
|
// Arrange
|
|
_options.AuthorityUrl = string.Empty;
|
|
var service = CreateService();
|
|
using var cts = new CancellationTokenSource();
|
|
|
|
// Act
|
|
await service.StartAsync(cts.Token);
|
|
await Task.Delay(50);
|
|
await service.StopAsync(cts.Token);
|
|
|
|
// Assert
|
|
_claimsProviderMock.Verify(
|
|
p => p.GetOverridesAsync(It.IsAny<CancellationToken>()),
|
|
Times.Never);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region ExecuteAsync Tests - Enabled
|
|
|
|
[Fact]
|
|
public async Task ExecuteAsync_WhenEnabled_FetchesClaims()
|
|
{
|
|
// Arrange
|
|
var service = CreateService();
|
|
using var cts = new CancellationTokenSource();
|
|
|
|
// Act
|
|
await service.StartAsync(cts.Token);
|
|
await Task.Delay(50);
|
|
await cts.CancelAsync();
|
|
await service.StopAsync(CancellationToken.None);
|
|
|
|
// Assert
|
|
_claimsProviderMock.Verify(
|
|
p => p.GetOverridesAsync(It.IsAny<CancellationToken>()),
|
|
Times.AtLeastOnce);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ExecuteAsync_UpdatesStoreWithOverrides()
|
|
{
|
|
// Arrange
|
|
var key = EndpointKey.Create("service", "GET", "/api/test");
|
|
var overrides = new Dictionary<EndpointKey, IReadOnlyList<ClaimRequirement>>
|
|
{
|
|
[key] = [new ClaimRequirement { Type = "role", Value = "admin" }]
|
|
};
|
|
_claimsProviderMock.Setup(p => p.GetOverridesAsync(It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(overrides);
|
|
|
|
var service = CreateService();
|
|
using var cts = new CancellationTokenSource();
|
|
|
|
// Act
|
|
await service.StartAsync(cts.Token);
|
|
await Task.Delay(50);
|
|
await cts.CancelAsync();
|
|
await service.StopAsync(CancellationToken.None);
|
|
|
|
// Assert
|
|
_claimsStoreMock.Verify(
|
|
s => s.UpdateFromAuthority(It.Is<IReadOnlyDictionary<EndpointKey, IReadOnlyList<ClaimRequirement>>>(
|
|
d => d.ContainsKey(key))),
|
|
Times.AtLeastOnce);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region ExecuteAsync Tests - Wait for Authority
|
|
|
|
[Fact]
|
|
public async Task ExecuteAsync_WaitForAuthority_FetchesOnStartup()
|
|
{
|
|
// Arrange
|
|
_options.WaitForAuthorityOnStartup = true;
|
|
_options.StartupTimeout = TimeSpan.FromMilliseconds(500);
|
|
|
|
// Authority is immediately available
|
|
_claimsProviderMock.Setup(p => p.IsAvailable).Returns(true);
|
|
|
|
var fetchCalled = false;
|
|
_claimsProviderMock.Setup(p => p.GetOverridesAsync(It.IsAny<CancellationToken>()))
|
|
.Callback(() => fetchCalled = true)
|
|
.ReturnsAsync(new Dictionary<EndpointKey, IReadOnlyList<ClaimRequirement>>());
|
|
|
|
var service = CreateService();
|
|
using var cts = new CancellationTokenSource();
|
|
|
|
// Act
|
|
await service.StartAsync(cts.Token);
|
|
await Task.Delay(100);
|
|
await cts.CancelAsync();
|
|
await service.StopAsync(CancellationToken.None);
|
|
|
|
// Assert - fetch was called during startup
|
|
fetchCalled.Should().BeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ExecuteAsync_WaitForAuthority_StopsAfterTimeout()
|
|
{
|
|
// Arrange
|
|
_options.WaitForAuthorityOnStartup = true;
|
|
_options.StartupTimeout = TimeSpan.FromMilliseconds(100);
|
|
|
|
_claimsProviderMock.Setup(p => p.IsAvailable).Returns(false);
|
|
|
|
var service = CreateService();
|
|
using var cts = new CancellationTokenSource();
|
|
|
|
// Act - should not block forever
|
|
var startTask = service.StartAsync(cts.Token);
|
|
await Task.Delay(300);
|
|
await cts.CancelAsync();
|
|
await service.StopAsync(CancellationToken.None);
|
|
|
|
// Assert - should complete even if Authority never becomes available
|
|
startTask.IsCompleted.Should().BeTrue();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Push Notification Tests
|
|
|
|
[Fact]
|
|
public async Task ExecuteAsync_WithPushNotifications_SubscribesToEvent()
|
|
{
|
|
// Arrange
|
|
_options.UseAuthorityPushNotifications = true;
|
|
var service = CreateService();
|
|
using var cts = new CancellationTokenSource();
|
|
|
|
// Act
|
|
await service.StartAsync(cts.Token);
|
|
await Task.Delay(50);
|
|
await cts.CancelAsync();
|
|
await service.StopAsync(CancellationToken.None);
|
|
|
|
// Assert - verify event subscription by checking it doesn't throw
|
|
_claimsProviderMock.VerifyAdd(
|
|
p => p.OverridesChanged += It.IsAny<EventHandler<ClaimsOverrideChangedEventArgs>>(),
|
|
Times.Once);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Dispose_WithPushNotifications_UnsubscribesFromEvent()
|
|
{
|
|
// Arrange
|
|
_options.UseAuthorityPushNotifications = true;
|
|
var service = CreateService();
|
|
using var cts = new CancellationTokenSource();
|
|
|
|
await service.StartAsync(cts.Token);
|
|
await Task.Delay(50);
|
|
|
|
// Act
|
|
await cts.CancelAsync();
|
|
service.Dispose();
|
|
|
|
// Assert
|
|
_claimsProviderMock.VerifyRemove(
|
|
p => p.OverridesChanged -= It.IsAny<EventHandler<ClaimsOverrideChangedEventArgs>>(),
|
|
Times.Once);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Error Handling Tests
|
|
|
|
[Fact]
|
|
public async Task ExecuteAsync_ProviderThrows_ContinuesRefreshLoop()
|
|
{
|
|
// Arrange
|
|
var callCount = 0;
|
|
_claimsProviderMock.Setup(p => p.GetOverridesAsync(It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(() =>
|
|
{
|
|
callCount++;
|
|
if (callCount == 1)
|
|
{
|
|
throw new HttpRequestException("Test error");
|
|
}
|
|
return new Dictionary<EndpointKey, IReadOnlyList<ClaimRequirement>>();
|
|
});
|
|
|
|
var service = CreateService();
|
|
using var cts = new CancellationTokenSource();
|
|
|
|
// Act
|
|
await service.StartAsync(cts.Token);
|
|
await Task.Delay(250); // Wait for at least 2 refresh cycles
|
|
await cts.CancelAsync();
|
|
await service.StopAsync(CancellationToken.None);
|
|
|
|
// Assert - should have continued after error
|
|
callCount.Should().BeGreaterThan(1);
|
|
}
|
|
|
|
#endregion
|
|
}
|