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
90 changes: 77 additions & 13 deletions RateLimiter.Tests/RateLimiterTest.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,77 @@
using NUnit.Framework;

namespace RateLimiter.Tests;

[TestFixture]
public class RateLimiterTest
{
[Test]
public void Example()
{
Assert.That(true, Is.True);
}
}
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using System.Collections.Concurrent;
namespace RateLimiter.Tests;

[TestFixture]
public class RateLimiterTests
{
private RateLimiter _rateLimiter;
private Dictionary<string, string>? _factors;
private ConcurrentQueue<RequestLogEntry> _log;

[SetUp]
public void Setup()
{
_rateLimiter = new RateLimiter();
_factors = new() { { "k", "v" } };
_log = new();
}

[Test]
public void IsRequestAllowed_ShouldReturnTrue_WhenAllRulesAllow()
{
// Arrange
var rule = new MockRateLimitingRule(_log) { IsAllowed = true };
_rateLimiter.AddGlobalRule(rule);

// Act
var result = _rateLimiter.IsRequestAllowed("resource1", "client1", null);

// Assert
Assert.IsTrue(result);
}

[Test]
public void IsRequestAllowed_ShouldReturnFalse_WhenAnyRuleDisallows()
{
// Arrange
var rule = new MockRateLimitingRule(_log) { IsAllowed = false };
_rateLimiter.AddGlobalRule(rule);

// Act
var result = _rateLimiter.IsRequestAllowed("resource1", "client1", null);

// Assert
Assert.IsFalse(result);
}

[Test]
public void GetRequestLog_ShouldReturnLogEntries()
{
// Arrange
var rule = new MockRateLimitingRule(_log) { IsAllowed = true };
_rateLimiter.AddGlobalRule(rule);
_rateLimiter.IsRequestAllowed("resource1", "client1", null);

// Act
var log = _rateLimiter.GetRequestLog();

// Assert
Assert.AreEqual(1, log.Count());
}
}

public class MockRateLimitingRule(IEnumerable<RequestLogEntry> log) : BaseRule(log)
{
public bool IsAllowed { get; set; }

public override bool IsRequestAllowed(string clientId, Dictionary<string, string>? factors)
{
return IsAllowed;
}
}



68 changes: 68 additions & 0 deletions RateLimiter.Tests/TimespanSinceLastCallRuleTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using NUnit.Framework;

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;

namespace RateLimiter.Tests
{
[TestFixture]
public class TimespanSinceLastCallRuleTests
{
private TimespanSinceLastCallRule _rule;
private Dictionary<string, string> _factors;
private ConcurrentQueue<RequestLogEntry> _log;

[SetUp]
public void Setup()
{
_log = new();
_rule = new(TimeSpan.FromSeconds(0.1), _log);
_factors = new() { {"k", "v" } };
}

[Test]
public void IsRequestAllowed_FirstRequest_ReturnsTrue()
{
var result = _rule.IsRequestAllowed("client1", _factors);
Assert.IsTrue(result);
}

[Test]
public void IsRequestAllowed_RequestWithinTimespan_ReturnsFalse()
{
var isAllowed = _rule.IsRequestAllowed("client1", _factors); // First request
System.Threading.Thread.Sleep(10); // Wait for 0.01 seconds

var entry = new RequestLogEntry
{
ClientId = "client1",
Resource = "resource",
Timestamp = DateTime.UtcNow,
IsAllowed = isAllowed,
Factors = new(_factors)
};

_log.Enqueue(entry);



var result = _rule.IsRequestAllowed("client1", _factors);
Assert.IsFalse(result);
}

[Test]
public void IsRequestAllowed_RequestAfterTimespan_ReturnsTrue()
{




_rule.IsRequestAllowed("client1", _factors); // First request
System.Threading.Thread.Sleep(110); // Wait for 0.11 seconds

var result = _rule.IsRequestAllowed("client1", _factors);
Assert.IsTrue(result);
}
}
}
101 changes: 101 additions & 0 deletions RateLimiter.Tests/XRequestsPerTimespanRuleTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
using NUnit.Framework;

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;

