Skip to content
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
42 changes: 42 additions & 0 deletions RateLimiter.Tests/Controllers/RateLimitTestControllerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using NUnit.Framework;
using RateLimiter.Controllers;
using Microsoft.AspNetCore.Mvc;

namespace RateLimiter.Tests.Controllers
{
[TestFixture]
public class RateLimitTestControllerTests
{
private RateLimitTestController _controller;

[SetUp]
public void Setup()
{
_controller = new RateLimitTestController();
}

[Test]
public void GetResource1_ReturnsOk()
{
// Act
var result = _controller.GetResource1() as OkObjectResult;

// Assert
Assert.NotNull(result);
Assert.AreEqual(200, result.StatusCode);
Assert.IsInstanceOf<OkObjectResult>(result);
}

[Test]
public void GetResource2_ReturnsOk()
{
// Act
var result = _controller.GetResource2() as OkObjectResult;

// Assert
Assert.NotNull(result);
Assert.AreEqual(200, result.StatusCode);
Assert.IsInstanceOf<OkObjectResult>(result);
}
}
}
125 changes: 125 additions & 0 deletions RateLimiter.Tests/Middleware/RateLimiterMiddlewareTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
using Microsoft.AspNetCore.Http;
using NUnit.Framework;
using RateLimiter.Interfaces;
using RateLimiter.Models;
using RateLimiter.Services;
using System.Collections.Generic;
using System.Threading.Tasks;
using System;

namespace RateLimiter.Tests.Middleware
{
public class RateLimiterMiddlewareTests
{
private RateLimiterMiddleware _middleware;
private RateLimiterManager _manager;
private DefaultHttpContext _context;

private const string TEST_RESOURCE = "/api/test/resource";
private const string CLIENT_A = "ClientA";
private const string HEADER_CLIENT_ID = "X-Client-Id";
private const string CONTENT_TYPE_JSON = "application/json";
private const string SWAGGER_PATH = "/swagger/index.html";
private const string HEALTH_CHECK_PATH = "/health";

private const int HTTP_STATUS_OK = 200;
private const int HTTP_STATUS_BAD_REQUEST = 400;
private const int HTTP_STATUS_TOO_MANY_REQUESTS = 429;

[SetUp]
public void Setup()
{
// Initialize with empty config list - specific tests will create their own configs
_manager = new RateLimiterManager(new List<ClientRateLimitConfig>());
_middleware = new RateLimiterMiddleware(
next: (context) => Task.CompletedTask,
_manager
);
_context = new DefaultHttpContext();
}

[Test]
public async Task GivenSwaggerRequest_WhenInvoked_SkipsRateLimiting()
{
_context.Request.Path = SWAGGER_PATH;

await _middleware.Invoke(_context);

Assert.That(_context.Response.StatusCode, Is.EqualTo(HTTP_STATUS_OK));
}

[Test]
public async Task GivenHealthCheckRequest_WhenInvoked_SkipsRateLimiting()
{
_context.Request.Path = HEALTH_CHECK_PATH;

await _middleware.Invoke(_context);

Assert.That(_context.Response.StatusCode, Is.EqualTo(HTTP_STATUS_OK));
}

[Test]
public async Task GivenMissingClientId_WhenInvoked_ReturnsBadRequest()
{
_context.Request.Path = TEST_RESOURCE;

await _middleware.Invoke(_context);

Assert.Multiple(() =>
{
Assert.That(_context.Response.StatusCode, Is.EqualTo(HTTP_STATUS_BAD_REQUEST));
Assert.That(_context.Response.ContentType, Is.EqualTo(CONTENT_TYPE_JSON));
});
}

[Test]
public async Task GivenRateLimitExceeded_WhenInvoked_ReturnsTooManyRequests()
{
// Create a new manager instance with the rate limit configuration
var config = new ClientRateLimitConfig
{
ClientId = CLIENT_A,
ResourceLimits = new List<ResourceRateLimitConfig>
{
new ResourceRateLimitConfig
{
Resource = TEST_RESOURCE,
Rules = new List<IRateLimitRule>
{
new FixedWindowRateLimit(1, TimeSpan.FromSeconds(5))
}
}
}
};

// Create new manager with the config
_manager = new RateLimiterManager(new List<ClientRateLimitConfig> { config });

// Create new middleware with the updated manager
_middleware = new RateLimiterMiddleware(
next: (context) => Task.CompletedTask,
_manager
);

_context.Request.Path = TEST_RESOURCE;
_context.Request.Headers[HEADER_CLIENT_ID] = CLIENT_A;

// First request should succeed
await _middleware.Invoke(_context);
Assert.That(_context.Response.StatusCode, Is.EqualTo(HTTP_STATUS_OK));

// Reset context for second request
_context = new DefaultHttpContext();
_context.Request.Path = TEST_RESOURCE;
_context.Request.Headers[HEADER_CLIENT_ID] = CLIENT_A;

// Second request should fail
await _middleware.Invoke(_context);
Assert.Multiple(() =>
{
Assert.That(_context.Response.StatusCode, Is.EqualTo(HTTP_STATUS_TOO_MANY_REQUESTS));
Assert.That(_context.Response.Headers.ContainsKey("Retry-After"), Is.True);
});
}
}
}
13 changes: 0 additions & 13 deletions RateLimiter.Tests/RateLimiterTest.cs

This file was deleted.

109 changes: 109 additions & 0 deletions RateLimiter.Tests/Services/RateLimiterManagerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
using NUnit.Framework;
using RateLimiter.Interfaces;
using RateLimiter.Models;
using RateLimiter.Services.Rules;
using RateLimiter.Services;
using System.Collections.Generic;
using System;

