doctor enhancements, setup, enhancements, ui functionality and design consolidation and , test projects fixes , product advisory attestation/rekor and delta verfications enhancements
This commit is contained in:
@@ -0,0 +1,236 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using StellaOps.Router.Transport.Messaging.Protocol;
|
||||
using StellaOps.Router.Common.Models;
|
||||
using StellaOps.Router.Common.Enums;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Router.Common.Tests.Protocol;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="CorrelationTracker"/> with proper timer testing using FakeTimeProvider.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class CorrelationTrackerTests : IDisposable
|
||||
{
|
||||
private readonly FakeTimeProvider _timeProvider;
|
||||
private readonly CorrelationTracker _tracker;
|
||||
|
||||
public CorrelationTrackerTests()
|
||||
{
|
||||
_timeProvider = new FakeTimeProvider(new DateTimeOffset(2026, 1, 18, 12, 0, 0, TimeSpan.Zero));
|
||||
_tracker = new CorrelationTracker(_timeProvider);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_tracker.Dispose();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RegisterRequestAsync_ReturnsTask()
|
||||
{
|
||||
// Arrange
|
||||
var correlationId = Guid.NewGuid().ToString();
|
||||
|
||||
// Act
|
||||
var task = _tracker.RegisterRequestAsync(correlationId, TimeSpan.FromSeconds(30), CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
task.Should().NotBeNull();
|
||||
task.IsCompleted.Should().BeFalse();
|
||||
|
||||
// Cleanup - complete the request
|
||||
var frame = new Frame { Type = FrameType.Response, CorrelationId = correlationId };
|
||||
_tracker.TryCompleteRequest(correlationId, frame);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TryCompleteRequest_WithRegisteredId_CompletesTask()
|
||||
{
|
||||
// Arrange
|
||||
var correlationId = Guid.NewGuid().ToString();
|
||||
var responseFrame = new Frame { Type = FrameType.Response, CorrelationId = correlationId, Payload = new byte[] { 1, 2, 3 } };
|
||||
var task = _tracker.RegisterRequestAsync(correlationId, TimeSpan.FromSeconds(30), CancellationToken.None);
|
||||
|
||||
// Act
|
||||
var completed = _tracker.TryCompleteRequest(correlationId, responseFrame);
|
||||
|
||||
// Assert
|
||||
completed.Should().BeTrue();
|
||||
var result = await task;
|
||||
result.Should().Be(responseFrame);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryCompleteRequest_WithUnknownId_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var frame = new Frame { Type = FrameType.Response, CorrelationId = "unknown" };
|
||||
|
||||
// Act
|
||||
var completed = _tracker.TryCompleteRequest("unknown", frame);
|
||||
|
||||
// Assert
|
||||
completed.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TryFailRequest_WithRegisteredId_FailsTask()
|
||||
{
|
||||
// Arrange
|
||||
var correlationId = Guid.NewGuid().ToString();
|
||||
var task = _tracker.RegisterRequestAsync(correlationId, TimeSpan.FromSeconds(30), CancellationToken.None);
|
||||
var exception = new InvalidOperationException("Test failure");
|
||||
|
||||
// Act
|
||||
var failed = _tracker.TryFailRequest(correlationId, exception);
|
||||
|
||||
// Assert
|
||||
failed.Should().BeTrue();
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => task);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryFailRequest_WithUnknownId_ReturnsFalse()
|
||||
{
|
||||
// Act
|
||||
var failed = _tracker.TryFailRequest("unknown", new Exception());
|
||||
|
||||
// Assert
|
||||
failed.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TryCancelRequest_WithRegisteredId_CancelsTask()
|
||||
{
|
||||
// Arrange
|
||||
var correlationId = Guid.NewGuid().ToString();
|
||||
var task = _tracker.RegisterRequestAsync(correlationId, TimeSpan.FromSeconds(30), CancellationToken.None);
|
||||
|
||||
// Act
|
||||
var cancelled = _tracker.TryCancelRequest(correlationId);
|
||||
|
||||
// Assert
|
||||
cancelled.Should().BeTrue();
|
||||
await Assert.ThrowsAsync<TaskCanceledException>(() => task);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryCancelRequest_WithUnknownId_ReturnsFalse()
|
||||
{
|
||||
// Act
|
||||
var cancelled = _tracker.TryCancelRequest("unknown");
|
||||
|
||||
// Assert
|
||||
cancelled.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PendingCount_ReturnsCorrectCount()
|
||||
{
|
||||
// Arrange
|
||||
_tracker.RegisterRequestAsync("id1", TimeSpan.FromSeconds(30), CancellationToken.None);
|
||||
_tracker.RegisterRequestAsync("id2", TimeSpan.FromSeconds(30), CancellationToken.None);
|
||||
_tracker.RegisterRequestAsync("id3", TimeSpan.FromSeconds(30), CancellationToken.None);
|
||||
|
||||
// Act
|
||||
var count = _tracker.PendingCount;
|
||||
|
||||
// Assert
|
||||
count.Should().Be(3);
|
||||
|
||||
// Cleanup
|
||||
_tracker.TryCancelRequest("id1");
|
||||
_tracker.TryCancelRequest("id2");
|
||||
_tracker.TryCancelRequest("id3");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RegisterRequestAsync_DuplicateId_Throws()
|
||||
{
|
||||
// Arrange
|
||||
var correlationId = Guid.NewGuid().ToString();
|
||||
_tracker.RegisterRequestAsync(correlationId, TimeSpan.FromSeconds(30), CancellationToken.None);
|
||||
|
||||
// Act
|
||||
var action = () => _tracker.RegisterRequestAsync(correlationId, TimeSpan.FromSeconds(30), CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
action.Should().ThrowAsync<InvalidOperationException>();
|
||||
|
||||
// Cleanup
|
||||
_tracker.TryCancelRequest(correlationId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CancellationToken_WhenCancelled_CancelsRequest()
|
||||
{
|
||||
// Arrange
|
||||
var correlationId = Guid.NewGuid().ToString();
|
||||
using var cts = new CancellationTokenSource();
|
||||
var task = _tracker.RegisterRequestAsync(correlationId, TimeSpan.FromSeconds(30), cts.Token);
|
||||
|
||||
// Act
|
||||
await cts.CancelAsync();
|
||||
|
||||
// Assert
|
||||
await Assert.ThrowsAsync<TaskCanceledException>(() => task);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CleanupTimer_AdvanceTime_CleansUpExpiredRequests()
|
||||
{
|
||||
// Arrange
|
||||
var correlationId = Guid.NewGuid().ToString();
|
||||
var task = _tracker.RegisterRequestAsync(correlationId, TimeSpan.FromSeconds(10), CancellationToken.None);
|
||||
|
||||
// Advance time past expiration AND past cleanup interval (30 seconds)
|
||||
_timeProvider.Advance(TimeSpan.FromSeconds(35));
|
||||
|
||||
// Allow timer callback to execute
|
||||
await Task.Delay(100);
|
||||
|
||||
// Assert - request should be expired and cleaned up with TimeoutException
|
||||
await Assert.ThrowsAsync<TimeoutException>(() => task);
|
||||
_tracker.PendingCount.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CleanupTimer_BeforeExpiration_DoesNotCleanup()
|
||||
{
|
||||
// Arrange
|
||||
var correlationId = Guid.NewGuid().ToString();
|
||||
var task = _tracker.RegisterRequestAsync(correlationId, TimeSpan.FromSeconds(60), CancellationToken.None);
|
||||
|
||||
// Advance time but not past expiration
|
||||
_timeProvider.Advance(TimeSpan.FromSeconds(35));
|
||||
|
||||
// Allow timer callback to execute
|
||||
await Task.Delay(100);
|
||||
|
||||
// Assert - request should still be pending
|
||||
_tracker.PendingCount.Should().Be(1);
|
||||
task.IsCompleted.Should().BeFalse();
|
||||
|
||||
// Cleanup
|
||||
_tracker.TryCancelRequest(correlationId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dispose_CancelsAllPendingRequests()
|
||||
{
|
||||
// Arrange
|
||||
var tracker = new CorrelationTracker(_timeProvider);
|
||||
var task1 = tracker.RegisterRequestAsync("id1", TimeSpan.FromSeconds(30), CancellationToken.None);
|
||||
var task2 = tracker.RegisterRequestAsync("id2", TimeSpan.FromSeconds(30), CancellationToken.None);
|
||||
|
||||
// Act
|
||||
tracker.Dispose();
|
||||
|
||||
// Assert
|
||||
task1.IsCanceled.Should().BeTrue();
|
||||
task2.IsCanceled.Should().BeTrue();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,11 +20,14 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" />
|
||||
<PackageReference Include="Moq" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.Router.Common\StellaOps.Router.Common.csproj" />
|
||||
<ProjectReference Include="..\__Libraries\StellaOps.Router.Testing\StellaOps.Router.Testing.csproj" />
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.Router.Transport.Messaging\StellaOps.Router.Transport.Messaging.csproj" />
|
||||
<ProjectReference Include="..\_Libraries\StellaOps.Router.Testing\StellaOps.Router.Testing.csproj" />
|
||||
<ProjectReference Include="..\..\..\__Libraries\StellaOps.TestKit\StellaOps.TestKit.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,126 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Router.Config.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="RouterConfigProvider"/> with timer-based hot reload using FakeTimeProvider.
|
||||
/// </summary>
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
public sealed class RouterConfigProviderHotReloadTests : IDisposable
|
||||
{
|
||||
private readonly FakeTimeProvider _timeProvider;
|
||||
private RouterConfigProvider? _provider;
|
||||
|
||||
public RouterConfigProviderHotReloadTests()
|
||||
{
|
||||
_timeProvider = new FakeTimeProvider(new DateTimeOffset(2026, 1, 18, 12, 0, 0, TimeSpan.Zero));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_provider?.Dispose();
|
||||
}
|
||||
|
||||
private RouterConfigProvider CreateProvider(RouterConfigOptions? options = null)
|
||||
{
|
||||
var opts = Options.Create(options ?? new RouterConfigOptions { EnableHotReload = true });
|
||||
_provider = new RouterConfigProvider(opts, NullLogger<RouterConfigProvider>.Instance, _timeProvider);
|
||||
return _provider;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithHotReloadEnabled_DoesNotThrow()
|
||||
{
|
||||
// Arrange & Act
|
||||
var action = () => CreateProvider(new RouterConfigOptions { EnableHotReload = true });
|
||||
|
||||
// Assert
|
||||
action.Should().NotThrow();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithTimeProvider_UsesTimeProvider()
|
||||
{
|
||||
// Arrange & Act
|
||||
var provider = CreateProvider(new RouterConfigOptions { EnableHotReload = true });
|
||||
|
||||
// Assert
|
||||
provider.Should().NotBeNull();
|
||||
provider.Current.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HotReloadTimer_WhenEnabled_ContinuesWorking()
|
||||
{
|
||||
// Arrange
|
||||
var provider = CreateProvider(new RouterConfigOptions
|
||||
{
|
||||
EnableHotReload = true
|
||||
});
|
||||
|
||||
// Act - advance time to simulate hot reload timer firing
|
||||
_timeProvider.Advance(TimeSpan.FromSeconds(35));
|
||||
|
||||
// Allow timer callback to execute
|
||||
await Task.Delay(100);
|
||||
|
||||
// Assert - provider should still be functional (no exception)
|
||||
provider.Current.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HotReloadTimer_MultipleIntervals_ContinuesChecking()
|
||||
{
|
||||
// Arrange
|
||||
var provider = CreateProvider(new RouterConfigOptions
|
||||
{
|
||||
EnableHotReload = true
|
||||
});
|
||||
|
||||
// Act - advance through multiple intervals
|
||||
_timeProvider.Advance(TimeSpan.FromSeconds(35));
|
||||
await Task.Delay(50);
|
||||
_timeProvider.Advance(TimeSpan.FromSeconds(30));
|
||||
await Task.Delay(50);
|
||||
_timeProvider.Advance(TimeSpan.FromSeconds(30));
|
||||
await Task.Delay(50);
|
||||
|
||||
// Assert - provider should still be functional
|
||||
provider.Current.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dispose_StopsTimer()
|
||||
{
|
||||
// Arrange
|
||||
var provider = CreateProvider(new RouterConfigOptions
|
||||
{
|
||||
EnableHotReload = true
|
||||
});
|
||||
|
||||
// Act
|
||||
provider.Dispose();
|
||||
|
||||
// Assert - should not throw when advancing time after dispose
|
||||
_timeProvider.Advance(TimeSpan.FromMinutes(5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dispose_CanBeCalledMultipleTimes()
|
||||
{
|
||||
// Arrange
|
||||
var provider = CreateProvider(new RouterConfigOptions { EnableHotReload = true });
|
||||
|
||||
// Act
|
||||
provider.Dispose();
|
||||
var action = () => provider.Dispose();
|
||||
|
||||
// Assert
|
||||
action.Should().NotThrow();
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,8 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" />
|
||||
<PackageReference Include="Moq" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using StellaOps.Router.Gateway.RateLimit;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Router.Gateway.Tests.RateLimit;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="InstanceRateLimiter"/> with proper timer testing using FakeTimeProvider.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class InstanceRateLimiterTests : IDisposable
|
||||
{
|
||||
private readonly FakeTimeProvider _timeProvider;
|
||||
private readonly InstanceRateLimiter _limiter;
|
||||
private readonly List<RateLimitRule> _defaultRules;
|
||||
|
||||
public InstanceRateLimiterTests()
|
||||
{
|
||||
_timeProvider = new FakeTimeProvider(new DateTimeOffset(2026, 1, 18, 12, 0, 0, TimeSpan.Zero));
|
||||
_defaultRules = new List<RateLimitRule>
|
||||
{
|
||||
new() { MaxRequests = 10, PerSeconds = 1 },
|
||||
new() { MaxRequests = 100, PerSeconds = 60 }
|
||||
};
|
||||
_limiter = new InstanceRateLimiter(_defaultRules, _timeProvider);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_limiter.Dispose();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryAcquire_UnderLimit_ReturnsAllow()
|
||||
{
|
||||
// Act
|
||||
var decision = _limiter.TryAcquire("test-service");
|
||||
|
||||
// Assert
|
||||
decision.Allowed.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryAcquire_AtLimit_ReturnsDeny()
|
||||
{
|
||||
// Arrange - exhaust the per-second limit
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
_limiter.TryAcquire("test-service");
|
||||
}
|
||||
|
||||
// Act
|
||||
var decision = _limiter.TryAcquire("test-service");
|
||||
|
||||
// Assert
|
||||
decision.Allowed.Should().BeFalse();
|
||||
decision.RetryAfterSeconds.Should().BeGreaterThan(0);
|
||||
}
|
||||
|
||||
[Fact(Skip = "SlidingWindowCounter uses Stopwatch.GetTimestamp() internally which doesn't respect FakeTimeProvider. Requires refactoring SlidingWindowCounter to use TimeProvider.")]
|
||||
public void TryAcquire_AfterWindowExpires_AllowsAgain()
|
||||
{
|
||||
// Arrange - exhaust the per-second limit
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
_limiter.TryAcquire("test-service");
|
||||
}
|
||||
|
||||
// Advance time past the window
|
||||
_timeProvider.Advance(TimeSpan.FromSeconds(2));
|
||||
|
||||
// Act
|
||||
var decision = _limiter.TryAcquire("test-service");
|
||||
|
||||
// Assert
|
||||
decision.Allowed.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryAcquire_DifferentMicroservices_IndependentLimits()
|
||||
{
|
||||
// Arrange - exhaust limit for service1
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
_limiter.TryAcquire("service1");
|
||||
}
|
||||
|
||||
// Act - service2 should still be allowed
|
||||
var decision = _limiter.TryAcquire("service2");
|
||||
|
||||
// Assert
|
||||
decision.Allowed.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryAcquire_WithNoRules_AlwaysAllows()
|
||||
{
|
||||
// Arrange
|
||||
using var limiter = new InstanceRateLimiter(new List<RateLimitRule>(), _timeProvider);
|
||||
|
||||
// Act
|
||||
var decision = limiter.TryAcquire("test-service");
|
||||
|
||||
// Assert
|
||||
decision.Allowed.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryAcquire_WithCustomRules_UsesCustomRules()
|
||||
{
|
||||
// Arrange
|
||||
var customRules = new List<RateLimitRule>
|
||||
{
|
||||
new() { MaxRequests = 2, PerSeconds = 1 }
|
||||
};
|
||||
|
||||
// Act - use up the custom limit
|
||||
_limiter.TryAcquire("test-service", customRules);
|
||||
_limiter.TryAcquire("test-service", customRules);
|
||||
var decision = _limiter.TryAcquire("test-service", customRules);
|
||||
|
||||
// Assert
|
||||
decision.Allowed.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetCurrentCount_ReturnsAccurateCount()
|
||||
{
|
||||
// Arrange
|
||||
_limiter.TryAcquire("test-service");
|
||||
_limiter.TryAcquire("test-service");
|
||||
_limiter.TryAcquire("test-service");
|
||||
|
||||
// Act
|
||||
var count = _limiter.GetCurrentCount("test-service");
|
||||
|
||||
// Assert
|
||||
count.Should().Be(3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetCurrentCount_NonExistentService_ReturnsZero()
|
||||
{
|
||||
// Act
|
||||
var count = _limiter.GetCurrentCount("nonexistent");
|
||||
|
||||
// Assert
|
||||
count.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsAllCounters()
|
||||
{
|
||||
// Arrange
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
_limiter.TryAcquire("test-service");
|
||||
}
|
||||
|
||||
// Act
|
||||
_limiter.Reset();
|
||||
|
||||
// Assert
|
||||
var count = _limiter.GetCurrentCount("test-service");
|
||||
count.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CleanupTimer_AdvanceTime_CleansUpStaleCounters()
|
||||
{
|
||||
// Arrange - make some requests
|
||||
_limiter.TryAcquire("test-service");
|
||||
|
||||
// Advance time significantly past cleanup interval (1 minute)
|
||||
_timeProvider.Advance(TimeSpan.FromMinutes(5));
|
||||
|
||||
// Allow timer callback to execute
|
||||
Task.Delay(100).Wait();
|
||||
|
||||
// After cleanup, service should start fresh
|
||||
var decision = _limiter.TryAcquire("test-service");
|
||||
|
||||
// Assert
|
||||
decision.Allowed.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryAcquire_EmptyMicroserviceName_UsesDefault()
|
||||
{
|
||||
// Act
|
||||
var decision1 = _limiter.TryAcquire("");
|
||||
var decision2 = _limiter.TryAcquire(null!);
|
||||
|
||||
// Assert
|
||||
decision1.Allowed.Should().BeTrue();
|
||||
decision2.Allowed.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryAcquire_MultipleRules_MostRestrictiveApplies()
|
||||
{
|
||||
// Arrange - rules: 10/1s and 100/60s
|
||||
// Exhaust the 10/1s rule
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
_limiter.TryAcquire("test-service");
|
||||
}
|
||||
|
||||
// Act - should be denied by the 10/1s rule even though 100/60s isn't exhausted
|
||||
var decision = _limiter.TryAcquire("test-service");
|
||||
|
||||
// Assert
|
||||
decision.Allowed.Should().BeFalse();
|
||||
decision.RetryAfterSeconds.Should().BeLessThanOrEqualTo(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<RootNamespace>StellaOps.Router.Gateway.Tests</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="Moq" />
|
||||
<PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.Router.Gateway\StellaOps.Router.Gateway.csproj" />
|
||||
<ProjectReference Include="..\..\..\__Libraries\StellaOps.TestKit\StellaOps.TestKit.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"$schema": "https://xunit.net/schema/current/xunit.runner.schema.json",
|
||||
"parallelizeTestCollections": false,
|
||||
"maxParallelThreads": 1
|
||||
}
|
||||
Reference in New Issue
Block a user