namespace RateLimiter.Tests
{
[TestFixture]
public class XRequestsPerTimespanRuleTests
{
//private TimespanSinceLastCallRule _rule;
private Dictionary<string, string> _factors;
private ConcurrentQueue<RequestLogEntry> _log;

[SetUp]
public void Setup()
{
//_rule = new(TimeSpan.FromSeconds(0.1));
_factors = new() { { "k", "v" } };
_log = new();
//_rule.CommonLog = _log;
}


[Test]
public void IsRequestAllowed_FirstRequest_ReturnsTrue()
{
var rule = new XRequestsPerTimespanRule(5, TimeSpan.FromSeconds(0.1), _log);
var result = rule.IsRequestAllowed("client1", null);
Assert.IsTrue(result);
}

[Test]
public void IsRequestAllowed_WithinLimit_ReturnsTrue()
{
var rule = new XRequestsPerTimespanRule(5, TimeSpan.FromSeconds(0.1), _log);
for (int i = 0; i < 4; i++)
{
var isAllowed = rule.IsRequestAllowed("client1", _factors);
var entry = new RequestLogEntry
{
ClientId = "client1",
Resource = "resource",
Timestamp = DateTime.UtcNow,
IsAllowed = isAllowed,
Factors = new(_factors)
};

_log.Enqueue(entry);
}
var result = rule.IsRequestAllowed("client1", null);
Assert.IsTrue(result);
}

[Test]
public void IsRequestAllowed_ExceedsLimit_ReturnsFalse()
{
var rule = new XRequestsPerTimespanRule(5, TimeSpan.FromSeconds(0.1), _log);
for (int i = 0; i < 5; i++)
{
var isAllowed = rule.IsRequestAllowed("client1", _factors);
var entry = new RequestLogEntry
{
ClientId = "client1",
Resource = "resource",
Timestamp = DateTime.UtcNow,
IsAllowed = isAllowed,
Factors = new(_factors)
};

_log.Enqueue(entry);
}
var result = rule.IsRequestAllowed("client1", null);
Assert.IsFalse(result);
}

[Test]
public void IsRequestAllowed_AfterTimespan_ReturnsTrue()
{
var rule = new XRequestsPerTimespanRule(5, TimeSpan.FromSeconds(0.1), _log);
for (int i = 0; i < 5; i++)
{
var isAllowed = rule.IsRequestAllowed("client1", _factors);
var entry = new RequestLogEntry
{
ClientId = "client1",
Resource = "resource",
Timestamp = DateTime.UtcNow,
IsAllowed = isAllowed,
Factors = new(_factors)
};

_log.Enqueue(entry);
}
System.Threading.Thread.Sleep(TimeSpan.FromSeconds(0.1));
var result = rule.IsRequestAllowed("client1", null);
Assert.IsTrue(result);
}
}
}
4 changes: 4 additions & 0 deletions RateLimiter.sln.DotSettings.user
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<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/=e887c3b7_002D1889_002D40b9_002Db6c0_002D5856a8c53b25/@EntryIndexedValue">&lt;SessionState ContinuousTestingMode="0" IsActive="True" Name="All tests from &amp;lt;RateLimiter.Tests&amp;gt;" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"&gt;&#xD;
&lt;Project Location="C:\Source\TulacoRateLimiter\RateLimiter.Tests" Presentation="&amp;lt;RateLimiter.Tests&amp;gt;" /&gt;&#xD;
&lt;/SessionState&gt;</s:String></wpf:ResourceDictionary>
60 changes: 60 additions & 0 deletions RateLimiter/BaseRule.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using System.Collections.Generic;
using System.Linq;

namespace RateLimiter;


/// <summary>
/// Interface for rate limiting rules
/// </summary>
public interface IRateLimitingRule
{
bool IsRequestAllowed(string clientId, Dictionary<string, string>? factors = null);
Dictionary<string, string>? Factors { get; set; }
IEnumerable<RequestLogEntry> CommonLog { get; set; }
}


/// <summary>
/// Base class for rate limiting rules
/// </summary>
public abstract class BaseRule(IEnumerable<RequestLogEntry> log) : IRateLimitingRule
{
/// <summary>
/// Factors that can be used to determine if the rule is applicable
/// </summary>
public Dictionary<string, string>? Factors { get; set; }


/// <summary>
/// Common log of requests
/// </summary>
public IEnumerable<RequestLogEntry> CommonLog { get; set; } = log;

/// <summary>
/// Check if the request is allowed
/// </summary>
/// <param name="clientId"></param>
/// <param name="factors"></param>
/// <returns></returns>
public virtual bool IsRequestAllowed(string clientId, Dictionary<string, string>? factors)
{
// If factors are not set or are not used, the rule is not applicable
return Factors != null
&& factors?.ContainsAllElements(Factors) != true;
}
}


/// <summary>
/// Extension methods for dictionaries
/// </summary>
public static class DictionaryComparer
{
public static bool ContainsAllElements<TKey, TValue>(
this Dictionary<TKey, TValue> mainDict,
Dictionary<TKey, TValue> subDict) where TKey : notnull
{
return subDict.All(kv => mainDict.ContainsKey(kv.Key) && EqualityComparer<TValue>.Default.Equals(mainDict[kv.Key], kv.Value));
}
}
Loading