-
Notifications
You must be signed in to change notification settings - Fork 232
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
73f3a7c
commit 6d3240f
Showing
14 changed files
with
572 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,163 @@ | ||
using NUnit.Framework; | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Text; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
|
||
namespace RateLimiter.Tests | ||
{ | ||
[TestFixture] | ||
public class RateLimitRuleTests | ||
{ | ||
[Test] | ||
public void RequestCountPerTimespanRule_AllowsRequestsWithinLimit() | ||
{ | ||
var rule = new RequestCountPerTimespanRule(5, TimeSpan.FromMinutes(1)); | ||
|
||
for (int i = 0; i < 5; i++) | ||
{ | ||
Assert.IsTrue(rule.AllowRequest("client1")); | ||
} | ||
|
||
Assert.IsFalse(rule.AllowRequest("client1")); | ||
} | ||
|
||
[Test] | ||
public void CooldownBetweenRequestsRule_AllowsRequestsAfterCooldown() | ||
{ | ||
var rule = new CooldownBetweenRequestsRule(TimeSpan.FromSeconds(1)); | ||
|
||
Assert.IsTrue(rule.AllowRequest("client1")); | ||
Assert.IsFalse(rule.AllowRequest("client1")); | ||
|
||
Thread.Sleep(1000); | ||
|
||
Assert.IsTrue(rule.AllowRequest("client1")); | ||
} | ||
|
||
[Test] | ||
public void GeoBasedRateLimitRule_UsesCorrectRuleBasedOnRegion() | ||
{ | ||
var usRule = new RequestCountPerTimespanRule(5, TimeSpan.FromMinutes(1)); | ||
var euRule = new CooldownBetweenRequestsRule(TimeSpan.FromSeconds(1)); | ||
var geoRule = new GeoBasedRateLimitRule(usRule, euRule, GetClientRegion); | ||
|
||
for (int i = 0; i < 5; i++) | ||
{ | ||
Assert.IsTrue(geoRule.AllowRequest("us_client")); | ||
} | ||
|
||
Assert.IsFalse(geoRule.AllowRequest("us_client")); | ||
|
||
Assert.IsTrue(geoRule.AllowRequest("eu_client")); | ||
Assert.IsFalse(geoRule.AllowRequest("eu_client")); | ||
} | ||
|
||
[Test] | ||
public void CombinedRateLimitRule_CombinesMultipleRules() | ||
{ | ||
var rule1 = new RequestCountPerTimespanRule(5, TimeSpan.FromMinutes(1)); | ||
var rule2 = new CooldownBetweenRequestsRule(TimeSpan.FromSeconds(1)); | ||
var combinedRule = new CombinedRateLimitRule(new List<RateLimitRule> { rule1, rule2 }); | ||
|
||
for (int i = 0; i < 5; i++) | ||
{ | ||
bool result = combinedRule.AllowRequest("client1"); | ||
Assert.IsTrue(result); | ||
Thread.Sleep(1000); | ||
} | ||
|
||
Assert.IsFalse(combinedRule.AllowRequest("client1")); | ||
} | ||
|
||
[Test] | ||
public void DailyLimitRule_AllowsRequestsWithinDailyLimit() | ||
{ | ||
var rule = new DailyLimitRule(10); | ||
|
||
for (int i = 0; i < 10; i++) | ||
{ | ||
Assert.IsTrue(rule.AllowRequest("client1")); | ||
} | ||
|
||
Assert.IsFalse(rule.AllowRequest("client1")); | ||
} | ||
|
||
[Test] | ||
public void BurstLimitRule_AllowsRequestsWithinBurstLimit() | ||
{ | ||
var rule = new BurstLimitRule(5, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(10)); | ||
|
||
for (int i = 0; i < 5; i++) | ||
{ | ||
Assert.IsTrue(rule.AllowRequest("client1")); | ||
} | ||
|
||
Assert.IsFalse(rule.AllowRequest("client1")); | ||
} | ||
|
||
[Test] | ||
public void TimeOfDayLimitRule_AllowsRequestsWithinTimeOfDay() | ||
{ | ||
var now = DateTime.UtcNow; | ||
var startTime = now.TimeOfDay - TimeSpan.FromHours(1); | ||
var endTime = now.TimeOfDay + TimeSpan.FromHours(1); | ||
|
||
var rule = new TimeOfDayLimitRule(startTime, endTime); | ||
|
||
Assert.IsTrue(rule.AllowRequest("client1")); | ||
} | ||
|
||
[Test] | ||
public void UserLevelRateLimitRule_AllowsRequestsBasedOnUserLevel() | ||
{ | ||
var rule = new UserLevelRateLimitRule(GetUserLevel); | ||
rule.ConfigureUserLevel("free", new RequestCountPerTimespanRule(5, TimeSpan.FromMinutes(1))); | ||
rule.ConfigureUserLevel("premium", new RequestCountPerTimespanRule(50, TimeSpan.FromMinutes(1))); | ||
|
||
for (int i = 0; i < 5; i++) | ||
{ | ||
Assert.IsTrue(rule.AllowRequest("free_user")); | ||
} | ||
|
||
Assert.IsFalse(rule.AllowRequest("free_user")); | ||
|
||
for (int i = 0; i < 50; i++) | ||
{ | ||
Assert.IsTrue(rule.AllowRequest("premium_user")); | ||
} | ||
|
||
Assert.IsFalse(rule.AllowRequest("premium_user")); | ||
} | ||
|
||
[Test] | ||
public void IPAddressRateLimitRule_AllowsRequestsBasedOnIPAddress() | ||
{ | ||
var rule = new IPAddressRateLimitRule(5, TimeSpan.FromMinutes(1), GetClientIPAddress); | ||
|
||
for (int i = 0; i < 5; i++) | ||
{ | ||
Assert.IsTrue(rule.AllowRequest("client1")); | ||
} | ||
|
||
Assert.IsFalse(rule.AllowRequest("client1")); | ||
} | ||
|
||
private string GetClientRegion(string clientId) | ||
{ | ||
return clientId.StartsWith("us") ? "US" : "EU"; | ||
} | ||
|
||
private string GetUserLevel(string clientId) | ||
{ | ||
return clientId.StartsWith("free") ? "free" : "premium"; | ||
} | ||
|
||
private string GetClientIPAddress(string clientId) | ||
{ | ||
return clientId.StartsWith("client") ? "192.168.0.1" : "10.0.0.1"; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,13 +1,58 @@ | ||
using NUnit.Framework; | ||
using System; | ||
using System.Threading; | ||
|
||
namespace RateLimiter.Tests; | ||
|
||
[TestFixture] | ||
public class RateLimiterTest | ||
{ | ||
[Test] | ||
public void Example() | ||
{ | ||
Assert.That(true, Is.True); | ||
} | ||
[Test] | ||
public void RateLimiter_AllowsRequestsWithinConfiguredLimits() | ||
{ | ||
var rateLimiter = new RateLimiter(); | ||
|
||
var dailyRule = new DailyLimitRule(100); | ||
var burstRule = new BurstLimitRule(10, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(10)); | ||
var timeOfDayRule = new TimeOfDayLimitRule(TimeSpan.FromHours(9), TimeSpan.FromHours(20)); | ||
var userLevelRule = new UserLevelRateLimitRule(GetUserLevel); | ||
var ipAddressRule = new IPAddressRateLimitRule(20, TimeSpan.FromMinutes(10), GetClientIPAddress); | ||
|
||
userLevelRule.ConfigureUserLevel("free", new RequestCountPerTimespanRule(5, TimeSpan.FromMinutes(1))); | ||
userLevelRule.ConfigureUserLevel("premium", new RequestCountPerTimespanRule(50, TimeSpan.FromMinutes(1))); | ||
|
||
rateLimiter.ConfigureResource("api/resource1", dailyRule); | ||
rateLimiter.ConfigureResource("api/resource2", burstRule); | ||
rateLimiter.ConfigureResource("api/resource3", timeOfDayRule); | ||
rateLimiter.ConfigureResource("api/resource4", userLevelRule); | ||
rateLimiter.ConfigureResource("api/resource5", ipAddressRule); | ||
|
||
var clientId = "premium_customer_1"; | ||
|
||
for (int i = 0; i < 10; i++) | ||
{ | ||
Assert.IsTrue(rateLimiter.AllowRequest("api/resource1", clientId), $"Request to resource1 at iteration {i} should be allowed."); | ||
Assert.IsTrue(rateLimiter.AllowRequest("api/resource2", clientId), $"Request to resource2 at iteration {i} should be allowed."); | ||
Assert.IsTrue(rateLimiter.AllowRequest("api/resource3", clientId), $"Request to resource3 at iteration {i} should be allowed."); | ||
Assert.IsTrue(rateLimiter.AllowRequest("api/resource4", clientId), $"Request to resource4 at iteration {i} should be allowed."); | ||
Assert.IsTrue(rateLimiter.AllowRequest("api/resource5", clientId), $"Request to resource5 at iteration {i} should be allowed."); | ||
} | ||
|
||
for (int i = 0; i < 90; i++) | ||
{ | ||
rateLimiter.AllowRequest("api/resource1", clientId); | ||
} | ||
bool finalRequestResult = rateLimiter.AllowRequest("api/resource1", clientId); | ||
Assert.IsFalse(finalRequestResult, "Request to resource1 after 100 requests should be denied."); | ||
} | ||
|
||
private string GetUserLevel(string clientId) | ||
{ | ||
return clientId.StartsWith("premium") ? "premium" : "free"; | ||
} | ||
|
||
private string GetClientIPAddress(string clientId) | ||
{ | ||
return clientId.StartsWith("client") ? "192.168.0.1" : "10.0.0.1"; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
using System; | ||
using System.Collections.Concurrent; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Text; | ||
using System.Threading.Tasks; | ||
|
||
namespace RateLimiter | ||
{ | ||
public class BurstLimitRule : RateLimitRule | ||
{ | ||
private readonly int _burstLimit; | ||
private readonly TimeSpan _burstPeriod; | ||
private readonly TimeSpan _cooldown; | ||
private readonly ConcurrentDictionary<string, List<DateTime>> _requests = new(); | ||
private readonly ConcurrentDictionary<string, DateTime> _cooldowns = new(); | ||
|
||
public BurstLimitRule(int burstLimit, TimeSpan burstPeriod, TimeSpan cooldown) | ||
{ | ||
_burstLimit = burstLimit; | ||
_burstPeriod = burstPeriod; | ||
_cooldown = cooldown; | ||
} | ||
|
||
public override bool AllowRequest(string clientId) | ||
{ | ||
var now = DateTime.UtcNow; | ||
|
||
if (_cooldowns.TryGetValue(clientId, out var cooldownEnd) && now < cooldownEnd) | ||
{ | ||
return false; | ||
} | ||
|
||
_requests.AddOrUpdate(clientId, new List<DateTime> { now }, (key, list) => | ||
{ | ||
list.Add(now); | ||
list.RemoveAll(t => t < now - _burstPeriod); | ||
return list; | ||
}); | ||
|
||
if (_requests[clientId].Count > _burstLimit) | ||
{ | ||
_cooldowns[clientId] = now + _cooldown; | ||
return false; | ||
} | ||
|
||
return true; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Text; | ||
using System.Threading.Tasks; | ||
|
||
namespace RateLimiter | ||
{ | ||
public class CombinedRateLimitRule : RateLimitRule | ||
{ | ||
private readonly List<RateLimitRule> _rules; | ||
|
||
public CombinedRateLimitRule(List<RateLimitRule> rules) | ||
{ | ||
_rules = rules; | ||
} | ||
|
||
public override bool AllowRequest(string clientId) | ||
{ | ||
return _rules.All(rule => rule.AllowRequest(clientId)); | ||
} | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
using System; | ||
using System.Collections.Concurrent; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Text; | ||
using System.Threading.Tasks; | ||
|
||
namespace RateLimiter | ||
{ | ||
public class CooldownBetweenRequestsRule : RateLimitRule | ||
{ | ||
private readonly TimeSpan _cooldown; | ||
private readonly ConcurrentDictionary<string, DateTime> _lastRequestTime = new(); | ||
|
||
public CooldownBetweenRequestsRule(TimeSpan cooldown) | ||
{ | ||
_cooldown = cooldown; | ||
} | ||
|
||
public override bool AllowRequest(string clientId) | ||
{ | ||
var now = DateTime.UtcNow; | ||
if (_lastRequestTime.TryGetValue(clientId, out var lastRequest) && now - lastRequest < _cooldown) | ||
{ | ||
return false; | ||
} | ||
|
||
_lastRequestTime[clientId] = now; | ||
return true; | ||
} | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
using System; | ||
using System.Collections.Concurrent; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Text; | ||
using System.Threading.Tasks; | ||
|
||
namespace RateLimiter | ||
{ | ||
public class DailyLimitRule : RateLimitRule | ||
{ | ||
private readonly int _dailyLimit; | ||
private readonly ConcurrentDictionary<string, List<DateTime>> _requests = new(); | ||
|
||
public DailyLimitRule(int dailyLimit) | ||
{ | ||
_dailyLimit = dailyLimit; | ||
} | ||
|
||
public override bool AllowRequest(string clientId) | ||
{ | ||
var now = DateTime.UtcNow; | ||
var today = now.Date; | ||
_requests.AddOrUpdate(clientId, new List<DateTime> { now }, (key, list) => | ||
{ | ||
list.Add(now); | ||
list.RemoveAll(t => t.Date < today); | ||
return list; | ||
}); | ||
|
||
return _requests[clientId].Count <= _dailyLimit; | ||
} | ||
} | ||
|
||
} |
Oops, something went wrong.