diff --git a/tools/test-proxy/Azure.Sdk.Tools.TestProxy.Tests/MatcherTests.cs b/tools/test-proxy/Azure.Sdk.Tools.TestProxy.Tests/MatcherTests.cs index a21610217f5..afcf5eafdb5 100644 --- a/tools/test-proxy/Azure.Sdk.Tools.TestProxy.Tests/MatcherTests.cs +++ b/tools/test-proxy/Azure.Sdk.Tools.TestProxy.Tests/MatcherTests.cs @@ -37,7 +37,7 @@ public void MatchesBadlyNormalizedHeader(string file, string targetHeader, strin public void BodilessMatcherMatchesIdenticalRequest() { var sessionForRetrieval = TestHelpers.LoadRecordSession("Test.RecordEntries/oauth_request.json"); - + var identicalRequest = TestHelpers.LoadRecordSession("Test.RecordEntries/oauth_request.json").Session.Entries[0]; var expectedIdenticalMatch = sessionForRetrieval.Session.Lookup(identicalRequest, BodilessMatcher, sanitizers: new List(), remove: false); @@ -51,7 +51,7 @@ public void BodilessMatcherMatchesBodilessRequest() var bodilessRequest = TestHelpers.LoadRecordSession("Test.RecordEntries/oauth_request.json").Session.Entries[0]; bodilessRequest.Request.Body = new byte[] { }; bodilessRequest.Request.Headers["Content-Length"] = new string[] { "0" }; - + var expectedBodilessMatch = sessionForRetrieval.Session.Lookup(bodilessRequest, BodilessMatcher, sanitizers: new List(), remove: false); } @@ -75,7 +75,8 @@ public void BodilessMatcherThrowsOnDiffUri() var identicalRequestDiffURI = TestHelpers.LoadRecordSession("Test.RecordEntries/oauth_request.json").Session.Entries[0]; identicalRequestDiffURI.RequestUri = identicalRequestDiffURI.RequestUri + "2"; - Assert.Throws(() => { + Assert.Throws(() => + { sessionForRetrieval.Session.Lookup(identicalRequestDiffURI, BodilessMatcher, sanitizers: new List(), remove: false); }); } @@ -88,7 +89,8 @@ public void BodilessMatcherThrowsOnDiffHeaders() var identicalBodyDiffHeaders = TestHelpers.LoadRecordSession("Test.RecordEntries/oauth_request.json").Session.Entries[0]; identicalBodyDiffHeaders.Request.Headers.Remove(identicalBodyDiffHeaders.Request.Headers.Keys.First()); - Assert.Throws(() => { + Assert.Throws(() => + { sessionForRetrieval.Session.Lookup(identicalBodyDiffHeaders, BodilessMatcher, sanitizers: new List(), remove: false); }); } @@ -119,7 +121,7 @@ public void HeaderlessMatcherMatchesIdenticalHeadersRequest() { var sessionForRetrieval = TestHelpers.LoadRecordSession("Test.RecordEntries/if_none_match_present.json"); var identicalHeaders = TestHelpers.LoadRecordSession("Test.RecordEntries/if_none_match_present.json").Session.Entries[0]; - + var expectedDiffBodyMatch = sessionForRetrieval.Session.Lookup(identicalHeaders, HeaderlessMatcher, sanitizers: new List(), remove: false); } @@ -130,7 +132,8 @@ public void HeaderlessMatcherThrowsOnDiffBody() var diffBodyRequest = TestHelpers.LoadRecordSession("Test.RecordEntries/if_none_match_present.json").Session.Entries[0]; diffBodyRequest.Request.Body = Encoding.UTF8.GetBytes("A Different Request Body"); - Assert.Throws(() => { + Assert.Throws(() => + { sessionForRetrieval.Session.Lookup(diffBodyRequest, HeaderlessMatcher, sanitizers: new List(), remove: false); }); } @@ -142,7 +145,8 @@ public void HeaderlessMatcherThrowsOnDiffUri() var differenUriRequest = TestHelpers.LoadRecordSession("Test.RecordEntries/if_none_match_present.json").Session.Entries[0]; differenUriRequest.RequestUri = "https://shouldntmatch.com"; - Assert.Throws(() => { + Assert.Throws(() => + { sessionForRetrieval.Session.Lookup(differenUriRequest, HeaderlessMatcher, sanitizers: new List(), remove: false); }); } @@ -202,7 +206,8 @@ public void CustomMatcherSpecifyIgnoredThrowsOnRequestNonPresence() var matcher = new CustomDefaultMatcher(ignoredHeaders: "Accept-Encoding"); - var assertion = Assert.Throws(() => { + var assertion = Assert.Throws(() => + { sessionForRetrieval.Session.Lookup(differentHeadersRequest, matcher, sanitizers: new List(), remove: false); }); @@ -218,7 +223,8 @@ public void CustomMatcherSpecifyIgnoredThrowsOnRecordNonPresence() var matcher = new CustomDefaultMatcher(ignoredHeaders: "Accept-Encoding"); - var assertion = Assert.Throws(() => { + var assertion = Assert.Throws(() => + { sessionForRetrieval.Session.Lookup(sameOriginalHeadersRequest, matcher, sanitizers: new List(), remove: false); }); @@ -245,7 +251,8 @@ public void CustomMatcherThrowsOnUnmatched() differentRequest.Request.Headers["Accept-Encoding"] = new string[] { "a-test-header-that-shouldn't-match" }; differentRequest.RequestUri = "https://shouldntmatch.com"; - Assert.Throws(() => { + Assert.Throws(() => + { sessionForRetrieval.Session.Lookup(differentRequest, HeaderlessMatcher, sanitizers: new List(), remove: false); }); } @@ -260,7 +267,7 @@ public async Task CustomMatcherMatchesDifferentUriOrder() var body = "{\"x-recording-file\":\"" + targetFile + "\"}"; playbackContext.Request.Body = TestHelpers.GenerateStreamRequestBody(body); playbackContext.Request.ContentLength = body.Length; - + var controller = new Playback(testRecordingHandler, new NullLoggerFactory()) { ControllerContext = new ControllerContext() @@ -343,6 +350,42 @@ public async Task EncodedUriAmpersandWorksCrossplat() Assert.Equal("Ref A: 980665086A12483993E2782EDFC9F29A Ref B: STBEDGE0106 Ref C: 2023-07-19T22:52:17Z", playbackContext.Response.Headers["X-MSEdge-Ref"].ToString()); Assert.Equal(200, playbackContext.Response.StatusCode); } + + [Fact] + public void MultiPartMatcherMatchesDifferentBoundary() + { + var sessionForRetrieval = TestHelpers.LoadRecordSession("Test.RecordEntries/multipart_request.json"); + var differentBoundaryRequest = TestHelpers.LoadRecordSession("Test.RecordEntries/multipart_request_diff_boundary.json").Session.Entries[0]; + var expectedDiffBodyMatch = sessionForRetrieval.Session.Lookup(differentBoundaryRequest, new RecordMatcher(), sanitizers: new List(), remove: false); + } + + [Fact] + public void MultiPartMultiLayerMatcherMatchesDifferentBoundary() + { + var sessionForRetrieval = TestHelpers.LoadRecordSession("Test.RecordEntries/multipart_request_two_layers.json"); + var differentBoundaryRequest = TestHelpers.LoadRecordSession("Test.RecordEntries/multipart_request_two_layers_diff_boundary.json").Session.Entries[0]; + var expectedDiffBodyMatch = sessionForRetrieval.Session.Lookup(differentBoundaryRequest, new RecordMatcher(), sanitizers: new List(), remove: false); + } + + [Fact] + public void MultiPartMatcherThrowsOnDiffBody() + { + var sessionForRetrieval = TestHelpers.LoadRecordSession("Test.RecordEntries/multipart_request.json"); + var diffBodyRequest = TestHelpers.LoadRecordSession("Test.RecordEntries/multipart_request_diff_body.json").Session.Entries[0]; + diffBodyRequest.Request.Body = Encoding.UTF8.GetBytes("A Different Request Body"); + Assert.Throws(() => + { + sessionForRetrieval.Session.Lookup(diffBodyRequest, new RecordMatcher(), sanitizers: new List(), remove: false); + }); + } + + [Fact] + public void MultiPartMatcherMatchesIdenticalBoundary() + { + var sessionForRetrieval = TestHelpers.LoadRecordSession("Test.RecordEntries/multipart_request.json"); + var identicalRequest = TestHelpers.LoadRecordSession("Test.RecordEntries/multipart_request.json").Session.Entries[0]; + var expectedIdenticalMatch = sessionForRetrieval.Session.Lookup(identicalRequest, new RecordMatcher(), sanitizers: new List(), remove: false); + } } } diff --git a/tools/test-proxy/Azure.Sdk.Tools.TestProxy.Tests/SanitizerTests.cs b/tools/test-proxy/Azure.Sdk.Tools.TestProxy.Tests/SanitizerTests.cs index 2696b9c9853..66e4b24a404 100644 --- a/tools/test-proxy/Azure.Sdk.Tools.TestProxy.Tests/SanitizerTests.cs +++ b/tools/test-proxy/Azure.Sdk.Tools.TestProxy.Tests/SanitizerTests.cs @@ -690,9 +690,11 @@ public async void GenStringSanitizerQuietExitForAllHttpComponents() Assert.Equal(0, matcher.CompareHeaderDictionaries(targetUntouchedEntry.Request.Headers, targetEntry.Request.Headers, new HashSet(), new HashSet())); Assert.Equal(0, matcher.CompareHeaderDictionaries(targetUntouchedEntry.Response.Headers, targetEntry.Response.Headers, new HashSet(), new HashSet())); - targetUntouchedEntry.Request.TryGetContentType(out var contentType); - Assert.Equal(0, matcher.CompareBodies(targetUntouchedEntry.Request.Body, targetEntry.Request.Body, contentType)); - Assert.Equal(0, matcher.CompareBodies(targetUntouchedEntry.Response.Body, targetEntry.Response.Body, contentType)); + targetUntouchedEntry.Request.TryGetContentType(out var requestContentType); + targetEntry.Request.TryGetContentType(out var recordContentType); + ContentTypeUtilities.TryGetTextEncoding(requestContentType, out var encoding); + Assert.Equal(0, matcher.CompareBodies(targetUntouchedEntry.Request.Body, targetEntry.Request.Body, requestContentType, recordContentType, encoding)); + Assert.Equal(0, matcher.CompareBodies(targetUntouchedEntry.Response.Body, targetEntry.Response.Body, requestContentType, recordContentType, encoding)); Assert.Equal(targetUntouchedEntry.RequestUri, targetEntry.RequestUri); } @@ -771,9 +773,11 @@ public async void BodyStringSanitizerQuietlyExits(string targetValue, string rep await session.Session.Sanitize(sanitizer); var resultBodyValue = Encoding.UTF8.GetString(targetEntry.Request.Body); - targetUntouchedEntry.Request.TryGetContentType(out var contentType); - Assert.Equal(0, matcher.CompareBodies(targetUntouchedEntry.Request.Body, targetEntry.Request.Body, contentType)); - Assert.Equal(0, matcher.CompareBodies(targetUntouchedEntry.Response.Body, targetEntry.Response.Body, contentType)); + targetUntouchedEntry.Request.TryGetContentType(out var requestContentType); + targetEntry.Request.TryGetContentType(out var recordContentType); + ContentTypeUtilities.TryGetTextEncoding(requestContentType, out var encoding); + Assert.Equal(0, matcher.CompareBodies(targetUntouchedEntry.Request.Body, targetEntry.Request.Body, requestContentType, recordContentType, encoding)); + Assert.Equal(0, matcher.CompareBodies(targetUntouchedEntry.Response.Body, targetEntry.Response.Body, requestContentType, recordContentType, encoding)); } [Fact] diff --git a/tools/test-proxy/Azure.Sdk.Tools.TestProxy.Tests/Test.RecordEntries/multipart_request.json b/tools/test-proxy/Azure.Sdk.Tools.TestProxy.Tests/Test.RecordEntries/multipart_request.json index 96546195049..5677b6ffae2 100644 --- a/tools/test-proxy/Azure.Sdk.Tools.TestProxy.Tests/Test.RecordEntries/multipart_request.json +++ b/tools/test-proxy/Azure.Sdk.Tools.TestProxy.Tests/Test.RecordEntries/multipart_request.json @@ -7,7 +7,7 @@ "Accept": "*/*", "Authorization": "Sanitized", "Content-Length": "3505", - "Content-Type": "multipart/mixed; boundary=REDACTED", + "Content-Type": "multipart/mixed; boundary=batch_41ee0728-dac8-4887-a5f6-fcf5a69252c9", "DataServiceVersion": "3.0", "Date": "Sat, 26 Apr 2025 01:09:57 GMT", "User-Agent": "azsdk-java-azure-data-tables/12.6.0-beta.1 (17.0.15; Windows 11; 10.0)", @@ -66,7 +66,7 @@ ], "StatusCode": 202, "ResponseHeaders": { - "Content-Type": "multipart/mixed; boundary=REDACTED", + "Content-Type": "multipart/mixed; boundary=batchresponse_32c36dfe-6fad-46c7-9a9a-5472c406b4a8", "Date": "Sat, 26 Apr 2025 01:09:58 GMT", "Transfer-Encoding": "chunked" }, diff --git a/tools/test-proxy/Azure.Sdk.Tools.TestProxy.Tests/Test.RecordEntries/multipart_request_diff_body.json b/tools/test-proxy/Azure.Sdk.Tools.TestProxy.Tests/Test.RecordEntries/multipart_request_diff_body.json new file mode 100644 index 00000000000..8fd3af79d32 --- /dev/null +++ b/tools/test-proxy/Azure.Sdk.Tools.TestProxy.Tests/Test.RecordEntries/multipart_request_diff_body.json @@ -0,0 +1,131 @@ +{ + "Entries": [ + { + "RequestUri": "https://REDACTED/$batch", + "RequestMethod": "POST", + "RequestHeaders": { + "Accept": "*/*", + "Authorization": "Sanitized", + "Content-Length": "3505", + "Content-Type": "multipart/mixed; boundary=batch_41ee0728-dac8-4887-a5f6-fcf5a69252c9", + "DataServiceVersion": "3.0", + "Date": "Sat, 26 Apr 2025 01:09:57 GMT", + "User-Agent": "azsdk-java-azure-data-tables/12.6.0-beta.1 (17.0.15; Windows 11; 10.0)", + "x-ms-client-request-id": "Sanitized", + "x-ms-version": "2020-12-06" + }, + "RequestBody": [ + "--batch_41ee0728-dac8-4887-a5f6-fcf5a69252c9\r\n", + "Content-Type: multipart/mixed; boundary=changeset_2bb92c1d-78ec-404c-87c7-6a5d8bad1099\r\n", + "\r\n", + "--changeset_2bb92c1d-78ec-404c-87c7-6a5d8bad1099\r\n", + "Content-Type: application/http\r\n", + "Content-Transfer-Encoding: binary\r\n", + "\r\n", + "b64:UE9TVCBodHRwczovL3RlOWQ3Zjk5N2QwM2M2ZjM1cHJpbS50YWJsZS5jb3Ntb3MuYXp1cmUuY29tOjQ0My90YWJsZW5hbWU3MzM2N2U4ZSBIVFRQLzEuMQ0KQ29udGVudC1MZW5ndGg6IDY1DQpQcmVmZXI6IHJldHVybi1uby1jb250ZW50DQpDb250ZW50LVR5cGU6IGFwcGxpY2F0aW9uL2pzb247b2RhdGE9bm9tZXRhZGF0YQ0KRGF0YVNlcnZpY2VWZXJzaW9uOiAzLjANCkFjY2VwdDogYXBwbGljYXRpb24vanNvbjtvZGF0YT1taW5pbWFsbWV0YWRhdGENCg0KeyJSb3dLZXkiOiJyb3drZXk3NDQwNDI2MmQiLCJQYXJ0aXRpb25LZXkiOiJwYXJ0aXRpb24na2V5MjQyNjNjIn0=", + "\r\n", + "--changeset_2bb92c1d-78ec-404c-87c7-6a5d8bad1099\r\n", + "Content-Type: application/http\r\n", + "Content-Transfer-Encoding: binary\r\n", + "\r\n", + "b64:TUVSR0UgaHR0cHM6Ly90ZTlkN2Y5OTdkMDNjNmYzNXByaW0udGFibGUuY29zbW9zLmF6dXJlLmNvbTo0NDMvdGFibGVuYW1lNzMzNjdlOGUoUGFydGl0aW9uS2V5PSdwYXJ0aXRpb24nJ2tleTI0MjYzYycsUm93S2V5PSdyb3drZXkwODIwNTE3ODknKSBIVFRQLzEuMQ0KQ29udGVudC1MZW5ndGg6IDY1DQpDb250ZW50LVR5cGU6IGFwcGxpY2F0aW9uL2pzb24NCkRhdGFTZXJ2aWNlVmVyc2lvbjogMy4wDQpBY2NlcHQ6IGFwcGxpY2F0aW9uL2pzb247b2RhdGE9bWluaW1hbG1ldGFkYXRhDQoNCnsiUm93S2V5Ijoicm93a2V5MDgyMDUxNzg5IiwiUGFydGl0aW9uS2V5IjoicGFydGl0aW9uJ2tleTI0MjYzYyJ9", + "\r\n", + "--changeset_2bb92c1d-78ec-404c-87c7-6a5d8bad1099\r\n", + "Content-Type: application/http\r\n", + "Content-Transfer-Encoding: binary\r\n", + "\r\n", + "b64:TUVSR0UgaHR0cHM6Ly90ZTlkN2Y5OTdkMDNjNmYzNXByaW0udGFibGUuY29zbW9zLmF6dXJlLmNvbTo0NDMvdGFibGVuYW1lNzMzNjdlOGUoUGFydGl0aW9uS2V5PSdwYXJ0aXRpb24nJ2tleTI0MjYzYycsUm93S2V5PSdyb3drZXkwOTQxNjYzMzEnKSBIVFRQLzEuMQ0KQ29udGVudC1MZW5ndGg6IDg2DQpDb250ZW50LVR5cGU6IGFwcGxpY2F0aW9uL2pzb24NCkRhdGFTZXJ2aWNlVmVyc2lvbjogMy4wDQpBY2NlcHQ6IGFwcGxpY2F0aW9uL2pzb247b2RhdGE9bWluaW1hbG1ldGFkYXRhDQoNCnsiUm93S2V5Ijoicm93a2V5MDk0MTY2MzMxIiwiVGVzdCI6Ik1lcmdlZFZhbHVlIiwiUGFydGl0aW9uS2V5IjoicGFydGl0aW9uJ2tleTI0MjYzYyJ9", + "\r\n", + "--changeset_2bb92c1d-78ec-404c-87c7-6a5d8bad1099\r\n", + "Content-Type: application/http\r\n", + "Content-Transfer-Encoding: binary\r\n", + "\r\n", + "b64:UFVUIGh0dHBzOi8vdGU5ZDdmOTk3ZDAzYzZmMzVwcmltLnRhYmxlLmNvc21vcy5henVyZS5jb206NDQzL3RhYmxlbmFtZTczMzY3ZThlKFBhcnRpdGlvbktleT0ncGFydGl0aW9uJydrZXkyNDI2M2MnLFJvd0tleT0ncm93a2V5NTM3NDdkYzYyJykgSFRUUC8xLjENCkNvbnRlbnQtTGVuZ3RoOiA4OA0KQ29udGVudC1UeXBlOiBhcHBsaWNhdGlvbi9qc29uDQpEYXRhU2VydmljZVZlcnNpb246IDMuMA0KQWNjZXB0OiBhcHBsaWNhdGlvbi9qc29uO29kYXRhPW1pbmltYWxtZXRhZGF0YQ0KDQp7IlJvd0tleSI6InJvd2tleTUzNzQ3ZGM2MiIsIlRlc3QiOiJSZXBsYWNlZFZhbHVlIiwiUGFydGl0aW9uS2V5IjoicGFydGl0aW9uJ2tleTI0MjYzYyJ9", + "\r\n", + "--changeset_2bb92c1d-78ec-404c-87c7-6a5d8bad1099\r\n", + "Content-Type: application/http\r\n", + "Content-Transfer-Encoding: binary\r\n", + "\r\n", + "b64:TUVSR0UgaHR0cHM6Ly90ZTlkN2Y5OTdkMDNjNmYzNXByaW0udGFibGUuY29zbW9zLmF6dXJlLmNvbTo0NDMvdGFibGVuYW1lNzMzNjdlOGUoUGFydGl0aW9uS2V5PSdwYXJ0aXRpb24nJ2tleTI0MjYzYycsUm93S2V5PSdyb3drZXkxNzY1NzE0ZTgnKSBIVFRQLzEuMQ0KQ29udGVudC1MZW5ndGg6IDg2DQpDb250ZW50LVR5cGU6IGFwcGxpY2F0aW9uL2pzb24NCklmLU1hdGNoOiAqDQpEYXRhU2VydmljZVZlcnNpb246IDMuMA0KQWNjZXB0OiBhcHBsaWNhdGlvbi9qc29uO29kYXRhPW1pbmltYWxtZXRhZGF0YQ0KDQp7IlJvd0tleSI6InJvd2tleTE3NjU3MTRlOCIsIlRlc3QiOiJNZXJnZWRWYWx1ZSIsIlBhcnRpdGlvbktleSI6InBhcnRpdGlvbidrZXkyNDI2M2MifQ==", + "\r\n", + "--changeset_2bb92c1d-78ec-404c-87c7-6a5d8bad1099\r\n", + "Content-Type: application/http\r\n", + "Content-Transfer-Encoding: binary\r\n", + "\r\n", + "b64:UFVUIGh0dHBzOi8vdGU5ZDdmOTk3ZDAzYzZmMzVwcmltLnRhYmxlLmNvc21vcy5henVyZS5jb206NDQzL3RhYmxlbmFtZTczMzY3ZThlKFBhcnRpdGlvbktleT0ncGFydGl0aW9uJydrZXkyNDI2M2MnLFJvd0tleT0ncm93a2V5MTU5NDBiNzY3JykgSFRUUC8xLjENCkNvbnRlbnQtTGVuZ3RoOiA4Ng0KQ29udGVudC1UeXBlOiBhcHBsaWNhdGlvbi9qc29uDQpJZi1NYXRjaDogKg0KRGF0YVNlcnZpY2VWZXJzaW9uOiAzLjANCkFjY2VwdDogYXBwbGljYXRpb24vanNvbjtvZGF0YT1taW5pbWFsbWV0YWRhdGENCg0KeyJSb3dLZXkiOiJyb3drZXkxNTk0MGI3NjciLCJUZXN0IjoiTWVyZ2VkVmFsdWUiLCJQYXJ0aXRpb25LZXkiOiJwYXJ0aXRpb24na2V5MjQyNjNjIn0=", + "\r\n", + "--changeset_2bb92c1d-78ec-404c-87c7-6a5d8bad1099\r\n", + "Content-Type: application/http\r\n", + "Content-Transfer-Encoding: binary\r\n", + "\r\n", + "b64:REVMRVRFIGh0dHBzOi8vdGU5ZDdmOTk3ZDAzYzZmMzVwcmltLnRhYmxlLmNvc21vcy5henVyZS5jb206NDQzL3RhYmxlbmFtZTczMzY3ZThlKFBhcnRpdGlvbktleT0ncGFydGl0aW9uJydrZXkyNDI2M2MnLFJvd0tleT0ncm93a2V5NTI1MTJiZWFmJykgSFRUUC8xLjENCkNvbnRlbnQtTGVuZ3RoOiAwDQpJZi1NYXRjaDogKg0KRGF0YVNlcnZpY2VWZXJzaW9uOiAzLjANCkFjY2VwdDogYXBwbGljYXRpb24vanNvbjtvZGF0YT1taW5pbWFsbWV0YWRhdGENCg0K", + "\r\n", + "--changeset_2bb92c1d-78ec-404c-87c7-6a5d8bad1099--\r\n", + "\r\n", + "--batch_41ee0728-dac8-4887-a5f6-fcf5a69252c9--\r\n" + ], + "StatusCode": 202, + "ResponseHeaders": { + "Content-Type": "multipart/mixed; boundary=batchresponse_32c36dfe-6fad-46c7-9a9a-5472c406b4a8", + "Date": "Sat, 26 Apr 2025 01:09:58 GMT", + "Transfer-Encoding": "chunked" + }, + "ResponseBody": [ + "--batchresponse_32c36dfe-6fad-46c7-9a9a-5472c406b4a8\r\n", + "Content-Type: multipart/mixed; boundary=changesetresponse_b03ffa4a-53fc-4036-8a02-509eec67ca53\r\n", + "\r\n", + "--changesetresponse_b03ffa4a-53fc-4036-8a02-509eec67ca53\r\n", + "Content-Type: application/http\r\n", + "Content-Transfer-Encoding: binary\r\n", + "\r\n", + "b64:SFRUUC8xLjEgMjA0IE5vIENvbnRlbnQNCkVUYWc6IFcvImRhdGV0aW1lJzIwMjUtMDQtMjZUMDElM0EwOSUzQTU4LjYwMzM2NzFaJyINClByZWZlcmVuY2UtQXBwbGllZDogcmV0dXJuLW5vLWNvbnRlbnQNCkxvY2F0aW9uOiBodHRwczovL3RlOWQ3Zjk5N2QwM2M2ZjM1cHJpbS50YWJsZS5jb3Ntb3MuYXp1cmUuY29tL3RhYmxlbmFtZTczMzY3ZThlKFBhcnRpdGlvbktleT0ncGFydGl0aW9uJydrZXkyNDI2M2MnLFJvd0tleT0ncm93a2V5NzQ0MDQyNjJkJykNCkNvbnRlbnQtSUQ6IDENCg0K", + "\r\n", + "--changesetresponse_b03ffa4a-53fc-4036-8a02-509eec67ca53\r\n", + "Content-Type: application/http\r\n", + "Content-Transfer-Encoding: binary\r\n", + "\r\n", + "b64:SFRUUC8xLjEgMjA0IE5vIENvbnRlbnQNCkVUYWc6IFcvImRhdGV0aW1lJzIwMjUtMDQtMjZUMDElM0EwOSUzQTU4LjYwMzc3NjdaJyINCkNvbnRlbnQtVHlwZTogYXBwbGljYXRpb24vanNvbjtvZGF0YT1taW5pbWFsbWV0YWRhdGENCkNvbnRlbnQtSUQ6IDINCg0K", + "\r\n", + "--changesetresponse_b03ffa4a-53fc-4036-8a02-509eec67ca53\r\n", + "Content-Type: application/http\r\n", + "Content-Transfer-Encoding: binary\r\n", + "\r\n", + "b64:SFRUUC8xLjEgMjA0IE5vIENvbnRlbnQNCkVUYWc6IFcvImRhdGV0aW1lJzIwMjUtMDQtMjZUMDElM0EwOSUzQTU4LjYwNTAwNTVaJyINCkNvbnRlbnQtVHlwZTogYXBwbGljYXRpb24vanNvbjtvZGF0YT1taW5pbWFsbWV0YWRhdGENCkNvbnRlbnQtSUQ6IDMNCg0K", + "\r\n", + "--changesetresponse_b03ffa4a-53fc-4036-8a02-509eec67ca53\r\n", + "Content-Type: application/http\r\n", + "Content-Transfer-Encoding: binary\r\n", + "\r\n", + "b64:SFRUUC8xLjEgMjA0IE5vIENvbnRlbnQNCkVUYWc6IFcvImRhdGV0aW1lJzIwMjUtMDQtMjZUMDElM0EwOSUzQTU4LjYwNTgyNDdaJyINCkNvbnRlbnQtVHlwZTogYXBwbGljYXRpb24vanNvbjtvZGF0YT1taW5pbWFsbWV0YWRhdGENCkNvbnRlbnQtSUQ6IDQNCg0K", + "\r\n", + "--changesetresponse_b03ffa4a-53fc-4036-8a02-509eec67ca53\r\n", + "Content-Type: application/http\r\n", + "Content-Transfer-Encoding: binary\r\n", + "\r\n", + "b64:SFRUUC8xLjEgMjA0IE5vIENvbnRlbnQNCkVUYWc6IFcvImRhdGV0aW1lJzIwMjUtMDQtMjZUMDElM0EwOSUzQTU4LjYwNjQzOTFaJyINCkNvbnRlbnQtSUQ6IDUNCg0K", + "\r\n", + "--changesetresponse_b03ffa4a-53fc-4036-8a02-509eec67ca53\r\n", + "Content-Type: application/http\r\n", + "Content-Transfer-Encoding: binary\r\n", + "\r\n", + "b64:SFRUUC8xLjEgMjA0IE5vIENvbnRlbnQNCkVUYWc6IFcvImRhdGV0aW1lJzIwMjUtMDQtMjZUMDElM0EwOSUzQTU4LjYwNjg0ODdaJyINCkNvbnRlbnQtSUQ6IDYNCg0K", + "\r\n", + "--changesetresponse_b03ffa4a-53fc-4036-8a02-509eec67ca53\r\n", + "Content-Type: application/http\r\n", + "Content-Transfer-Encoding: binary\r\n", + "\r\n", + "b64:SFRUUC8xLjEgMjA0IE5vIENvbnRlbnQNCkNvbnRlbnQtSUQ6IDcNCg0K", + "\r\n", + "--changesetresponse_b03ffa4a-53fc-4036-8a02-509eec67ca53\r\n", + "Content-Type: application/http\r\n", + "Content-Transfer-Encoding: binary\r\n", + "\r\n", + "b64:SFRUUC8xLjEgMjA0IE5vIENvbnRlbnQNCkNvbnRlbnQtSUQ6IDcNCg0K", + "\r\n", + "--changesetresponse_b03ffa4a-53fc-4036-8a02-509eec67ca53--\r\n", + "\r\n", + "--batchresponse_32c36dfe-6fad-46c7-9a9a-5472c406b4a8--\r\n" + ] + } + ] +} diff --git a/tools/test-proxy/Azure.Sdk.Tools.TestProxy.Tests/Test.RecordEntries/multipart_request_diff_boundary.json b/tools/test-proxy/Azure.Sdk.Tools.TestProxy.Tests/Test.RecordEntries/multipart_request_diff_boundary.json new file mode 100644 index 00000000000..08e7a4b384c --- /dev/null +++ b/tools/test-proxy/Azure.Sdk.Tools.TestProxy.Tests/Test.RecordEntries/multipart_request_diff_boundary.json @@ -0,0 +1,125 @@ +{ + "Entries": [ + { + "RequestUri": "https://REDACTED/$batch", + "RequestMethod": "POST", + "RequestHeaders": { + "Accept": "*/*", + "Authorization": "Sanitized", + "Content-Length": "3505", + "Content-Type": "multipart/mixed; boundary=batch_41ee0728-dac8-4887-a5f6-fcf5a69252c9-DIFF", + "DataServiceVersion": "3.0", + "Date": "Sat, 26 Apr 2025 01:09:57 GMT", + "User-Agent": "azsdk-java-azure-data-tables/12.6.0-beta.1 (17.0.15; Windows 11; 10.0)", + "x-ms-client-request-id": "Sanitized", + "x-ms-version": "2020-12-06" + }, + "RequestBody": [ + "--batch_41ee0728-dac8-4887-a5f6-fcf5a69252c9-DIFF\r\n", + "Content-Type: multipart/mixed; boundary=changeset_2bb92c1d-78ec-404c-87c7-6a5d8bad1099-DIFF\r\n", + "\r\n", + "--changeset_2bb92c1d-78ec-404c-87c7-6a5d8bad1099-DIFF\r\n", + "Content-Type: application/http\r\n", + "Content-Transfer-Encoding: binary\r\n", + "\r\n", + "b64:UE9TVCBodHRwczovL3RlOWQ3Zjk5N2QwM2M2ZjM1cHJpbS50YWJsZS5jb3Ntb3MuYXp1cmUuY29tOjQ0My90YWJsZW5hbWU3MzM2N2U4ZSBIVFRQLzEuMQ0KQ29udGVudC1MZW5ndGg6IDY1DQpQcmVmZXI6IHJldHVybi1uby1jb250ZW50DQpDb250ZW50LVR5cGU6IGFwcGxpY2F0aW9uL2pzb247b2RhdGE9bm9tZXRhZGF0YQ0KRGF0YVNlcnZpY2VWZXJzaW9uOiAzLjANCkFjY2VwdDogYXBwbGljYXRpb24vanNvbjtvZGF0YT1taW5pbWFsbWV0YWRhdGENCg0KeyJSb3dLZXkiOiJyb3drZXk3NDQwNDI2MmQiLCJQYXJ0aXRpb25LZXkiOiJwYXJ0aXRpb24na2V5MjQyNjNjIn0=", + "\r\n", + "--changeset_2bb92c1d-78ec-404c-87c7-6a5d8bad1099-DIFF\r\n", + "Content-Type: application/http\r\n", + "Content-Transfer-Encoding: binary\r\n", + "\r\n", + "b64:TUVSR0UgaHR0cHM6Ly90ZTlkN2Y5OTdkMDNjNmYzNXByaW0udGFibGUuY29zbW9zLmF6dXJlLmNvbTo0NDMvdGFibGVuYW1lNzMzNjdlOGUoUGFydGl0aW9uS2V5PSdwYXJ0aXRpb24nJ2tleTI0MjYzYycsUm93S2V5PSdyb3drZXkwODIwNTE3ODknKSBIVFRQLzEuMQ0KQ29udGVudC1MZW5ndGg6IDY1DQpDb250ZW50LVR5cGU6IGFwcGxpY2F0aW9uL2pzb24NCkRhdGFTZXJ2aWNlVmVyc2lvbjogMy4wDQpBY2NlcHQ6IGFwcGxpY2F0aW9uL2pzb247b2RhdGE9bWluaW1hbG1ldGFkYXRhDQoNCnsiUm93S2V5Ijoicm93a2V5MDgyMDUxNzg5IiwiUGFydGl0aW9uS2V5IjoicGFydGl0aW9uJ2tleTI0MjYzYyJ9", + "\r\n", + "--changeset_2bb92c1d-78ec-404c-87c7-6a5d8bad1099-DIFF\r\n", + "Content-Type: application/http\r\n", + "Content-Transfer-Encoding: binary\r\n", + "\r\n", + "b64:TUVSR0UgaHR0cHM6Ly90ZTlkN2Y5OTdkMDNjNmYzNXByaW0udGFibGUuY29zbW9zLmF6dXJlLmNvbTo0NDMvdGFibGVuYW1lNzMzNjdlOGUoUGFydGl0aW9uS2V5PSdwYXJ0aXRpb24nJ2tleTI0MjYzYycsUm93S2V5PSdyb3drZXkwOTQxNjYzMzEnKSBIVFRQLzEuMQ0KQ29udGVudC1MZW5ndGg6IDg2DQpDb250ZW50LVR5cGU6IGFwcGxpY2F0aW9uL2pzb24NCkRhdGFTZXJ2aWNlVmVyc2lvbjogMy4wDQpBY2NlcHQ6IGFwcGxpY2F0aW9uL2pzb247b2RhdGE9bWluaW1hbG1ldGFkYXRhDQoNCnsiUm93S2V5Ijoicm93a2V5MDk0MTY2MzMxIiwiVGVzdCI6Ik1lcmdlZFZhbHVlIiwiUGFydGl0aW9uS2V5IjoicGFydGl0aW9uJ2tleTI0MjYzYyJ9", + "\r\n", + "--changeset_2bb92c1d-78ec-404c-87c7-6a5d8bad1099-DIFF\r\n", + "Content-Type: application/http\r\n", + "Content-Transfer-Encoding: binary\r\n", + "\r\n", + "b64:UFVUIGh0dHBzOi8vdGU5ZDdmOTk3ZDAzYzZmMzVwcmltLnRhYmxlLmNvc21vcy5henVyZS5jb206NDQzL3RhYmxlbmFtZTczMzY3ZThlKFBhcnRpdGlvbktleT0ncGFydGl0aW9uJydrZXkyNDI2M2MnLFJvd0tleT0ncm93a2V5NTM3NDdkYzYyJykgSFRUUC8xLjENCkNvbnRlbnQtTGVuZ3RoOiA4OA0KQ29udGVudC1UeXBlOiBhcHBsaWNhdGlvbi9qc29uDQpEYXRhU2VydmljZVZlcnNpb246IDMuMA0KQWNjZXB0OiBhcHBsaWNhdGlvbi9qc29uO29kYXRhPW1pbmltYWxtZXRhZGF0YQ0KDQp7IlJvd0tleSI6InJvd2tleTUzNzQ3ZGM2MiIsIlRlc3QiOiJSZXBsYWNlZFZhbHVlIiwiUGFydGl0aW9uS2V5IjoicGFydGl0aW9uJ2tleTI0MjYzYyJ9", + "\r\n", + "--changeset_2bb92c1d-78ec-404c-87c7-6a5d8bad1099-DIFF\r\n", + "Content-Type: application/http\r\n", + "Content-Transfer-Encoding: binary\r\n", + "\r\n", + "b64:TUVSR0UgaHR0cHM6Ly90ZTlkN2Y5OTdkMDNjNmYzNXByaW0udGFibGUuY29zbW9zLmF6dXJlLmNvbTo0NDMvdGFibGVuYW1lNzMzNjdlOGUoUGFydGl0aW9uS2V5PSdwYXJ0aXRpb24nJ2tleTI0MjYzYycsUm93S2V5PSdyb3drZXkxNzY1NzE0ZTgnKSBIVFRQLzEuMQ0KQ29udGVudC1MZW5ndGg6IDg2DQpDb250ZW50LVR5cGU6IGFwcGxpY2F0aW9uL2pzb24NCklmLU1hdGNoOiAqDQpEYXRhU2VydmljZVZlcnNpb246IDMuMA0KQWNjZXB0OiBhcHBsaWNhdGlvbi9qc29uO29kYXRhPW1pbmltYWxtZXRhZGF0YQ0KDQp7IlJvd0tleSI6InJvd2tleTE3NjU3MTRlOCIsIlRlc3QiOiJNZXJnZWRWYWx1ZSIsIlBhcnRpdGlvbktleSI6InBhcnRpdGlvbidrZXkyNDI2M2MifQ==", + "\r\n", + "--changeset_2bb92c1d-78ec-404c-87c7-6a5d8bad1099-DIFF\r\n", + "Content-Type: application/http\r\n", + "Content-Transfer-Encoding: binary\r\n", + "\r\n", + "b64:UFVUIGh0dHBzOi8vdGU5ZDdmOTk3ZDAzYzZmMzVwcmltLnRhYmxlLmNvc21vcy5henVyZS5jb206NDQzL3RhYmxlbmFtZTczMzY3ZThlKFBhcnRpdGlvbktleT0ncGFydGl0aW9uJydrZXkyNDI2M2MnLFJvd0tleT0ncm93a2V5MTU5NDBiNzY3JykgSFRUUC8xLjENCkNvbnRlbnQtTGVuZ3RoOiA4Ng0KQ29udGVudC1UeXBlOiBhcHBsaWNhdGlvbi9qc29uDQpJZi1NYXRjaDogKg0KRGF0YVNlcnZpY2VWZXJzaW9uOiAzLjANCkFjY2VwdDogYXBwbGljYXRpb24vanNvbjtvZGF0YT1taW5pbWFsbWV0YWRhdGENCg0KeyJSb3dLZXkiOiJyb3drZXkxNTk0MGI3NjciLCJUZXN0IjoiTWVyZ2VkVmFsdWUiLCJQYXJ0aXRpb25LZXkiOiJwYXJ0aXRpb24na2V5MjQyNjNjIn0=", + "\r\n", + "--changeset_2bb92c1d-78ec-404c-87c7-6a5d8bad1099-DIFF\r\n", + "Content-Type: application/http\r\n", + "Content-Transfer-Encoding: binary\r\n", + "\r\n", + "b64:REVMRVRFIGh0dHBzOi8vdGU5ZDdmOTk3ZDAzYzZmMzVwcmltLnRhYmxlLmNvc21vcy5henVyZS5jb206NDQzL3RhYmxlbmFtZTczMzY3ZThlKFBhcnRpdGlvbktleT0ncGFydGl0aW9uJydrZXkyNDI2M2MnLFJvd0tleT0ncm93a2V5NTI1MTJiZWFmJykgSFRUUC8xLjENCkNvbnRlbnQtTGVuZ3RoOiAwDQpJZi1NYXRjaDogKg0KRGF0YVNlcnZpY2VWZXJzaW9uOiAzLjANCkFjY2VwdDogYXBwbGljYXRpb24vanNvbjtvZGF0YT1taW5pbWFsbWV0YWRhdGENCg0K", + "\r\n", + "--changeset_2bb92c1d-78ec-404c-87c7-6a5d8bad1099-DIFF--\r\n", + "\r\n", + "--batch_41ee0728-dac8-4887-a5f6-fcf5a69252c9-DIFF--\r\n" + ], + "StatusCode": 202, + "ResponseHeaders": { + "Content-Type": "multipart/mixed; boundary=batchresponse_32c36dfe-6fad-46c7-9a9a-5472c406b4a8-DIFF", + "Date": "Sat, 26 Apr 2025 01:09:58 GMT", + "Transfer-Encoding": "chunked" + }, + "ResponseBody": [ + "--batchresponse_32c36dfe-6fad-46c7-9a9a-5472c406b4a8-DIFF\r\n", + "Content-Type: multipart/mixed; boundary=changesetresponse_b03ffa4a-53fc-4036-8a02-509eec67ca53-DIFF\r\n", + "\r\n", + "--changesetresponse_b03ffa4a-53fc-4036-8a02-509eec67ca53-DIFF\r\n", + "Content-Type: application/http\r\n", + "Content-Transfer-Encoding: binary\r\n", + "\r\n", + "b64:SFRUUC8xLjEgMjA0IE5vIENvbnRlbnQNCkVUYWc6IFcvImRhdGV0aW1lJzIwMjUtMDQtMjZUMDElM0EwOSUzQTU4LjYwMzM2NzFaJyINClByZWZlcmVuY2UtQXBwbGllZDogcmV0dXJuLW5vLWNvbnRlbnQNCkxvY2F0aW9uOiBodHRwczovL3RlOWQ3Zjk5N2QwM2M2ZjM1cHJpbS50YWJsZS5jb3Ntb3MuYXp1cmUuY29tL3RhYmxlbmFtZTczMzY3ZThlKFBhcnRpdGlvbktleT0ncGFydGl0aW9uJydrZXkyNDI2M2MnLFJvd0tleT0ncm93a2V5NzQ0MDQyNjJkJykNCkNvbnRlbnQtSUQ6IDENCg0K", + "\r\n", + "--changesetresponse_b03ffa4a-53fc-4036-8a02-509eec67ca53-DIFF\r\n", + "Content-Type: application/http\r\n", + "Content-Transfer-Encoding: binary\r\n", + "\r\n", + "b64:SFRUUC8xLjEgMjA0IE5vIENvbnRlbnQNCkVUYWc6IFcvImRhdGV0aW1lJzIwMjUtMDQtMjZUMDElM0EwOSUzQTU4LjYwMzc3NjdaJyINCkNvbnRlbnQtVHlwZTogYXBwbGljYXRpb24vanNvbjtvZGF0YT1taW5pbWFsbWV0YWRhdGENCkNvbnRlbnQtSUQ6IDINCg0K", + "\r\n", + "--changesetresponse_b03ffa4a-53fc-4036-8a02-509eec67ca53-DIFF\r\n", + "Content-Type: application/http\r\n", + "Content-Transfer-Encoding: binary\r\n", + "\r\n", + "b64:SFRUUC8xLjEgMjA0IE5vIENvbnRlbnQNCkVUYWc6IFcvImRhdGV0aW1lJzIwMjUtMDQtMjZUMDElM0EwOSUzQTU4LjYwNTAwNTVaJyINCkNvbnRlbnQtVHlwZTogYXBwbGljYXRpb24vanNvbjtvZGF0YT1taW5pbWFsbWV0YWRhdGENCkNvbnRlbnQtSUQ6IDMNCg0K", + "\r\n", + "--changesetresponse_b03ffa4a-53fc-4036-8a02-509eec67ca53-DIFF\r\n", + "Content-Type: application/http\r\n", + "Content-Transfer-Encoding: binary\r\n", + "\r\n", + "b64:SFRUUC8xLjEgMjA0IE5vIENvbnRlbnQNCkVUYWc6IFcvImRhdGV0aW1lJzIwMjUtMDQtMjZUMDElM0EwOSUzQTU4LjYwNTgyNDdaJyINCkNvbnRlbnQtVHlwZTogYXBwbGljYXRpb24vanNvbjtvZGF0YT1taW5pbWFsbWV0YWRhdGENCkNvbnRlbnQtSUQ6IDQNCg0K", + "\r\n", + "--changesetresponse_b03ffa4a-53fc-4036-8a02-509eec67ca53-DIFF\r\n", + "Content-Type: application/http\r\n", + "Content-Transfer-Encoding: binary\r\n", + "\r\n", + "b64:SFRUUC8xLjEgMjA0IE5vIENvbnRlbnQNCkVUYWc6IFcvImRhdGV0aW1lJzIwMjUtMDQtMjZUMDElM0EwOSUzQTU4LjYwNjQzOTFaJyINCkNvbnRlbnQtSUQ6IDUNCg0K", + "\r\n", + "--changesetresponse_b03ffa4a-53fc-4036-8a02-509eec67ca53-DIFF\r\n", + "Content-Type: application/http\r\n", + "Content-Transfer-Encoding: binary\r\n", + "\r\n", + "b64:SFRUUC8xLjEgMjA0IE5vIENvbnRlbnQNCkVUYWc6IFcvImRhdGV0aW1lJzIwMjUtMDQtMjZUMDElM0EwOSUzQTU4LjYwNjg0ODdaJyINCkNvbnRlbnQtSUQ6IDYNCg0K", + "\r\n", + "--changesetresponse_b03ffa4a-53fc-4036-8a02-509eec67ca53-DIFF\r\n", + "Content-Type: application/http\r\n", + "Content-Transfer-Encoding: binary\r\n", + "\r\n", + "b64:SFRUUC8xLjEgMjA0IE5vIENvbnRlbnQNCkNvbnRlbnQtSUQ6IDcNCg0K", + "\r\n", + "--changesetresponse_b03ffa4a-53fc-4036-8a02-509eec67ca53-DIFF--\r\n", + "\r\n", + "--batchresponse_32c36dfe-6fad-46c7-9a9a-5472c406b4a8-DIFF--\r\n" + ] + } + ] +} diff --git a/tools/test-proxy/Azure.Sdk.Tools.TestProxy.Tests/Test.RecordEntries/multipart_request_two_layers.json b/tools/test-proxy/Azure.Sdk.Tools.TestProxy.Tests/Test.RecordEntries/multipart_request_two_layers.json new file mode 100644 index 00000000000..e8e0c00c532 --- /dev/null +++ b/tools/test-proxy/Azure.Sdk.Tools.TestProxy.Tests/Test.RecordEntries/multipart_request_two_layers.json @@ -0,0 +1,142 @@ +{ + "Entries": [ + { + "RequestUri": "https://REDACTED/$batch", + "RequestMethod": "POST", + "RequestHeaders": { + "Accept": "*/*", + "Authorization": "Sanitized", + "Content-Length": "3505", + "Content-Type": "multipart/mixed; boundary=batch_41ee0728-dac8-4887-a5f6-fcf5a69252c9", + "DataServiceVersion": "3.0", + "Date": "Sat, 26 Apr 2025 01:09:57 GMT", + "User-Agent": "azsdk-java-azure-data-tables/12.6.0-beta.1 (17.0.15; Windows 11; 10.0)", + "x-ms-client-request-id": "Sanitized", + "x-ms-version": "2020-12-06" + }, + "RequestBody": [ + "--batch_41ee0728-dac8-4887-a5f6-fcf5a69252c9\r\n", + "Content-Type: multipart/mixed; boundary=changeset_2bb92c1d-78ec-404c-87c7-6a5d8bad1099\r\n", + "\r\n", + "--changeset_2bb92c1d-78ec-404c-87c7-6a5d8bad1099\r\n", + "Content-Type: application/http\r\n", + "Content-Transfer-Encoding: binary\r\n", + "\r\n", + "b64:UE9TVCBodHRwczovL3RlOWQ3Zjk5N2QwM2M2ZjM1cHJpbS50YWJsZS5jb3Ntb3MuYXp1cmUuY29tOjQ0My90YWJsZW5hbWU3MzM2N2U4ZSBIVFRQLzEuMQ0KQ29udGVudC1MZW5ndGg6IDY1DQpQcmVmZXI6IHJldHVybi1uby1jb250ZW50DQpDb250ZW50LVR5cGU6IGFwcGxpY2F0aW9uL2pzb247b2RhdGE9bm9tZXRhZGF0YQ0KRGF0YVNlcnZpY2VWZXJzaW9uOiAzLjANCkFjY2VwdDogYXBwbGljYXRpb24vanNvbjtvZGF0YT1taW5pbWFsbWV0YWRhdGENCg0KeyJSb3dLZXkiOiJyb3drZXk3NDQwNDI2MmQiLCJQYXJ0aXRpb25LZXkiOiJwYXJ0aXRpb24na2V5MjQyNjNjIn0=", + "\r\n", + "--changeset_2bb92c1d-78ec-404c-87c7-6a5d8bad1099\r\n", + "Content-Type: multipart/mixed; boundary=changeset_2bb92c1d-78ec-404c-87c7-6a5d8bad1099-secondLayer\r\n", + "\r\n", + "--changeset_2bb92c1d-78ec-404c-87c7-6a5d8bad1099-secondLayer\r\n", + "Content-Type: application/http\r\n", + "Content-Transfer-Encoding: binary\r\n", + "\r\n", + "b64:TUVSR0UgaHR0cHM6Ly90ZTlkN2Y5OTdkMDNjNmYzNXByaW0udGFibGUuY29zbW9zLmF6dXJlLmNvbTo0NDMvdGFibGVuYW1lNzMzNjdlOGUoUGFydGl0aW9uS2V5PSdwYXJ0aXRpb24nJ2tleTI0MjYzYycsUm93S2V5PSdyb3drZXkwODIwNTE3ODknKSBIVFRQLzEuMQ0KQ29udGVudC1MZW5ndGg6IDY1DQpDb250ZW50LVR5cGU6IGFwcGxpY2F0aW9uL2pzb24NCkRhdGFTZXJ2aWNlVmVyc2lvbjogMy4wDQpBY2NlcHQ6IGFwcGxpY2F0aW9uL2pzb247b2RhdGE9bWluaW1hbG1ldGFkYXRhDQoNCnsiUm93S2V5Ijoicm93a2V5MDgyMDUxNzg5IiwiUGFydGl0aW9uS2V5IjoicGFydGl0aW9uJ2tleTI0MjYzYyJ9", + "\r\n", + "--changeset_2bb92c1d-78ec-404c-87c7-6a5d8bad1099-secondLayer\r\n", + "Content-Type: application/http\r\n", + "Content-Transfer-Encoding: binary\r\n", + "\r\n", + "b64:TUVSR0UgaHR0cHM6Ly90ZTlkN2Y5OTdkMDNjNmYzNXByaW0udGFibGUuY29zbW9zLmF6dXJlLmNvbTo0NDMvdGFibGVuYW1lNzMzNjdlOGUoUGFydGl0aW9uS2V5PSdwYXJ0aXRpb24nJ2tleTI0MjYzYycsUm93S2V5PSdyb3drZXkxNzY1NzE0ZTgnKSBIVFRQLzEuMQ0KQ29udGVudC1MZW5ndGg6IDg2DQpDb250ZW50LVR5cGU6IGFwcGxpY2F0aW9uL2pzb24NCklmLU1hdGNoOiAqDQpEYXRhU2VydmljZVZlcnNpb246IDMuMA0KQWNjZXB0OiBhcHBsaWNhdGlvbi9qc29uO29kYXRhPW1pbmltYWxtZXRhZGF0YQ0KDQp7IlJvd0tleSI6InJvd2tleTE3NjU3MTRlOCIsIlRlc3QiOiJNZXJnZWRWYWx1ZSIsIlBhcnRpdGlvbktleSI6InBhcnRpdGlvbidrZXkyNDI2M2MifQ==", + "\r\n", + "--changeset_2bb92c1d-78ec-404c-87c7-6a5d8bad1099-secondLayer--\r\n", + "\r\n", + "--changeset_2bb92c1d-78ec-404c-87c7-6a5d8bad1099\r\n", + "Content-Type: application/http\r\n", + "Content-Transfer-Encoding: binary\r\n", + "\r\n", + "b64:TUVSR0UgaHR0cHM6Ly90ZTlkN2Y5OTdkMDNjNmYzNXByaW0udGFibGUuY29zbW9zLmF6dXJlLmNvbTo0NDMvdGFibGVuYW1lNzMzNjdlOGUoUGFydGl0aW9uS2V5PSdwYXJ0aXRpb24nJ2tleTI0MjYzYycsUm93S2V5PSdyb3drZXkwOTQxNjYzMzEnKSBIVFRQLzEuMQ0KQ29udGVudC1MZW5ndGg6IDg2DQpDb250ZW50LVR5cGU6IGFwcGxpY2F0aW9uL2pzb24NCkRhdGFTZXJ2aWNlVmVyc2lvbjogMy4wDQpBY2NlcHQ6IGFwcGxpY2F0aW9uL2pzb247b2RhdGE9bWluaW1hbG1ldGFkYXRhDQoNCnsiUm93S2V5Ijoicm93a2V5MDk0MTY2MzMxIiwiVGVzdCI6Ik1lcmdlZFZhbHVlIiwiUGFydGl0aW9uS2V5IjoicGFydGl0aW9uJ2tleTI0MjYzYyJ9", + "\r\n", + "--changeset_2bb92c1d-78ec-404c-87c7-6a5d8bad1099\r\n", + "Content-Type: application/http\r\n", + "Content-Transfer-Encoding: binary\r\n", + "\r\n", + "b64:UFVUIGh0dHBzOi8vdGU5ZDdmOTk3ZDAzYzZmMzVwcmltLnRhYmxlLmNvc21vcy5henVyZS5jb206NDQzL3RhYmxlbmFtZTczMzY3ZThlKFBhcnRpdGlvbktleT0ncGFydGl0aW9uJydrZXkyNDI2M2MnLFJvd0tleT0ncm93a2V5NTM3NDdkYzYyJykgSFRUUC8xLjENCkNvbnRlbnQtTGVuZ3RoOiA4OA0KQ29udGVudC1UeXBlOiBhcHBsaWNhdGlvbi9qc29uDQpEYXRhU2VydmljZVZlcnNpb246IDMuMA0KQWNjZXB0OiBhcHBsaWNhdGlvbi9qc29uO29kYXRhPW1pbmltYWxtZXRhZGF0YQ0KDQp7IlJvd0tleSI6InJvd2tleTUzNzQ3ZGM2MiIsIlRlc3QiOiJSZXBsYWNlZFZhbHVlIiwiUGFydGl0aW9uS2V5IjoicGFydGl0aW9uJ2tleTI0MjYzYyJ9", + "\r\n", + "--changeset_2bb92c1d-78ec-404c-87c7-6a5d8bad1099\r\n", + "Content-Type: application/http\r\n", + "Content-Transfer-Encoding: binary\r\n", + "\r\n", + "b64:TUVSR0UgaHR0cHM6Ly90ZTlkN2Y5OTdkMDNjNmYzNXByaW0udGFibGUuY29zbW9zLmF6dXJlLmNvbTo0NDMvdGFibGVuYW1lNzMzNjdlOGUoUGFydGl0aW9uS2V5PSdwYXJ0aXRpb24nJ2tleTI0MjYzYycsUm93S2V5PSdyb3drZXkxNzY1NzE0ZTgnKSBIVFRQLzEuMQ0KQ29udGVudC1MZW5ndGg6IDg2DQpDb250ZW50LVR5cGU6IGFwcGxpY2F0aW9uL2pzb24NCklmLU1hdGNoOiAqDQpEYXRhU2VydmljZVZlcnNpb246IDMuMA0KQWNjZXB0OiBhcHBsaWNhdGlvbi9qc29uO29kYXRhPW1pbmltYWxtZXRhZGF0YQ0KDQp7IlJvd0tleSI6InJvd2tleTE3NjU3MTRlOCIsIlRlc3QiOiJNZXJnZWRWYWx1ZSIsIlBhcnRpdGlvbktleSI6InBhcnRpdGlvbidrZXkyNDI2M2MifQ==", + "\r\n", + "--changeset_2bb92c1d-78ec-404c-87c7-6a5d8bad1099\r\n", + "Content-Type: application/http\r\n", + "Content-Transfer-Encoding: binary\r\n", + "\r\n", + "b64:UFVUIGh0dHBzOi8vdGU5ZDdmOTk3ZDAzYzZmMzVwcmltLnRhYmxlLmNvc21vcy5henVyZS5jb206NDQzL3RhYmxlbmFtZTczMzY3ZThlKFBhcnRpdGlvbktleT0ncGFydGl0aW9uJydrZXkyNDI2M2MnLFJvd0tleT0ncm93a2V5MTU5NDBiNzY3JykgSFRUUC8xLjENCkNvbnRlbnQtTGVuZ3RoOiA4Ng0KQ29udGVudC1UeXBlOiBhcHBsaWNhdGlvbi9qc29uDQpJZi1NYXRjaDogKg0KRGF0YVNlcnZpY2VWZXJzaW9uOiAzLjANCkFjY2VwdDogYXBwbGljYXRpb24vanNvbjtvZGF0YT1taW5pbWFsbWV0YWRhdGENCg0KeyJSb3dLZXkiOiJyb3drZXkxNTk0MGI3NjciLCJUZXN0IjoiTWVyZ2VkVmFsdWUiLCJQYXJ0aXRpb25LZXkiOiJwYXJ0aXRpb24na2V5MjQyNjNjIn0=", + "\r\n", + "--changeset_2bb92c1d-78ec-404c-87c7-6a5d8bad1099\r\n", + "Content-Type: application/http\r\n", + "Content-Transfer-Encoding: binary\r\n", + "\r\n", + "b64:REVMRVRFIGh0dHBzOi8vdGU5ZDdmOTk3ZDAzYzZmMzVwcmltLnRhYmxlLmNvc21vcy5henVyZS5jb206NDQzL3RhYmxlbmFtZTczMzY3ZThlKFBhcnRpdGlvbktleT0ncGFydGl0aW9uJydrZXkyNDI2M2MnLFJvd0tleT0ncm93a2V5NTI1MTJiZWFmJykgSFRUUC8xLjENCkNvbnRlbnQtTGVuZ3RoOiAwDQpJZi1NYXRjaDogKg0KRGF0YVNlcnZpY2VWZXJzaW9uOiAzLjANCkFjY2VwdDogYXBwbGljYXRpb24vanNvbjtvZGF0YT1taW5pbWFsbWV0YWRhdGENCg0K", + "\r\n", + "--changeset_2bb92c1d-78ec-404c-87c7-6a5d8bad1099--\r\n", + "\r\n", + "--batch_41ee0728-dac8-4887-a5f6-fcf5a69252c9\r\n", + "Content-Type: application/http\r\n", + "Content-Transfer-Encoding: binary\r\n", + "\r\n", + "b64:REVMRVRFIGh0dHBzOi8vdGU5ZDdmOTk3ZDAzYzZmMzVwcmltLnRhYmxlLmNvc21vcy5henVyZS5jb206NDQzL3RhYmxlbmFtZTczMzY3ZThlKFBhcnRpdGlvbktleT0ncGFydGl0aW9uJydrZXkyNDI2M2MnLFJvd0tleT0ncm93a2V5NTI1MTJiZWFmJykgSFRUUC8xLjENCkNvbnRlbnQtTGVuZ3RoOiAwDQpJZi1NYXRjaDogKg0KRGF0YVNlcnZpY2VWZXJzaW9uOiAzLjANCkFjY2VwdDogYXBwbGljYXRpb24vanNvbjtvZGF0YT1taW5pbWFsbWV0YWRhdGENCg0K", + "\r\n", + "--batch_41ee0728-dac8-4887-a5f6-fcf5a69252c9--\r\n" + ], + "StatusCode": 202, + "ResponseHeaders": { + "Content-Type": "multipart/mixed; boundary=batchresponse_32c36dfe-6fad-46c7-9a9a-5472c406b4a8", + "Date": "Sat, 26 Apr 2025 01:09:58 GMT", + "Transfer-Encoding": "chunked" + }, + "ResponseBody": [ + "--batchresponse_32c36dfe-6fad-46c7-9a9a-5472c406b4a8\r\n", + "Content-Type: multipart/mixed; boundary=changesetresponse_b03ffa4a-53fc-4036-8a02-509eec67ca53\r\n", + "\r\n", + "--changesetresponse_b03ffa4a-53fc-4036-8a02-509eec67ca53\r\n", + "Content-Type: application/http\r\n", + "Content-Transfer-Encoding: binary\r\n", + "\r\n", + "b64:SFRUUC8xLjEgMjA0IE5vIENvbnRlbnQNCkVUYWc6IFcvImRhdGV0aW1lJzIwMjUtMDQtMjZUMDElM0EwOSUzQTU4LjYwMzM2NzFaJyINClByZWZlcmVuY2UtQXBwbGllZDogcmV0dXJuLW5vLWNvbnRlbnQNCkxvY2F0aW9uOiBodHRwczovL3RlOWQ3Zjk5N2QwM2M2ZjM1cHJpbS50YWJsZS5jb3Ntb3MuYXp1cmUuY29tL3RhYmxlbmFtZTczMzY3ZThlKFBhcnRpdGlvbktleT0ncGFydGl0aW9uJydrZXkyNDI2M2MnLFJvd0tleT0ncm93a2V5NzQ0MDQyNjJkJykNCkNvbnRlbnQtSUQ6IDENCg0K", + "\r\n", + "--changesetresponse_b03ffa4a-53fc-4036-8a02-509eec67ca53\r\n", + "Content-Type: application/http\r\n", + "Content-Transfer-Encoding: binary\r\n", + "\r\n", + "b64:SFRUUC8xLjEgMjA0IE5vIENvbnRlbnQNCkVUYWc6IFcvImRhdGV0aW1lJzIwMjUtMDQtMjZUMDElM0EwOSUzQTU4LjYwMzc3NjdaJyINCkNvbnRlbnQtVHlwZTogYXBwbGljYXRpb24vanNvbjtvZGF0YT1taW5pbWFsbWV0YWRhdGENCkNvbnRlbnQtSUQ6IDINCg0K", + "\r\n", + "--changesetresponse_b03ffa4a-53fc-4036-8a02-509eec67ca53\r\n", + "Content-Type: application/http\r\n", + "Content-Transfer-Encoding: binary\r\n", + "\r\n", + "b64:SFRUUC8xLjEgMjA0IE5vIENvbnRlbnQNCkVUYWc6IFcvImRhdGV0aW1lJzIwMjUtMDQtMjZUMDElM0EwOSUzQTU4LjYwNTAwNTVaJyINCkNvbnRlbnQtVHlwZTogYXBwbGljYXRpb24vanNvbjtvZGF0YT1taW5pbWFsbWV0YWRhdGENCkNvbnRlbnQtSUQ6IDMNCg0K", + "\r\n", + "--changesetresponse_b03ffa4a-53fc-4036-8a02-509eec67ca53\r\n", + "Content-Type: application/http\r\n", + "Content-Transfer-Encoding: binary\r\n", + "\r\n", + "b64:SFRUUC8xLjEgMjA0IE5vIENvbnRlbnQNCkVUYWc6IFcvImRhdGV0aW1lJzIwMjUtMDQtMjZUMDElM0EwOSUzQTU4LjYwNTgyNDdaJyINCkNvbnRlbnQtVHlwZTogYXBwbGljYXRpb24vanNvbjtvZGF0YT1taW5pbWFsbWV0YWRhdGENCkNvbnRlbnQtSUQ6IDQNCg0K", + "\r\n", + "--changesetresponse_b03ffa4a-53fc-4036-8a02-509eec67ca53\r\n", + "Content-Type: application/http\r\n", + "Content-Transfer-Encoding: binary\r\n", + "\r\n", + "b64:SFRUUC8xLjEgMjA0IE5vIENvbnRlbnQNCkVUYWc6IFcvImRhdGV0aW1lJzIwMjUtMDQtMjZUMDElM0EwOSUzQTU4LjYwNjQzOTFaJyINCkNvbnRlbnQtSUQ6IDUNCg0K", + "\r\n", + "--changesetresponse_b03ffa4a-53fc-4036-8a02-509eec67ca53\r\n", + "Content-Type: application/http\r\n", + "Content-Transfer-Encoding: binary\r\n", + "\r\n", + "b64:SFRUUC8xLjEgMjA0IE5vIENvbnRlbnQNCkVUYWc6IFcvImRhdGV0aW1lJzIwMjUtMDQtMjZUMDElM0EwOSUzQTU4LjYwNjg0ODdaJyINCkNvbnRlbnQtSUQ6IDYNCg0K", + "\r\n", + "--changesetresponse_b03ffa4a-53fc-4036-8a02-509eec67ca53\r\n", + "Content-Type: application/http\r\n", + "Content-Transfer-Encoding: binary\r\n", + "\r\n", + "b64:SFRUUC8xLjEgMjA0IE5vIENvbnRlbnQNCkNvbnRlbnQtSUQ6IDcNCg0K", + "\r\n", + "--changesetresponse_b03ffa4a-53fc-4036-8a02-509eec67ca53--\r\n", + "\r\n", + "--batchresponse_32c36dfe-6fad-46c7-9a9a-5472c406b4a8--\r\n" + ] + } + ] +} diff --git a/tools/test-proxy/Azure.Sdk.Tools.TestProxy.Tests/Test.RecordEntries/multipart_request_two_layers_diff_boundary.json b/tools/test-proxy/Azure.Sdk.Tools.TestProxy.Tests/Test.RecordEntries/multipart_request_two_layers_diff_boundary.json new file mode 100644 index 00000000000..9d010479091 --- /dev/null +++ b/tools/test-proxy/Azure.Sdk.Tools.TestProxy.Tests/Test.RecordEntries/multipart_request_two_layers_diff_boundary.json @@ -0,0 +1,142 @@ +{ + "Entries": [ + { + "RequestUri": "https://REDACTED/$batch", + "RequestMethod": "POST", + "RequestHeaders": { + "Accept": "*/*", + "Authorization": "Sanitized", + "Content-Length": "3505", + "Content-Type": "multipart/mixed; boundary=batch_41ee0728-dac8-4887-a5f6-fcf5a69252c9-DIFF", + "DataServiceVersion": "3.0", + "Date": "Sat, 26 Apr 2025 01:09:57 GMT", + "User-Agent": "azsdk-java-azure-data-tables/12.6.0-beta.1 (17.0.15; Windows 11; 10.0)", + "x-ms-client-request-id": "Sanitized", + "x-ms-version": "2020-12-06" + }, + "RequestBody": [ + "--batch_41ee0728-dac8-4887-a5f6-fcf5a69252c9-DIFF\r\n", + "Content-Type: multipart/mixed; boundary=changeset_2bb92c1d-78ec-404c-87c7-6a5d8bad1099-DIFF\r\n", + "\r\n", + "--changeset_2bb92c1d-78ec-404c-87c7-6a5d8bad1099-DIFF\r\n", + "Content-Type: application/http\r\n", + "Content-Transfer-Encoding: binary\r\n", + "\r\n", + "b64:UE9TVCBodHRwczovL3RlOWQ3Zjk5N2QwM2M2ZjM1cHJpbS50YWJsZS5jb3Ntb3MuYXp1cmUuY29tOjQ0My90YWJsZW5hbWU3MzM2N2U4ZSBIVFRQLzEuMQ0KQ29udGVudC1MZW5ndGg6IDY1DQpQcmVmZXI6IHJldHVybi1uby1jb250ZW50DQpDb250ZW50LVR5cGU6IGFwcGxpY2F0aW9uL2pzb247b2RhdGE9bm9tZXRhZGF0YQ0KRGF0YVNlcnZpY2VWZXJzaW9uOiAzLjANCkFjY2VwdDogYXBwbGljYXRpb24vanNvbjtvZGF0YT1taW5pbWFsbWV0YWRhdGENCg0KeyJSb3dLZXkiOiJyb3drZXk3NDQwNDI2MmQiLCJQYXJ0aXRpb25LZXkiOiJwYXJ0aXRpb24na2V5MjQyNjNjIn0=", + "\r\n", + "--changeset_2bb92c1d-78ec-404c-87c7-6a5d8bad1099-DIFF\r\n", + "Content-Type: multipart/mixed; boundary=changeset_2bb92c1d-78ec-404c-87c7-6a5d8bad1099-secondLayer-DIFF\r\n", + "\r\n", + "--changeset_2bb92c1d-78ec-404c-87c7-6a5d8bad1099-secondLayer-DIFF\r\n", + "Content-Type: application/http\r\n", + "Content-Transfer-Encoding: binary\r\n", + "\r\n", + "b64:TUVSR0UgaHR0cHM6Ly90ZTlkN2Y5OTdkMDNjNmYzNXByaW0udGFibGUuY29zbW9zLmF6dXJlLmNvbTo0NDMvdGFibGVuYW1lNzMzNjdlOGUoUGFydGl0aW9uS2V5PSdwYXJ0aXRpb24nJ2tleTI0MjYzYycsUm93S2V5PSdyb3drZXkwODIwNTE3ODknKSBIVFRQLzEuMQ0KQ29udGVudC1MZW5ndGg6IDY1DQpDb250ZW50LVR5cGU6IGFwcGxpY2F0aW9uL2pzb24NCkRhdGFTZXJ2aWNlVmVyc2lvbjogMy4wDQpBY2NlcHQ6IGFwcGxpY2F0aW9uL2pzb247b2RhdGE9bWluaW1hbG1ldGFkYXRhDQoNCnsiUm93S2V5Ijoicm93a2V5MDgyMDUxNzg5IiwiUGFydGl0aW9uS2V5IjoicGFydGl0aW9uJ2tleTI0MjYzYyJ9", + "\r\n", + "--changeset_2bb92c1d-78ec-404c-87c7-6a5d8bad1099-secondLayer-DIFF\r\n", + "Content-Type: application/http\r\n", + "Content-Transfer-Encoding: binary\r\n", + "\r\n", + "b64:TUVSR0UgaHR0cHM6Ly90ZTlkN2Y5OTdkMDNjNmYzNXByaW0udGFibGUuY29zbW9zLmF6dXJlLmNvbTo0NDMvdGFibGVuYW1lNzMzNjdlOGUoUGFydGl0aW9uS2V5PSdwYXJ0aXRpb24nJ2tleTI0MjYzYycsUm93S2V5PSdyb3drZXkxNzY1NzE0ZTgnKSBIVFRQLzEuMQ0KQ29udGVudC1MZW5ndGg6IDg2DQpDb250ZW50LVR5cGU6IGFwcGxpY2F0aW9uL2pzb24NCklmLU1hdGNoOiAqDQpEYXRhU2VydmljZVZlcnNpb246IDMuMA0KQWNjZXB0OiBhcHBsaWNhdGlvbi9qc29uO29kYXRhPW1pbmltYWxtZXRhZGF0YQ0KDQp7IlJvd0tleSI6InJvd2tleTE3NjU3MTRlOCIsIlRlc3QiOiJNZXJnZWRWYWx1ZSIsIlBhcnRpdGlvbktleSI6InBhcnRpdGlvbidrZXkyNDI2M2MifQ==", + "\r\n", + "--changeset_2bb92c1d-78ec-404c-87c7-6a5d8bad1099-secondLayer-DIFF--\r\n", + "\r\n", + "--changeset_2bb92c1d-78ec-404c-87c7-6a5d8bad1099-DIFF\r\n", + "Content-Type: application/http\r\n", + "Content-Transfer-Encoding: binary\r\n", + "\r\n", + "b64:TUVSR0UgaHR0cHM6Ly90ZTlkN2Y5OTdkMDNjNmYzNXByaW0udGFibGUuY29zbW9zLmF6dXJlLmNvbTo0NDMvdGFibGVuYW1lNzMzNjdlOGUoUGFydGl0aW9uS2V5PSdwYXJ0aXRpb24nJ2tleTI0MjYzYycsUm93S2V5PSdyb3drZXkwOTQxNjYzMzEnKSBIVFRQLzEuMQ0KQ29udGVudC1MZW5ndGg6IDg2DQpDb250ZW50LVR5cGU6IGFwcGxpY2F0aW9uL2pzb24NCkRhdGFTZXJ2aWNlVmVyc2lvbjogMy4wDQpBY2NlcHQ6IGFwcGxpY2F0aW9uL2pzb247b2RhdGE9bWluaW1hbG1ldGFkYXRhDQoNCnsiUm93S2V5Ijoicm93a2V5MDk0MTY2MzMxIiwiVGVzdCI6Ik1lcmdlZFZhbHVlIiwiUGFydGl0aW9uS2V5IjoicGFydGl0aW9uJ2tleTI0MjYzYyJ9", + "\r\n", + "--changeset_2bb92c1d-78ec-404c-87c7-6a5d8bad1099-DIFF\r\n", + "Content-Type: application/http\r\n", + "Content-Transfer-Encoding: binary\r\n", + "\r\n", + "b64:UFVUIGh0dHBzOi8vdGU5ZDdmOTk3ZDAzYzZmMzVwcmltLnRhYmxlLmNvc21vcy5henVyZS5jb206NDQzL3RhYmxlbmFtZTczMzY3ZThlKFBhcnRpdGlvbktleT0ncGFydGl0aW9uJydrZXkyNDI2M2MnLFJvd0tleT0ncm93a2V5NTM3NDdkYzYyJykgSFRUUC8xLjENCkNvbnRlbnQtTGVuZ3RoOiA4OA0KQ29udGVudC1UeXBlOiBhcHBsaWNhdGlvbi9qc29uDQpEYXRhU2VydmljZVZlcnNpb246IDMuMA0KQWNjZXB0OiBhcHBsaWNhdGlvbi9qc29uO29kYXRhPW1pbmltYWxtZXRhZGF0YQ0KDQp7IlJvd0tleSI6InJvd2tleTUzNzQ3ZGM2MiIsIlRlc3QiOiJSZXBsYWNlZFZhbHVlIiwiUGFydGl0aW9uS2V5IjoicGFydGl0aW9uJ2tleTI0MjYzYyJ9", + "\r\n", + "--changeset_2bb92c1d-78ec-404c-87c7-6a5d8bad1099-DIFF\r\n", + "Content-Type: application/http\r\n", + "Content-Transfer-Encoding: binary\r\n", + "\r\n", + "b64:TUVSR0UgaHR0cHM6Ly90ZTlkN2Y5OTdkMDNjNmYzNXByaW0udGFibGUuY29zbW9zLmF6dXJlLmNvbTo0NDMvdGFibGVuYW1lNzMzNjdlOGUoUGFydGl0aW9uS2V5PSdwYXJ0aXRpb24nJ2tleTI0MjYzYycsUm93S2V5PSdyb3drZXkxNzY1NzE0ZTgnKSBIVFRQLzEuMQ0KQ29udGVudC1MZW5ndGg6IDg2DQpDb250ZW50LVR5cGU6IGFwcGxpY2F0aW9uL2pzb24NCklmLU1hdGNoOiAqDQpEYXRhU2VydmljZVZlcnNpb246IDMuMA0KQWNjZXB0OiBhcHBsaWNhdGlvbi9qc29uO29kYXRhPW1pbmltYWxtZXRhZGF0YQ0KDQp7IlJvd0tleSI6InJvd2tleTE3NjU3MTRlOCIsIlRlc3QiOiJNZXJnZWRWYWx1ZSIsIlBhcnRpdGlvbktleSI6InBhcnRpdGlvbidrZXkyNDI2M2MifQ==", + "\r\n", + "--changeset_2bb92c1d-78ec-404c-87c7-6a5d8bad1099-DIFF\r\n", + "Content-Type: application/http\r\n", + "Content-Transfer-Encoding: binary\r\n", + "\r\n", + "b64:UFVUIGh0dHBzOi8vdGU5ZDdmOTk3ZDAzYzZmMzVwcmltLnRhYmxlLmNvc21vcy5henVyZS5jb206NDQzL3RhYmxlbmFtZTczMzY3ZThlKFBhcnRpdGlvbktleT0ncGFydGl0aW9uJydrZXkyNDI2M2MnLFJvd0tleT0ncm93a2V5MTU5NDBiNzY3JykgSFRUUC8xLjENCkNvbnRlbnQtTGVuZ3RoOiA4Ng0KQ29udGVudC1UeXBlOiBhcHBsaWNhdGlvbi9qc29uDQpJZi1NYXRjaDogKg0KRGF0YVNlcnZpY2VWZXJzaW9uOiAzLjANCkFjY2VwdDogYXBwbGljYXRpb24vanNvbjtvZGF0YT1taW5pbWFsbWV0YWRhdGENCg0KeyJSb3dLZXkiOiJyb3drZXkxNTk0MGI3NjciLCJUZXN0IjoiTWVyZ2VkVmFsdWUiLCJQYXJ0aXRpb25LZXkiOiJwYXJ0aXRpb24na2V5MjQyNjNjIn0=", + "\r\n", + "--changeset_2bb92c1d-78ec-404c-87c7-6a5d8bad1099-DIFF\r\n", + "Content-Type: application/http\r\n", + "Content-Transfer-Encoding: binary\r\n", + "\r\n", + "b64:REVMRVRFIGh0dHBzOi8vdGU5ZDdmOTk3ZDAzYzZmMzVwcmltLnRhYmxlLmNvc21vcy5henVyZS5jb206NDQzL3RhYmxlbmFtZTczMzY3ZThlKFBhcnRpdGlvbktleT0ncGFydGl0aW9uJydrZXkyNDI2M2MnLFJvd0tleT0ncm93a2V5NTI1MTJiZWFmJykgSFRUUC8xLjENCkNvbnRlbnQtTGVuZ3RoOiAwDQpJZi1NYXRjaDogKg0KRGF0YVNlcnZpY2VWZXJzaW9uOiAzLjANCkFjY2VwdDogYXBwbGljYXRpb24vanNvbjtvZGF0YT1taW5pbWFsbWV0YWRhdGENCg0K", + "\r\n", + "--changeset_2bb92c1d-78ec-404c-87c7-6a5d8bad1099-DIFF--\r\n", + "\r\n", + "--batch_41ee0728-dac8-4887-a5f6-fcf5a69252c9-DIFF\r\n", + "Content-Type: application/http\r\n", + "Content-Transfer-Encoding: binary\r\n", + "\r\n", + "b64:REVMRVRFIGh0dHBzOi8vdGU5ZDdmOTk3ZDAzYzZmMzVwcmltLnRhYmxlLmNvc21vcy5henVyZS5jb206NDQzL3RhYmxlbmFtZTczMzY3ZThlKFBhcnRpdGlvbktleT0ncGFydGl0aW9uJydrZXkyNDI2M2MnLFJvd0tleT0ncm93a2V5NTI1MTJiZWFmJykgSFRUUC8xLjENCkNvbnRlbnQtTGVuZ3RoOiAwDQpJZi1NYXRjaDogKg0KRGF0YVNlcnZpY2VWZXJzaW9uOiAzLjANCkFjY2VwdDogYXBwbGljYXRpb24vanNvbjtvZGF0YT1taW5pbWFsbWV0YWRhdGENCg0K", + "\r\n", + "--batch_41ee0728-dac8-4887-a5f6-fcf5a69252c9-DIFF--\r\n" + ], + "StatusCode": 202, + "ResponseHeaders": { + "Content-Type": "multipart/mixed; boundary=batchresponse_32c36dfe-6fad-46c7-9a9a-5472c406b4a8-DIFF", + "Date": "Sat, 26 Apr 2025 01:09:58 GMT", + "Transfer-Encoding": "chunked" + }, + "ResponseBody": [ + "--batchresponse_32c36dfe-6fad-46c7-9a9a-5472c406b4a8-DIFF\r\n", + "Content-Type: multipart/mixed; boundary=changesetresponse_b03ffa4a-53fc-4036-8a02-509eec67ca53-DIFF\r\n", + "\r\n", + "--changesetresponse_b03ffa4a-53fc-4036-8a02-509eec67ca53-DIFF\r\n", + "Content-Type: application/http\r\n", + "Content-Transfer-Encoding: binary\r\n", + "\r\n", + "b64:SFRUUC8xLjEgMjA0IE5vIENvbnRlbnQNCkVUYWc6IFcvImRhdGV0aW1lJzIwMjUtMDQtMjZUMDElM0EwOSUzQTU4LjYwMzM2NzFaJyINClByZWZlcmVuY2UtQXBwbGllZDogcmV0dXJuLW5vLWNvbnRlbnQNCkxvY2F0aW9uOiBodHRwczovL3RlOWQ3Zjk5N2QwM2M2ZjM1cHJpbS50YWJsZS5jb3Ntb3MuYXp1cmUuY29tL3RhYmxlbmFtZTczMzY3ZThlKFBhcnRpdGlvbktleT0ncGFydGl0aW9uJydrZXkyNDI2M2MnLFJvd0tleT0ncm93a2V5NzQ0MDQyNjJkJykNCkNvbnRlbnQtSUQ6IDENCg0K", + "\r\n", + "--changesetresponse_b03ffa4a-53fc-4036-8a02-509eec67ca53-DIFF\r\n", + "Content-Type: application/http\r\n", + "Content-Transfer-Encoding: binary\r\n", + "\r\n", + "b64:SFRUUC8xLjEgMjA0IE5vIENvbnRlbnQNCkVUYWc6IFcvImRhdGV0aW1lJzIwMjUtMDQtMjZUMDElM0EwOSUzQTU4LjYwMzc3NjdaJyINCkNvbnRlbnQtVHlwZTogYXBwbGljYXRpb24vanNvbjtvZGF0YT1taW5pbWFsbWV0YWRhdGENCkNvbnRlbnQtSUQ6IDINCg0K", + "\r\n", + "--changesetresponse_b03ffa4a-53fc-4036-8a02-509eec67ca53-DIFF\r\n", + "Content-Type: application/http\r\n", + "Content-Transfer-Encoding: binary\r\n", + "\r\n", + "b64:SFRUUC8xLjEgMjA0IE5vIENvbnRlbnQNCkVUYWc6IFcvImRhdGV0aW1lJzIwMjUtMDQtMjZUMDElM0EwOSUzQTU4LjYwNTAwNTVaJyINCkNvbnRlbnQtVHlwZTogYXBwbGljYXRpb24vanNvbjtvZGF0YT1taW5pbWFsbWV0YWRhdGENCkNvbnRlbnQtSUQ6IDMNCg0K", + "\r\n", + "--changesetresponse_b03ffa4a-53fc-4036-8a02-509eec67ca53-DIFF\r\n", + "Content-Type: application/http\r\n", + "Content-Transfer-Encoding: binary\r\n", + "\r\n", + "b64:SFRUUC8xLjEgMjA0IE5vIENvbnRlbnQNCkVUYWc6IFcvImRhdGV0aW1lJzIwMjUtMDQtMjZUMDElM0EwOSUzQTU4LjYwNTgyNDdaJyINCkNvbnRlbnQtVHlwZTogYXBwbGljYXRpb24vanNvbjtvZGF0YT1taW5pbWFsbWV0YWRhdGENCkNvbnRlbnQtSUQ6IDQNCg0K", + "\r\n", + "--changesetresponse_b03ffa4a-53fc-4036-8a02-509eec67ca53-DIFF\r\n", + "Content-Type: application/http\r\n", + "Content-Transfer-Encoding: binary\r\n", + "\r\n", + "b64:SFRUUC8xLjEgMjA0IE5vIENvbnRlbnQNCkVUYWc6IFcvImRhdGV0aW1lJzIwMjUtMDQtMjZUMDElM0EwOSUzQTU4LjYwNjQzOTFaJyINCkNvbnRlbnQtSUQ6IDUNCg0K", + "\r\n", + "--changesetresponse_b03ffa4a-53fc-4036-8a02-509eec67ca53-DIFF\r\n", + "Content-Type: application/http\r\n", + "Content-Transfer-Encoding: binary\r\n", + "\r\n", + "b64:SFRUUC8xLjEgMjA0IE5vIENvbnRlbnQNCkVUYWc6IFcvImRhdGV0aW1lJzIwMjUtMDQtMjZUMDElM0EwOSUzQTU4LjYwNjg0ODdaJyINCkNvbnRlbnQtSUQ6IDYNCg0K", + "\r\n", + "--changesetresponse_b03ffa4a-53fc-4036-8a02-509eec67ca53-DIFF\r\n", + "Content-Type: application/http\r\n", + "Content-Transfer-Encoding: binary\r\n", + "\r\n", + "b64:SFRUUC8xLjEgMjA0IE5vIENvbnRlbnQNCkNvbnRlbnQtSUQ6IDcNCg0K", + "\r\n", + "--changesetresponse_b03ffa4a-53fc-4036-8a02-509eec67ca53-DIFF--\r\n", + "\r\n", + "--batchresponse_32c36dfe-6fad-46c7-9a9a-5472c406b4a8-DIFF--\r\n" + ] + } + ] +} diff --git a/tools/test-proxy/Azure.Sdk.Tools.TestProxy/Common/ContentTypeUtilities.cs b/tools/test-proxy/Azure.Sdk.Tools.TestProxy/Common/ContentTypeUtilities.cs index 6bfff0e4e4d..8fc6a25c72e 100644 --- a/tools/test-proxy/Azure.Sdk.Tools.TestProxy/Common/ContentTypeUtilities.cs +++ b/tools/test-proxy/Azure.Sdk.Tools.TestProxy/Common/ContentTypeUtilities.cs @@ -21,7 +21,7 @@ public static bool IsManifestContentType(string contentType) return contentType.Contains(dockerManifest) || contentType.Contains(dockerIndex); } - public static bool IsMultipartMixed(IDictionary headers, + public static bool IsMultipart(IDictionary headers, out string boundary) { boundary = null; @@ -30,14 +30,30 @@ public static bool IsMultipartMixed(IDictionary headers, return false; var ct = values[0]; - if (!ct.StartsWith("multipart/mixed", StringComparison.OrdinalIgnoreCase)) + /* + * For the most part, the multipart serialization/deserialization code should be universal across + * all the various types of multipart/* payloads. + * + * Until we discover otherwise, we'll treat them the same. + */ + if (!ct.StartsWith("multipart/", StringComparison.OrdinalIgnoreCase)) + return false; + + return IsMultiPart(ct, out boundary); + } + + public static bool IsMultiPart(string contentType, out string boundary) + { + boundary = null; + + if (contentType is null) return false; const string key = "boundary="; - var idx = ct.IndexOf(key, StringComparison.OrdinalIgnoreCase); + var idx = contentType.IndexOf(key, StringComparison.OrdinalIgnoreCase); if (idx == -1) return false; - boundary = ct[(idx + key.Length)..] // everything after “boundary=” + boundary = contentType[(idx + key.Length)..] // everything after “boundary=” .Trim() // strip spaces .Trim('"'); // strip optional quotes return boundary.Length > 0; diff --git a/tools/test-proxy/Azure.Sdk.Tools.TestProxy/Common/MultipartUtilities.cs b/tools/test-proxy/Azure.Sdk.Tools.TestProxy/Common/MultipartUtilities.cs index f3187b733f3..cb78ef88cd2 100644 --- a/tools/test-proxy/Azure.Sdk.Tools.TestProxy/Common/MultipartUtilities.cs +++ b/tools/test-proxy/Azure.Sdk.Tools.TestProxy/Common/MultipartUtilities.cs @@ -1,21 +1,21 @@ -using Microsoft.AspNetCore.WebUtilities; -using Microsoft.Extensions.Primitives; +using System; +using System.Buffers; using System.Collections.Generic; using System.IO; -using System.Text.Json; +using System.Net; using System.Text; -using System; -using Microsoft.Net.Http.Headers; -using Microsoft.Extensions.Logging; +using System.Text.Json; using Azure.Sdk.Tools.TestProxy.Common.Exceptions; -using Newtonsoft.Json.Linq; -using System.Net; +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; namespace Azure.Sdk.Tools.TestProxy.Common { public static class MultipartUtilities { - public static readonly byte[] CrLf = new byte[] { (byte)'\r', (byte)'\n' }; + public static readonly byte[] CrLf = "\r\n"u8.ToArray(); public static byte[] ReadAllBytes(Stream s) { @@ -285,5 +285,233 @@ public static byte[] DeserializeMultipartBody(JsonElement property, string bound return ms.ToArray(); } + + /// + /// Given a multipart body, remove the boundary delimiters and return only the content. + /// Used for normalizing multipart bodies for comparison. + /// Uses to avoid unnecessary allocations. + /// + /// The body of a multipart request or response. + /// The boundary. + /// The encoding to use for comparing known strings to parts of the . + public static ReadOnlySequence RemoveBoundaries(byte[] body, ReadOnlySpan boundary, Encoding encoding) + { + if (body == null || body.Length == 0) + { + return ReadOnlySequence.Empty; + } + + if (boundary == null || boundary.Length == 0) + { + return new ReadOnlySequence(body); + } + + Segment first = null; + Segment last = null; + int position = 0; + + RemoveCurrentBoundary(body, boundary, ref position, ref first, ref last, encoding.GetBytes("Content-Type: multipart/"), encoding.GetBytes("boundary=")); + + if (first == null) + { + return ReadOnlySequence.Empty; + } + + return new ReadOnlySequence(first, 0, last, last.Memory.Length); + } + + /// + /// Add a new segment to the . + /// + /// First in the sequence. + /// Last in the sequence. + /// The original byte array. + /// The start index of the slice. + /// The length of the slice. + private static void AddSlice(ref Segment first, ref Segment last, byte[] body, int start, int length) + { + if (length <= 0) return; + + var mem = new ReadOnlyMemory(body, start, length); + if (first == null) + { + first = last = new Segment(mem, 0); + } + else + { + last = last.Append(mem); + } + } + + /// + /// Recursively remove boundaries from a multipart body. + /// Each part itself can be another multipart body. + /// + /// The original byte array. + /// Current boundary we are processing. + /// Current position in the . + /// First of the sequence. + /// Last of the sequence. + /// Static byte array representation of "Content-Type: multipart/" in the correct encoding. + /// Static byte array representation of "boundary=" in the correct encoding. + private static void RemoveCurrentBoundary( + byte[] body, + ReadOnlySpan boundary, + ref int position, + ref Segment first, + ref Segment last, + ReadOnlySpan contentTypeMpfdBytes, + ReadOnlySpan boundaryBytes) + { + ReadOnlySpan span = body.AsSpan(); + int spanLength = span.Length; + int boundaryBytesLength = boundaryBytes.Length; + + const byte DASH = 0x2D; + + // Scan line by line (CRLF terminated). We will skip lines that are boundary delimiter lines: + // --boundary + // --boundary-- + int boundaryLength = boundary.Length; + int partStart = position; + while (position < spanLength) + { + int crlfIndex = span.Slice(position).IndexOf(CrLf); + if (crlfIndex == -1) + { + AddSlice(ref first, ref last, body, partStart, spanLength - partStart); + break; + } + + ReadOnlySpan line = span.Slice(position, crlfIndex); + position += crlfIndex + 2; + + // if we have a new content type with a boundary recursively process it + // "Content-Type: multipart/mixed; boundary=changesetresponse_b03ffa4a-53fc-4036-8a02-509eec67ca53\r\n", + if (line.StartsWith(contentTypeMpfdBytes)) + { + int boundaryBytesIndex = line.IndexOf(boundaryBytes); + if (boundaryBytesIndex != -1) + { + int innerBoundaryStart = boundaryBytesIndex + boundaryBytesLength; + ReadOnlySpan innerBoundary = line.Slice(innerBoundaryStart, crlfIndex - innerBoundaryStart); + RemoveCurrentBoundary(body, innerBoundary, ref position, ref first, ref last, contentTypeMpfdBytes, boundaryBytes); + partStart = position; + continue; + } + } + + bool isBoundaryLine = false; + bool isEndBoundaryLine = false; + if (line.Length >= 2 + boundaryLength) + { + // Must start with "--" + if (line[0] == DASH && line[1] == DASH) + { + var afterDashes = line.Slice(2); + if (afterDashes.StartsWith(boundary)) + { + int remaining = afterDashes.Length - boundaryLength; + isBoundaryLine = remaining == 0; + isEndBoundaryLine = remaining == 2 && afterDashes[boundaryLength] == DASH && afterDashes[boundaryLength + 1] == DASH; + } + } + } + + if (isBoundaryLine || isEndBoundaryLine) + { + AddSlice(ref first, ref last, body, partStart, position - (crlfIndex + 2) - partStart); + partStart = position; + } + + if (isEndBoundaryLine) + { + break; // end boundary, stop processing this layer + } + } + } + + /// + /// A class representing a segment in a . + /// + private sealed class Segment : ReadOnlySequenceSegment + { + public Segment(ReadOnlyMemory memory, long runningIndex) + { + Memory = memory; + RunningIndex = runningIndex; + } + + public Segment Append(ReadOnlyMemory memory) + { + var next = new Segment(memory, RunningIndex + Memory.Length); + Next = next; + return next; + } + } + + /// + /// Compares to instances for equality, returning the index and length of the first segment that has a difference. + /// + /// The first sequence to compare. + /// The second sequence to compare. + /// The index of the first difference. + /// The length of the differing segment. + /// True if the sequences are equal; otherwise, false. + public static bool SequenceEqual(this in ReadOnlySequence a, in ReadOnlySequence b, out int index, out int length) + { + index = 0; + length = 0; + + // skipping length compare to get the index of first difference + + if (a.IsSingleSegment && b.IsSingleSegment) + { + bool equal = a.FirstSpan.SequenceEqual(b.FirstSpan); + if (!equal) + { + length = (int)Math.Min(a.Length, b.Length); + } + return equal; + } + + var ra = new SequenceReader(a); + var rb = new SequenceReader(b); + + while (!ra.End && !rb.End) + { + var sa = ra.UnreadSpan; + var sb = rb.UnreadSpan; + length = Math.Min(sa.Length, sb.Length); + + index = (int)ra.Consumed; + if (!sa.Slice(0, length).SequenceEqual(sb.Slice(0, length))) + return false; + + ra.Advance(length); + rb.Advance(length); + } + + return ra.End && rb.End; + } + + /// + /// Given a slice of a , convert it to a string using the provided encoding. + /// + /// The sequence to convert. + /// The starting index of the slice. + /// The length of the slice. + /// The encoding to use. + /// The string representation of the slice. + public static string SliceToString(this in ReadOnlySequence seq, long index, long length, Encoding encoding) + { + encoding ??= Encoding.UTF8; + var slice = seq.Slice(index, length); + + if (slice.IsSingleSegment) + return encoding.GetString(slice.FirstSpan); + + return encoding.GetString(slice.ToArray()); + } } } diff --git a/tools/test-proxy/Azure.Sdk.Tools.TestProxy/Common/RecordEntry.cs b/tools/test-proxy/Azure.Sdk.Tools.TestProxy/Common/RecordEntry.cs index 3d8e5bd42ec..8bbf212f808 100644 --- a/tools/test-proxy/Azure.Sdk.Tools.TestProxy/Common/RecordEntry.cs +++ b/tools/test-proxy/Azure.Sdk.Tools.TestProxy/Common/RecordEntry.cs @@ -120,7 +120,7 @@ private static void DeserializeBody(RequestOrResponse requestOrResponse, in Json // TODO consider versioning RecordSession so that we can stop doing the below for newly created recordings NormalizeJsonBody(requestOrResponse); } - else if (ContentTypeUtilities.IsMultipartMixed(requestOrResponse.Headers, out var boundary) && property.ValueKind == JsonValueKind.Array) + else if (ContentTypeUtilities.IsMultipart(requestOrResponse.Headers, out var boundary) && property.ValueKind == JsonValueKind.Array) { requestOrResponse.Body = MultipartUtilities.DeserializeMultipartBody(property, boundary); } @@ -263,7 +263,7 @@ private void SerializeBody(Utf8JsonWriter jsonWriter, string name, byte[] reques jsonWriter.WriteEndArray(); } } - else if (ContentTypeUtilities.IsMultipartMixed(headers, out var boundary)) + else if (ContentTypeUtilities.IsMultipart(headers, out var boundary)) { MultipartUtilities.SerializeMultipartBody(jsonWriter, name, requestBody, boundary); } diff --git a/tools/test-proxy/Azure.Sdk.Tools.TestProxy/Common/RecordMatcher.cs b/tools/test-proxy/Azure.Sdk.Tools.TestProxy/Common/RecordMatcher.cs index e07b95bfff7..556db911bc9 100644 --- a/tools/test-proxy/Azure.Sdk.Tools.TestProxy/Common/RecordMatcher.cs +++ b/tools/test-proxy/Azure.Sdk.Tools.TestProxy/Common/RecordMatcher.cs @@ -1,13 +1,14 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using Azure.Core; using System; +using System.Buffers; using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; using System.Text; using System.Web; +using Azure.Core; namespace Azure.Sdk.Tools.TestProxy.Common { @@ -113,9 +114,10 @@ public virtual RecordEntry FindMatch(RecordEntry request, IList ent { score += CompareHeaderDictionaries(request.Request.Headers, entry.Request.Headers, IgnoredHeaders, ExcludeHeaders); - request.Request.TryGetContentType(out var contentType); - - score += CompareBodies(request.Request.Body, entry.Request.Body, descriptionBuilder: null, contentType: contentType); + request.Request.TryGetContentType(out var requestContentType); + entry.Request.TryGetContentType(out var recordContentType); + ContentTypeUtilities.TryGetTextEncoding(requestContentType, out var encoding); + score += CompareBodies(request.Request.Body, entry.Request.Body, requestContentType, recordContentType, encoding, descriptionBuilder: null); } if (score == 0) @@ -133,7 +135,7 @@ public virtual RecordEntry FindMatch(RecordEntry request, IList ent throw new TestRecordingMismatchException(GenerateException(request, bestScoreEntry, entries)); } - public virtual int CompareBodies(byte[] requestBody, byte[] recordBody, string contentType, StringBuilder descriptionBuilder = null) + public virtual int CompareBodies(byte[] requestBody, byte[] recordBody, string requestContentType, string recordContentType, Encoding encoding, StringBuilder descriptionBuilder = null) { if (!_compareBodies) { @@ -157,11 +159,33 @@ public virtual int CompareBodies(byte[] requestBody, byte[] recordBody, string c return 1; } + if (ContentTypeUtilities.IsMultiPart(requestContentType, out var requestBoundary) && + ContentTypeUtilities.IsMultiPart(recordContentType, out var recordBoundary) && + !requestBoundary.AsSpan().SequenceEqual(recordBoundary.AsSpan())) + { + encoding ??= Encoding.UTF8; + + ReadOnlySequence requestMinusBoundary = MultipartUtilities.RemoveBoundaries(requestBody, encoding.GetBytes(requestBoundary), encoding); + ReadOnlySequence recordMinusBoundary = MultipartUtilities.RemoveBoundaries(recordBody, encoding.GetBytes(recordBoundary), encoding); + + if (!requestMinusBoundary.SequenceEqual(recordMinusBoundary, out int index, out int length)) + { + descriptionBuilder?.AppendLine("Multipart bodies differ after removing boundaries."); + if (length > 0) + { + descriptionBuilder?.AppendLine($"First difference inside the snippet from {index} to {length}."); + descriptionBuilder?.AppendLine($"Request snippet: \"{requestMinusBoundary.SliceToString(index, Math.Min(length, requestMinusBoundary.Length - index), encoding)}\""); + descriptionBuilder?.AppendLine($"Record snippet: \"{recordMinusBoundary.SliceToString(index, Math.Min(length, recordMinusBoundary.Length - index), encoding)}\""); + } + return 1; + } + return 0; + } if (!requestBody.SequenceEqual(recordBody)) { // we just failed sequence equality, before erroring, lets check if we're a json body and check for property equality - if (!string.IsNullOrWhiteSpace(contentType) && contentType.Contains("json")) + if (!string.IsNullOrWhiteSpace(requestContentType) && requestContentType.Contains("json")) { var jsonDifferences = JsonComparer.CompareJson(requestBody, recordBody); @@ -179,8 +203,9 @@ public virtual int CompareBodies(byte[] requestBody, byte[] recordBody, string c return 1; } - } - else { + } + else + { if (descriptionBuilder != null) { var minLength = Math.Min(requestBody.Length, recordBody.Length); @@ -268,8 +293,10 @@ private string GenerateException(RecordEntry request, RecordEntry bestScoreEntry builder.AppendLine("Body differences:"); - request.Request.TryGetContentType(out var contentType); - CompareBodies(request.Request.Body, bestScoreEntry.Request.Body, contentType, descriptionBuilder: builder); + request.Request.TryGetContentType(out var requestContentType); + bestScoreEntry.Request.TryGetContentType(out var recordContentType); + ContentTypeUtilities.TryGetTextEncoding(requestContentType, out var encoding); + CompareBodies(request.Request.Body, bestScoreEntry.Request.Body, requestContentType, recordContentType, encoding, descriptionBuilder: builder); if (entries != null && entries.Count >= 1) { @@ -328,6 +355,28 @@ public virtual int CompareHeaderDictionaries(SortedDictionary entryHeaderValues = RenormalizeContentHeaders(entryHeaderValues); } + if (headerName.Equals("Content-Type", StringComparison.Ordinal)) + { + if (requestHeaderValues.Length == 1 && entryHeaderValues.Length == 1) + { + var requestContentType = requestHeaderValues[0]; + var entryContentType = entryHeaderValues[0]; + if (ContentTypeUtilities.IsMultiPart(requestContentType, out var requestBoundary) && + ContentTypeUtilities.IsMultiPart(entryContentType, out var entryBoundary)) + { + string requestContentTypeWithoutBoundary = requestContentType.Replace(requestBoundary, string.Empty); + string entryContentTypeWithoutBoundary = entryContentType.Replace(entryBoundary, string.Empty); + if (!requestContentTypeWithoutBoundary.Equals(entryContentTypeWithoutBoundary, StringComparison.Ordinal)) + { + difference++; + descriptionBuilder?.AppendLine($" <{headerName}> values differ, request <{requestContentTypeWithoutBoundary}>, record <{entryContentTypeWithoutBoundary}>"); + } + remaining.Remove(headerName); + continue; + } + } + } + remaining.Remove(headerName); if (!entryHeaderValues.SequenceEqual(requestHeaderValues)) { diff --git a/tools/test-proxy/Azure.Sdk.Tools.TestProxy/Common/RecordedTestSanitizer.cs b/tools/test-proxy/Azure.Sdk.Tools.TestProxy/Common/RecordedTestSanitizer.cs index 5dcc1aa8fc9..e7f2cf6a136 100644 --- a/tools/test-proxy/Azure.Sdk.Tools.TestProxy/Common/RecordedTestSanitizer.cs +++ b/tools/test-proxy/Azure.Sdk.Tools.TestProxy/Common/RecordedTestSanitizer.cs @@ -205,7 +205,7 @@ public virtual void SanitizeBody(RequestOrResponse message) { message.TryGetContentType(out string contentType); - if (ContentTypeUtilities.IsMultipartMixed(message.Headers, out var boundary)) + if (ContentTypeUtilities.IsMultipart(message.Headers, out var boundary)) { message.Body = SanitizeMultipartBody(boundary, message.Body); }