Skip to content

Commit c53261c

Browse files
Copilotanushakolan
andcommitted
Address PR feedback: add unknown property skip, fix docs, update logging, and add compression integration tests
Co-authored-by: anushakolan <[email protected]>
1 parent 1a8cd13 commit c53261c

4 files changed

Lines changed: 312 additions & 2 deletions

File tree

src/Cli.Tests/ConfigureOptionsTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -543,7 +543,7 @@ public void TestUpdateTTLForCacheSettings(int updatedTtlValue)
543543
/// <summary>
544544
/// Tests that running "dab configure --runtime.compression.level {value}" on a config with various values results
545545
/// in runtime config update. Takes in updated value for compression.level and
546-
/// validates whether the runtime config reflects those updated values
546+
/// validates whether the runtime config reflects those updated values.
547547
[DataTestMethod]
548548
[DataRow(CompressionLevel.Fastest, DisplayName = "Update Compression.Level to fastest.")]
549549
[DataRow(CompressionLevel.Optimal, DisplayName = "Update Compression.Level to optimal.")]

src/Cli/ConfigGenerator.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1238,7 +1238,7 @@ private static bool TryUpdateConfiguredCompressionValues(
12381238
}
12391239
catch (Exception ex)
12401240
{
1241-
_logger.LogError("Failed to update RuntimeConfig.Compression with exception message: {exceptionMessage}.", ex.Message);
1241+
_logger.LogError("Failed to configure RuntimeConfig.Compression with exception message: {exceptionMessage}.", ex.Message);
12421242
return false;
12431243
}
12441244
}

src/Config/Converters/CompressionOptionsConverterFactory.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,11 @@ private class CompressionOptionsConverter : JsonConverter<CompressionOptions>
7373
}
7474
}
7575
}
76+
else
77+
{
78+
// Skip unknown properties and their values (including objects/arrays)
79+
reader.Skip();
80+
}
7681
}
7782
}
7883

