diff --git a/src/Authoring/IOutboundContext.cs b/src/Authoring/IOutboundContext.cs
index 2e7b12c..1368a4c 100644
--- a/src/Authoring/IOutboundContext.cs
+++ b/src/Authoring/IOutboundContext.cs
@@ -88,7 +88,7 @@ public interface IOutboundContext : IHaveExpressionContext
///
///
///
- void CacheStore(uint duration, bool? cacheResponse);
+ void CacheStore(uint duration, bool? cacheResponse = null);
///
/// TODO
diff --git a/src/Testing/Document/MockCacheStoreProvider.cs b/src/Testing/Document/MockCacheStoreProvider.cs
index b1b1fed..e432835 100644
--- a/src/Testing/Document/MockCacheStoreProvider.cs
+++ b/src/Testing/Document/MockCacheStoreProvider.cs
@@ -2,7 +2,6 @@
// Licensed under the MIT License.
using Azure.ApiManagement.PolicyToolkit.Authoring;
-using Azure.ApiManagement.PolicyToolkit.Testing.Emulator;
using Azure.ApiManagement.PolicyToolkit.Testing.Emulator.Policies;
namespace Azure.ApiManagement.PolicyToolkit.Testing.Document;
@@ -36,5 +35,10 @@ internal Setup(
public void WithCallback(Action callback) =>
_handler.CallbackHooks.Add((_predicate, callback).ToTuple());
+
+ public void WithCacheKey(Func callback) =>
+ _handler.CacheKeyProvider.Add((_predicate, callback).ToTuple());
+
+ public void WithCacheKey(string key) => this.WithCacheKey((_, _, _) => key);
}
}
\ No newline at end of file
diff --git a/src/Testing/Document/TestDocumentExtensions.cs b/src/Testing/Document/TestDocumentExtensions.cs
index 6336346..5e3b6b7 100644
--- a/src/Testing/Document/TestDocumentExtensions.cs
+++ b/src/Testing/Document/TestDocumentExtensions.cs
@@ -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;
}
\ No newline at end of file
diff --git a/src/Testing/Emulator/Data/CacheInfo.cs b/src/Testing/Emulator/Data/CacheInfo.cs
new file mode 100644
index 0000000..e2f7925
--- /dev/null
+++ b/src/Testing/Emulator/Data/CacheInfo.cs
@@ -0,0 +1,93 @@
+using System.Text;
+
+using Azure.ApiManagement.PolicyToolkit.Authoring;
+
+namespace Azure.ApiManagement.PolicyToolkit.Testing.Emulator.Data;
+
+public class CacheInfo
+{
+ internal bool CacheSetup = false;
+
+ internal bool VaryByDeveloper = false;
+ internal bool VaryByDeveloperGroups = false;
+ internal string CachingType = "prefer-external";
+ internal string DownstreamCachingType = "none";
+ internal bool MustRevalidate = true;
+ internal bool AllowPrivateResponseCaching = false;
+ internal string[]? VaryByHeaders;
+ internal string[]? VaryByQueryParameters;
+
+ public CacheInfo WithExecutedCacheLookup(bool isSetup = true)
+ {
+ CacheSetup = isSetup;
+ return this;
+ }
+
+ internal 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, "¶ms:", 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 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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Testing/Emulator/Policies/CacheStoreHandler.cs b/src/Testing/Emulator/Policies/CacheStoreHandler.cs
index 807e41e..29896e7 100644
--- a/src/Testing/Emulator/Policies/CacheStoreHandler.cs
+++ b/src/Testing/Emulator/Policies/CacheStoreHandler.cs
@@ -2,6 +2,7 @@
// Licensed under the MIT License.
using Azure.ApiManagement.PolicyToolkit.Authoring;
+using Azure.ApiManagement.PolicyToolkit.Testing.Emulator.Data;
namespace Azure.ApiManagement.PolicyToolkit.Testing.Emulator.Policies;
@@ -13,6 +14,11 @@ public List
>> CallbackHooks { get; } = new();
+ public List,
+ Func
+ >> CacheKeyProvider { get; } = new();
+
public string PolicyName => nameof(IOutboundContext.CacheStore);
public object? Handle(GatewayContext context, object?[]? args)
@@ -32,9 +38,31 @@ public List 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)
@@ -49,12 +77,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));
}
diff --git a/src/Testing/Expressions/MockResponse.cs b/src/Testing/Expressions/MockResponse.cs
index cd8477f..1fe11c3 100644
--- a/src/Testing/Expressions/MockResponse.cs
+++ b/src/Testing/Expressions/MockResponse.cs
@@ -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, },
+ };
}
\ No newline at end of file
diff --git a/src/Testing/GatewayContext.cs b/src/Testing/GatewayContext.cs
index e478d20..113c6a8 100644
--- a/src/Testing/GatewayContext.cs
+++ b/src/Testing/GatewayContext.cs
@@ -16,6 +16,7 @@ public class GatewayContext : MockExpressionContext
internal readonly SectionContextProxy OnErrorProxy;
internal readonly CertificateStore CertificateStore = new();
internal readonly CacheStore CacheStore = new();
+ internal readonly CacheInfo CacheInfo = new();
public GatewayContext()
{
diff --git a/test/Test.Testing/Emulator/Policies/CacheStoreTests.cs b/test/Test.Testing/Emulator/Policies/CacheStoreTests.cs
new file mode 100644
index 0000000..fe14d9b
--- /dev/null
+++ b/test/Test.Testing/Emulator/Policies/CacheStoreTests.cs
@@ -0,0 +1,101 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using Azure.ApiManagement.PolicyToolkit.Authoring;
+using Azure.ApiManagement.PolicyToolkit.Authoring.Expressions;
+using Azure.ApiManagement.PolicyToolkit.Testing;
+using Azure.ApiManagement.PolicyToolkit.Testing.Document;
+
+namespace Test.Emulator.Emulator.Policies;
+
+[TestClass]
+public class CacheStoreTests
+{
+ class SimpleCacheStore : IDocument
+ {
+ public void Outbound(IOutboundContext context)
+ {
+ context.CacheStore(10);
+ }
+ }
+
+ class SimpleCacheStoreStoreResponse : IDocument
+ {
+ public void Outbound(IOutboundContext context)
+ {
+ context.CacheStore(10, true);
+ }
+ }
+
+ [TestMethod]
+ public void CacheStore_Callback()
+ {
+ var test = new SimpleCacheStore().AsTestDocument();
+ var executedCallback = false;
+ test.SetupOutbound().CacheStore().WithCallback((_, _, _) =>
+ {
+ executedCallback = true;
+ });
+
+ test.RunOutbound();
+
+ executedCallback.Should().BeTrue();
+ }
+
+ [TestMethod]
+ public void CacheStore_StoreResponseInCache()
+ {
+ var test = new SimpleCacheStore().AsTestDocument();
+ var cache = test.SetupCacheStore();
+ test.SetupCacheInfo().WithExecutedCacheLookup();
+ test.SetupOutbound().CacheStore().WithCacheKey("key");
+
+ test.RunOutbound();
+
+ var cacheValue = cache.InternalCache.Should().ContainKey("key").WhoseValue;
+ cacheValue.Duration.Should().Be(10);
+ var response = cacheValue.Value.Should().BeAssignableTo().Which;
+ var contextResponse = test.Context.Response;
+ response.Should().NotBeSameAs(contextResponse, "Should be a copy of response");
+ response.StatusCode.Should().Be(contextResponse.StatusCode);
+ response.StatusReason.Should().Be(contextResponse.StatusReason);
+ response.Headers.Should().Equal(contextResponse.Headers);
+ }
+
+ [TestMethod]
+ public void CacheStore_NotStoreIfResponseIsNot200()
+ {
+ var test = new SimpleCacheStore().AsTestDocument();
+ test.Context.Response.StatusCode = 401;
+ test.Context.Response.StatusReason = "Unauthorized";
+ var cache = test.SetupCacheStore();
+ test.SetupCacheInfo().WithExecutedCacheLookup();
+ test.SetupOutbound().CacheStore().WithCacheKey("key");
+
+ test.RunOutbound();
+
+ cache.InternalCache.Should().NotContainKey("key");
+ }
+
+ [TestMethod]
+ public void CacheStore_StoreIfResponseIsNot200_WhenCacheResponseIsSetToTrue()
+ {
+ var test = new SimpleCacheStoreStoreResponse().AsTestDocument();
+ var contextResponse = test.Context.Response;
+ contextResponse.StatusCode = 401;
+ contextResponse.StatusReason = "Unauthorized";
+ var cache = test.SetupCacheStore();
+ test.SetupCacheInfo().WithExecutedCacheLookup();
+ test.SetupOutbound().CacheStore().WithCacheKey("key");
+
+ test.RunOutbound();
+
+ var cacheValue = cache.InternalCache.Should().ContainKey("key").WhoseValue;
+ cacheValue.Duration.Should().Be(10);
+ var response = cacheValue.Value.Should().BeAssignableTo().Which;
+ response.Should().NotBeSameAs(contextResponse, "Should be a copy of response");
+ response.StatusCode.Should().Be(contextResponse.StatusCode);
+ response.StatusReason.Should().Be(contextResponse.StatusReason);
+ response.Headers.Should().Equal(contextResponse.Headers);
+ }
+}
\ No newline at end of file