Skip to content

Commit 3b346ef

Browse files
committed
Add cache store to emulator
1 parent 0540793 commit 3b346ef

File tree

8 files changed

+245
-7
lines changed

8 files changed

+245
-7
lines changed

src/Authoring/IOutboundContext.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ public interface IOutboundContext : IHaveExpressionContext
8888
/// </summary>
8989
/// <param name="duration"></param>
9090
/// <param name="cacheResponse"></param>
91-
void CacheStore(uint duration, bool? cacheResponse);
91+
void CacheStore(uint duration, bool? cacheResponse = null);
9292

9393
/// <summary>
9494
/// TODO

src/Testing/Document/MockCacheStoreProvider.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
// Licensed under the MIT License.
33

44
using Azure.ApiManagement.PolicyToolkit.Authoring;
5-
using Azure.ApiManagement.PolicyToolkit.Testing.Emulator;
65
using Azure.ApiManagement.PolicyToolkit.Testing.Emulator.Policies;
76

87
namespace Azure.ApiManagement.PolicyToolkit.Testing.Document;
@@ -36,5 +35,10 @@ internal Setup(
3635

3736
public void WithCallback(Action<GatewayContext, uint, bool> callback) =>
3837
_handler.CallbackHooks.Add((_predicate, callback).ToTuple());
38+
39+
public void WithCacheKey(Func<GatewayContext, uint, bool, string> callback) =>
40+
_handler.CacheKeyProvider.Add((_predicate, callback).ToTuple());
41+
42+
public void WithCacheKey(string key) => this.WithCacheKey((_, _, _) => key);
3943
}
4044
}

src/Testing/Document/TestDocumentExtensions.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,7 @@ public static CertificateStore SetupCertificateStore(this TestDocument document)
2525

2626
public static CacheStore SetupCacheStore(this TestDocument document) =>
2727
document.Context.CacheStore;
28+
29+
public static CacheInfo SetupCacheInfo(this TestDocument document) =>
30+
document.Context.CacheInfo;
2831
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
using System.Text;
2+
3+
using Azure.ApiManagement.PolicyToolkit.Authoring;
4+
5+
namespace Azure.ApiManagement.PolicyToolkit.Testing.Emulator.Data;
6+
7+
public class CacheInfo
8+
{
9+
internal bool CacheSetup = false;
10+
11+
internal bool VaryByDeveloper = false;
12+
internal bool VaryByDeveloperGroups = false;
13+
internal string CachingType = "prefer-external";
14+
internal string DownstreamCachingType = "none";
15+
internal bool MustRevalidate = true;
16+
internal bool AllowPrivateResponseCaching = false;
17+
internal string[]? VaryByHeaders;
18+
internal string[]? VaryByQueryParameters;
19+
20+
public CacheInfo WithExecutedCacheLookup(bool isSetup = true)
21+
{
22+
CacheSetup = isSetup;
23+
return this;
24+
}
25+
26+
internal CacheInfo WithExecutedCacheLookup(CacheLookupConfig config)
27+
{
28+
CacheSetup = true;
29+
VaryByDeveloper = config.VaryByDeveloper;
30+
VaryByDeveloperGroups = config.VaryByDeveloperGroups;
31+
CachingType = config.CachingType ?? CachingType;
32+
DownstreamCachingType = config.DownstreamCachingType ?? DownstreamCachingType;
33+
MustRevalidate = config.MustRevalidate ?? MustRevalidate;
34+
AllowPrivateResponseCaching = config.AllowPrivateResponseCaching ?? AllowPrivateResponseCaching;
35+
VaryByHeaders = config.VaryByHeaders;
36+
VaryByQueryParameters = config.VaryByQueryParameters;
37+
return this;
38+
}
39+
40+
internal static string CacheKey(GatewayContext context)
41+
{
42+
var keyBuilder = new StringBuilder("key:");
43+
44+
if (context.Product is not null)
45+
{
46+
keyBuilder.Append("&product:").Append(context.Product.Id).Append(':');
47+
}
48+
49+
keyBuilder.Append("&api:").Append(context.Api.Id).Append(':');
50+
keyBuilder.Append("&operation:").Append(context.Operation.Id).Append(':');
51+
52+
ProcessVaryBy(keyBuilder, "&params:", context.CacheInfo.VaryByQueryParameters, context.Request.Url.Query);
53+
ProcessVaryBy(keyBuilder, "&headers:", context.CacheInfo.VaryByHeaders, context.Request.Headers);
54+
55+
if (context.CacheInfo.VaryByDeveloper)
56+
{
57+
keyBuilder.Append("&bydeveloper:").Append(context.User?.Id);
58+
}
59+
60+
if (context.CacheInfo.VaryByDeveloperGroups)
61+
{
62+
keyBuilder.Append("&bygroups:");
63+
if (context.User is not null)
64+
{
65+
keyBuilder.AppendJoin(",", context.User.Groups.Select(g => g.Id));
66+
}
67+
}
68+
69+
return keyBuilder.ToString();
70+
}
71+
72+
private static void ProcessVaryBy(StringBuilder builder, string prefix, string[]? keys,
73+
Dictionary<string, string[]> map)
74+
{
75+
if (keys is null || keys.Length == 0)
76+
{
77+
return;
78+
}
79+
80+
builder.Append(prefix);
81+
var keyList = keys.ToList();
82+
keyList.Sort(StringComparer.InvariantCultureIgnoreCase);
83+
foreach (var key in keyList)
84+
{
85+
if (!map.TryGetValue(key, out var v))
86+
{
87+
continue;
88+
}
89+
90+
builder.Append(key).Append('=').AppendJoin(",", v);
91+
}
92+
}
93+
}

