Skip to content

Commit

Permalink
Implement basic functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
Zyertdox committed Feb 10, 2025
1 parent d0a5741 commit 01df551
Show file tree
Hide file tree
Showing 19 changed files with 349 additions and 6 deletions.
13 changes: 13 additions & 0 deletions .idea/.idea.RateLimiter/.idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions .idea/.idea.RateLimiter/.idea/.name

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions .idea/.idea.RateLimiter/.idea/encodings.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions .idea/.idea.RateLimiter/.idea/indexLayout.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

58 changes: 58 additions & 0 deletions RateLimiter.Tests/DelayRateLimiterRuleTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using System;
using NUnit.Framework;
using RateLimiter.Exceptions;
using RateLimiter.Implementations;

namespace RateLimiter.Tests;

[TestFixture]
public class DelayRateLimiterRuleTests
{
[Test]
public void Validate_EnoughTime_Ok()
{
// Arrange
const int delay = 10;
var token = Guid.NewGuid().ToString("N");
var previousRequests = Moq.RequestsDates(delay + delay / 2, delay * 2);
var limiter = new DelayRateLimiterRule(TimeSpan.FromSeconds(delay));
// Act, Assert
Assert.DoesNotThrow(() => limiter.Validate(token, previousRequests));
}

[Test]
public void Validate_NotEnoughTime_ThrowsException()
{
// Arrange
const int delay = 10;
var token = Guid.NewGuid().ToString("N");
var previousRequests = Moq.RequestsDates(delay / 2);
var limiter = new DelayRateLimiterRule(TimeSpan.FromSeconds(delay));
// Act, Assert
Assert.Throws<RateLimitException>(() => limiter.Validate(token, previousRequests));
}

[Test]
public void Validate_NotEnoughTimeWithSelector_ThrowsException()
{
// Arrange
const int delay = 10;
var token = "US-" + Guid.NewGuid().ToString("N");
var previousRequests = Moq.RequestsDates(delay / 2);
var limiter = new DelayRateLimiterRule(TimeSpan.FromSeconds(delay), Moq.PrefixSelector("US-"));
// Act, Assert
Assert.Throws<RateLimitException>(() => limiter.Validate(token, previousRequests));
}

[Test]
public void Validate_NotApplicable_Ok()
{
// Arrange
const int delay = 10;
var token = "EU-" + Guid.NewGuid().ToString("N");
var previousRequests = Moq.RequestsDates(delay / 2);
var limiter = new DelayRateLimiterRule(TimeSpan.FromSeconds(delay), Moq.PrefixSelector("US-"));
// Act, Assert
Assert.DoesNotThrow(() => limiter.Validate(token, previousRequests));
}
}
35 changes: 35 additions & 0 deletions RateLimiter.Tests/Moq.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Moq;
using RateLimiter.Abstractions;
using RateLimiter.Exceptions;
using RateLimiter.Infrastructure;

namespace RateLimiter.Tests;

public static class Moq
{
public static IRateLimiterRule Rule(bool isSuccess)
{
var mock = new Mock<IRateLimiterRule>();
if (!isSuccess)
{
mock.Setup(x => x.Validate(It.IsAny<string>(), It.IsAny<IReadOnlyCollection<DateTime>>()))
.Throws(new RateLimitException(string.Empty, string.Empty));
}
return mock.Object;
}

public static Func<string, bool> PrefixSelector(string prefix) => x => x.StartsWith(prefix);

public static IRequestsRepository RepositoryFromDelays(params int[] delays)
{
var mock = new Mock<IRequestsRepository>();
mock.Setup(x => x.GetPreviousRequests(It.IsAny<string>()))
.Returns(RequestsDates(delays));
return mock.Object;
}

public static List<DateTime> RequestsDates(params int[] delays) => delays.Select(x => DateTime.UtcNow.AddSeconds(-x)).ToList();
}
1 change: 1 addition & 0 deletions RateLimiter.Tests/RateLimiter.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
</ItemGroup>
<ItemGroup>
<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
34 changes: 28 additions & 6 deletions RateLimiter.Tests/RateLimiterTest.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,35 @@
using NUnit.Framework;
using System;
using Microsoft.Extensions.Logging;
using Moq;
using NUnit.Framework;
using RateLimiter.Exceptions;

namespace RateLimiter.Tests;

