Skip to content

Commit 2485b8c

Browse files
authored
Exclude test data from functions endpoint response (#10943) (#10981)
1 parent af98a6e commit 2485b8c

File tree

9 files changed

+169
-35
lines changed

9 files changed

+169
-35
lines changed

release_notes.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,6 @@
22

33
<!-- Please add your release notes in the following format:
44
- My change description (#PR)
5-
-->
5+
-->
6+
7+
- Added the option to exclude test data from the `/functions` endpoint API response (#10943)

src/WebJobs.Script.WebHost/Extensions/FunctionMetadataExtensions.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public static class FunctionMetadataExtensions
2020
/// <param name="functionMetadata">FunctionMetadata to be mapped.</param>
2121
/// <param name="hostOptions">The host options.</param>
2222
/// <returns>Promise of a FunctionMetadataResponse.</returns>
23-
public static async Task<FunctionMetadataResponse> ToFunctionMetadataResponse(this FunctionMetadata functionMetadata, ScriptJobHostOptions hostOptions, string routePrefix, string baseUrl)
23+
public static async Task<FunctionMetadataResponse> ToFunctionMetadataResponse(this FunctionMetadata functionMetadata, ScriptJobHostOptions hostOptions, string routePrefix, string baseUrl, bool excludeTestData)
2424
{
2525
string functionPath = GetFunctionPathOrNull(hostOptions.RootScriptPath, functionMetadata.Name);
2626
string functionMetadataFilePath = GetMetadataPathOrNull(functionPath);
@@ -54,7 +54,7 @@ public static async Task<FunctionMetadataResponse> ToFunctionMetadataResponse(th
5454
response.ConfigHref = VirtualFileSystem.FilePathToVfsUri(functionMetadataFilePath, baseUrl, hostOptions);
5555
}
5656

57-
if (!string.IsNullOrEmpty(hostOptions.TestDataPath))
57+
if (!excludeTestData && !string.IsNullOrEmpty(hostOptions.TestDataPath))
5858
{
5959
var testDataFilePath = functionMetadata.GetTestDataFilePath(hostOptions);
6060
response.TestDataHref = VirtualFileSystem.FilePathToVfsUri(testDataFilePath, baseUrl, hostOptions);
@@ -183,7 +183,7 @@ private static JObject GetFunctionConfigFromMetadata(FunctionMetadata metadata)
183183

184184
private static async Task<string> GetTestData(string testDataPath, ScriptJobHostOptions config)
185185
{
186-
if (!File.Exists(testDataPath))
186+
if (!FileUtility.FileExists(testDataPath))
187187
{
188188
FileUtility.EnsureDirectoryExists(Path.GetDirectoryName(testDataPath));
189189
await FileUtility.WriteAsync(testDataPath, string.Empty);

src/WebJobs.Script.WebHost/Management/FunctionsSyncManager.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,7 @@ private async Task<SyncTriggersPayload> GetSyncTriggersPayload()
339339

340340
// Add all listable functions details to the payload
341341
var listableFunctions = _functionMetadataManager.GetFunctionMetadata().Where(m => !m.IsCodeless());
342-
var functionDetails = await WebFunctionsManager.GetFunctionMetadataResponse(listableFunctions, hostOptions, _hostNameProvider);
342+
var functionDetails = await WebFunctionsManager.GetFunctionMetadataResponse(listableFunctions, hostOptions, _hostNameProvider, excludeTestData: _hostingConfigOptions.Value.IsTestDataSuppressionEnabled);
343343
result.Add("functions", new JArray(functionDetails.Select(p => JObject.FromObject(p))));
344344

345345
// TEMP: refactor this code to properly add extensions in all scenario(#7394)

src/WebJobs.Script.WebHost/Management/WebFunctionsManager.cs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using System.Net.Http;
99
using System.Threading.Tasks;
1010
using Microsoft.AspNetCore.Http;
11+
using Microsoft.Azure.WebJobs.Script.Config;
1112
using Microsoft.Azure.WebJobs.Script.Description;
1213
using Microsoft.Azure.WebJobs.Script.Management.Models;
1314
using Microsoft.Azure.WebJobs.Script.WebHost.Extensions;
@@ -27,8 +28,9 @@ public class WebFunctionsManager : IWebFunctionsManager
2728
private readonly IFunctionsSyncManager _functionsSyncManager;
2829
private readonly HostNameProvider _hostNameProvider;
2930
private readonly IFunctionMetadataManager _functionMetadataManager;
31+
private readonly IOptionsMonitor<FunctionsHostingConfigOptions> _hostingConfigOptions;
3032

31-
public WebFunctionsManager(IOptionsMonitor<ScriptApplicationHostOptions> applicationHostOptions, ILoggerFactory loggerFactory, IHttpClientFactory httpClientFactory, ISecretManagerProvider secretManagerProvider, IFunctionsSyncManager functionsSyncManager, HostNameProvider hostNameProvider, IFunctionMetadataManager functionMetadataManager)
33+
public WebFunctionsManager(IOptionsMonitor<ScriptApplicationHostOptions> applicationHostOptions, ILoggerFactory loggerFactory, IHttpClientFactory httpClientFactory, ISecretManagerProvider secretManagerProvider, IFunctionsSyncManager functionsSyncManager, HostNameProvider hostNameProvider, IFunctionMetadataManager functionMetadataManager, IOptionsMonitor<FunctionsHostingConfigOptions> hostingConfigOptions)
3234
{
3335
_applicationHostOptions = applicationHostOptions;
3436
_logger = loggerFactory?.CreateLogger(ScriptConstants.LogCategoryHostGeneral);
@@ -37,21 +39,22 @@ public WebFunctionsManager(IOptionsMonitor<ScriptApplicationHostOptions> applica
3739
_functionsSyncManager = functionsSyncManager;
3840
_hostNameProvider = hostNameProvider;
3941
_functionMetadataManager = functionMetadataManager;
42+
_hostingConfigOptions = hostingConfigOptions;
4043
}
4144

4245
public async Task<IEnumerable<FunctionMetadataResponse>> GetFunctionsMetadata(bool includeProxies)
4346
{
4447
var hostOptions = _applicationHostOptions.CurrentValue.ToHostOptions();
4548
var functionsMetadata = GetFunctionsMetadata(includeProxies, forceRefresh: false);
4649

47-
return await GetFunctionMetadataResponse(functionsMetadata, hostOptions, _hostNameProvider);
50+
return await GetFunctionMetadataResponse(functionsMetadata, hostOptions, _hostNameProvider, excludeTestData: _hostingConfigOptions.CurrentValue.IsTestDataSuppressionEnabled);
4851
}
4952

50-
internal static async Task<IEnumerable<FunctionMetadataResponse>> GetFunctionMetadataResponse(IEnumerable<FunctionMetadata> functionsMetadata, ScriptJobHostOptions hostOptions, HostNameProvider hostNameProvider)
53+
internal static async Task<IEnumerable<FunctionMetadataResponse>> GetFunctionMetadataResponse(IEnumerable<FunctionMetadata> functionsMetadata, ScriptJobHostOptions hostOptions, HostNameProvider hostNameProvider, bool excludeTestData)
5154
{
5255
string baseUrl = GetBaseUrl(hostNameProvider);
5356
string routePrefix = await GetRoutePrefix(hostOptions.RootScriptPath);
54-
var tasks = functionsMetadata.Select(p => p.ToFunctionMetadataResponse(hostOptions, routePrefix, baseUrl));
57+
var tasks = functionsMetadata.Select(p => p.ToFunctionMetadataResponse(hostOptions, routePrefix, baseUrl, excludeTestData));
5558

5659
return await tasks.WhenAll();
5760
}
@@ -139,7 +142,7 @@ await functionMetadata
139142
configChanged = true;
140143
}
141144

142-
if (functionMetadata.TestData != null)
145+
if (functionMetadata.TestData != null && !_hostingConfigOptions.CurrentValue.IsTestDataSuppressionEnabled)
143146
{
144147
await FileUtility.WriteAsync(dataFilePath, functionMetadata.TestData);
145148
}
@@ -180,7 +183,7 @@ await functionMetadata
180183
{
181184
string routePrefix = await GetRoutePrefix(hostOptions.RootScriptPath);
182185
var baseUrl = $"{request.Scheme}://{request.Host}";
183-
return (true, await functionMetadata.ToFunctionMetadataResponse(hostOptions, routePrefix, baseUrl));
186+
return (true, await functionMetadata.ToFunctionMetadataResponse(hostOptions, routePrefix, baseUrl, excludeTestData: _hostingConfigOptions.CurrentValue.IsTestDataSuppressionEnabled));
184187
}
185188
else
186189
{

src/WebJobs.Script/Config/FunctionsHostingConfigOptions.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,22 @@ internal bool EnableOrderedInvocationMessages
147147
}
148148
}
149149

150+
/// <summary>
151+
/// Gets or sets a value indicating whether to ignore the TestData property during read and write operations of functions metadata.
152+
/// </summary>
153+
internal bool IsTestDataSuppressionEnabled
154+
{
155+
get
156+
{
157+
return GetFeatureAsBooleanOrDefault(ScriptConstants.FeatureFlagEnableTestDataSuppression, false);
158+
}
159+
160+
set
161+
{
162+
_features[ScriptConstants.FeatureFlagEnableTestDataSuppression] = value ? "1" : "0";
163+
}
164+
}
165+
150166
/// <summary>
151167
/// Gets the highest version of extension bundle v3 supported.
152168
/// </summary>

src/WebJobs.Script/ScriptConstants.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ public static class ScriptConstants
138138
public const string FeatureFlagEnableOrderedInvocationmessages = "EnableOrderedInvocationMessages";
139139
public const string FeatureFlagDisableOrderedInvocationMessages = "DisableOrderedInvocationMessages";
140140
public const string FeatureFlagEnableAzureMonitorTimeIsoFormat = "EnableAzureMonitorTimeIsoFormat";
141+
public const string FeatureFlagEnableTestDataSuppression = "EnableTestDataSuppression";
141142
public const string HostingConfigDisableLinuxAppServiceDetailedExecutionEvents = "DisableLinuxExecutionDetails";
142143
public const string HostingConfigDisableLinuxAppServiceExecutionEventLogBackoff = "DisableLinuxLogBackoff";
143144
public const string FeatureFlagEnableLegacyDurableVersionCheck = "EnableLegacyDurableVersionCheck";

test/WebJobs.Script.Tests/Configuration/FunctionsHostingConfigOptionsTest.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,9 @@ public void Property_Validation()
135135
(nameof(FunctionsHostingConfigOptions.WorkerRuntimeStrictValidationEnabled), "WORKER_RUNTIME_STRICT_VALIDATION_ENABLED=1", true),
136136

137137
(nameof(FunctionsHostingConfigOptions.InternalAuthApisAllowList), "InternalAuthApisAllowList=|", "|"),
138-
(nameof(FunctionsHostingConfigOptions.InternalAuthApisAllowList), "InternalAuthApisAllowList=/admin/host/foo|/admin/host/bar", "/admin/host/foo|/admin/host/bar")
138+
(nameof(FunctionsHostingConfigOptions.InternalAuthApisAllowList), "InternalAuthApisAllowList=/admin/host/foo|/admin/host/bar", "/admin/host/foo|/admin/host/bar"),
139+
140+
(nameof(FunctionsHostingConfigOptions.IsTestDataSuppressionEnabled), "EnableTestDataSuppression=1", true)
139141
};
140142

141143
// use reflection to ensure that we have a test that uses every value exposed on FunctionsHostingConfigOptions

test/WebJobs.Script.Tests/Extensions/FunctionMetadataExtensionsTests.cs

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@
22
// Licensed under the MIT License. See License.txt in the project root for license information.
33

44
using System.IO;
5+
using System.IO.Abstractions;
6+
using System.Text;
57
using System.Threading.Tasks;
68
using Microsoft.Azure.WebJobs.Script.Description;
79
using Microsoft.Azure.WebJobs.Script.WebHost.Extensions;
10+
using Moq;
811
using Newtonsoft.Json.Linq;
912
using Xunit;
1013

@@ -123,7 +126,7 @@ public async Task ToFunctionMetadataResponse_WithoutFiles_ReturnsExpected()
123126
};
124127

125128
AddSampleBindings(functionMetadata);
126-
var result = await functionMetadata.ToFunctionMetadataResponse(options, string.Empty, null);
129+
var result = await functionMetadata.ToFunctionMetadataResponse(options, string.Empty, null, excludeTestData: false);
127130

128131
Assert.Null(result.ScriptRootPathHref);
129132
Assert.Null(result.ConfigHref);
@@ -133,6 +136,66 @@ public async Task ToFunctionMetadataResponse_WithoutFiles_ReturnsExpected()
133136
Assert.Equal("httpTrigger", binding[0]["type"].Value<string>());
134137
}
135138

139+
[Theory]
140+
[InlineData(true)]
141+
[InlineData(false)]
142+
public async Task ToFunctionMetadataResponse_TestData_Exclusion(bool shouldExcludeTestDataFromApiResponse)
143+
{
144+
var functionName = "TestFunction1";
145+
var functionMetadata = new FunctionMetadata
146+
{
147+
Name = functionName
148+
};
149+
var testDataFileName = $"{functionName}.dat";
150+
var testDataFilePath = Path.Combine(_testRootScriptPath, testDataFileName);
151+
var options = new ScriptJobHostOptions
152+
{
153+
RootScriptPath = _testRootScriptPath,
154+
TestDataPath = testDataFilePath
155+
};
156+
157+
IFileSystem fileSystem = FileUtility.Instance;
158+
159+
try
160+
{
161+
var testDataContent = @"
162+
{
163+
""method"": ""POST"",
164+
""headers"": [],
165+
""body"": ""foo""
166+
}";
167+
168+
var memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(testDataContent))
169+
{
170+
Position = 0
171+
};
172+
// Mock the file system
173+
var mockFileSystem = new Mock<IFileSystem>();
174+
var mockFile = new Mock<FileBase>();
175+
mockFileSystem.Setup(f => f.Directory.Exists(It.IsAny<string>())).Returns(true);
176+
mockFile.Setup(f => f.Exists(It.Is<string>(path => path.EndsWith(Path.Combine(functionName, "function.json"))))).Returns(true);
177+
mockFile.Setup(f => f.Exists(It.Is<string>(path => path.EndsWith(testDataFileName)))).Returns(true);
178+
mockFile.Setup(f => f.Open(It.Is<string>(path => path.EndsWith(testDataFileName)), It.IsAny<FileMode>(), It.IsAny<FileAccess>(), It.IsAny<FileShare>()))
179+
.Returns(memoryStream);
180+
mockFileSystem.Setup(fs => fs.File).Returns(mockFile.Object);
181+
FileUtility.Instance = mockFileSystem.Object;
182+
183+
AddSampleBindings(functionMetadata);
184+
var result = await functionMetadata.ToFunctionMetadataResponse(options, string.Empty, null, excludeTestData: shouldExcludeTestDataFromApiResponse);
185+
186+
Assert.Equal(functionName, result.Name);
187+
Assert.Equal(shouldExcludeTestDataFromApiResponse, result.TestData == null);
188+
if (!shouldExcludeTestDataFromApiResponse)
189+
{
190+
Assert.True(result.TestData.Trim().Length > 0);
191+
}
192+
}
193+
finally
194+
{
195+
FileUtility.Instance = fileSystem;
196+
}
197+
}
198+
136199
[Fact]
137200
public void GetFunctionInvokeUrlTemplate_ReturnsExpectedResult()
138201
{

0 commit comments

Comments
 (0)