Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Michael Griffin #261

Closed
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
96 changes: 96 additions & 0 deletions RateLimiter.Tests/EuropeanRateLimiterPolicyTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Configuration;
using Moq;
using RateLimiter.Services;

namespace RateLimiter.Tests
{
public class EuropeanRateLimiterPolicyTests
{
[Fact]
public void TestRateLimitingNotAppliedBelowOrEqualToMaxRequests()
{
const int maxAllowedRequestsPerFixedWindow = 2;
const string unitOfTime = "Minute";
const int cacheExpirationMinutes = 5;
var mockConfig = BuildTestConfig(maxAllowedRequestsPerFixedWindow, unitOfTime, cacheExpirationMinutes);

using var memoryCache = new MemoryCache(new MemoryCacheOptions());
var clientBehaviorCache = new ClientBehaviorCache(memoryCache, mockConfig.Object);
var policy = new EuropeanRateLimiterPolicy(clientBehaviorCache, mockConfig.Object);

string testApiKey = "test-api-key";
var initialRequestTime = new DateTime(2024, 12, 9, 10, 10, 0);
string noRateLimitingExpectedMessage = $"Rate limiting should not apply until exceeding {maxAllowedRequestsPerFixedWindow} requests per period";

clientBehaviorCache.Add(testApiKey, initialRequestTime);
Assert.False(policy.IsApplicable(testApiKey, initialRequestTime), noRateLimitingExpectedMessage);

var secondRequestTime = initialRequestTime.AddSeconds(58);
clientBehaviorCache.Add(testApiKey, secondRequestTime);
Assert.False(policy.IsApplicable(testApiKey, secondRequestTime), noRateLimitingExpectedMessage);
}

[Fact]
public void TestRateLimitingAppliedWhenMaxRequestsExceeded()
{
const int maxAllowedRequestsPerFixedWindow = 2;
const string unitOfTime = "Minute";
const int cacheExpirationMinutes = 5;
var mockConfig = BuildTestConfig(maxAllowedRequestsPerFixedWindow, unitOfTime, cacheExpirationMinutes);

using var memoryCache = new MemoryCache(new MemoryCacheOptions());
var clientBehaviorCache = new ClientBehaviorCache(memoryCache, mockConfig.Object);
var policy = new EuropeanRateLimiterPolicy(clientBehaviorCache, mockConfig.Object);

string testApiKey = "test-api-key";
var initialRequestTime = new DateTime(2024, 12, 9, 10, 10, 0);
var secondRequestTime = initialRequestTime.AddSeconds(58);
var thirdRequestTime = initialRequestTime.AddSeconds(59);

clientBehaviorCache.Add(testApiKey, initialRequestTime);
clientBehaviorCache.Add(testApiKey, secondRequestTime);
clientBehaviorCache.Add(testApiKey, thirdRequestTime);

string rateLimitingExpectedMessage = $"Rate limiting applies after exceeding {maxAllowedRequestsPerFixedWindow} requests per period";
Assert.True(policy.IsApplicable(testApiKey, thirdRequestTime), rateLimitingExpectedMessage);
}

[Fact]
public void TestCanExceedRateLimitByAllocatingRequestsAcrossDistinctFixedWindows()
{
const int maxAllowedRequestsPerFixedWindow = 2;
const string unitOfTime = "Minute";
const int cacheExpirationMinutes = 5;
var mockConfig = BuildTestConfig(maxAllowedRequestsPerFixedWindow, unitOfTime, cacheExpirationMinutes);

using var memoryCache = new MemoryCache(new MemoryCacheOptions());
var clientBehaviorCache = new ClientBehaviorCache(memoryCache, mockConfig.Object);
var policy = new EuropeanRateLimiterPolicy(clientBehaviorCache, mockConfig.Object);

string testApiKey = "test-api-key";
var firstRequestTime = new DateTime(2024, 12, 9, 10, 10, 58);
var secondRequestTime = new DateTime(2024, 12, 9, 10, 10, 59);
var thirdRequestTime = new DateTime(2024, 12, 9, 10, 11, 1);
var finalRequestTime = new DateTime(2024, 12, 9, 10, 11, 2);

clientBehaviorCache.Add(testApiKey, firstRequestTime);
clientBehaviorCache.Add(testApiKey, secondRequestTime);
clientBehaviorCache.Add(testApiKey, thirdRequestTime);
clientBehaviorCache.Add(testApiKey, finalRequestTime);

string msg = "Should be able to exceed fixed window rate limit by allocating requests at end of window and at start of the next.";
Assert.False(policy.IsApplicable(testApiKey, finalRequestTime), msg);
}

private static Mock<IConfiguration> BuildTestConfig(int maxAllowedRequestsPerFixedWindow, string unitOfTime, int cacheExpirationMinutes)
{
var mockConfig = new Mock<IConfiguration>();
mockConfig.Setup(c => c["RateLimiting:FixedWindow:MaxRequests"]).Returns(maxAllowedRequestsPerFixedWindow.ToString());
mockConfig.Setup(c => c["RateLimiting:FixedWindow:UnitOfTime"]).Returns(unitOfTime);
mockConfig.Setup(c => c["RateLimiting:CacheExpirationMinutes"]).Returns(cacheExpirationMinutes.ToString());

return mockConfig;
}
}
}
102 changes: 102 additions & 0 deletions RateLimiter.Tests/NorthAmericanRateLimiterPolicyTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Configuration;
using Moq;
using RateLimiter.Services;

