Skip to content

Commit

Permalink
#4 WIP - switching LimitProvider to allow multiple providers
Browse files Browse the repository at this point in the history
unit test rely heavily on verifying internal behaviour and mocking, preparing to switch to integration tests
using client and server full stack.
  • Loading branch information
wwwlicious committed Aug 14, 2018
1 parent 53853b3 commit 70e21e9
Show file tree
Hide file tree
Showing 2 changed files with 41 additions and 200 deletions.
28 changes: 20 additions & 8 deletions src/ServiceStack.RateLimit.Redis/RateLimitFeature.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@
namespace ServiceStack.RateLimit.Redis
{
using System;
using System.Linq;
using System.Runtime.Serialization;
using Headers;
using Interfaces;
using Logging;
using Models;
using ServiceStack;
using ServiceStack.OrmLite;
using ServiceStack.Redis;
using Text;
using Utilities;
Expand Down Expand Up @@ -40,7 +42,7 @@ public class RateLimitFeature : IPlugin
/// <summary>
/// Provides a list of limits per request
/// </summary>
public ILimitProvider LimitProvider { get; set; }
public ILimitProvider[] LimitProviders { get; set; }

/// <summary>
/// Provides a variety of unique keys for requests.
Expand Down Expand Up @@ -69,16 +71,22 @@ public void Register(IAppHost appHost)

public virtual void ProcessRequest(IRequest request, IResponse response, object obj)
{
var limits = LimitProvider.GetLimits(request);
var limits = LimitProviders.Select(x => x.GetLimits(request)).ToArray();

if (limits == null)
if (limits.IsEmpty())
{
// No limits for request, continue
log.Warn($"No limits found for request {request.AbsoluteUri}");
log.Debug($"No limits found for request {request.AbsoluteUri}");
return;
}

var rateLimitResult = GetLimitResult(request, limits);
var combinedLimits = new Limits
{
User = new LimitGroup { Limits = limits.SelectMany(x => x.User.Limits) },
Request = new LimitGroup { Limits = limits.SelectMany(x => x.Request.Limits) }
};

var rateLimitResult = GetLimitResult(request, combinedLimits);
ProcessResult(response, rateLimitResult);
}

Expand Down Expand Up @@ -171,7 +179,7 @@ private static int SecondsFromUnixTime()

private string GetSha1()
{
var scriptFromConfig = LimitProvider.GetRateLimitScriptId();
var scriptFromConfig = LimitProviders.First().GetRateLimitScriptId();
if (!string.IsNullOrWhiteSpace(scriptFromConfig))
{
log.Debug($"Got Lua script sha1 {scriptFromConfig} from config");
Expand All @@ -191,8 +199,12 @@ private void EnsureDependencies(IAppHost appHost)
if (KeyGenerator == null)
KeyGenerator = new LimitKeyGenerator();

if (LimitProvider == null)
LimitProvider = new AppSettingsLimitProvider(KeyGenerator, appHost.AppSettings);
if (LimitProviders.IsEmpty())
LimitProviders = new ILimitProvider[]
{
new AppSettingsLimitProvider(KeyGenerator, appHost.AppSettings),
new AttributeLimitProvider(appHost.AppSettings)
};
}
}
}
213 changes: 21 additions & 192 deletions test/ServiceStack.RateLimit.Redis.Tests/RateLimitFeatureTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,32 +20,19 @@ namespace ServiceStack.RateLimit.Redis.Tests
[Collection("RateLimitFeature")]
public class RateLimitFeatureTests
{
private readonly ILimitKeyGenerator keyGenerator;
private readonly ILimitProvider limitProvider;
private readonly IRedisClientsManager redisManager;
private readonly Limits limit;
private readonly RateLimitFeature rateLimitFeature;
private IServiceClient client;
private IServiceClient authenticatedClient;

public RateLimitFeatureTests()
public RateLimitFeatureTests(RateLimitAppHostFixture fixture)
{
redisManager = A.Fake<IRedisClientsManager>();
limitProvider = A.Fake<ILimitProvider>();
keyGenerator = A.Fake<ILimitKeyGenerator>();

var fixture = new Fixture().Customize(new AutoFakeItEasyCustomization());
limit = fixture.Create<Limits>();
A.CallTo(() => limitProvider.GetLimits(A<IRequest>.Ignored)).Returns(limit);
}

private RateLimitFeature GetSut(bool setupDefaults = true)
{
var feature = new RateLimitFeature(redisManager);

if (setupDefaults)
{
feature.LimitProvider = limitProvider;
feature.KeyGenerator = keyGenerator;
}
return feature;
client = fixture.CreateClient();
authenticatedClient = fixture.CreateAuthenticatedClient();

rateLimitFeature = fixture.Apphost.GetPlugin<RateLimitFeature>();

rateLimitFeature.LimitProviders.Should().HaveCount(2);
rateLimitFeature.KeyGenerator.Should().BeOfType<LimitKeyGenerator>();
}

[Fact]
Expand All @@ -55,162 +42,11 @@ public void Ctor_ThrowsArgumentNullException_IfRedisManagerNull()
action.Should().Throw<ArgumentNullException>();
}

