Skip to content

Commit 25aa714

Browse files
authored
Resolve Digest Round Trip Issues (#8325)
* we have a test case that actually exercises the round tripping * we have successful round trip repro of the problem. time to figure out how to fix it * ensure that content type 'application/vnd.docker.distribution.manifest.v2' is not treated as a text type, ensuring that the whitespace gets stored exactly as-is
1 parent cc51871 commit 25aa714

4 files changed

Lines changed: 177 additions & 10 deletions

File tree

tools/test-proxy/Azure.Sdk.Tools.TestProxy.Tests/RecordSessionTests.cs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,14 @@
99
using System.Text;
1010
using System.Text.Encodings.Web;
1111
using System.Text.Json;
12+
using System.Threading.Tasks;
1213
using Azure.Core;
1314
using Azure.Core.Pipeline;
1415
using Azure.Sdk.Tools.TestProxy.Common;
16+
using Microsoft.AspNetCore.Http;
17+
using Microsoft.VisualBasic;
1518
using Moq;
19+
using NuGet.ContentModel;
1620
using Xunit;
1721

1822
namespace Azure.Sdk.Tools.TestProxy.Tests
@@ -95,6 +99,82 @@ public void CanRoundtripSessionRecord(string body, string contentType)
9599
Assert.Equal(bodyBytes, deserializedRecord.Response.Body);
96100
}
97101