src/Testing/Emulator/Policies/CacheStoreHandler.cs

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the MIT License.
33

44
using Azure.ApiManagement.PolicyToolkit.Authoring;
5+
using Azure.ApiManagement.PolicyToolkit.Testing.Emulator.Data;
56

67
namespace Azure.ApiManagement.PolicyToolkit.Testing.Emulator.Policies;
78

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

17+
public List<Tuple<
18+
Func<GatewayContext, uint, bool, bool>,
19+
Func<GatewayContext, uint, bool, string>
20+
>> CacheKeyProvider { get; } = new();
21+
1622
public string PolicyName => nameof(IOutboundContext.CacheStore);
1723

1824
public object? Handle(GatewayContext context, object?[]? args)
@@ -32,9 +38,31 @@ public List<Tuple<
3238
return null;
3339
}
3440

35-
protected void Handle(GatewayContext context, uint duration, bool cacheResponse)
41+
private void Handle(GatewayContext context, uint duration, bool cacheResponse)
3642
{
37-
throw new NotImplementedException();
43+
if (!context.CacheInfo.CacheSetup)
44+
{
45+
return;
46+
}
47+
48+
var store = context.CacheStore.GetCache(context.CacheInfo.CachingType);
49+
if (store is null)
50+
{
51+
return;
52+
}
53+
54+
if (context.Response.StatusCode != 200 && !cacheResponse)
55+
{
56+
return;
57+
}
58+
59+
var cacheValue = context.Response.Clone();
60+
61+
var key = CacheKeyProvider.Find(hook => hook.Item1(context, duration, cacheResponse))
62+
?.Item2(context, duration, cacheResponse)
63+
?? CacheInfo.CacheKey(context);
64+
65+
store[key] = new CacheValue(cacheValue) { Duration = duration };
3866
}
3967

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

52-
if (args.Length != 2)
80+
if (args.Length != 2 || args[1] is null)
5381
{
54-
return (duration, true);
82+
return (duration, false);
5583
}
5684

57-
if (args[0] is not bool cacheValue)
85+
if (args[1] is not bool cacheValue)
5886
{
5987
throw new ArgumentException($"Expected {typeof(bool).Name} as second argument", nameof(args));
6088
}

src/Testing/Expressions/MockResponse.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,12 @@ public class MockResponse : MockMessage, IResponse
1414
public int StatusCode { get; set; } = 200;
1515

1616
public string StatusReason { get; set; } = "OK";
17+
18+
public MockResponse Clone() => new()
19+
{
20+
StatusCode = StatusCode,
21+
StatusReason = StatusReason,
22+
Headers = Headers.ToDictionary(pair => pair.Key, pair => (string[])pair.Value.Clone()),
23+
Body = new MockBody() { Content = Body.Content, },
24+
};
1725
}

src/Testing/GatewayContext.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ public class GatewayContext : MockExpressionContext
1616
internal readonly SectionContextProxy<IOnErrorContext> OnErrorProxy;
1717
internal readonly CertificateStore CertificateStore = new();
1818
internal readonly CacheStore CacheStore = new();
19+
internal readonly CacheInfo CacheInfo = new();
1920