[Fact]
public void Register_SetsDefaultLimitKeyGenerator_IfNotSet()
{
var appHost = A.Fake<IAppHost>();
var feature = GetSut(false);
feature.Register(appHost);

feature.KeyGenerator.Should().BeOfType<LimitKeyGenerator>();
}

[Fact]
public void Register_DoesNotSetDefaultLimitKeyGenerator_IfSet()
{
var appHost = A.Fake<IAppHost>();
var feature = GetSut(false);
feature.KeyGenerator = keyGenerator;
feature.Register(appHost);

feature.KeyGenerator.Should().Be(keyGenerator);
}

[Fact]
public void Register_SetsDefaultLimitProvider_IfNotSet()
{
var appHost = A.Fake<IAppHost>();
var feature = GetSut(false);
feature.Register(appHost);

feature.LimitProvider.Should().BeOfType<AppSettingsLimitProvider>();
}

[Fact]
public void Register_DoesNotSetDefaultLimitProviderBase_IfSet()
{
var appHost = A.Fake<IAppHost>();
var feature = GetSut(false);
feature.LimitProvider = limitProvider;
feature.Register(appHost);

feature.LimitProvider.Should().Be(limitProvider);
}

[Fact]
public void Register_AddsGlobalRequestFilter()
{
var appHost = A.Fake<IAppHost>();
appHost.GlobalRequestFilters.Count.Should().Be(0);

var feature = GetSut();
feature.Register(appHost);

appHost.GlobalRequestFilters.Count.Should().Be(1);
}

[Fact]
public void ProcessRequest_CallsGetLimits()
{
var mockHttpRequest = new MockHttpRequest();
A.CallTo(() => limitProvider.GetLimits(mockHttpRequest)).Returns(null);

var feature = GetSut();
feature.ProcessRequest(mockHttpRequest, new MockHttpResponse(), null);

A.CallTo(() => limitProvider.GetLimits(mockHttpRequest)).MustHaveHappened();
}

[Fact]
public void ProcessRequest_HandlesNullLimit()
{
var mockHttpRequest = new MockHttpRequest();
A.CallTo(() => limitProvider.GetLimits(mockHttpRequest)).Returns(null);

var feature = GetSut();
feature.ProcessRequest(mockHttpRequest, new MockHttpResponse(), null);

// No assert here - not throwing is enough
}

[Fact]
public void ProcessRequest_GetsConsumerId()
{
var mockHttpRequest = new MockHttpRequest();

var feature = GetSut();
feature.ProcessRequest(mockHttpRequest, new MockHttpResponse(), null);

A.CallTo(() => keyGenerator.GetConsumerId(mockHttpRequest)).MustHaveHappened();
}

[Fact]
public void ProcessRequest_GetRequestId()
{
var mockHttpRequest = new MockHttpRequest();

var feature = GetSut();
feature.ProcessRequest(mockHttpRequest, new MockHttpResponse(), null);

A.CallTo(() => keyGenerator.GetRequestId(mockHttpRequest)).MustHaveHappened();
}

[Fact]
public void ProcessRequest_GetsRateLimitScriptFromConfig()
{
var mockHttpRequest = new MockHttpRequest();

var feature = GetSut();
feature.ProcessRequest(mockHttpRequest, new MockHttpResponse(), null);

A.CallTo(() => limitProvider.GetRateLimitScriptId()).MustHaveHappened();
}

