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
84 changes: 84 additions & 0 deletions RateLimiter.Tests/CacheHelperTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
using NUnit.Framework;
using RateLimiter;
using System;
using System.Collections.Generic;

namespace RateLimiter.Tests
{
[TestFixture]
public class CacheHelperTests
{
private CacheHelper _cacheHelper;

[SetUp]
public void SetUp()
{
_cacheHelper = new CacheHelper();
}

[Test]
public void LastRequestTime_ShouldReturnMaxValue_WhenNoRequests()
{
// Act
var result = _cacheHelper.LastRequestTime("test-key");

// Assert
Assert.AreEqual(TimeSpan.MaxValue, result);
}

[Test]
public void LastRequestTime_ShouldReturnTimeSinceLastRequest_WhenRequestsExist()
{
// Arrange
var key = "test-key";
_cacheHelper.AddRequest(key);
System.Threading.Thread.Sleep(1000); // Wait for 1 second

// Act
var result = _cacheHelper.LastRequestTime(key);

// Assert
Assert.IsTrue(result.TotalMilliseconds >= 1000);
}

[Test]
public void RequestsCount_ShouldReturnZero_WhenNoRequests()
{
// Act
var result = _cacheHelper.RequestsCount("test-key", TimeSpan.FromSeconds(1));

// Assert
Assert.AreEqual(0, result);
}

[Test]
public void RequestsCount_ShouldReturnCorrectCount_WhenRequestsExist()
{
// Arrange
var key = "test-key";
_cacheHelper.AddRequest(key);
_cacheHelper.AddRequest(key);
System.Threading.Thread.Sleep(1000); // Wait for 1 second

// Act
var result = _cacheHelper.RequestsCount(key, TimeSpan.FromSeconds(2));

// Assert
Assert.AreEqual(2, result);
}

[Test]
public void AddRequest_ShouldAddRequestToCache()
{
// Arrange
var key = "test-key";

// Act
_cacheHelper.AddRequest(key);
var result = _cacheHelper.RequestsCount(key, TimeSpan.FromSeconds(1));

// Assert
Assert.AreEqual(1, result);
}
}
}
2 changes: 2 additions & 0 deletions RateLimiter.Tests/RateLimiter.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
<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" />
</ItemGroup>
Expand Down
77 changes: 66 additions & 11 deletions RateLimiter.Tests/RateLimiterTest.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,68 @@
using NUnit.Framework;
using Microsoft.AspNetCore.Http;
using Moq;
using NUnit.Framework;
using System.Threading.Tasks;

namespace RateLimiter.Tests;

[TestFixture]
public class RateLimiterTest
namespace RateLimiter.Tests
{
[Test]
public void Example()
{
Assert.That(true, Is.True);
}
}
[TestFixture]
public class RequestLimiterTests
{
private Mock<RequestDelegate> _mockNext;
private Mock<IRulesFactory> _mockRulesFactory;
private Mock<IContextHelper> _mockContextHelper;
private Mock<ICacheHelper> _mockCacheHelper;
private RequestLimiter _requestLimiter;
private DefaultHttpContext _httpContext;

[SetUp]
public void SetUp()
{
_mockNext = new Mock<RequestDelegate>();
_mockRulesFactory = new Mock<IRulesFactory>();
_mockContextHelper = new Mock<IContextHelper>();
_mockContextHelper.Setup(ch => ch.GetDto(It.IsAny<HttpContext>())).Returns(new ContextDto { Id = "test-id", Path = "", Region = "US" });
_mockCacheHelper = new Mock<ICacheHelper>();
_requestLimiter = new RequestLimiter(_mockNext.Object, _mockRulesFactory.Object, _mockContextHelper.Object, _mockCacheHelper.Object);
_httpContext = new DefaultHttpContext();
}

[Test]
public async Task InvokeAsync_ShouldCallNextDelegate_WhenRequestIsAllowed()
{
// Arrange
var contextDto = new ContextDto { Id = "test-id" };
var mockRule = new Mock<IRule>();
mockRule.Setup(r => r.CheckLimit()).Returns(true);
_mockContextHelper.Setup(ch => ch.GetDto(_httpContext)).Returns(contextDto);
_mockRulesFactory.Setup(rf => rf.GetRule(contextDto)).Returns(mockRule.Object);

// Act
await _requestLimiter.InvokeAsync(_httpContext);

// Assert
_mockNext.Verify(next => next(_httpContext), Times.Once);
_mockCacheHelper.Verify(ch => ch.AddRequest(contextDto.Id), Times.Once);
}

[Test]
public async Task InvokeAsync_ShouldReturnTooManyRequests_WhenRequestIsNotAllowed()
{
// Arrange
var contextDto = new ContextDto { Id = "test-id" };
var mockRule = new Mock<IRule>();
mockRule.Setup(r => r.CheckLimit()).Returns(false);
_mockContextHelper.Setup(ch => ch.GetDto(_httpContext)).Returns(contextDto);
_mockRulesFactory.Setup(rf => rf.GetRule(contextDto)).Returns(mockRule.Object);

// Act
await _requestLimiter.InvokeAsync(_httpContext);

// Assert
Assert.AreEqual(StatusCodes.Status429TooManyRequests, _httpContext.Response.StatusCode);
_mockNext.Verify(next => next(_httpContext), Times.Never);
_mockCacheHelper.Verify(ch => ch.AddRequest(It.IsAny<string>()), Times.Never);
}
}
}