2021
public GatewayContext()
2122
{
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using Azure.ApiManagement.PolicyToolkit.Authoring;
5+
using Azure.ApiManagement.PolicyToolkit.Authoring.Expressions;
6+
using Azure.ApiManagement.PolicyToolkit.Testing;
7+
using Azure.ApiManagement.PolicyToolkit.Testing.Document;
8+
9+
namespace Test.Emulator.Emulator.Policies;
10+
11+
[TestClass]
12+
public class CacheStoreTests
13+
{
14+
class SimpleCacheStore : IDocument
15+
{
16+
public void Outbound(IOutboundContext context)
17+
{
18+
context.CacheStore(10);
19+
}
20+
}
21+
22+
class SimpleCacheStoreStoreResponse : IDocument
23+
{
24+
public void Outbound(IOutboundContext context)
25+
{
26+
context.CacheStore(10, true);
27+
}
28+
}
29+
30+
[TestMethod]
31+
public void CacheStore_Callback()
32+
{
33+
var test = new SimpleCacheStore().AsTestDocument();
34+
var executedCallback = false;
35+
test.SetupOutbound().CacheStore().WithCallback((_, _, _) =>
36+
{
37+
executedCallback = true;
38+
});
39+
40+
test.RunOutbound();
41+
42+
executedCallback.Should().BeTrue();
43+
}
44+
45+
[TestMethod]
46+
public void CacheStore_StoreResponseInCache()
47+
{
48+
var test = new SimpleCacheStore().AsTestDocument();
49+
var cache = test.SetupCacheStore();
50+
test.SetupCacheInfo().WithExecutedCacheLookup();
51+
test.SetupOutbound().CacheStore().WithCacheKey("key");
52+
53+
test.RunOutbound();
54+
55+
var cacheValue = cache.InternalCache.Should().ContainKey("key").WhoseValue;
56+
cacheValue.Duration.Should().Be(10);
57+
var response = cacheValue.Value.Should().BeAssignableTo<IResponse>().Which;
58+
var contextResponse = test.Context.Response;
59+
response.Should().NotBeSameAs(contextResponse, "Should be a copy of response");
60+
response.StatusCode.Should().Be(contextResponse.StatusCode);
61+
response.StatusReason.Should().Be(contextResponse.StatusReason);
62+
response.Headers.Should().Equal(contextResponse.Headers);
63+
}
64+
65+
[TestMethod]
66+
public void CacheStore_NotStoreIfResponseIsNot200()
67+
{
68+
var test = new SimpleCacheStore().AsTestDocument();
69+
test.Context.Response.StatusCode = 401;
70+
test.Context.Response.StatusReason = "Unauthorized";
71+
var cache = test.SetupCacheStore();
72+
test.SetupCacheInfo().WithExecutedCacheLookup();
73+
test.SetupOutbound().CacheStore().WithCacheKey("key");
74+
75+
test.RunOutbound();
76+
77+
cache.InternalCache.Should().NotContainKey("key");
78+
}
79+
80+
[TestMethod]
81+
public void CacheStore_StoreIfResponseIsNot200_WhenCacheResponseIsSetToTrue()
82+
{
83+
var test = new SimpleCacheStoreStoreResponse().AsTestDocument();
84+
var contextResponse = test.Context.Response;
85+
contextResponse.StatusCode = 401;
86+
contextResponse.StatusReason = "Unauthorized";
87+
var cache = test.SetupCacheStore();
88+
test.SetupCacheInfo().WithExecutedCacheLookup();
89+
test.SetupOutbound().CacheStore().WithCacheKey("key");
90+
91+
test.RunOutbound();
92+
93+
var cacheValue = cache.InternalCache.Should().ContainKey("key").WhoseValue;
94+
cacheValue.Duration.Should().Be(10);
95+
var response = cacheValue.Value.Should().BeAssignableTo<IResponse>().Which;
96+
response.Should().NotBeSameAs(contextResponse, "Should be a copy of response");
97+
response.StatusCode.Should().Be(contextResponse.StatusCode);
98+
response.StatusReason.Should().Be(contextResponse.StatusReason);
99+
response.Headers.Should().Equal(contextResponse.Headers);
100+
}
101+
}

0 commit comments

Comments
 (0)