Skip to content
Open
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
74 changes: 74 additions & 0 deletions RateLimiter.Tests/Middleware/RateLimiterMiddlewareTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Moq;
using Xunit;
using RateLimiter.Middleware;
using RateLimiter.Services;

namespace RateLimiter.Tests.Middleware;

public class RateLimiterMiddlewareTests
{
[Fact]
public async Task InvokeAsync_ShouldReturn401_WhenTokenIsMissing()
{
// Arrange
var rateLimiterMock = new Mock<IRateLimiterService>();
var middleware = new RateLimiterMiddleware(async (context) => { await Task.CompletedTask; }, rateLimiterMock.Object);

var context = new DefaultHttpContext();
context.Request.Path = "/test/endpoint";

// Act
await middleware.InvokeAsync(context);

// Assert
Assert.Equal(StatusCodes.Status401Unauthorized, context.Response.StatusCode);
}

[Fact]
public async Task InvokeAsync_ShouldReturn429_WhenRateLimitIsExceeded()
{
// Arrange
var rateLimiterMock = new Mock<IRateLimiterService>();
rateLimiterMock
.Setup(r => r.IsRequestAllowed(It.IsAny<string>(), It.IsAny<string>()))
.Returns(false);

var middleware = new RateLimiterMiddleware(async (context) => { await Task.CompletedTask; }, rateLimiterMock.Object);

var context = new DefaultHttpContext();
context.Request.Path = "/test/endpoint";
context.Request.Headers["X-Client-Token"] = "client-token";

// Act
await middleware.InvokeAsync(context);

// Assert
Assert.Equal(StatusCodes.Status429TooManyRequests, context.Response.StatusCode);
}

[Fact]
public async Task InvokeAsync_ShouldCallNextMiddleware_WhenRequestIsAllowed()
{
// Arrange
var rateLimiterMock = new Mock<IRateLimiterService>();
rateLimiterMock
.Setup(r => r.IsRequestAllowed(It.IsAny<string>(), It.IsAny<string>()))
.Returns(true);

var nextMiddlewareCalled = false;
var middleware = new RateLimiterMiddleware(async (context) => { nextMiddlewareCalled = true; await Task.CompletedTask; }, rateLimiterMock.Object);

var context = new DefaultHttpContext();
context.Request.Path = "/test/endpoint";
context.Request.Headers["X-Client-Token"] = "client-token";

// Act
await middleware.InvokeAsync(context);

// Assert
Assert.True(nextMiddlewareCalled);
Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
}
}
7 changes: 7 additions & 0 deletions RateLimiter.Tests/RateLimiter.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,15 @@
<ProjectReference Include="..\RateLimiter\RateLimiter.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.3.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.4.2" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
</Project>
84 changes: 84 additions & 0 deletions RateLimiter.Tests/Rules/RequestPerTimeSpanRuleTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
using System;
using System.Collections.Generic;
using Xunit;
using RateLimiter.Rules;

namespace RateLimiter.Tests.Rules;

public class RequestPerTimeSpanRuleTests
{
[Fact]
public void IsRequestAllowed_ShouldReturnTrue_WhenRequestLogsAreEmpty()
{
// Arrange
var rule = new RequestPerTimeSpanRule(5, TimeSpan.FromMinutes(1));
var requestLogs = new List<DateTime>();
var timeOfRequest = DateTime.UtcNow;

// Act
bool result = rule.IsRequestAllowed(requestLogs, timeOfRequest);

// Assert
Assert.True(result);
}

[Fact]
public void IsRequestAllowed_ShouldReturnTrue_WhenRequestCountIsBelowLimit()
{
// Arrange
var rule = new RequestPerTimeSpanRule(5, TimeSpan.FromMinutes(1));
var requestLogs = new List<DateTime>
{
DateTime.UtcNow.AddSeconds(-10),
DateTime.UtcNow.AddSeconds(-20)
};
var timeOfRequest = DateTime.UtcNow;

// Act
bool result = rule.IsRequestAllowed(requestLogs, timeOfRequest);

// Assert
Assert.True(result);
}

[Fact]
public void IsRequestAllowed_ShouldReturnFalse_WhenRequestCountExceedsLimit()
{
// Arrange
var rule = new RequestPerTimeSpanRule(3, TimeSpan.FromMinutes(1));
var requestLogs = new List<DateTime>
{
DateTime.UtcNow.AddSeconds(-10),
DateTime.UtcNow.AddSeconds(-20),
DateTime.UtcNow.AddSeconds(-30)
};
var timeOfRequest = DateTime.UtcNow;

// Act
bool result = rule.IsRequestAllowed(requestLogs, timeOfRequest);

// Assert
Assert.False(result);
}

[Fact]
public void IsRequestAllowed_ShouldReturnTrue_WhenRequestCountIsBelowLimitWithOutdatedRequests()
{
// Arrange
var rule = new RequestPerTimeSpanRule(3, TimeSpan.FromSeconds(15));
var requestLogs = new List<DateTime>
{
DateTime.UtcNow.AddSeconds(-20),
DateTime.UtcNow.AddSeconds(-10),
DateTime.UtcNow.AddSeconds(-5)
};
var timeOfRequest = DateTime.UtcNow;

// Act
bool result = rule.IsRequestAllowed(requestLogs, timeOfRequest);

// Assert
Assert.True(result);
Assert.Equal(2, requestLogs.Count);
}
}
100 changes: 100 additions & 0 deletions RateLimiter.Tests/Rules/TimeSpanSinceLastRequestRuleTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
using System;
using System.Collections.Generic;
using Xunit;
using RateLimiter.Rules;