[TestFixture]
public class RateLimiterTest
{
[Test]
public void Example()
{
Assert.That(true, Is.True);
}
[Test]
public void LimitRequestsForToken_Ok()
{
// Arrange
var token = Guid.NewGuid().ToString("N");
var limiter = new Implementations.RateLimiter([Moq.Rule(true)],
Moq.RepositoryFromDelays(),
Mock.Of<ILogger<Implementations.RateLimiter>>());
// Act, Assert
Assert.DoesNotThrow(() => limiter.LimitRequestsForToken(token));
}

[Test]
public void LimitRequestsForToken_Fails()
{
// Arrange
var token = Guid.NewGuid().ToString("N");
var limiter = new Implementations.RateLimiter([Moq.Rule(false)],
Moq.RepositoryFromDelays(),
Mock.Of<ILogger<Implementations.RateLimiter>>());
// Act, Assert
Assert.Throws<RateLimitException>(() => limiter.LimitRequestsForToken(token));
}
}
62 changes: 62 additions & 0 deletions RateLimiter.Tests/TimeWindowRateLimiterRuleTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
using System;
using NUnit.Framework;
using RateLimiter.Exceptions;
using RateLimiter.Implementations;

namespace RateLimiter.Tests;

[TestFixture]
public class TimeWindowRateLimiterRuleTests
{
[Test]
public void Validate_EnoughTime_Ok()
{
// Arrange
const int delay = 10;
const int maxRequests = 3;
var token = Guid.NewGuid().ToString("N");
var previousRequests = Moq.RequestsDates(delay / 2, delay / 2, delay * 2);
var limiter = new TimeWindowRateLimiterRule(TimeSpan.FromSeconds(delay), maxRequests);
// Act, Assert
Assert.DoesNotThrow(() => limiter.Validate(token, previousRequests));
}

[Test]
public void Validate_NotEnoughTime_ThrowsException()
{
// Arrange
const int delay = 10;
const int maxRequests = 3;
var token = Guid.NewGuid().ToString("N");
var previousRequests = Moq.RequestsDates(delay / 2, delay / 2, delay / 2);
var limiter = new TimeWindowRateLimiterRule(TimeSpan.FromSeconds(delay), maxRequests);
// Act, Assert
Assert.Throws<RateLimitException>(() => limiter.Validate(token, previousRequests));
}

[Test]
public void Validate_NotEnoughTimeWithSelector_ThrowsException()
{
// Arrange
const int delay = 10;
const int maxRequests = 3;
var token = "US-" + Guid.NewGuid().ToString("N");
var previousRequests = Moq.RequestsDates(delay / 2, delay / 2, delay / 2);
var limiter = new TimeWindowRateLimiterRule(TimeSpan.FromSeconds(delay), maxRequests, Moq.PrefixSelector("US-"));
// Act, Assert
Assert.Throws<RateLimitException>(() => limiter.Validate(token, previousRequests));
}

[Test]
public void Validate_NotApplicable_Ok()
{
// Arrange
const int delay = 10;
const int maxRequests = 3;
var token = "EU-" + Guid.NewGuid().ToString("N");
var previousRequests = Moq.RequestsDates(delay / 2, delay / 2, delay / 2);
var limiter = new TimeWindowRateLimiterRule(TimeSpan.FromSeconds(delay), maxRequests, Moq.PrefixSelector("US-"));
// Act, Assert
Assert.DoesNotThrow(() => limiter.Validate(token, previousRequests));
}
}
8 changes: 8 additions & 0 deletions RateLimiter.sln.DotSettings.user
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=623d5d3a_002D67d2_002D4d1c_002Db3d7_002Dcabda30b9e25/@EntryIndexedValue">&lt;SessionState ContinuousTestingMode="0" IsActive="True" Name="DelayRateLimiterRuleTests" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"&gt;&#xD;
&lt;TestAncestor&gt;&#xD;
&lt;TestId&gt;NUnit3x::C4F9249B-010E-46BE-94B8-DD20D82F1E60::net6.0::RateLimiter.Tests.DelayRateLimiterRuleTests&lt;/TestId&gt;&#xD;
&lt;TestId&gt;NUnit3x::C4F9249B-010E-46BE-94B8-DD20D82F1E60::net6.0::RateLimiter.Tests.TimeWindowRateLimiterRuleTests&lt;/TestId&gt;&#xD;
&lt;TestId&gt;NUnit3x::C4F9249B-010E-46BE-94B8-DD20D82F1E60::net6.0::RateLimiter.Tests.RateLimiterTest&lt;/TestId&gt;&#xD;
&lt;/TestAncestor&gt;&#xD;
&lt;/SessionState&gt;</s:String></wpf:ResourceDictionary>
6 changes: 6 additions & 0 deletions RateLimiter/Abstractions/IRateLimiter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace RateLimiter.Abstractions;