namespace RateLimiter.Tests
{
public class NorthAmericanRateLimiterPolicyTests
{
[Fact]
public void TestRateLimitingNotAppliedBelowOrEqualToMaxRequests()
{
const int maxAllowedRequestsPerSlidingWindow = 3;
const int windowLengthSeconds = 30;
const int cacheExpirationMinutes = 5;
var mockConfig = BuildTestConfig(maxAllowedRequestsPerSlidingWindow, windowLengthSeconds, cacheExpirationMinutes);

string testApiKey = "test-api-key";
var firstRequestTime = new DateTime(2024, 12, 9, 10, 10, 0);
var secondRequestTime = firstRequestTime.AddSeconds(1);
var thirdRequestTime = firstRequestTime.AddSeconds(2);

using var memoryCache = new MemoryCache(new MemoryCacheOptions());
var clientBehaviorCache = new ClientBehaviorCache(memoryCache, mockConfig.Object);
var policy = new NorthAmericanRateLimiterPolicy(clientBehaviorCache, mockConfig.Object);

clientBehaviorCache.Add(testApiKey, firstRequestTime);
clientBehaviorCache.Add(testApiKey, secondRequestTime);
clientBehaviorCache.Add(testApiKey, thirdRequestTime);

string msg = "No rate limiting expected if number of requests equal to max allowed";
Assert.False(policy.IsApplicable(testApiKey, thirdRequestTime), msg);
}

[Fact]
public void TestRateLimitingAppliedWhenMaxRequestsExceeded()
{
const int maxAllowedRequestsPerSlidingWindow = 3;
const int windowLengthSeconds = 30;
const int cacheExpirationMinutes = 5;
var mockConfig = BuildTestConfig(maxAllowedRequestsPerSlidingWindow, windowLengthSeconds, cacheExpirationMinutes);

string testApiKey = "test-api-key";
var firstRequestTime = new DateTime(2024, 12, 9, 10, 10, 0);
var secondRequestTime = firstRequestTime.AddSeconds(1);
var thirdRequestTime = firstRequestTime.AddSeconds(28);
var fourthRequestTime = firstRequestTime.AddSeconds(windowLengthSeconds);

using var memoryCache = new MemoryCache(new MemoryCacheOptions());
var clientBehaviorCache = new ClientBehaviorCache(memoryCache, mockConfig.Object);
var policy = new NorthAmericanRateLimiterPolicy(clientBehaviorCache, mockConfig.Object);

clientBehaviorCache.Add(testApiKey, firstRequestTime);
clientBehaviorCache.Add(testApiKey, secondRequestTime);
clientBehaviorCache.Add(testApiKey, thirdRequestTime);
clientBehaviorCache.Add(testApiKey, fourthRequestTime);

string msg = "Rate limiting is expected when request count exceeds maximum allowed per period";
Assert.True(policy.IsApplicable(testApiKey, thirdRequestTime), msg);
}

[Fact]
public void TestRateLimiterDoesNotApplyWhenRequestsAreDistributedOverTime()
{
const int maxAllowedRequestsPerSlidingWindow = 3;
const int windowLengthSeconds = 30;
const int cacheExpirationMinutes = 5;
var mockConfig = BuildTestConfig(maxAllowedRequestsPerSlidingWindow, windowLengthSeconds, cacheExpirationMinutes);

string testApiKey = "test-api-key";
var firstRequestTime = new DateTime(2024, 12, 9, 10, 10, 0);
var secondRequestTime = firstRequestTime.AddSeconds(1);
var thirdRequestTime = firstRequestTime.AddSeconds(2);

// This request is more than 30 seconds removed from initial request. No rate limiting expected.
var fourthRequestTime = firstRequestTime.AddSeconds(windowLengthSeconds + 1);

using var memoryCache = new MemoryCache(new MemoryCacheOptions());
var clientBehaviorCache = new ClientBehaviorCache(memoryCache, mockConfig.Object);
var policy = new NorthAmericanRateLimiterPolicy(clientBehaviorCache, mockConfig.Object);

clientBehaviorCache.Add(testApiKey, firstRequestTime);
clientBehaviorCache.Add(testApiKey, secondRequestTime);
clientBehaviorCache.Add(testApiKey, thirdRequestTime);
clientBehaviorCache.Add(testApiKey, fourthRequestTime);

string msg = "Rate limiter should allow many requests as long as they don't fall into same sliding window";
Assert.False(policy.IsApplicable(testApiKey, fourthRequestTime), msg);
}

private static Mock<IConfiguration> BuildTestConfig(int maxAllowedRequestsPerSlidingWindow, int windowLengthSeconds, int cacheExpirationMinutes)
{
var mockConfig = new Mock<IConfiguration>();
mockConfig.Setup(c => c["RateLimiting:SlidingWindow:MaxRequests"]).Returns(maxAllowedRequestsPerSlidingWindow.ToString());
mockConfig.Setup(c => c["RateLimiting:SlidingWindow:WindowLengthSeconds"]).Returns(windowLengthSeconds.ToString());
mockConfig.Setup(c => c["RateLimiting:CacheExpirationMinutes"]).Returns(cacheExpirationMinutes.ToString());

return mockConfig;
}
}

}
43 changes: 28 additions & 15 deletions RateLimiter.Tests/RateLimiter.Tests.csproj
Original file line number Diff line number Diff line change
@@ -1,15 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\RateLimiter\RateLimiter.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.4.2" />
</ItemGroup>
</Project>
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="xunit" Version="2.5.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\RateLimiter\RateLimiter.csproj" />
</ItemGroup>