102+
[Fact]
103+
public async Task CanRoundTripDockerDigest()
104+
{
105+
// get everything organized
106+
var sampleExpected = "{\n \"schemaVersion\": 2,\n \"mediaType\": \"application/vnd.docker.distribution.manifest.v2+json\",\n \"config\": {\n \"mediaType\": \"application/vnd.docker.container.image.v1+json\",\n \"size\"" +
107+
": 1472,\n \"digest\": \"sha256:042a816809aac8d0f7d7cacac7965782ee2ecac3f21bcf9f24b1de1a7387b769\"\n },\n \"layers\": [\n {\n \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n \"size\"" + "" +
108+
": 3370628,\n \"digest\": \"sha256:8921db27df2831fa6eaa85321205a2470c669b855f3ec95d5a3c2b46de0442c9\"\n }\n ]\n}";
109+
var testName = "roundtrip.json";
110+
DefaultHttpContext ctx = new DefaultHttpContext();
111+
Assets assets = new Assets()
112+
{
113+
AssetsRepo = "Azure/azure-sdk-assets-integration",
114+
AssetsRepoPrefixPath = "pull/scenarios",
115+
AssetsRepoId = "",
116+
TagPrefix = "language/tables",
117+
Tag = "python/tables_fc54d0"
118+
};
119+
var folderStructure = new string[]
120+
{
121+
GitStoretests.AssetsJson
122+
};
123+
var testEntry = new RecordEntry()
124+
{
125+
RequestUri = "https://Sanitized.azurecr.io/v2/alpine/manifests/3.17.1",
126+
RequestMethod = RequestMethod.Get,
127+
Request = new RequestOrResponse()
128+
{
129+
Headers = new SortedDictionary<string, string[]>()
130+
{
131+
{ "Accept", new string[]{ "application/json", "application/vnd.docker.distribution.manifest.v2+json" } },
132+
{ "Accept-Encoding", new string[]{ "gzip" } },
133+
{ "Authorization", new string[]{ "Sanitized" } },
134+
{ "User-Agent", new string[]{ "azsdk-go-azcontainerregistry/v0.2.2 (go1.21.6; linux)" } },
135+
},
136+
Body = null,
137+
},
138+
StatusCode = 200,
139+
Response = new RequestOrResponse()
140+
{
141+
Headers = new SortedDictionary<string, string[]>()
142+
{
143+
{ "Access-Control-Expose-Headers", new string[]{ "Docker-Content-Digest", "WWW-Authenticate", "Link","X-Ms-Correlation-Request-Id" } },
144+
{ "Connection", new string[]{ "keep-alive" } },
145+
{ "Content-Length", new string[]{ "528" } },
146+
{ "Content-Type", new string[]{ "application/vnd.docker.distribution.manifest.v2+json" } },
147+
{ "Date", new string[]{ "Fri, 17 May 2024 21:42:34 GMT" } },
148+
{ "Docker-Content-Digest", new string[]{ "sha256:93d5a28ff72d288d69b5997b8ba47396d2cbb62a72b5d87cd3351094b5d578a0" } },
149+
{ "Docker-Distribution-Api-Version", new string[]{ "registry/2.0" } },
150+
{ "ETag", new string[]{ "\"sha256:93d5a28ff72d288d69b5997b8ba47396d2cbb62a72b5d87cd3351094b5d578a0\"" } },
151+
{ "Server", new string[]{ "AzureContainerRegistry" } },
152+
{ "Strict-Transport-Security", new string[]{ "max-age=31536000; includeSubDomains", "max-age=31536000; includeSubDomains" } },
153+
{ "X-Content-Type-Options", new string[]{ "nosniff" } },
154+
{ "X-Ms-Client-Request-Id", new string[]{ "" } },
155+
{ "X-Ms-Correlation-Request-Id", new string[]{ "caf56438-d3ba-469d-a30c-360a4ff536c1" } },
156+
{ "X-Ms-Request-Id", new string[]{ "Sanitized" } },
157+
},
158+
Body = Encoding.UTF8.GetBytes(sampleExpected)
159+
},
160+
};
161+
162+
// create the session which will be saved to disk, then save it
163+
var testFolder = TestHelpers.DescribeTestFolder(assets, folderStructure);
164+
var handler = new RecordingHandler(testFolder);
165+
await handler.StartRecordingAsync(testName, ctx.Response);
166+
var recordingId = ctx.Response.Headers["x-recording-id"].ToString();
167+
var session = handler.RecordingSessions[recordingId];
168+
session.Session.Entries.Add(testEntry);
169+
handler.StopRecording(recordingId);
170+
171+
// now load it, did we avoid mangling it?
172+
var sessionFromDisk = TestHelpers.LoadRecordSession(Path.Combine(testFolder, testName));
173+
var targetEntry = sessionFromDisk.Session.Entries[0];
174+
var content = Encoding.UTF8.GetString(targetEntry.Response.Body);
175+
Assert.Equal(sampleExpected, content);
176+
}
177+
98178
[Fact]
99179
public void EnsureJsonEscaping()
100180
{
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
{
2+
"Entries": [
3+
{
4+
"RequestUri": "https://Sanitized.azurecr.io/v2/alpine/manifests/3.17.1",
5+
"RequestMethod": "GET",
6+
"RequestHeaders": {
7+
"Accept": [
8+
"application/json",
9+
"application/vnd.docker.distribution.manifest.v2+json"
10+
],
11+
"Accept-Encoding": "gzip",
12+
"Authorization": "Sanitized",
13+
"User-Agent": "azsdk-go-azcontainerregistry/v0.2.2 (go1.22.2; Windows_NT)"
14+
},
15+
"RequestBody": null,
16+
"StatusCode": 200,
17+
"ResponseHeaders": {
18+
"Access-Control-Expose-Headers": [
19+
"Docker-Content-Digest",
20+
"WWW-Authenticate",
21+
"Link",
22+
"X-Ms-Correlation-Request-Id"
23+
],
24+
"Connection": "keep-alive",
25+
"Content-Length": "528",
26+
"Content-Type": "application/vnd.docker.distribution.manifest.v2+json",
27+
"Date": "Wed, 22 May 2024 20:40:43 GMT",
28+
"Docker-Content-Digest": "sha256:93d5a28ff72d288d69b5997b8ba47396d2cbb62a72b5d87cd3351094b5d578a0",
29+
"Docker-Distribution-Api-Version": "registry/2.0",
30+
"ETag": "\"sha256:93d5a28ff72d288d69b5997b8ba47396d2cbb62a72b5d87cd3351094b5d578a0\"",
31+
"Server": "AzureContainerRegistry",
32+
"Strict-Transport-Security": [
33+
"max-age=31536000; includeSubDomains",
34+
"max-age=31536000; includeSubDomains"
35+
],
36+
"X-Content-Type-Options": "nosniff",
37+
"X-Ms-Client-Request-Id": "",
38+
"X-Ms-Correlation-Request-Id": "19affbee-3510-45b1-8248-9dc23982613b",
39+
"X-Ms-Request-Id": "Sanitized"
40+
},
41+
"ResponseBody": {
42+
"schemaVersion": 2,
43+
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
44+
"config": {
45+
"mediaType": "application/vnd.docker.container.image.v1+json",
46+
"size": 1472,
47+
"digest": "sha256:042a816809aac8d0f7d7cacac7965782ee2ecac3f21bcf9f24b1de1a7387b769"
48+
},
49+
"layers": [
50+
{
51+
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
52+
"size": 3370628,
53+
"digest": "sha256:8921db27df2831fa6eaa85321205a2470c669b855f3ec95d5a3c2b46de0442c9"
54+
}
55+
]
56+
}
57+
}
58+
],
59+
"Variables": {}
60+
}

tools/test-proxy/Azure.Sdk.Tools.TestProxy.Tests/TestHelpers.cs

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,27 @@ public static void WriteTestFile(string content, string path)
148148
File.WriteAllText(path, content);
149149
}
150150

