Skip to content
Closed

- #303

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions RateLimiter.Tests/Examples/UsageExamples.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using NUnit.Framework;
using RateLimiter.Abstractions;
using RateLimiter.Extensions;
using RateLimiter.Rules;

namespace RateLimiter.Tests.Examples
{
[TestFixture]
public class UsageExamples
{
[Test]
public void Example_BasicConfiguration()
{
var services = new ServiceCollection();

services.AddRateLimiter(options =>
{
var requestCountRule = new RequestCountRule(new Storage.InMemoryRequestTracker())
{
MaxRequests = 100,
TimeWindow = TimeSpan.FromMinutes(1)
};

options.DefaultRules.Add(requestCountRule);
});

var serviceProvider = services.BuildServiceProvider();

Assert.Pass("Example code compiles successfully");
}

[Test]
public void Example_AdvancedConfiguration()
{
var services = new ServiceCollection();

services.AddRateLimiter(options =>
{
var requestTracker = new Storage.InMemoryRequestTracker();

var requestCountRule = new RequestCountRule(requestTracker)
{
MaxRequests = 100,
TimeWindow = TimeSpan.FromMinutes(1)
};

var timeSinceLastCallRule = new TimeSinceLastCallRule(requestTracker)
{
MinInterval = TimeSpan.FromSeconds(1)
};

var regionRule = new RegionCompositeRule(requestCountRule);

var euRule = new TimeSinceLastCallRule(requestTracker)
{
MinInterval = TimeSpan.FromSeconds(2)
};
regionRule.AddRegionRule("EU", euRule);

options.DefaultRules.Add(regionRule);

options.EndpointRules.Add("/api/high-load", new List<IRateLimitRule>
{
new RequestCountRule(requestTracker)
{
MaxRequests = 10,
TimeWindow = TimeSpan.FromMinutes(1)
}
});
});

var serviceProvider = services.BuildServiceProvider();

Assert.Pass("Example code compiles successfully");
}
}
}
114 changes: 114 additions & 0 deletions RateLimiter.Tests/Http/RateLimiterMiddlewareTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using Moq;
using NUnit.Framework;
using RateLimiter.Abstractions;
using RateLimiter.Configuration;
using RateLimiter.Http;
using RateLimiter.Models;

namespace RateLimiter.Tests.Http
{
[TestFixture]
public class RateLimiterMiddlewareTests
{
private Mock<RequestDelegate> _mockNext = null!;
private Mock<IRequestContextFactory> _mockContextFactory = null!;
private Mock<IOptionsMonitor<RateLimitOptions>> _mockOptionsMonitor = null!;
private Mock<IRateLimitRule> _mockRule = null!;
private RateLimiterMiddleware _middleware = null!;
private DefaultHttpContext _httpContext = null!;
private RequestContext _requestContext = null!;
private RateLimitOptions _options = null!;

[SetUp]
public void Setup()
{
_mockNext = new Mock<RequestDelegate>();
_mockContextFactory = new Mock<IRequestContextFactory>();
_mockOptionsMonitor = new Mock<IOptionsMonitor<RateLimitOptions>>();
_mockRule = new Mock<IRateLimitRule>();

_middleware = new RateLimiterMiddleware(_mockNext.Object, _mockContextFactory.Object);

_httpContext = new DefaultHttpContext();
_httpContext.Request.Path = "/api/resource";

_requestContext = new RequestContext
{
ClientToken = "test-token",
ResourcePath = "/api/resource"
};

_options = new RateLimitOptions();
_options.DefaultRules.Add(_mockRule.Object);

_mockContextFactory.Setup(f => f.Create(It.IsAny<HttpContext>()))
.Returns(_requestContext);

_mockOptionsMonitor.Setup(o => o.CurrentValue)
.Returns(_options);
}

[Test]
public async Task InvokeAsync_WhenRateLimitAllowed_CallsNextMiddleware()
{
_mockRule.Setup(s => s.ValidateAsync(It.IsAny<RequestContext>()))
.ReturnsAsync(true);

await _middleware.InvokeAsync(_httpContext, _mockOptionsMonitor.Object);

_mockNext.Verify(n => n(_httpContext), Times.Once);
Assert.That(_httpContext.Response.StatusCode, Is.EqualTo(StatusCodes.Status200OK));
}

[Test]
public async Task InvokeAsync_WhenRateLimitExceeded_Returns429()
{
_mockRule.Setup(s => s.ValidateAsync(It.IsAny<RequestContext>()))
.ReturnsAsync(false);

await _middleware.InvokeAsync(_httpContext, _mockOptionsMonitor.Object);

_mockNext.Verify(n => n(It.IsAny<HttpContext>()), Times.Never);
Assert.That(_httpContext.Response.StatusCode, Is.EqualTo(StatusCodes.Status429TooManyRequests));
Assert.That(_httpContext.Response.Headers.ContainsKey("Retry-After"), Is.True);
}

[Test]
public async Task InvokeAsync_WithMultipleRules_ChecksAllRules()
{
var mockRule2 = new Mock<IRateLimitRule>();
_options.DefaultRules.Add(mockRule2.Object);

_mockRule.Setup(s => s.ValidateAsync(It.IsAny<RequestContext>()))
.ReturnsAsync(true);
mockRule2.Setup(s => s.ValidateAsync(It.IsAny<RequestContext>()))
.ReturnsAsync(true);

await _middleware.InvokeAsync(_httpContext, _mockOptionsMonitor.Object);

_mockRule.Verify(s => s.ValidateAsync(_requestContext), Times.Once);
mockRule2.Verify(s => s.ValidateAsync(_requestContext), Times.Once);
_mockNext.Verify(n => n(_httpContext), Times.Once);
}

[Test]
public async Task InvokeAsync_FirstRuleFails_DoesNotCheckSecondRule()
{
var mockRule2 = new Mock<IRateLimitRule>();
_options.DefaultRules.Add(mockRule2.Object);

_mockRule.Setup(s => s.ValidateAsync(It.IsAny<RequestContext>()))
.ReturnsAsync(false);

await _middleware.InvokeAsync(_httpContext, _mockOptionsMonitor.Object);

_mockRule.Verify(s => s.ValidateAsync(_requestContext), Times.Once);
mockRule2.Verify(s => s.ValidateAsync(It.IsAny<RequestContext>()), Times.Never);
_mockNext.Verify(n => n(It.IsAny<HttpContext>()), Times.Never);
}
}
}
93 changes: 93 additions & 0 deletions RateLimiter.Tests/Integration/RateLimiterIntegrationTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using NUnit.Framework;
using RateLimiter.Abstractions;
using RateLimiter.Configuration;
using RateLimiter.Extensions;
using RateLimiter.Http;
using RateLimiter.Rules;