<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>

</Project>
13 changes: 0 additions & 13 deletions RateLimiter.Tests/RateLimiterTest.cs

This file was deleted.

72 changes: 36 additions & 36 deletions RateLimiter.sln
Original file line number Diff line number Diff line change
@@ -1,36 +1,36 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26730.15
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RateLimiter", "RateLimiter\RateLimiter.csproj", "{36F4BDC6-D3DA-403A-8DB7-0C79F94B938F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RateLimiter.Tests", "RateLimiter.Tests\RateLimiter.Tests.csproj", "{C4F9249B-010E-46BE-94B8-DD20D82F1E60}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{9B206889-9841-4B5E-B79B-D5B2610CCCFF}"
ProjectSection(SolutionItems) = preProject
README.md = README.md
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{36F4BDC6-D3DA-403A-8DB7-0C79F94B938F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{36F4BDC6-D3DA-403A-8DB7-0C79F94B938F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{36F4BDC6-D3DA-403A-8DB7-0C79F94B938F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{36F4BDC6-D3DA-403A-8DB7-0C79F94B938F}.Release|Any CPU.Build.0 = Release|Any CPU
{C4F9249B-010E-46BE-94B8-DD20D82F1E60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C4F9249B-010E-46BE-94B8-DD20D82F1E60}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C4F9249B-010E-46BE-94B8-DD20D82F1E60}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C4F9249B-010E-46BE-94B8-DD20D82F1E60}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {67D05CB6-8603-4C96-97E5-C6CEFBEC6134}
EndGlobalSection
EndGlobal

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.12.35514.174
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{9B206889-9841-4B5E-B79B-D5B2610CCCFF}"
ProjectSection(SolutionItems) = preProject
README.md = README.md
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RateLimiter", "RateLimiter\RateLimiter.csproj", "{96053C2C-25E5-4DC1-80B8-B6B4102C5585}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RateLimiter.Tests", "RateLimiter.Tests\RateLimiter.Tests.csproj", "{E66B10CF-321E-4AFF-AD4B-EF3BF9D15D7D}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{96053C2C-25E5-4DC1-80B8-B6B4102C5585}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{96053C2C-25E5-4DC1-80B8-B6B4102C5585}.Debug|Any CPU.Build.0 = Debug|Any CPU
{96053C2C-25E5-4DC1-80B8-B6B4102C5585}.Release|Any CPU.ActiveCfg = Release|Any CPU
{96053C2C-25E5-4DC1-80B8-B6B4102C5585}.Release|Any CPU.Build.0 = Release|Any CPU
{E66B10CF-321E-4AFF-AD4B-EF3BF9D15D7D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E66B10CF-321E-4AFF-AD4B-EF3BF9D15D7D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E66B10CF-321E-4AFF-AD4B-EF3BF9D15D7D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E66B10CF-321E-4AFF-AD4B-EF3BF9D15D7D}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {67D05CB6-8603-4C96-97E5-C6CEFBEC6134}
EndGlobalSection
EndGlobal
33 changes: 33 additions & 0 deletions RateLimiter/Controllers/WeatherForecastController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using Microsoft.AspNetCore.Mvc;

namespace RateLimiter.Controllers
{
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};

private readonly ILogger<WeatherForecastController> _logger;

public WeatherForecastController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
}

[HttpGet(Name = "GetWeatherForecast")]
public IEnumerable<WeatherForecast> Get()
{
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
.ToArray();
}
}
}
Loading
Loading