Lines changed: 305 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,305 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.IO;
6+
using System.IO.Compression;
7+
using System.Linq;
8+
using System.Net;
9+
using System.Net.Http;
10+
using System.Net.Http.Headers;
11+
using System.Text;
12+
using System.Text.Json;
13+
using System.Threading.Tasks;
14+
using Azure.DataApiBuilder.Core.Models;
15+
using Azure.DataApiBuilder.Core.Services;
16+
using Microsoft.AspNetCore.Builder;
17+
using Microsoft.AspNetCore.Hosting;
18+
using Microsoft.AspNetCore.Http;
19+
using Microsoft.AspNetCore.ResponseCompression;
20+
using Microsoft.AspNetCore.TestHost;
21+
using Microsoft.Extensions.DependencyInjection;
22+
using Microsoft.Extensions.Hosting;
23+
using Microsoft.VisualStudio.TestTools.UnitTesting;
24+
using DabCompressionLevel = Azure.DataApiBuilder.Config.ObjectModel.CompressionLevel;
25+
using SystemCompressionLevel = System.IO.Compression.CompressionLevel;
26+
27+
namespace Azure.DataApiBuilder.Service.Tests.Configuration
28+
{
29+
/// <summary>
30+
/// Integration tests for HTTP response compression middleware.
31+
/// Validates that compression reduces payload sizes and doesn't break existing functionality.
32+
/// </summary>
33+
[TestClass]
34+
public class CompressionIntegrationTests
35+
{
36+
// Sample JSON payload for testing compression
37+
private static readonly string _sampleJsonPayload = JsonSerializer.Serialize(new
38+
{
39+
data = Enumerable.Range(1, 100).Select(i => new
40+
{
41+
id = i,
42+
title = $"Book Title {i}",
43+
author = $"Author Name {i}",
44+
description = $"This is a long description for book {i} to ensure we have enough data to compress effectively. " +
45+
"Compression works best with repetitive text and structured data like JSON."
46+
})
47+
});
48+
49+
#region Positive Tests
50+
51+
/// <summary>
52+
/// Verify that responses are compressed when client sends Accept-Encoding header with gzip.
53+
/// </summary>
54+
[TestMethod("Responses are compressed with gzip when Accept-Encoding header is present.")]
55+
public async Task TestResponseIsCompressedWithGzip()
56+
{
57+
IHost host = await CreateCompressionConfiguredWebHost(DabCompressionLevel.Optimal);
58+
TestServer server = host.GetTestServer();
59+
60+
HttpContext returnContext = await server.SendAsync(context =>
61+
{
62+
context.Request.Headers.AcceptEncoding = "gzip";
63+
});
64+
65+
// Verify Content-Encoding header is present
66+
Assert.IsTrue(returnContext.Response.Headers.ContentEncoding.Contains("gzip"),
67+
"Response should have gzip Content-Encoding header");
68+
69+
// Verify response body exists by checking if we can read it
70+
using (var reader = new StreamReader(returnContext.Response.Body, leaveOpen: false))
71+
{
72+
string content = await reader.ReadToEndAsync();
73+
Assert.IsTrue(content.Length > 0, "Response body should not be empty");
74+
}
75+
}
76+
77+
/// <summary>
78+
/// Verify that responses are compressed with Brotli when client requests it.
79+
/// </summary>
80+
[TestMethod("Responses are compressed with Brotli when Accept-Encoding header specifies br.")]
81+
public async Task TestResponseIsCompressedWithBrotli()
82+
{
83+
IHost host = await CreateCompressionConfiguredWebHost(DabCompressionLevel.Optimal);
84+
TestServer server = host.GetTestServer();
85+
86+
HttpContext returnContext = await server.SendAsync(context =>
87+
{
88+
context.Request.Headers.AcceptEncoding = "br";
89+
});
90+
91+
Assert.IsTrue(returnContext.Response.Headers.ContentEncoding.Contains("br"),
92+
"Response should have br Content-Encoding header");
93+
}
94+
95+
/// <summary>
96+
/// Verify that compression reduces payload size significantly.
97+
/// </summary>
98+
[TestMethod("Compression reduces payload size for JSON responses.")]
99+
public async Task TestCompressionReducesPayloadSize()
100+
{
101+
IHost host = await CreateCompressionConfiguredWebHost(DabCompressionLevel.Optimal);
102+
TestServer server = host.GetTestServer();
103+
104+
// Get uncompressed response
105+
HttpContext uncompressedContext = await server.SendAsync(context =>
106+
{
107+
// Don't set Accept-Encoding
108+
});
109+
110+
using (var ms = new MemoryStream())
111+
{
112+
await uncompressedContext.Response.Body.CopyToAsync(ms);
113+
long uncompressedSize = ms.Length;
114+
115+
// Get compressed response
116+
HttpContext compressedContext = await server.SendAsync(context =>
117+
{
118+
context.Request.Headers.AcceptEncoding = "gzip";
119+
});
120+
121+
using (var cms = new MemoryStream())
122+
{
123+
await compressedContext.Response.Body.CopyToAsync(cms);
124+
long compressedSize = cms.Length;
125+
126+
// Verify compressed size is smaller
127+
Assert.IsTrue(compressedSize < uncompressedSize,
128+
$"Compressed size ({compressedSize}) should be less than uncompressed size ({uncompressedSize})");
129+
130+
// Calculate compression ratio
131+
double compressionRatio = (double)(uncompressedSize - compressedSize) / uncompressedSize * 100;
132+
Console.WriteLine($"Compression achieved: {compressionRatio:F2}% reduction (from {uncompressedSize} to {compressedSize} bytes)");
133+
134+
// Verify at least some compression occurred (at least 10% for JSON)
135+
Assert.IsTrue(compressionRatio > 10, $"Compression ratio should be at least 10%, got {compressionRatio:F2}%");
136+
}
137+
}
138+
}
139+
140+
/// <summary>
141+
/// Verify that compression is disabled when level is set to None.
142+
/// </summary>
143+
[TestMethod("Responses are not compressed when compression level is None.")]
144+
public async Task TestCompressionDisabledWhenLevelIsNone()
145+
{
146+
IHost host = await CreateCompressionConfiguredWebHost(DabCompressionLevel.None);
147+
TestServer server = host.GetTestServer();
148+
149+
HttpContext returnContext = await server.SendAsync(context =>
150+
{
151+
context.Request.Headers.AcceptEncoding = "gzip";
152+
});
153+
154+
Assert.IsFalse(returnContext.Response.Headers.ContentEncoding.Any(),
155+
"Response should not have Content-Encoding header when compression is disabled");
156+
}
157+
158+
/// <summary>
159+
/// Verify that responses are not compressed when client doesn't send Accept-Encoding.
160+
/// </summary>
161+
[TestMethod("Responses are not compressed without Accept-Encoding header.")]
162+
public async Task TestNoCompressionWithoutAcceptEncoding()
163+
{
164+
IHost host = await CreateCompressionConfiguredWebHost(DabCompressionLevel.Optimal);
165+
TestServer server = host.GetTestServer();
166+
167+
HttpContext returnContext = await server.SendAsync(context =>
168+
{
169+
// Don't set Accept-Encoding header
170+
});
171+
172+
Assert.IsFalse(returnContext.Response.Headers.ContentEncoding.Any(),
173+
"Response should not be compressed without Accept-Encoding header");
174+
}
175+
176+
/// <summary>
177+
/// Verify that fastest compression level works correctly.
178+
/// </summary>
179+
[TestMethod("Compression works with fastest level.")]
180+
public async Task TestCompressionWithFastestLevel()
181+
{
182+
IHost host = await CreateCompressionConfiguredWebHost(DabCompressionLevel.Fastest);
183+
TestServer server = host.GetTestServer();
184+
185+
HttpContext returnContext = await server.SendAsync(context =>
186+
{
187+
context.Request.Headers.AcceptEncoding = "gzip";
188+
});
189+
190+
Assert.IsTrue(returnContext.Response.Headers.ContentEncoding.Contains("gzip"),
191+
"Response should be compressed with fastest level");
192+
}
193+
194+
/// <summary>
195+
/// Verify that compressed content can be decompressed correctly.
196+
/// </summary>
197+
[TestMethod("Compressed content can be decompressed and is valid JSON.")]
198+
public async Task TestCompressedContentCanBeDecompressed()
199+
{
200+
IHost host = await CreateCompressionConfiguredWebHost(DabCompressionLevel.Optimal);
201+
TestServer server = host.GetTestServer();
202+
203+
HttpContext returnContext = await server.SendAsync(context =>
204+
{
205+
context.Request.Headers.AcceptEncoding = "gzip";
206+
});
207+
208+
// Read compressed data
209+
using (var ms = new MemoryStream())
210+
{
211+
await returnContext.Response.Body.CopyToAsync(ms);
212+
byte[] compressedData = ms.ToArray();
213+
214+
// Decompress
215+
string decompressedContent = DecompressGzip(compressedData);
216+
Assert.IsFalse(string.IsNullOrEmpty(decompressedContent), "Decompressed content should not be empty");
217+
218+
// Verify it's valid JSON matching our sample
219+
JsonDocument doc = JsonDocument.Parse(decompressedContent);
220+
Assert.IsTrue(doc.RootElement.TryGetProperty("data", out _), "Decompressed JSON should contain 'data' property");
221+
}
222+
}
223+
224+
#endregion
225+
226+
#region Helper Methods
227+
228+
/// <summary>
229+
/// Creates a minimal compression-configured WebHost for testing.
230+
/// </summary>
231+
private static async Task<IHost> CreateCompressionConfiguredWebHost(DabCompressionLevel level)
232+
{
233+
return await new HostBuilder()
234+
.ConfigureWebHost(webBuilder =>
235+
{
236+
webBuilder
237+
.UseTestServer()
238+
.ConfigureServices(services =>
239+
{
240+
services.AddHttpContextAccessor();
241+
242+
// Add response compression based on level
243+
if (level != DabCompressionLevel.None)
244+
{
245+
SystemCompressionLevel systemLevel = level switch
246+
{
247+
DabCompressionLevel.Fastest => SystemCompressionLevel.Fastest,
248+
DabCompressionLevel.Optimal => SystemCompressionLevel.Optimal,
249+
_ => SystemCompressionLevel.Optimal
250+
};
251+
252+
services.AddResponseCompression(options =>
253+
{
254+
options.EnableForHttps = true;
255+
options.Providers.Add<GzipCompressionProvider>();
256+
options.Providers.Add<BrotliCompressionProvider>();
257+
});
258+
259+
services.Configure<GzipCompressionProviderOptions>(options =>
260+
{
261+
options.Level = systemLevel;
262+
});
263+
264+
services.Configure<BrotliCompressionProviderOptions>(options =>
265+
{
266+
options.Level = systemLevel;
267+
});
268+
}
269+
})
270+
.Configure(app =>
271+
{
272+
// Add response compression middleware
273+
if (level != DabCompressionLevel.None)
274+
{
275+
app.UseResponseCompression();
276+
}
277+
278+
// Simple endpoint that returns JSON
279+
app.Run(async context =>
280+
{
281+
context.Response.ContentType = "application/json";
282+
await context.Response.WriteAsync(_sampleJsonPayload);
283+
});
284+
});
285+
})
286+
.StartAsync();
287+
}
288+
289+
/// <summary>
290+
/// Decompresses gzip-compressed data.
291+
/// </summary>
292+
private static string DecompressGzip(byte[] data)
293+
{
294+
using (var compressedStream = new MemoryStream(data))
295+
using (var gzipStream = new GZipStream(compressedStream, CompressionMode.Decompress))
296+
using (var resultStream = new MemoryStream())
297+
{
298+
gzipStream.CopyTo(resultStream);
299+
return Encoding.UTF8.GetString(resultStream.ToArray());
300+
}
301+
}
302+
303+
#endregion
304+
}
305+
}

0 commit comments

Comments
 (0)