namespace RateLimiter.Tests.Rules;

public class TimeSpanSinceLastRequestRuleTests
{
[Fact]
public void IsRequestAllowed_ShouldReturnTrue_WhenRequestLogsAreEmpty()
{
// Arrange
var rule = new TimeSpanSinceLastRequestRule(TimeSpan.FromSeconds(10));
var requestLogs = new List<DateTime>();
var timeOfRequest = DateTime.UtcNow;

// Act
bool result = rule.IsRequestAllowed(requestLogs, timeOfRequest);

// Assert
Assert.True(result);
}

[Fact]
public void IsRequestAllowed_ShouldReturnTrue_WhenTimeSinceLastRequestExceedsLimit()
{
// Arrange
var rule = new TimeSpanSinceLastRequestRule(TimeSpan.FromSeconds(10));
var requestLogs = new List<DateTime>
{
DateTime.UtcNow.AddSeconds(-15)
};
var timeOfRequest = DateTime.UtcNow;

// Act
bool result = rule.IsRequestAllowed(requestLogs, timeOfRequest);

// Assert
Assert.True(result);
}

[Fact]
public void IsRequestAllowed_ShouldReturnFalse_WhenTimeSinceLastRequestIsBelowLimit()
{
// Arrange
var rule = new TimeSpanSinceLastRequestRule(TimeSpan.FromSeconds(10));
var requestLogs = new List<DateTime>
{
DateTime.UtcNow.AddSeconds(-5)
};
var timeOfRequest = DateTime.UtcNow;

// Act
bool result = rule.IsRequestAllowed(requestLogs, timeOfRequest);

// Assert
Assert.False(result);
}

[Fact]
public void IsRequestAllowed_ShouldReturnTrue_WhenLastRequestExceedsLimitWithMultipleOlderRequestsInLogs()
{
// Arrange
var rule = new TimeSpanSinceLastRequestRule(TimeSpan.FromSeconds(10));
var requestLogs = new List<DateTime>
{
DateTime.UtcNow.AddSeconds(-30),
DateTime.UtcNow.AddSeconds(-25),
DateTime.UtcNow.AddSeconds(-20)
};
var timeOfRequest = DateTime.UtcNow;

// Act
bool result = rule.IsRequestAllowed(requestLogs, timeOfRequest);

// Assert
Assert.True(result);
}

[Fact]
public void IsRequestAllowed_ShouldReturnFalse_WhenLastRequestIsBelowLimitWithMultipleRequestsInLogs()
{
// Arrange
var rule = new TimeSpanSinceLastRequestRule(TimeSpan.FromSeconds(10));
var requestLogs = new List<DateTime>
{
DateTime.UtcNow.AddSeconds(-20),
DateTime.UtcNow.AddSeconds(-15),
DateTime.UtcNow.AddSeconds(-5)
};
var timeOfRequest = DateTime.UtcNow;

// Act
bool result = rule.IsRequestAllowed(requestLogs, timeOfRequest);

// Assert
Assert.False(result);
}
}
97 changes: 97 additions & 0 deletions RateLimiter.Tests/Services/RateLimiterServiceTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
using System;
using System.Collections.Generic;
using Xunit;
using RateLimiter.Rules;
using RateLimiter.Services;

namespace RateLimiter.Tests.Services;

public class RateLimiterServiceTests
{
[Fact]
public void IsRequestAllowed_ShouldReturnTrue_WhenNoRulesExist()
{
// Arrange
var rules = new Dictionary<string, List<IRateLimitRule>>();
var service = new RateLimiterService(rules);

// Act
bool result = service.IsRequestAllowed("/test/endpoint", "client-token");

// Assert
Assert.True(result);
}

[Fact]
public void IsRequestAllowed_ShouldReturnTrue_WhenNoRulesExistForEndpoint()
{
// Arrange
var rules = new Dictionary<string, List<IRateLimitRule>>
{
{
"/test/endpoint",
new List<IRateLimitRule>
{
new RequestPerTimeSpanRule(1, TimeSpan.FromMinutes(1))
}
}
};
var service = new RateLimiterService(rules);

// Act
bool result = service.IsRequestAllowed("/test/endpoint-one", "client-token");

// Assert
Assert.True(result);
}

[Fact]
public void IsRequestAllowed_ShouldReturnFalse_WhenRuleBlocksRequest()
{
// Arrange
var rules = new Dictionary<string, List<IRateLimitRule>>
{
{
"/test/endpoint",
new List<IRateLimitRule>
{
new RequestPerTimeSpanRule(1, TimeSpan.FromMinutes(1))
}
}
};
var service = new RateLimiterService(rules);

// Act
bool firstRequest = service.IsRequestAllowed("/test/endpoint", "client-token");
bool secondRequest = service.IsRequestAllowed("/test/endpoint", "client-token");

// Assert
Assert.True(firstRequest);
Assert.False(secondRequest);
}

[Fact]
public void IsRequestAllowed_ShouldReturnTrue_WhenRequestsAreFromDifferentClients()
{
// Arrange
var rules = new Dictionary<string, List<IRateLimitRule>>
{
{
"/test/endpoint",
new List<IRateLimitRule>
{
new RequestPerTimeSpanRule(1, TimeSpan.FromMinutes(1))
}
}
};
var service = new RateLimiterService(rules);

// Act
bool token1Request = service.IsRequestAllowed("/test/endpoint", "client-token-1");
bool token2Request = service.IsRequestAllowed("/test/endpoint", "client-token-2");

// Assert
Assert.True(token1Request);
Assert.True(token2Request);
}
}
Loading