Skip to content
Draft
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
12 changes: 11 additions & 1 deletion src/Testing/Document/MockCacheLookupProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ public static Setup CacheLookup(

public class Setup
{
private readonly Func<GatewayContext, CacheLookupConfig, bool> _predicate;
private readonly CacheLookupHandler _handler;
private readonly Func<GatewayContext, CacheLookupConfig, bool> _predicate;

internal Setup(
Func<GatewayContext, CacheLookupConfig, bool> predicate,
Expand All @@ -34,5 +34,15 @@ internal Setup(

public void WithCallback(Action<GatewayContext, CacheLookupConfig> callback) =>
_handler.CallbackSetup.Add((_predicate, callback).ToTuple());

public void WithCacheKey(Func<GatewayContext, CacheLookupConfig, string> callback)
{
_handler.CacheKeyProvider.Add((_predicate, callback).ToTuple());
}

public void WithCacheKey(string key)
{
WithCacheKey((_, _) => key);
}
}
}
5 changes: 5 additions & 0 deletions src/Testing/Document/MockCacheStoreProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,10 @@ internal Setup(

public void WithCallback(Action<GatewayContext, uint, bool> callback) =>
_handler.CallbackHooks.Add((_predicate, callback).ToTuple());

public void WithCacheKey(Func<GatewayContext, uint, bool, string> callback) =>
_handler.CacheKeyProvider.Add((_predicate, callback).ToTuple());

public void WithCacheKey(string key) => this.WithCacheKey((_, _, _) => key);
}
}
3 changes: 3 additions & 0 deletions src/Testing/Document/TestDocumentExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,7 @@ public static CertificateStore SetupCertificateStore(this TestDocument document)

public static CacheStore SetupCacheStore(this TestDocument document) =>
document.Context.CacheStore;

public static CacheInfo SetupCacheInfo(this TestDocument document) =>
document.Context.CacheInfo;
}
94 changes: 94 additions & 0 deletions src/Testing/Emulator/Data/CacheInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
using System.Text;

using Microsoft.Azure.ApiManagement.PolicyToolkit.Authoring;

namespace Microsoft.Azure.ApiManagement.PolicyToolkit.Testing.Emulator.Data;

public class CacheInfo
{
internal bool AllowPrivateResponseCaching;
internal bool CacheSetup = false;
internal string CachingType = "prefer-external";
internal string DownstreamCachingType = "none";
internal bool MustRevalidate = true;
internal bool ShouldBeCached = false;

internal bool VaryByDeveloper = false;
internal bool VaryByDeveloperGroups = false;
internal string[]? VaryByHeaders;
internal string[]? VaryByQueryParameters;

public CacheInfo WithExecutedCacheLookup(bool isSetup = true)
{
CacheSetup = isSetup;
return this;
}

public CacheInfo WithExecutedCacheLookup(CacheLookupConfig config)
{
CacheSetup = true;
VaryByDeveloper = config.VaryByDeveloper;
VaryByDeveloperGroups = config.VaryByDeveloperGroups;
CachingType = config.CachingType ?? CachingType;
DownstreamCachingType = config.DownstreamCachingType ?? DownstreamCachingType;
MustRevalidate = config.MustRevalidate ?? MustRevalidate;
AllowPrivateResponseCaching = config.AllowPrivateResponseCaching ?? AllowPrivateResponseCaching;
VaryByHeaders = config.VaryByHeaders;
VaryByQueryParameters = config.VaryByQueryParameters;
return this;
}

internal static string CacheKey(GatewayContext context)
{
var keyBuilder = new StringBuilder("key:");

if (context.Product is not null)
{
keyBuilder.Append("&product:").Append(context.Product.Id).Append(':');
}

keyBuilder.Append("&api:").Append(context.Api.Id).Append(':');
keyBuilder.Append("&operation:").Append(context.Operation.Id).Append(':');

ProcessVaryBy(keyBuilder, "&params:", context.CacheInfo.VaryByQueryParameters, context.Request.Url.Query);
ProcessVaryBy(keyBuilder, "&headers:", context.CacheInfo.VaryByHeaders, context.Request.Headers);

if (context.CacheInfo.VaryByDeveloper)
{
keyBuilder.Append("&bydeveloper:").Append(context.User?.Id);
}

if (context.CacheInfo.VaryByDeveloperGroups)
{
keyBuilder.Append("&bygroups:");
if (context.User is not null)
{
keyBuilder.AppendJoin(",", context.User.Groups.Select(g => g.Id));
}
}

return keyBuilder.ToString();
}

private static void ProcessVaryBy(StringBuilder builder, string prefix, string[]? keys,
Dictionary<string, string[]> map)
{
if (keys is null || keys.Length == 0)
{
return;
}

builder.Append(prefix);
var keyList = keys.ToList();
keyList.Sort(StringComparer.InvariantCultureIgnoreCase);
foreach (var key in keyList)
{
if (!map.TryGetValue(key, out var v))
{
continue;
}

builder.Append(key).Append('=').AppendJoin(",", v);
}
}
}
36 changes: 35 additions & 1 deletion src/Testing/Emulator/Policies/CacheLookupHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,50 @@
// Licensed under the MIT License.

using Microsoft.Azure.ApiManagement.PolicyToolkit.Authoring;
using Microsoft.Azure.ApiManagement.PolicyToolkit.Testing.Emulator.Data;
using Microsoft.Azure.ApiManagement.PolicyToolkit.Testing.Expressions;