[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public void ProcessRequest_RegistersNewScript_IfNoneInConfig(string sha1)
{
var mockHttpRequest = new MockHttpRequest();
A.CallTo(() => limitProvider.GetRateLimitScriptId()).Returns(sha1);
var client = A.Fake<IRedisClient>();
A.CallTo(() => redisManager.GetClient()).Returns(client);

var feature = GetSut();
feature.ProcessRequest(mockHttpRequest, new MockHttpResponse(), null);

A.CallTo(() => client.LoadLuaScript(A<string>.Ignored)).MustHaveHappened();
}

[Theory, InlineAutoData]
public void ProcessRequest_ExecutesLuaScript(string sha1)
{
var mockHttpRequest = new MockHttpRequest();

var client = A.Fake<IRedisClient>();
A.CallTo(() => redisManager.GetClient()).Returns(client);
A.CallTo(() => limitProvider.GetRateLimitScriptId()).Returns(sha1);

var feature = GetSut();
feature.ProcessRequest(mockHttpRequest, new MockHttpResponse(), null);

A.CallTo(() => client.ExecLuaSha(sha1, A<string[]>.Ignored, A<string[]>.Ignored)).MustHaveHappened();
}

[Theory, InlineAutoData]
public void ProcessRequest_ExecutesLuaScriptWithLimit(string sha1, RateLimitResult rateLimitResult)
{
var client = A.Fake<IRedisClient>();
A.CallTo(() => redisManager.GetClient()).Returns(client);
A.CallTo(() => limitProvider.GetRateLimitScriptId()).Returns(sha1);

A.CallTo(() => client.ExecLuaSha(A<string>.Ignored, A<string[]>.Ignored, A<string[]>.Ignored))
.Returns(new RedisText { Text = rateLimitResult.ToJson() });

var feature = GetSut();
var mockHttpResponse = new MockHttpResponse();
feature.ProcessRequest(new MockHttpRequest(), mockHttpResponse, null);
rateLimitFeature.ProcessRequest(new MockHttpRequest(), mockHttpResponse, null);

mockHttpResponse.Headers[Redis.Headers.HttpHeaders.RateLimitUser].Should().NotBeNullOrWhiteSpace();
mockHttpResponse.Headers[Redis.Headers.HttpHeaders.RateLimitRequest].Should().NotBeNullOrWhiteSpace();
Expand All @@ -219,14 +55,9 @@ public void ProcessRequest_ExecutesLuaScriptWithLimit(string sha1, RateLimitResu
[Fact]
public void ProcessRequest_Returns429_IfLimitBreached()
{
var client = A.Fake<IRedisClient>();
A.CallTo(() => client.ExecLuaSha(A<string>.Ignored, A<string[]>.Ignored, A<string[]>.Ignored))
.Returns(new RedisText { Text = new RateLimitResult { Access = false }.ToJson() });

var feature = GetSut();
var response = new MockHttpResponse();

feature.ProcessRequest(new MockHttpRequest(), response, null);
rateLimitFeature.ProcessRequest(new MockHttpRequest(), response, null);

response.StatusCode.Should().Be(429);
}
Expand All @@ -235,31 +66,29 @@ public void ProcessRequest_Returns429_IfLimitBreached()
public void ProcessRequest_ReturnsCustomCode_IfSetAndLimitBreached()
{
const int statusCode = 503;
var client = A.Fake<IRedisClient>();
A.CallTo(() => client.ExecLuaSha(A<string>.Ignored, A<string[]>.Ignored, A<string[]>.Ignored))
.Returns(new RedisText { Text = new RateLimitResult { Access = false }.ToJson() });

var feature = GetSut();
feature.LimitStatusCode = statusCode;
var defaultCode = rateLimitFeature.LimitStatusCode;
rateLimitFeature.LimitStatusCode = statusCode;

var response = new MockHttpResponse();

feature.ProcessRequest(new MockHttpRequest(), response, null);
rateLimitFeature.ProcessRequest(new MockHttpRequest(), response, null);

response.StatusCode.Should().Be(statusCode);

rateLimitFeature.LimitStatusCode = defaultCode;
}

[Fact]
public void ProcessRequest_CallsRequestIdDelegate_IfProvided()
{
bool called = false;
var feature = GetSut();
feature.CorrelationIdExtractor = request =>
rateLimitFeature.CorrelationIdExtractor = request =>
{
called = true;
return "124";
};

feature.ProcessRequest(new MockHttpRequest(), new MockHttpResponse(), null);
rateLimitFeature.ProcessRequest(new MockHttpRequest(), new MockHttpResponse(), null);
called.Should().BeTrue();
}
}
Expand Down

0 comments on commit 70e21e9

Please sign in to comment.