Skip to content

Commit

Permalink
Rate Limiter and Tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Giorgi committed Jan 17, 2025
1 parent d0a5741 commit 482dc13
Show file tree
Hide file tree
Showing 14 changed files with 568 additions and 10 deletions.
275 changes: 269 additions & 6 deletions RateLimiter.Tests/RateLimiterTest.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,276 @@
using NUnit.Framework;
using System;
using System.Threading;
using RateLimiter.DataStore;
using System.Collections.Generic;
using RateLimiter.Ruls.Abstract;
using RateLimiter.Ruls;
using RateLimiter.User;

namespace RateLimiter.Tests;

[TestFixture]
public class RateLimiterTest
{
[Test]
public void Example()
{
Assert.That(true, Is.True);
}
{
[Test]
public void IsAllowedWithNoRules()
{
Cashing.Clear();
// Arrange
var rules = new RateLimiterRuleDecorator[] { };

var userData = new UserData() { CountryCode = "US", Token = "tempToken", IpAddress = "192.168.18.22" };

// Act
var result = new ConcreteRateLimiter(rules).IsAllowed(userData);

// Assert
Assert.IsTrue(result);
}
[Test]
public void IsAllowedWithAllRulesPositive()
{
Cashing.Clear();
Dictionary<string, int> restrictionsByCountry = new()
{
{ "US", 2 },
{ "GE", 1 }
};

// Arrange
var rules = new RateLimiterRuleDecorator[] {
new IpWhiteListRule(new string[] { "192.168.18.22", "192.168.18.23" }),
new RequestMinAllowedTimeRule(TimeSpan.FromSeconds(1)),
new IpBlackListRule(new string[] { "192.168.18.48", "192.168.18.49" }),
new MaxRequestAmountInTimeSpanRule(TimeSpan.FromSeconds(1),5),
new MaxRequestAmountInTimeSpanByCountryRule(TimeSpan.FromSeconds(1), restrictionsByCountry, 2),
};

var userData = new UserData() { CountryCode = "US",Token="tempToken",IpAddress = "192.168.18.22" };

// Act
var result = new ConcreteRateLimiter(rules).IsAllowed(userData);

// Assert
Assert.IsTrue(result);
}
[Test]
public void IpWhtieListRuleNegative()
{
Cashing.Clear();
// Arrange
var rules = new RateLimiterRuleDecorator[] { new IpWhiteListRule(new string[] { "192.168.18.22", "192.168.18.23" }) };

var userData = new UserData() { CountryCode = "US", Token = "tempToken", IpAddress = "192.168.18.30" };

// Act
var result = new ConcreteRateLimiter(rules).IsAllowed(userData);


// Assert
Assert.IsFalse(result);
}
[Test]
public void IpBlackListRulePositive()
{
Cashing.Clear();
// Arrange
var rules = new RateLimiterRuleDecorator[] { new IpBlackListRule(new string[] { "192.168.18.22", "192.168.18.23" }) };

var userData = new UserData() { CountryCode = "US", Token = "tempToken", IpAddress = "192.168.18.30" };

// Act
var result = new ConcreteRateLimiter(rules).IsAllowed(userData);

// Assert
Assert.IsTrue(result);
}
[Test]
public void IpBlackListRuleNegative()
{
Cashing.Clear();
// Arrange
var rules = new RateLimiterRuleDecorator[] { new IpBlackListRule(new string[] { "192.168.18.22", "192.168.18.23" }) };

var userData = new UserData() { CountryCode = "US", Token = "tempToken", IpAddress = "192.168.18.22" };

// Act
var result = new ConcreteRateLimiter(rules).IsAllowed(userData);

// Assert
Assert.IsFalse(result);
}
[Test]
public void IsAllowedWithRequestMinAllowedTimePositive()
{
Cashing.Clear();
// Arrange
var rules = new RateLimiterRuleDecorator[] { new RequestMinAllowedTimeRule(TimeSpan.FromSeconds(1)) };

var userData = new UserData() { CountryCode = "US", Token = "tempToken", IpAddress = "192.168.18.22" };

// Act
new ConcreteRateLimiter(rules).IsAllowed(userData);
Thread.Sleep(2000);
var result = new ConcreteRateLimiter(rules).IsAllowed(userData);

// Assert
Assert.IsTrue(result);
}
[Test]
public void IsAllowedRequestMinAllowedTimeNegative()
{
Cashing.Clear();
// Arrange
var rules = new RateLimiterRuleDecorator[] { new RequestMinAllowedTimeRule(TimeSpan.FromSeconds(1)) };

var userData = new UserData() { CountryCode = "US", Token = "tempToken", IpAddress = "192.168.18.22" };

// Act
new ConcreteRateLimiter(rules).IsAllowed(userData);
var result = new ConcreteRateLimiter(rules).IsAllowed(userData);

// Assert
Assert.IsFalse(result);
}
[Test]
public void IsAllowedMaxRequestAmountInTimeSpanRulePositive()
{
Cashing.Clear();
// Arrange
var rules = new RateLimiterRuleDecorator[] { new MaxRequestAmountInTimeSpanRule(TimeSpan.FromSeconds(1),5) };

var userData = new UserData() { CountryCode = "US", Token = "tempToken", IpAddress = "192.168.18.22" };

// Act
new ConcreteRateLimiter(rules).IsAllowed(userData);
new ConcreteRateLimiter(rules).IsAllowed(userData);
var result = new ConcreteRateLimiter(rules).IsAllowed(userData);

// Assert
Assert.IsTrue(result);
}
[Test]
public void IsAllowedMaxRequestAmountInTimeSpanRuleWithDeleyPositive()
{
Cashing.Clear();
// Arrange
var rules = new RateLimiterRuleDecorator[] { new MaxRequestAmountInTimeSpanRule(TimeSpan.FromSeconds(1), 2) };

var userData = new UserData() { CountryCode = "US", Token = "tempToken", IpAddress = "192.168.18.22" };

// Act
new ConcreteRateLimiter(rules).IsAllowed(userData);
new ConcreteRateLimiter(rules).IsAllowed(userData);
Thread.Sleep(2000);
var result = new ConcreteRateLimiter(rules).IsAllowed(userData);

// Assert
Assert.IsTrue(result);
}
[Test]
public void IsAllowedMaxRequestAmountInTimeSpanRuleNegative()
{
Cashing.Clear();
// Arrange
var rules = new RateLimiterRuleDecorator[] { new MaxRequestAmountInTimeSpanRule(TimeSpan.FromSeconds(1), 2) };

var userData = new UserData() { CountryCode = "US", Token = "tempToken", IpAddress = "192.168.18.22" };

// Act
new ConcreteRateLimiter(rules).IsAllowed(userData);
new ConcreteRateLimiter(rules).IsAllowed(userData);
var result = new ConcreteRateLimiter(rules).IsAllowed(userData);

// Assert
Assert.IsFalse(result);
}
[Test]
public void IsAllowedMaxRequestAmountInTimeSpanByCountryPositive()
{
Cashing.Clear();
// Arrange
Dictionary<string, int> restrictionsByCountry = new()
{
{ "US", 2 },
{ "GE", 1 }
};
var rules = new RateLimiterRuleDecorator[] { new MaxRequestAmountInTimeSpanByCountryRule( TimeSpan.FromSeconds(1), restrictionsByCountry, 2) };

var userData = new UserData() { CountryCode = "US", Token = "tempToken", IpAddress = "192.168.18.22" };

// Act
new ConcreteRateLimiter(rules).IsAllowed(userData);
var result = new ConcreteRateLimiter(rules).IsAllowed(userData);

// Assert
Assert.IsTrue(result);
}
[Test]
public void IsAllowedMaxRequestAmountInTimeSpanByCountryNegative()
{
Cashing.Clear();
// Arrange
Dictionary<string, int> restrictionsByCountry = new()
{
{ "US", 2 },
{ "GE", 1 }
};
var rules = new RateLimiterRuleDecorator[] { new MaxRequestAmountInTimeSpanByCountryRule(TimeSpan.FromSeconds(1), restrictionsByCountry, 1) };

var userData = new UserData() { CountryCode = "US", Token = "tempToken", IpAddress = "192.168.18.22" };

// Act
new ConcreteRateLimiter(rules).IsAllowed(userData);
new ConcreteRateLimiter(rules).IsAllowed(userData);
var result = new ConcreteRateLimiter(rules).IsAllowed(userData);

// Assert
Assert.IsFalse(result);
}
[Test]
public void IsAllowedMaxRequestAmountInTimeSpanByCountryDefoultPositive()
{
Cashing.Clear();
// Arrange
Dictionary<string, int> restrictionsByCountry = new()
{
{ "US", 2 },
{ "GE", 1 }
};
var rules = new RateLimiterRuleDecorator[] { new MaxRequestAmountInTimeSpanByCountryRule(TimeSpan.FromSeconds(1), restrictionsByCountry, 2) };

var userData = new UserData() { CountryCode = "TR", Token = "tempToken", IpAddress = "192.168.18.22" };

// Act
new ConcreteRateLimiter(rules).IsAllowed(userData);
var result = new ConcreteRateLimiter(rules).IsAllowed(userData);

// Assert
Assert.IsTrue(result);
}
[Test]
public void IsAllowedMaxRequestAmountInTimeSpanByCountryDefoultNegative()
{
Cashing.Clear();
// Arrange
Dictionary<string, int> restrictionsByCountry = new()
{
{ "US", 2 },
{ "GE", 1 }
};
var rules = new RateLimiterRuleDecorator[] { new MaxRequestAmountInTimeSpanByCountryRule(TimeSpan.FromSeconds(1), restrictionsByCountry, 2) };

var userData = new UserData() { CountryCode = "TR", Token = "tempToken", IpAddress = "192.168.18.22" };

// Act
new ConcreteRateLimiter(rules).IsAllowed(userData);
new ConcreteRateLimiter(rules).IsAllowed(userData);
var result = new ConcreteRateLimiter(rules).IsAllowed(userData);

// Assert
Assert.IsFalse(result);
}


}
8 changes: 4 additions & 4 deletions RateLimiter.sln
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26730.15
# Visual Studio Version 17
VisualStudioVersion = 17.5.33502.453
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RateLimiter", "RateLimiter\RateLimiter.csproj", "{36F4BDC6-D3DA-403A-8DB7-0C79F94B938F}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RateLimiter", "RateLimiter\RateLimiter.csproj", "{36F4BDC6-D3DA-403A-8DB7-0C79F94B938F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RateLimiter.Tests", "RateLimiter.Tests\RateLimiter.Tests.csproj", "{C4F9249B-010E-46BE-94B8-DD20D82F1E60}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RateLimiter.Tests", "RateLimiter.Tests\RateLimiter.Tests.csproj", "{C4F9249B-010E-46BE-94B8-DD20D82F1E60}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{9B206889-9841-4B5E-B79B-D5B2610CCCFF}"
ProjectSection(SolutionItems) = preProject
Expand Down
30 changes: 30 additions & 0 deletions RateLimiter/ConcreteRateLimiter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using RateLimiter.Ruls.Abstract;
using RateLimiter.User;

namespace RateLimiter
{
public class ConcreteRateLimiter
{
private readonly RateLimiterRuleDecorator? _rateLimiter;
public ConcreteRateLimiter(RateLimiterRuleDecorator[] rules)
{
foreach (var rule in rules)
{
if (_rateLimiter == null)
{
_rateLimiter = rule;
}
else
{
rule.RateLimiterRule = _rateLimiter;
_rateLimiter = rule;
}
}
}
public bool IsAllowed(IUserData userData)
{
return _rateLimiter == null || _rateLimiter.IsAllowed(userData);
}

}
}
20 changes: 20 additions & 0 deletions RateLimiter/DataStore/Cashing.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System.Collections.Concurrent;

namespace RateLimiter.DataStore
{
//this is only for demonstration in real world scenario will be used redis or other data store
public static class Cashing
{
private static ConcurrentDictionary<string, string> Store = new();

public static string? Get(string key) {
Store.TryGetValue(key, out string? temp);
return temp;
}

public static void Set(string key, string value) {
Store.AddOrUpdate(key, value, (existingKey, existingValue) => value);
}
public static void Clear() { Store.Clear(); }
}
}
10 changes: 10 additions & 0 deletions RateLimiter/Ruls/Abstract/IRateLimiterRuleDecorator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using RateLimiter.User;

namespace RateLimiter.Ruls.Abstract
{
public abstract class RateLimiterRuleDecorator
{
public RateLimiterRuleDecorator? RateLimiterRule { get; set; }
public abstract bool IsAllowed(IUserData userData);
}
}
22 changes: 22 additions & 0 deletions RateLimiter/Ruls/IpBlackListRule.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using RateLimiter.Ruls.Abstract;
using RateLimiter.User;
using System;
using System.Linq;

namespace RateLimiter.Ruls
{
public class IpBlackListRule : RateLimiterRuleDecorator
{
private readonly string[] _restrictedIpAddresses;

public IpBlackListRule(string[] allowedIpAddresses)
{
_restrictedIpAddresses = allowedIpAddresses;
}

public override bool IsAllowed(IUserData userData)
{
return !_restrictedIpAddresses.Contains(userData.IpAddress) && (RateLimiterRule == null || RateLimiterRule.IsAllowed(userData));
}
}
}
Loading

0 comments on commit 482dc13

Please sign in to comment.