151+
public static string GetTmpPath(string[] pathsBeyondFolder = null)
152+
{
153+
var pathSuffix = string.Empty;
154+
155+
if (pathsBeyondFolder != null && pathsBeyondFolder.Length > 0) {
156+
pathSuffix += Path.Combine(pathsBeyondFolder);
157+
}
158+
else
159+
{
160+
pathSuffix = Guid.NewGuid().ToString();
161+
}
162+
163+
var tmpPath = Path.Join(Path.GetTempPath(), pathSuffix);
164+
165+
if (!Directory.Exists(tmpPath)) {
166+
Directory.CreateDirectory(tmpPath);
167+
}
168+
169+
return tmpPath;
170+
}
171+
151172
/// <summary>
152173
/// Used to define any set of file constructs we want. This enables us to roll a target environment to point various GitStore functionalities at.
153174
///
@@ -169,9 +190,8 @@ public static string DescribeTestFolder(Assets assets, string[] sampleFiles, str
169190
}
170191
// the guid will be used to create a unique test folder root and, if this is a push test,
171192
// it'll be used as part of the generated branch name
172-
string testGuid = Guid.NewGuid().ToString();
173-
// generate a test folder root
174-
var tmpPath = Path.Join(Path.GetTempPath(), testGuid);
193+
var testGuid = Guid.NewGuid().ToString();
194+
var tmpPath = GetTmpPath(new string[] { testGuid });
175195

176196
// Push tests need some special setup for automation
177197
// 1. The AssetsReproBranch

tools/test-proxy/Azure.Sdk.Tools.TestProxy/Common/ContentTypeUtilities.cs

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) Microsoft Corporation. All rights reserved.
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

44
#nullable disable
@@ -22,6 +22,7 @@ public static bool TryGetTextEncoding(string contentType, out Encoding encoding)
2222

2323
// Default is technically US-ASCII, but will default to UTF-8 which is a superset.
2424
const string appFormUrlEncoded = "application/x-www-form-urlencoded";
25+
const string dockerManifest = "application/vnd.docker.distribution.manifest.v2";
2526

2627
if (contentType == null)
2728
{
@@ -40,17 +41,23 @@ public static bool TryGetTextEncoding(string contentType, out Encoding encoding)
4041
}
4142
}
4243

43-
if (contentType.StartsWith(textContentTypePrefix, StringComparison.OrdinalIgnoreCase) ||
44-
contentType.EndsWith(jsonSuffix, StringComparison.OrdinalIgnoreCase) ||
45-
contentType.EndsWith(xmlSuffix, StringComparison.OrdinalIgnoreCase) ||
46-
contentType.EndsWith(urlEncodedSuffix, StringComparison.OrdinalIgnoreCase) ||
47-
contentType.StartsWith(appJsonPrefix, StringComparison.OrdinalIgnoreCase) ||
48-
contentType.StartsWith(appFormUrlEncoded, StringComparison.OrdinalIgnoreCase))
44+
45+
if (
46+
(
47+
contentType.StartsWith(textContentTypePrefix, StringComparison.OrdinalIgnoreCase) ||
48+
contentType.EndsWith(jsonSuffix, StringComparison.OrdinalIgnoreCase) ||
49+
contentType.EndsWith(xmlSuffix, StringComparison.OrdinalIgnoreCase) ||
50+
contentType.EndsWith(urlEncodedSuffix, StringComparison.OrdinalIgnoreCase) ||
51+
contentType.StartsWith(appJsonPrefix, StringComparison.OrdinalIgnoreCase) ||
52+
contentType.StartsWith(appFormUrlEncoded, StringComparison.OrdinalIgnoreCase)
53+
) && !contentType.Contains(dockerManifest)
54+
)
4955
{
5056
encoding = Encoding.UTF8;
5157
return true;
5258
}
5359

60+
5461
encoding = null;
5562
return false;
5663
}

0 commit comments

Comments
 (0)