namespace Microsoft.Azure.ApiManagement.PolicyToolkit.Testing.Emulator.Policies;

[Section(nameof(IInboundContext))]
internal class CacheLookupHandler : PolicyHandler<CacheLookupConfig>
{
public List<Tuple<
Func<GatewayContext, CacheLookupConfig, bool>,
Func<GatewayContext, CacheLookupConfig, string>
>> CacheKeyProvider { get; } = new();

public override string PolicyName => nameof(IInboundContext.CacheLookup);

protected override void Handle(GatewayContext context, CacheLookupConfig config)
{
throw new NotImplementedException();
if (context.CacheInfo.CacheSetup)
{
return;
}

Dictionary<string, CacheValue>? store = context.CacheStore.GetCache(context.CacheInfo.CachingType);
if (store is null)
{
return;
}

context.CacheInfo.WithExecutedCacheLookup(config);

string key = CacheKeyProvider.Find(hook => hook.Item1(context, config))
?.Item2(context, config)
?? CacheInfo.CacheKey(context);
if (!store.TryGetValue(key, out CacheValue? cacheHit))
{
return;
}

if (cacheHit.Value is not MockResponse cachedResponse)
{
return;
}

context.Response = cachedResponse.Clone();
throw new FinishSectionProcessingException();
}
}
49 changes: 44 additions & 5 deletions src/Testing/Emulator/Policies/CacheStoreHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License.

using Microsoft.Azure.ApiManagement.PolicyToolkit.Authoring;
using Microsoft.Azure.ApiManagement.PolicyToolkit.Testing.Emulator.Data;

namespace Microsoft.Azure.ApiManagement.PolicyToolkit.Testing.Emulator.Policies;

Expand All @@ -13,6 +14,11 @@ public List<Tuple<
Action<GatewayContext, uint, bool>
>> CallbackHooks { get; } = new();

public List<Tuple<
Func<GatewayContext, uint, bool, bool>,
Func<GatewayContext, uint, bool, string>
>> CacheKeyProvider { get; } = new();

public string PolicyName => nameof(IOutboundContext.CacheStore);

public object? Handle(GatewayContext context, object?[]? args)
Expand All @@ -32,9 +38,42 @@ public List<Tuple<
return null;
}

protected void Handle(GatewayContext context, uint duration, bool cacheResponse)
private void Handle(GatewayContext context, uint duration, bool cacheResponse)
{
throw new NotImplementedException();
if (!context.CacheInfo.CacheSetup)
{
return;
}

if (!context.Request.Method.Equals("GET", StringComparison.InvariantCultureIgnoreCase))
{
return;
}

var store = context.CacheStore.GetCache(context.CacheInfo.CachingType);
if (store is null)
{
return;
}

if (!cacheResponse || context.Response.StatusCode != 200)
{
return;
}

if (context.Response.Headers.ContainsKey("Authorization") && !context.CacheInfo.AllowPrivateResponseCaching)
{
return;
}

var cacheValue = context.Response.Clone();

var key = CacheKeyProvider.Find(hook => hook.Item1(context, duration, cacheResponse))
?.Item2(context, duration, cacheResponse)
?? CacheInfo.CacheKey(context);


store[key] = new CacheValue(cacheValue) { Duration = duration };
}

private static (uint, bool) ExtractParameters(object?[]? args)
Expand All @@ -49,12 +88,12 @@ private static (uint, bool) ExtractParameters(object?[]? args)
throw new ArgumentException($"Expected {typeof(uint).Name} as first argument", nameof(args));
}

if (args.Length != 2)
if (args.Length != 2 || args[1] is null)
{
return (duration, true);
return (duration, false);
}

if (args[0] is not bool cacheValue)
if (args[1] is not bool cacheValue)
{
throw new ArgumentException($"Expected {typeof(bool).Name} as second argument", nameof(args));
}
Expand Down
8 changes: 8 additions & 0 deletions src/Testing/Expressions/MockResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,12 @@ public class MockResponse : MockMessage, IResponse
public int StatusCode { get; set; } = 200;

public string StatusReason { get; set; } = "OK";

public MockResponse Clone() => new()
{
StatusCode = StatusCode,
StatusReason = StatusReason,
Headers = Headers.ToDictionary(pair => pair.Key, pair => (string[])pair.Value.Clone()),
Body = new MockBody() { Content = Body.Content, },
};
}
1 change: 1 addition & 0 deletions src/Testing/GatewayContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public class GatewayContext : MockExpressionContext
internal readonly SectionContextProxy<IOnErrorContext> OnErrorProxy;
internal readonly CertificateStore CertificateStore = new();
internal readonly CacheStore CacheStore = new();
internal readonly CacheInfo CacheInfo = new();

public GatewayContext()
{
Expand Down
23 changes: 23 additions & 0 deletions test/Test.Testing/Emulator/Policies/CacheLookupTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using Azure.ApiManagement.PolicyToolkit.Authoring;

namespace Test.Emulator.Emulator.Policies;

[TestClass]
public class CacheLookupTests
{
private class SimpleCacheLookup : IDocument
{
public void Inbound(IInboundContext context)
{
context.CacheLookup(new CacheLookupConfig { VaryByDeveloper = false, VaryByDeveloperGroups = false });
}
}

private class SimpleCacheLookup1 : IDocument
{
public void Outbound(IOutboundContext context)
{
context.CacheStore(10, true);
}
}
}
Loading
Loading