75 changes: 75 additions & 0 deletions RateLimiter.Tests/RulesFactoryTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
using NUnit.Framework;
using Moq;
using RateLimiter;
using RateLimiter.Rules;
using System.Collections.Generic;

namespace RateLimiter.Tests
{
[TestFixture]
public class RulesFactoryTests
{
private Mock<IConfigLoader> _mockConfigLoader;
private Mock<ICacheHelper> _mockCacheHelper;
private RulesFactory _rulesFactory;

[SetUp]
public void SetUp()
{
_mockConfigLoader = new Mock<IConfigLoader>();
_mockCacheHelper = new Mock<ICacheHelper>();
_rulesFactory = new RulesFactory(_mockConfigLoader.Object, _mockCacheHelper.Object);
}

[Test]
public void GetRule_ShouldReturnEmptyRule_WhenNoRulesAreLoaded()
{
// Arrange
var context = new ContextDto {Id = "test-id", Path = "", Region = "US" };
_mockConfigLoader.Setup(cl => cl.LoadRules(context)).Returns(new List<RuleDefinition>());

// Act
var result = _rulesFactory.GetRule(context);

// Assert
Assert.IsInstanceOf<EmptyRule>(result);
}

[Test]
public void GetRule_ShouldReturnRequestsLimitRule_WhenRequestsLimitRuleIsLoaded()
{
// Arrange
var context = new ContextDto {Id = "test-id", Path = "", Region = "US" };
var rules = new List<RuleDefinition>
{
new RuleDefinition { Name = "RequestsLimit", Variables = new Dictionary<string, string>() { { "time", "2000" }, { "requests", "40" } } }
};
_mockConfigLoader.Setup(cl => cl.LoadRules(context)).Returns(rules);

// Act
var result = _rulesFactory.GetRule(context);

// Assert
Assert.IsInstanceOf<RequestsLimitRule>(result);
}

[Test]
public void GetRule_ShouldReturnTimeLimitRule_WhenTimeLimitRuleIsLoaded()
{
// Arrange
var context = new ContextDto { Id = "test-id", Path = "", Region = "US" };
var rules = new List<RuleDefinition>
{
new RuleDefinition { Name = "TimeLimit", Variables = new Dictionary<string, string>() { { "time", "2000" } } }
};
_mockConfigLoader.Setup(cl => cl.LoadRules(context)).Returns(rules);

// Act
var result = _rulesFactory.GetRule(context);

// Assert
Assert.IsInstanceOf<TimeLimitRule>(result);
}

}
}
43 changes: 43 additions & 0 deletions RateLimiter/CacheHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace RateLimiter
{

public class CacheHelper : ICacheHelper
{
private readonly Dictionary<string, List<DateTime>> _cache = new();
public int MaxTimes { get; set; } = 60000; // Default: 60 seconds

public TimeSpan LastRequestTime(string key)
{
if (_cache.TryGetValue(key, out var timestamps) && timestamps.Count > 0)
{
return DateTime.UtcNow - timestamps[^1];
}
return TimeSpan.MaxValue;
}

public int RequestsCount(string key, TimeSpan time)
{
if (_cache.TryGetValue(key, out var timestamps))
{
var threshold = DateTime.UtcNow - time;
return timestamps.RemoveAll(t => t > threshold);
}
return 0;
}

public void AddRequest(string key)
{
if (!_cache.ContainsKey(key))
{
_cache[key] = new List<DateTime>();
}
_cache[key].Add(DateTime.UtcNow);
}
}
}
15 changes: 15 additions & 0 deletions RateLimiter/ContextDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace RateLimiter
{
public class ContextDto
{
public string Path { get; set; }
public string Region { get; set; }
public string Id { get; set; }
}
}
16 changes: 16 additions & 0 deletions RateLimiter/ICacheHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace RateLimiter
{
public interface ICacheHelper
{
int MaxTimes { get; set; }
TimeSpan LastRequestTime(string key);
int RequestsCount(string key, TimeSpan time);
void AddRequest(string key);
}
}
37 changes: 37 additions & 0 deletions RateLimiter/IConfigLoader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using Microsoft.Extensions.Configuration;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace RateLimiter
{
public interface IConfigLoader
{
IEnumerable<RuleDefinition> LoadRules(ContextDto contextDto);
}

internal class ConfigLoader : IConfigLoader
{
public IEnumerable<RuleDefinition> LoadRules(ContextDto contextDto)
{
var json = File.ReadAllText("config.json");//hardcoded path and another types of hardcode are bad in real project, but it can be another way to configure rules
var section = JsonConvert.DeserializeObject<Dictionary<string, Dictionary<string, List<RuleDefinition>>>>(json) ?? new Dictionary<string, Dictionary<string, List<RuleDefinition>>>();
var result = section.Where(x => x.Key == contextDto.Path)
.SelectMany(x => x.Value)
.Where(x => x.Key == contextDto.Region)
.SelectMany(x => x.Value)
.Where(x => x.Variables.All(v => contextDto.Id.Contains(v.Value)))
.ToList();

return result;

}
}


}
14 changes: 14 additions & 0 deletions RateLimiter/IContextHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using Microsoft.AspNetCore.Http;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace RateLimiter
{
public interface IContextHelper
{
ContextDto GetDto(HttpContext context);
}
}
14 changes: 14 additions & 0 deletions RateLimiter/IRequestLimiter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace RateLimiter
{
internal interface IRequestLimiter
{
//IHttpContextAccessor
//bool CheckLimit(AccsessToken accsessToken);
}
}
Loading