public interface IRateLimiter
{
public void LimitRequestsForToken(string token);
}
9 changes: 9 additions & 0 deletions RateLimiter/Abstractions/IRateLimiterRule.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System;
using System.Collections.Generic;

namespace RateLimiter.Abstractions;

public interface IRateLimiterRule
{
void Validate(string token, IReadOnlyCollection<DateTime> previousRequests);
}
8 changes: 8 additions & 0 deletions RateLimiter/Exceptions/RateLimitException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using System;

namespace RateLimiter.Exceptions;

public class RateLimitException(string message, string ruleType) : Exception(message)
{
public string RuleType => ruleType;
}
17 changes: 17 additions & 0 deletions RateLimiter/Implementations/DelayRateLimiterRule.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
using RateLimiter.Exceptions;

namespace RateLimiter.Implementations;

public class DelayRateLimiterRule(TimeSpan timeSpan, Func<string, bool>? selector = null) : SpecificRateLimiterRule(selector)
{
protected override void ValidateIfRequired(string token, IReadOnlyCollection<DateTime> previousRequests)
{
if (previousRequests.Any(x => x + timeSpan >= DateTime.UtcNow))
{
throw new RateLimitException("Not enough Time since last request", nameof(DelayRateLimiterRule));
}
}
}
39 changes: 39 additions & 0 deletions RateLimiter/Implementations/RateLimiter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System.Collections.Generic;
using Microsoft.Extensions.Logging;
using RateLimiter.Abstractions;
using RateLimiter.Exceptions;
using RateLimiter.Infrastructure;

namespace RateLimiter.Implementations;

public class RateLimiter : IRateLimiter
{
private readonly ILogger<RateLimiter> _logger;
private readonly IRequestsRepository _requestsRepository;
private readonly IEnumerable<IRateLimiterRule> _rules;

public RateLimiter(IEnumerable<IRateLimiterRule> rules, IRequestsRepository requestsRepository, ILogger<RateLimiter> logger)
{
_rules = rules;
_requestsRepository = requestsRepository;
_logger = logger;
}

public void LimitRequestsForToken(string token)
{
var previousRequests = _requestsRepository.GetPreviousRequests(token);

try
{
foreach (var rule in _rules)
{
rule.Validate(token, previousRequests);
}
}
catch (RateLimitException ex)
{
_logger.LogInformation("Too many Attempts for Token: {Token}; Rule: {Rule}.", token, ex.RuleType);
throw;
}
}
}
22 changes: 22 additions & 0 deletions RateLimiter/Implementations/SpecificRateLimiterRule.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using RateLimiter.Abstractions;

namespace RateLimiter.Implementations;

public abstract class SpecificRateLimiterRule(Func<string, bool>? selector = null) : IRateLimiterRule
{
private readonly Func<string, bool> _appliedRule = selector ?? (_ => true);

public void Validate(string token, IReadOnlyCollection<DateTime> previousRequests)
{
if (!_appliedRule(token))
{
return;
}

ValidateIfRequired(token, previousRequests);
}

protected abstract void ValidateIfRequired(string token, IReadOnlyCollection<DateTime> previousRequests);
}
17 changes: 17 additions & 0 deletions RateLimiter/Implementations/TimeWindowRateLimiterRule.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
using RateLimiter.Exceptions;

namespace RateLimiter.Implementations;

public class TimeWindowRateLimiterRule(TimeSpan timeSpan, int maxRequests, Func<string, bool>? selector = null) : SpecificRateLimiterRule(selector)
{
protected override void ValidateIfRequired(string token, IReadOnlyCollection<DateTime> previousRequests)
{
if (previousRequests.Count(x => DateTime.UtcNow - x <= timeSpan) >= maxRequests)
{
throw new RateLimitException("Expected less requests", nameof(TimeWindowRateLimiterRule));
}
}
}
10 changes: 10 additions & 0 deletions RateLimiter/Infrastructure/IRequestsRepository.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System;
using System.Collections.Generic;

namespace RateLimiter.Infrastructure;

public interface IRequestsRepository
{
public IReadOnlyCollection<DateTime> GetPreviousRequests(string token);
public void AddRequest(string token, DateTime date);
}
Loading

0 comments on commit 01df551

Please sign in to comment.