namespace RateLimiter.Tests.Integration
{
[TestFixture]
public class RateLimiterIntegrationTests
{
private IServiceProvider _serviceProvider = null!;
private DefaultHttpContext _httpContext = null!;

[SetUp]
public void Setup()
{
var services = new ServiceCollection();

services.AddRateLimiter(options =>
{
var requestCountRule = new RequestCountRule(new Storage.InMemoryRequestTracker())
{
MaxRequests = 3,
TimeWindow = TimeSpan.FromSeconds(10)
};

options.DefaultRules.Add(requestCountRule);
});

_serviceProvider = services.BuildServiceProvider();

_httpContext = new DefaultHttpContext
{
RequestServices = _serviceProvider
};
_httpContext.Request.Path = "/api/resource";
_httpContext.Request.Headers.Add("Authorization", "test-token");
}

[Test]
public async Task RateLimiter_AllowsRequestsUpToLimit()
{
var middleware = new RateLimiterMiddleware(
next: (context) => Task.CompletedTask,
contextFactory: _serviceProvider.GetRequiredService<IRequestContextFactory>()
);

var options = _serviceProvider.GetRequiredService<Microsoft.Extensions.Options.IOptionsMonitor<RateLimitOptions>>();

for (int i = 0; i < 3; i++)
{
await middleware.InvokeAsync(_httpContext, options);
Assert.That(_httpContext.Response.StatusCode, Is.EqualTo(StatusCodes.Status200OK));
}

await middleware.InvokeAsync(_httpContext, options);
Assert.That(_httpContext.Response.StatusCode, Is.EqualTo(StatusCodes.Status429TooManyRequests));
}

[Test]
public async Task RateLimiter_TracksRequestsPerClientAndResource()
{
var middleware = new RateLimiterMiddleware(
next: (context) => Task.CompletedTask,
contextFactory: _serviceProvider.GetRequiredService<IRequestContextFactory>()
);

var options = _serviceProvider.GetRequiredService<Microsoft.Extensions.Options.IOptionsMonitor<RateLimitOptions>>();

for (int i = 0; i < 3; i++)
{
_httpContext.Request.Path = "/api/resource1";
await middleware.InvokeAsync(_httpContext, options);
Assert.That(_httpContext.Response.StatusCode, Is.EqualTo(StatusCodes.Status200OK));
}

_httpContext.Request.Path = "/api/resource1";
await middleware.InvokeAsync(_httpContext, options);
Assert.That(_httpContext.Response.StatusCode, Is.EqualTo(StatusCodes.Status429TooManyRequests));

_httpContext.Request.Path = "/api/resource2";
_httpContext.Response.StatusCode = StatusCodes.Status200OK;
await middleware.InvokeAsync(_httpContext, options);
Assert.That(_httpContext.Response.StatusCode, Is.EqualTo(StatusCodes.Status200OK));
}
}
}
5 changes: 4 additions & 1 deletion RateLimiter.Tests/RateLimiter.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@
<ProjectReference Include="..\RateLimiter\RateLimiter.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
<PackageReference Include="Moq" Version="4.18.4" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.4.2" />
</ItemGroup>
</Project>
</Project>
Loading