namespace RateLimiter.Tests.Services
{
public class RateLimiterManagerTests
{
private const string TEST_RESOURCE = "/api/test/resource";
private const string DIFFERENT_RESOURCE = "/different/resource";
private const string CLIENT_ID = "TestClient";
private const string UNKNOWN_CLIENT = "UnknownClient";

private const int HTTP_STATUS_OK = 200;
private const int RATE_LIMIT_COUNT = 1;
private static readonly TimeSpan RATE_LIMIT_DURATION = TimeSpan.FromSeconds(5);

[Test]
public void GivenUnknownClient_WhenRequestMade_ReturnsFalse()
{
var manager = new RateLimiterManager(new List<ClientRateLimitConfig>());
var result = manager.IsRequestAllowed(UNKNOWN_CLIENT, TEST_RESOURCE);
Assert.That(result.IsAllowed, Is.False);
}

[Test]
public void GivenUnconfiguredResource_WhenRequestMade_ReturnsFalse()
{
var config = CreateTestConfig(CLIENT_ID, TEST_RESOURCE);
var manager = new RateLimiterManager(new List<ClientRateLimitConfig> { config });

var result = manager.IsRequestAllowed(CLIENT_ID, DIFFERENT_RESOURCE);
Assert.That(result.IsAllowed, Is.False);
}

[Test]
public void GivenValidConfiguration_WhenRequestMade_AllowsRequests()
{
var config = CreateTestConfig(CLIENT_ID, TEST_RESOURCE);
var manager = new RateLimiterManager(new List<ClientRateLimitConfig> { config });

var result = manager.IsRequestAllowed(CLIENT_ID, TEST_RESOURCE);
Assert.That(result.IsAllowed, Is.True);
}

[Test]
public void GivenDuplicateClientConfig_WhenCreated_ThrowsException()
{
var config = CreateTestConfig(CLIENT_ID, TEST_RESOURCE);
var duplicateConfig = CreateTestConfig(CLIENT_ID, TEST_RESOURCE);

Assert.Throws<ArgumentException>(() =>
new RateLimiterManager(new List<ClientRateLimitConfig> { config, duplicateConfig }));
}

[Test]
public void GivenMultipleRules_WhenOneFails_RequestIsDenied()
{
var config = new ClientRateLimitConfig
{
ClientId = CLIENT_ID,
ResourceLimits = new List<ResourceRateLimitConfig>
{
new ResourceRateLimitConfig
{
Resource = TEST_RESOURCE,
Rules = new List<IRateLimitRule>
{
new FixedWindowRateLimit(RATE_LIMIT_COUNT, RATE_LIMIT_DURATION),
new SlidingWindowRateLimit(RATE_LIMIT_COUNT, RATE_LIMIT_DURATION)
}
}
}
};

var manager = new RateLimiterManager(new List<ClientRateLimitConfig> { config });

// First request should pass both rules
Assert.That(manager.IsRequestAllowed(CLIENT_ID, TEST_RESOURCE).IsAllowed, Is.True);

// Second request should fail both rules
var result = manager.IsRequestAllowed(CLIENT_ID, TEST_RESOURCE);
Assert.That(result.IsAllowed, Is.False);
}

private static ClientRateLimitConfig CreateTestConfig(string clientId, string resource)
{
return new ClientRateLimitConfig
{
ClientId = clientId,
ResourceLimits = new List<ResourceRateLimitConfig>
{
new ResourceRateLimitConfig
{
Resource = resource,
Rules = new List<IRateLimitRule>
{
new FixedWindowRateLimit(RATE_LIMIT_COUNT, RATE_LIMIT_DURATION)
}
}
}
};
}
}
}
71 changes: 71 additions & 0 deletions RateLimiter.Tests/Services/Rules/FixedWindowRateLimitTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using NUnit.Framework;
using System;
using System.Threading;

namespace RateLimiter.Tests.Services.Rules
{
public class FixedWindowRateLimitTests
{
private FixedWindowRateLimit _rateLimit;
private const string CLIENT_ID = "TestClient";

[SetUp]
public void Setup()
{
_rateLimit = new FixedWindowRateLimit(3, TimeSpan.FromSeconds(5));
}

[Test]
public void WhenFirstRequest_IsAllowed()
{
var result = _rateLimit.IsRequestAllowed(CLIENT_ID);
Assert.That(result.IsAllowed, Is.True);
}

[Test]
public void WhenUnderLimit_AllRequestsAllowed()
{
Assert.Multiple(() =>
{
Assert.That(_rateLimit.IsRequestAllowed(CLIENT_ID).IsAllowed, Is.True);
Assert.That(_rateLimit.IsRequestAllowed(CLIENT_ID).IsAllowed, Is.True);
Assert.That(_rateLimit.IsRequestAllowed(CLIENT_ID).IsAllowed, Is.True);
});
}

[Test]
public void WhenOverLimit_RequestsBlocked()
{
// Use up the limit
for (int i = 0; i < 3; i++)
{
_rateLimit.IsRequestAllowed(CLIENT_ID);
}

var result = _rateLimit.IsRequestAllowed(CLIENT_ID);
Assert.Multiple(() =>
{
Assert.That(result.IsAllowed, Is.False);
Assert.That(result.RetryAfter, Is.GreaterThan(TimeSpan.Zero));
});
}

[Test]
public void WhenWindowExpires_AllowsNewRequests()
{
// Use up the limit
for (int i = 0; i < 3; i++)
{
_rateLimit.IsRequestAllowed(CLIENT_ID);
}

// Wait for window to expire
Thread.Sleep(5100);

var result = _rateLimit.IsRequestAllowed(CLIENT_ID);
Assert.That(result.IsAllowed, Is.True);
}


}
}
Loading