From 30c39e18b0762fe96e787a31578485eb3f7217f9 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Thu, 11 Sep 2025 15:54:24 -0700 Subject: [PATCH 01/24] SortsQueryParams --- .../ConfigurationClientBuilder.java | 4 + .../implementation/QueryParamPolicy.java | 62 ++++ .../implementation/QueryParamPolicyTest.java | 280 ++++++++++++++++++ 3 files changed, 346 insertions(+) create mode 100644 sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/implementation/QueryParamPolicy.java create mode 100644 sdk/appconfiguration/azure-data-appconfiguration/src/test/java/com/azure/data/appconfiguration/implementation/QueryParamPolicyTest.java diff --git a/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/ConfigurationClientBuilder.java b/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/ConfigurationClientBuilder.java index 79c0bb5c80f2..7e209d811dd7 100644 --- a/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/ConfigurationClientBuilder.java +++ b/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/ConfigurationClientBuilder.java @@ -42,6 +42,7 @@ import com.azure.data.appconfiguration.implementation.AzureAppConfigurationImpl; import com.azure.data.appconfiguration.implementation.ConfigurationClientCredentials; import com.azure.data.appconfiguration.implementation.ConfigurationCredentialsPolicy; +import com.azure.data.appconfiguration.implementation.QueryParamPolicy; import com.azure.data.appconfiguration.implementation.SyncTokenPolicy; import com.azure.data.appconfiguration.models.ConfigurationAudience; @@ -263,6 +264,9 @@ private HttpPipeline createDefaultHttpPipeline(SyncTokenPolicy syncTokenPolicy, policies.add(new AddHeadersFromContextPolicy()); policies.add(ADD_HEADERS_POLICY); + // Add query parameter reordering policy + policies.add(new QueryParamPolicy()); + policies.addAll(perCallPolicies); HttpPolicyProviders.addBeforeRetryPolicies(policies); diff --git a/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/implementation/QueryParamPolicy.java b/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/implementation/QueryParamPolicy.java new file mode 100644 index 000000000000..4b56d047400e --- /dev/null +++ b/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/implementation/QueryParamPolicy.java @@ -0,0 +1,62 @@ +package com.azure.data.appconfiguration.implementation; + +import java.net.MalformedURLException; +import java.util.Map; +import java.util.TreeMap; + +import com.azure.core.http.HttpPipelineCallContext; +import com.azure.core.http.HttpPipelineNextPolicy; +import com.azure.core.http.HttpRequest; +import com.azure.core.http.HttpResponse; +import com.azure.core.http.policy.HttpPipelinePolicy; +import com.azure.core.util.UrlBuilder; +import com.azure.core.util.logging.ClientLogger; + +import reactor.core.publisher.Mono; + +public class QueryParamPolicy implements HttpPipelinePolicy { + private static final ClientLogger LOGGER = new ClientLogger(QueryParamPolicy.class); + + @Override + public Mono process(HttpPipelineCallContext context, HttpPipelineNextPolicy next) { + HttpRequest request = context.getHttpRequest(); + + try { + UrlBuilder urlBuilder = UrlBuilder.parse(request.getUrl()); + Map queryParams = urlBuilder.getQuery(); + + if (queryParams != null && !queryParams.isEmpty()) { + // Create a new TreeMap to automatically sort by keys alphabetically + Map sortedParams = new TreeMap<>(); + + // Process each query parameter: convert key to lowercase and add to sorted map + for (Map.Entry entry : queryParams.entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + + // Convert key to lowercase, but preserve special cases like $Select -> $select + String lowercaseKey = key.toLowerCase(); + sortedParams.put(lowercaseKey, value); + } + + // Clear existing query parameters and add sorted ones + urlBuilder.setQuery(null); + for (Map.Entry entry : sortedParams.entrySet()) { + urlBuilder.addQueryParameter(entry.getKey(), entry.getValue()); + } + + // Update the request URL with reordered parameters + request.setUrl(urlBuilder.toUrl()); + } + } catch (MalformedURLException e) { + // If URL parsing fails, continue without modification + LOGGER.warning( + "Failed to parse URL for query parameter normalization. " + + "Request will proceed with original URL. URL: {}, Error: {}", + request.getUrl(), e.getMessage(), e); + } + + return next.process(); + } + +} diff --git a/sdk/appconfiguration/azure-data-appconfiguration/src/test/java/com/azure/data/appconfiguration/implementation/QueryParamPolicyTest.java b/sdk/appconfiguration/azure-data-appconfiguration/src/test/java/com/azure/data/appconfiguration/implementation/QueryParamPolicyTest.java new file mode 100644 index 000000000000..7c42d5dc93d6 --- /dev/null +++ b/sdk/appconfiguration/azure-data-appconfiguration/src/test/java/com/azure/data/appconfiguration/implementation/QueryParamPolicyTest.java @@ -0,0 +1,280 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.data.appconfiguration.implementation; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.net.MalformedURLException; + +import org.junit.jupiter.api.Test; + +import com.azure.core.http.HttpMethod; +import com.azure.core.http.HttpPipeline; +import com.azure.core.http.HttpPipelineBuilder; +import com.azure.core.http.HttpRequest; +import com.azure.core.http.HttpResponse; +import com.azure.core.http.policy.HttpPipelinePolicy; +import com.azure.core.test.SyncAsyncExtension; +import com.azure.core.test.annotation.SyncAsyncTest; +import com.azure.core.test.http.NoOpHttpClient; +import com.azure.core.util.Context; + +import reactor.core.publisher.Mono; + +/** + * Unit tests for QueryParamPolicy + */ +public class QueryParamPolicyTest { + private static final String BASE_URL = "http://localhost:8080"; + private static final String ENDPOINT_PATH = "/kv/test"; + + /** + * Test that query parameters are sorted alphabetically + */ + @SyncAsyncTest + public void queryParametersAreSortedAlphabetically() { + final String originalUrl = BASE_URL + ENDPOINT_PATH + "?zebra=value1&alpha=value2&beta=value3"; + final String expectedUrl = BASE_URL + ENDPOINT_PATH + "?alpha=value2&beta=value3&zebra=value1"; + + QueryParamPolicy queryParamPolicy = new QueryParamPolicy(); + + HttpPipelinePolicy auditorPolicy = (context, next) -> { + final String actualUrl = context.getHttpRequest().getUrl().toString(); + assertEquals(expectedUrl, actualUrl, "Query parameters should be sorted alphabetically"); + return next.process(); + }; + + final HttpPipeline pipeline = new HttpPipelineBuilder().httpClient(new NoOpHttpClient()) + .policies(queryParamPolicy, auditorPolicy) + .build(); + + SyncAsyncExtension.execute(() -> sendRequestSync(pipeline, originalUrl), + () -> sendRequest(pipeline, originalUrl)); + } + + /** + * Test that query parameter keys are converted to lowercase + */ + @SyncAsyncTest + public void queryParameterKeysAreConvertedToLowercase() { + final String originalUrl = BASE_URL + ENDPOINT_PATH + "?SELECT=field1&FILTER=condition&orderBy=field2"; + final String expectedUrl = BASE_URL + ENDPOINT_PATH + "?filter=condition&orderby=field2&select=field1"; + + QueryParamPolicy queryParamPolicy = new QueryParamPolicy(); + + HttpPipelinePolicy auditorPolicy = (context, next) -> { + final String actualUrl = context.getHttpRequest().getUrl().toString(); + assertEquals(expectedUrl, actualUrl, "Query parameter keys should be lowercase and sorted"); + return next.process(); + }; + + final HttpPipeline pipeline = new HttpPipelineBuilder().httpClient(new NoOpHttpClient()) + .policies(queryParamPolicy, auditorPolicy) + .build(); + + SyncAsyncExtension.execute(() -> sendRequestSync(pipeline, originalUrl), + () -> sendRequest(pipeline, originalUrl)); + } + + /** + * Test that OData-style parameters like $select are handled correctly + */ + @SyncAsyncTest + public void oDataParametersAreHandledCorrectly() { + final String originalUrl + = BASE_URL + ENDPOINT_PATH + "?$Select=name,value&$Filter=startsWith(key,'test')&api-version=1.0"; + final String expectedUrl + = BASE_URL + ENDPOINT_PATH + "?$filter=startsWith(key,'test')&$select=name,value&api-version=1.0"; + + QueryParamPolicy queryParamPolicy = new QueryParamPolicy(); + + HttpPipelinePolicy auditorPolicy = (context, next) -> { + final String actualUrl = context.getHttpRequest().getUrl().toString(); + assertEquals(expectedUrl, actualUrl, "OData parameters should be lowercase and sorted"); + return next.process(); + }; + + final HttpPipeline pipeline = new HttpPipelineBuilder().httpClient(new NoOpHttpClient()) + .policies(queryParamPolicy, auditorPolicy) + .build(); + + SyncAsyncExtension.execute(() -> sendRequestSync(pipeline, originalUrl), + () -> sendRequest(pipeline, originalUrl)); + } + + /** + * Test that URLs without query parameters are not modified + */ + @SyncAsyncTest + public void urlsWithoutQueryParametersAreNotModified() { + final String originalUrl = BASE_URL + ENDPOINT_PATH; + + QueryParamPolicy queryParamPolicy = new QueryParamPolicy(); + + HttpPipelinePolicy auditorPolicy = (context, next) -> { + final String actualUrl = context.getHttpRequest().getUrl().toString(); + assertEquals(originalUrl, actualUrl, "URLs without query parameters should not be modified"); + return next.process(); + }; + + final HttpPipeline pipeline = new HttpPipelineBuilder().httpClient(new NoOpHttpClient()) + .policies(queryParamPolicy, auditorPolicy) + .build(); + + SyncAsyncExtension.execute(() -> sendRequestSync(pipeline, originalUrl), + () -> sendRequest(pipeline, originalUrl)); + } + + /** + * Test that empty query parameters are handled correctly + */ + @SyncAsyncTest + public void emptyQueryParametersAreHandledCorrectly() { + final String originalUrl = BASE_URL + ENDPOINT_PATH + "?"; + + QueryParamPolicy queryParamPolicy = new QueryParamPolicy(); + + HttpPipelinePolicy auditorPolicy = (context, next) -> { + final String actualUrl = context.getHttpRequest().getUrl().toString(); + // The URL should either be cleaned up or preserved as is + assertTrue(actualUrl.equals(BASE_URL + ENDPOINT_PATH) || actualUrl.equals(originalUrl), + "Empty query parameters should be handled gracefully"); + return next.process(); + }; + + final HttpPipeline pipeline = new HttpPipelineBuilder().httpClient(new NoOpHttpClient()) + .policies(queryParamPolicy, auditorPolicy) + .build(); + + SyncAsyncExtension.execute(() -> sendRequestSync(pipeline, originalUrl), + () -> sendRequest(pipeline, originalUrl)); + } + + /** + * Test that query parameter values are preserved exactly + */ + @SyncAsyncTest + public void queryParameterValuesArePreserved() { + final String originalUrl = BASE_URL + ENDPOINT_PATH + "?key1=Value%20With%20Spaces&key2=SimpleValue&key3="; + + QueryParamPolicy queryParamPolicy = new QueryParamPolicy(); + + HttpPipelinePolicy auditorPolicy = (context, next) -> { + final String actualUrl = context.getHttpRequest().getUrl().toString(); + // Check that all values are preserved + assertTrue(actualUrl.contains("Value%20With%20Spaces"), "Values with spaces should be preserved"); + assertTrue(actualUrl.contains("SimpleValue"), "Simple values should be preserved"); + assertTrue(actualUrl.contains("key3="), "Empty values should be preserved"); + return next.process(); + }; + + final HttpPipeline pipeline = new HttpPipelineBuilder().httpClient(new NoOpHttpClient()) + .policies(queryParamPolicy, auditorPolicy) + .build(); + + SyncAsyncExtension.execute(() -> sendRequestSync(pipeline, originalUrl), + () -> sendRequest(pipeline, originalUrl)); + } + + /** + * Test that duplicate query parameter keys are handled correctly + */ + @SyncAsyncTest + public void duplicateQueryParameterKeysAreHandled() { + final String originalUrl = BASE_URL + ENDPOINT_PATH + "?filter=condition1&select=field1&filter=condition2"; + + QueryParamPolicy queryParamPolicy = new QueryParamPolicy(); + + HttpPipelinePolicy auditorPolicy = (context, next) -> { + final String actualUrl = context.getHttpRequest().getUrl().toString(); + // The policy should handle duplicates gracefully (TreeMap behavior) + assertTrue(actualUrl.contains("filter="), "Filter parameter should be present"); + assertTrue(actualUrl.contains("select="), "Select parameter should be present"); + return next.process(); + }; + + final HttpPipeline pipeline = new HttpPipelineBuilder().httpClient(new NoOpHttpClient()) + .policies(queryParamPolicy, auditorPolicy) + .build(); + + SyncAsyncExtension.execute(() -> sendRequestSync(pipeline, originalUrl), + () -> sendRequest(pipeline, originalUrl)); + } + + /** + * Test that malformed URLs are handled gracefully + */ + @Test + public void malformedUrlsAreHandledGracefully() { + // This test uses a synchronous approach since we're testing error handling + final String malformedUrl = "not-a-valid-url://[invalid"; + + QueryParamPolicy queryParamPolicy = new QueryParamPolicy(); + + HttpPipelinePolicy auditorPolicy = (context, next) -> { + // The URL should remain unchanged when malformed + final String actualUrl = context.getHttpRequest().getUrl().toString(); + assertEquals(malformedUrl, actualUrl, "Malformed URLs should remain unchanged"); + return next.process(); + }; + + final HttpPipeline pipeline = new HttpPipelineBuilder().httpClient(new NoOpHttpClient()) + .policies(queryParamPolicy, auditorPolicy) + .build(); + + // Test should not throw an exception + try { + HttpRequest request = new HttpRequest(HttpMethod.GET, malformedUrl); + pipeline.send(request, Context.NONE).block(); + } catch (Exception e) { + // If an exception occurs, it should not be from the QueryParamPolicy + assertTrue(e.getCause() instanceof MalformedURLException || e.getMessage().contains("not-a-valid-url"), + "Exception should be related to the malformed URL, not the policy"); + } + } + + /** + * Test comprehensive scenario with mixed case, special characters, and sorting + */ + @SyncAsyncTest + public void comprehensiveQueryParameterNormalization() { + final String originalUrl = BASE_URL + ENDPOINT_PATH + + "?$TOP=10&API-Version=2023-10-01&$select=key,value&label=prod&$filter=startsWith(key,'app')&maxItems=100"; + + QueryParamPolicy queryParamPolicy = new QueryParamPolicy(); + + HttpPipelinePolicy auditorPolicy = (context, next) -> { + final String actualUrl = context.getHttpRequest().getUrl().toString(); + + // Verify alphabetical ordering and lowercase conversion + String[] expectedOrder = { "$filter", "$select", "$top", "api-version", "label", "maxitems" }; + String queryString = actualUrl.substring(actualUrl.indexOf('?') + 1); + String[] actualParams = queryString.split("&"); + + for (int i = 0; i < expectedOrder.length && i < actualParams.length; i++) { + String actualKey = actualParams[i].split("=")[0]; + assertEquals(expectedOrder[i], actualKey, + "Parameter at position " + i + " should be " + expectedOrder[i]); + } + + return next.process(); + }; + + final HttpPipeline pipeline = new HttpPipelineBuilder().httpClient(new NoOpHttpClient()) + .policies(queryParamPolicy, auditorPolicy) + .build(); + + SyncAsyncExtension.execute(() -> sendRequestSync(pipeline, originalUrl), + () -> sendRequest(pipeline, originalUrl)); + } + + private Mono sendRequest(HttpPipeline pipeline, String url) { + return pipeline.send(new HttpRequest(HttpMethod.GET, url), Context.NONE); + } + + private HttpResponse sendRequestSync(HttpPipeline pipeline, String url) { + return pipeline.send(new HttpRequest(HttpMethod.GET, url), Context.NONE).block(); + } +} From 2a863f2b03af9b03f8eeae82a2492f4219dddbce Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Thu, 11 Sep 2025 15:57:56 -0700 Subject: [PATCH 02/24] Update CHANGELOG.md --- sdk/appconfiguration/azure-data-appconfiguration/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sdk/appconfiguration/azure-data-appconfiguration/CHANGELOG.md b/sdk/appconfiguration/azure-data-appconfiguration/CHANGELOG.md index 80067dd3a17d..0cd665375e9c 100644 --- a/sdk/appconfiguration/azure-data-appconfiguration/CHANGELOG.md +++ b/sdk/appconfiguration/azure-data-appconfiguration/CHANGELOG.md @@ -10,6 +10,8 @@ ### Other Changes +- Added a pipeline policy to handle query parameters to make sure the keys are always in lower case and in alphabetical order. + ## 1.8.3 (2025-08-21) ### Other Changes From c6e085f823a2e2e0f88bb6d41480a270b48840e6 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Thu, 11 Sep 2025 16:05:02 -0700 Subject: [PATCH 03/24] Update QueryParamPolicy.java --- .../data/appconfiguration/implementation/QueryParamPolicy.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/implementation/QueryParamPolicy.java b/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/implementation/QueryParamPolicy.java index 4b56d047400e..5b6b6ab7ddc5 100644 --- a/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/implementation/QueryParamPolicy.java +++ b/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/implementation/QueryParamPolicy.java @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + package com.azure.data.appconfiguration.implementation; import java.net.MalformedURLException; From 1c451cf385f4e4ca888e00e39beae44c60d0fcc8 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Fri, 12 Sep 2025 10:32:20 -0700 Subject: [PATCH 04/24] Fixing tags --- .../implementation/QueryParamPolicy.java | 51 ++- .../implementation/QueryParamPolicyTest.java | 349 ++++++++++++++++++ 2 files changed, 370 insertions(+), 30 deletions(-) diff --git a/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/implementation/QueryParamPolicy.java b/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/implementation/QueryParamPolicy.java index 5b6b6ab7ddc5..d36b9d863f8a 100644 --- a/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/implementation/QueryParamPolicy.java +++ b/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/implementation/QueryParamPolicy.java @@ -3,16 +3,14 @@ package com.azure.data.appconfiguration.implementation; -import java.net.MalformedURLException; -import java.util.Map; -import java.util.TreeMap; +import java.util.Arrays; +import java.util.stream.Collectors; import com.azure.core.http.HttpPipelineCallContext; import com.azure.core.http.HttpPipelineNextPolicy; import com.azure.core.http.HttpRequest; import com.azure.core.http.HttpResponse; import com.azure.core.http.policy.HttpPipelinePolicy; -import com.azure.core.util.UrlBuilder; import com.azure.core.util.logging.ClientLogger; import reactor.core.publisher.Mono; @@ -25,33 +23,27 @@ public Mono process(HttpPipelineCallContext context, HttpPipelineN HttpRequest request = context.getHttpRequest(); try { - UrlBuilder urlBuilder = UrlBuilder.parse(request.getUrl()); - Map queryParams = urlBuilder.getQuery(); - - if (queryParams != null && !queryParams.isEmpty()) { - // Create a new TreeMap to automatically sort by keys alphabetically - Map sortedParams = new TreeMap<>(); - - // Process each query parameter: convert key to lowercase and add to sorted map - for (Map.Entry entry : queryParams.entrySet()) { - String key = entry.getKey(); - String value = entry.getValue(); - - // Convert key to lowercase, but preserve special cases like $Select -> $select - String lowercaseKey = key.toLowerCase(); - sortedParams.put(lowercaseKey, value); + String url = request.getUrl().toString(); + + // Find the query string manually to preserve URL encoding + int queryIndex = url.indexOf('?'); + if (queryIndex != -1) { + String query = url.substring(queryIndex + 1); + + if (!query.isEmpty()) { + String normalizedQuery + = Arrays.stream(query.split("&")).filter(pair -> !pair.isEmpty()).map(pair -> { + int equalIndex = pair.indexOf('='); + return equalIndex != -1 + ? pair.substring(0, equalIndex).toLowerCase() + "=" + pair.substring(equalIndex + 1) + : pair.toLowerCase() + "="; + }).sorted().collect(Collectors.joining("&")); + String urlWithoutQuery = url.substring(0, queryIndex); + String newUrl = urlWithoutQuery + "?" + normalizedQuery; + request.setUrl(newUrl); } - - // Clear existing query parameters and add sorted ones - urlBuilder.setQuery(null); - for (Map.Entry entry : sortedParams.entrySet()) { - urlBuilder.addQueryParameter(entry.getKey(), entry.getValue()); - } - - // Update the request URL with reordered parameters - request.setUrl(urlBuilder.toUrl()); } - } catch (MalformedURLException e) { + } catch (Exception e) { // If URL parsing fails, continue without modification LOGGER.warning( "Failed to parse URL for query parameter normalization. " @@ -61,5 +53,4 @@ public Mono process(HttpPipelineCallContext context, HttpPipelineN return next.process(); } - } diff --git a/sdk/appconfiguration/azure-data-appconfiguration/src/test/java/com/azure/data/appconfiguration/implementation/QueryParamPolicyTest.java b/sdk/appconfiguration/azure-data-appconfiguration/src/test/java/com/azure/data/appconfiguration/implementation/QueryParamPolicyTest.java index 7c42d5dc93d6..99bbc5188306 100644 --- a/sdk/appconfiguration/azure-data-appconfiguration/src/test/java/com/azure/data/appconfiguration/implementation/QueryParamPolicyTest.java +++ b/sdk/appconfiguration/azure-data-appconfiguration/src/test/java/com/azure/data/appconfiguration/implementation/QueryParamPolicyTest.java @@ -25,6 +25,22 @@ /** * Unit tests for QueryParamPolicy + * + * IMPORTANT BUG IDENTIFIED: + * The QueryParamPolicy currently has a bug where multiple query parameters with the same key + * (like multiple "tags" parameters) get incorrectly merged into comma-separated values + * instead of being preserved as separate parameters. + * + * This is problematic for Azure App Configuration API which expects: + * - Multiple tags parameters: "?tags=tag1%3D&tags=tag2%3D" + * - NOT comma-separated: "?tags=tag1%3D,tag2%3D" + * + * The issue occurs because: + * 1. UrlBuilder.getQuery() merges duplicate keys with comma separation + * 2. QueryParamPolicy uses TreeMap which only stores one value per key + * + * To fix: The policy needs to handle multiple values per key properly, preserving + * separate parameters while still providing lowercase conversion and alphabetical sorting. */ public class QueryParamPolicyTest { private static final String BASE_URL = "http://localhost:8080"; @@ -270,6 +286,145 @@ public void comprehensiveQueryParameterNormalization() { () -> sendRequest(pipeline, originalUrl)); } + /** + * Test debugging multiple tags parameters to see actual URL transformation + * This test shows what actually happens with the current implementation + */ + @SyncAsyncTest + public void debugMultipleTagsParametersActualBehavior() { + final String originalUrl = BASE_URL + ENDPOINT_PATH + + "?api-version=2023-11-01&key=*&label=dev&tags=tag1%3Dvalue1&tags=tag2%3Dvalue2"; + + QueryParamPolicy queryParamPolicy = new QueryParamPolicy(); + + HttpPipelinePolicy auditorPolicy = (context, next) -> { + final String actualUrl = context.getHttpRequest().getUrl().toString(); + + // Output for debugging to understand current behavior + System.out.println("Original URL: " + originalUrl); + System.out.println("Transformed URL: " + actualUrl); + + // Basic validations that should always pass + assertTrue(actualUrl.contains("api-version=2023-11-01"), "API version should be preserved"); + assertTrue(actualUrl.contains("key=*"), "Key parameter should be preserved"); + assertTrue(actualUrl.contains("label=dev"), "Label parameter should be preserved"); + assertTrue(actualUrl.contains("tags="), "At least one tags parameter should be present"); + + // Verify alphabetical ordering is maintained + int apiVersionPos = actualUrl.indexOf("api-version="); + int keyPos = actualUrl.indexOf("key="); + int labelPos = actualUrl.indexOf("label="); + int tagsPos = actualUrl.indexOf("tags="); + + assertTrue(apiVersionPos < keyPos, "api-version should come before key"); + assertTrue(keyPos < labelPos, "key should come before label"); + assertTrue(labelPos < tagsPos, "label should come before tags"); + + return next.process(); + }; + + final HttpPipeline pipeline = new HttpPipelineBuilder().httpClient(new NoOpHttpClient()) + .policies(queryParamPolicy, auditorPolicy) + .build(); + + SyncAsyncExtension.execute(() -> sendRequestSync(pipeline, originalUrl), + () -> sendRequest(pipeline, originalUrl)); + } + + /** + * Test that multiple tags parameters are correctly preserved as separate parameters + * + * This test verifies that multiple tags parameters with the same key are preserved + * as separate query parameters instead of being merged into comma-separated values. + * + * Expected behavior: + * "?tags=tag1%3D&tags=tag2%3D" remains as two separate parameters + * + * This is the correct behavior for Azure App Configuration API which expects + * multiple tags parameters, not comma-separated values. + */ + @SyncAsyncTest + public void multipleTagsParametersFixedBehavior() { + final String originalUrl + = BASE_URL + ENDPOINT_PATH + "?api-version=2023-11-01&key=*&label=dev&tags=tag1%3D&tags=tag2%3D"; + + QueryParamPolicy queryParamPolicy = new QueryParamPolicy(); + + HttpPipelinePolicy auditorPolicy = (context, next) -> { + final String actualUrl = context.getHttpRequest().getUrl().toString(); + + // Count how many separate tags parameters exist + int tagsCount = (actualUrl.length() - actualUrl.replace("tags=", "").length()) / "tags=".length(); + + // Verify that 2 separate tags parameters are preserved + assertTrue(tagsCount == 2, "Multiple tags parameters should be preserved as separate parameters. Found " + + tagsCount + " tags parameter(s), expected 2 separate tags parameters."); + + // Verify both tags parameters are preserved separately + if (tagsCount == 2) { + // Both original tag values should be present as separate parameters + assertTrue(actualUrl.contains("tag1%3D") && actualUrl.contains("tag2%3D"), + "Both tags parameters should be preserved separately with original values"); + } + + System.out.println("Original URL: " + originalUrl); + System.out.println("Transformed URL: " + actualUrl); + System.out.println("Tags parameters found: " + tagsCount); + + return next.process(); + }; + + final HttpPipeline pipeline = new HttpPipelineBuilder().httpClient(new NoOpHttpClient()) + .policies(queryParamPolicy, auditorPolicy) + .build(); + + SyncAsyncExtension.execute(() -> sendRequestSync(pipeline, originalUrl), + () -> sendRequest(pipeline, originalUrl)); + } + + /** + * Test that demonstrates the correct behavior for multiple tags parameters + * + * This test verifies that the policy correctly preserves multiple tags parameters + * as separate parameters instead of merging them into comma-separated values. + * + * This test validates that the QueryParamPolicy implementation properly handles + * the Azure App Configuration API requirement for separate tags parameters. + */ + @SyncAsyncTest + public void multipleTagsParametersCorrectBehavior() { + final String originalUrl + = BASE_URL + ENDPOINT_PATH + "?api-version=2023-11-01&key=*&label=dev&tags=tag1%3D&tags=tag2%3D"; + + // This is what the URL SHOULD look like after processing + final String expectedUrl + = BASE_URL + ENDPOINT_PATH + "?api-version=2023-11-01&key=*&label=dev&tags=tag1%3D&tags=tag2%3D"; + + QueryParamPolicy queryParamPolicy = new QueryParamPolicy(); + + HttpPipelinePolicy auditorPolicy = (context, next) -> { + final String actualUrl = context.getHttpRequest().getUrl().toString(); + + // THIS ASSERTION WILL FAIL with current implementation + // Multiple tags parameters should be preserved as separate parameters + int tagsCount = (actualUrl.length() - actualUrl.replace("tags=", "").length()) / "tags=".length(); + assertEquals(2, tagsCount, "CORRECT BEHAVIOR: Multiple tags parameters should be preserved separately. " + + "Expected 2 separate tags parameters, but got " + tagsCount); + + // The URL should remain unchanged (except for sorting) when multiple tags exist + assertEquals(expectedUrl, actualUrl, "Multiple tags parameters should be preserved in their original form"); + + return next.process(); + }; + + final HttpPipeline pipeline = new HttpPipelineBuilder().httpClient(new NoOpHttpClient()) + .policies(queryParamPolicy, auditorPolicy) + .build(); + + SyncAsyncExtension.execute(() -> sendRequestSync(pipeline, originalUrl), + () -> sendRequest(pipeline, originalUrl)); + } + private Mono sendRequest(HttpPipeline pipeline, String url) { return pipeline.send(new HttpRequest(HttpMethod.GET, url), Context.NONE); } @@ -277,4 +432,198 @@ private Mono sendRequest(HttpPipeline pipeline, String url) { private HttpResponse sendRequestSync(HttpPipeline pipeline, String url) { return pipeline.send(new HttpRequest(HttpMethod.GET, url), Context.NONE).block(); } + + /** + * Test single tags parameter is handled correctly + */ + @SyncAsyncTest + public void singleTagsParameterIsHandledCorrectly() { + final String originalUrl = BASE_URL + ENDPOINT_PATH + "?api-version=2023-11-01&key=*&tags=environment%3Dprod"; + + QueryParamPolicy queryParamPolicy = new QueryParamPolicy(); + + HttpPipelinePolicy auditorPolicy = (context, next) -> { + final String actualUrl = context.getHttpRequest().getUrl().toString(); + + // Verify alphabetical ordering: api-version, key, tags + assertTrue(actualUrl.contains("api-version=2023-11-01"), "API version should be preserved"); + assertTrue(actualUrl.contains("key=*"), "Key parameter should be preserved"); + assertTrue(actualUrl.contains("tags=environment%3Dprod"), "Tags parameter should be preserved"); + + // Verify order: api-version should come before key, key should come before tags + int apiVersionPos = actualUrl.indexOf("api-version="); + int keyPos = actualUrl.indexOf("key="); + int tagsPos = actualUrl.indexOf("tags="); + + assertTrue(apiVersionPos < keyPos, "api-version should come before key"); + assertTrue(keyPos < tagsPos, "key should come before tags"); + + return next.process(); + }; + + final HttpPipeline pipeline = new HttpPipelineBuilder().httpClient(new NoOpHttpClient()) + .policies(queryParamPolicy, auditorPolicy) + .build(); + + SyncAsyncExtension.execute(() -> sendRequestSync(pipeline, originalUrl), + () -> sendRequest(pipeline, originalUrl)); + } + + /** + * Test that multiple tags parameters are preserved correctly with proper alphabetical ordering + * + * Verifies that the policy preserves multiple tags parameters as separate parameters + * while maintaining alphabetical ordering of all query parameters. + */ + @SyncAsyncTest + public void multipleTagsParametersWithOrderingVerification() { + final String originalUrl + = BASE_URL + ENDPOINT_PATH + "?api-version=2023-11-01&key=*&label=dev&tags=tag1%3D&tags=tag2%3D"; + + QueryParamPolicy queryParamPolicy = new QueryParamPolicy(); + + HttpPipelinePolicy auditorPolicy = (context, next) -> { + final String actualUrl = context.getHttpRequest().getUrl().toString(); + + // Verify alphabetical ordering: api-version, key, label, tags + assertTrue(actualUrl.contains("api-version=2023-11-01"), "API version should be preserved"); + assertTrue(actualUrl.contains("key=*"), "Key parameter should be preserved"); + assertTrue(actualUrl.contains("label=dev"), "Label parameter should be preserved"); + + // Verify that the policy correctly preserves multiple tags as separate parameters + // This is the correct behavior for Azure App Configuration API + int tagsCount = (actualUrl.length() - actualUrl.replace("tags=", "").length()) / "tags=".length(); + assertEquals(2, tagsCount, "Multiple tags parameters should be preserved as separate parameters. " + + "Expected " + 2 + " separate tags parameters, but found " + tagsCount + "."); + + // Verify parameters are in alphabetical order + int apiVersionPos = actualUrl.indexOf("api-version="); + int keyPos = actualUrl.indexOf("key="); + int labelPos = actualUrl.indexOf("label="); + int tagsPos = actualUrl.indexOf("tags="); + + assertTrue(apiVersionPos < keyPos, "api-version should come before key"); + assertTrue(keyPos < labelPos, "key should come before label"); + assertTrue(labelPos < tagsPos, "label should come before tags"); + + return next.process(); + }; + + final HttpPipeline pipeline = new HttpPipelineBuilder().httpClient(new NoOpHttpClient()) + .policies(queryParamPolicy, auditorPolicy) + .build(); + + SyncAsyncExtension.execute(() -> sendRequestSync(pipeline, originalUrl), + () -> sendRequest(pipeline, originalUrl)); + } + + /** + * Test tags parameters with complex values and URL encoding + */ + @SyncAsyncTest + public void tagsParametersWithComplexValues() { + final String originalUrl + = BASE_URL + ENDPOINT_PATH + "?tags=environment%3Dproduction&tags=team%3Dbackend&api-version=2023-11-01"; + + QueryParamPolicy queryParamPolicy = new QueryParamPolicy(); + + HttpPipelinePolicy auditorPolicy = (context, next) -> { + final String actualUrl = context.getHttpRequest().getUrl().toString(); + + // Verify parameters are sorted alphabetically + assertTrue(actualUrl.contains("api-version=2023-11-01"), "API version should be preserved"); + assertTrue(actualUrl.contains("tags="), "Tags parameter should be present"); + + // Verify alphabetical order: api-version comes before tags + int apiVersionPos = actualUrl.indexOf("api-version="); + int tagsPos = actualUrl.indexOf("tags="); + assertTrue(apiVersionPos < tagsPos, "api-version should come before tags"); + + return next.process(); + }; + + final HttpPipeline pipeline = new HttpPipelineBuilder().httpClient(new NoOpHttpClient()) + .policies(queryParamPolicy, auditorPolicy) + .build(); + + SyncAsyncExtension.execute(() -> sendRequestSync(pipeline, originalUrl), + () -> sendRequest(pipeline, originalUrl)); + } + + /** + * Test tags parameters mixed with other App Configuration parameters + */ + @SyncAsyncTest + public void tagsParametersMixedWithOtherParameters() { + final String originalUrl = BASE_URL + ENDPOINT_PATH + + "?$select=key,value&tags=feature%3Dauth&label=*&api-version=2023-11-01&$filter=startsWith(key,'app')&tags=env%3Dtest"; + + QueryParamPolicy queryParamPolicy = new QueryParamPolicy(); + + HttpPipelinePolicy auditorPolicy = (context, next) -> { + final String actualUrl = context.getHttpRequest().getUrl().toString(); + + // Verify we have the expected parameters + assertTrue(actualUrl.contains("$filter="), "$filter parameter should be present"); + assertTrue(actualUrl.contains("$select="), "$select parameter should be present"); + assertTrue(actualUrl.contains("api-version="), "api-version parameter should be present"); + assertTrue(actualUrl.contains("label="), "label parameter should be present"); + assertTrue(actualUrl.contains("tags="), "tags parameter should be present"); + + // Verify alphabetical ordering of the first occurrence of each parameter type + int filterPos = actualUrl.indexOf("$filter="); + int selectPos = actualUrl.indexOf("$select="); + int apiVersionPos = actualUrl.indexOf("api-version="); + int labelPos = actualUrl.indexOf("label="); + int tagsPos = actualUrl.indexOf("tags="); + + assertTrue(filterPos < selectPos, "$filter should come before $select"); + assertTrue(selectPos < apiVersionPos, "$select should come before api-version"); + assertTrue(apiVersionPos < labelPos, "api-version should come before label"); + assertTrue(labelPos < tagsPos, "label should come before tags"); + + return next.process(); + }; + final HttpPipeline pipeline = new HttpPipelineBuilder().httpClient(new NoOpHttpClient()) + .policies(queryParamPolicy, auditorPolicy) + .build(); + + SyncAsyncExtension.execute(() -> sendRequestSync(pipeline, originalUrl), + () -> sendRequest(pipeline, originalUrl)); + } + + /** + * Test tags parameters with special characters and case sensitivity + */ + @SyncAsyncTest + public void tagsParametersWithSpecialCharacters() { + final String originalUrl + = BASE_URL + ENDPOINT_PATH + "?TAGS=Priority%3DHigh&api-version=2023-11-01&Tags=Status%3DActive"; + + QueryParamPolicy queryParamPolicy = new QueryParamPolicy(); + + HttpPipelinePolicy auditorPolicy = (context, next) -> { + final String actualUrl = context.getHttpRequest().getUrl().toString(); + + // Verify that tag keys are converted to lowercase + assertTrue(actualUrl.contains("tags="), "Tags parameter key should be lowercase"); + // Should not contain uppercase versions + assertTrue(!actualUrl.contains("TAGS=") || actualUrl.indexOf("tags=") >= 0, + "Uppercase TAGS should be converted to lowercase"); + assertTrue(!actualUrl.contains("Tags=") || actualUrl.indexOf("tags=") >= 0, + "Mixed case Tags should be converted to lowercase"); + + // Verify values are preserved with their encoding + assertTrue(actualUrl.contains("%3D"), "Special characters in values should be preserved"); + + return next.process(); + }; + + final HttpPipeline pipeline = new HttpPipelineBuilder().httpClient(new NoOpHttpClient()) + .policies(queryParamPolicy, auditorPolicy) + .build(); + + SyncAsyncExtension.execute(() -> sendRequestSync(pipeline, originalUrl), + () -> sendRequest(pipeline, originalUrl)); + } } From cd062ee72268f1a9368abeedf8503d2c20ac1961 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Fri, 12 Sep 2025 10:43:51 -0700 Subject: [PATCH 05/24] Style fixes --- .../implementation/QueryParamPolicy.java | 12 +++-------- .../implementation/QueryParamPolicyTest.java | 21 +------------------ 2 files changed, 4 insertions(+), 29 deletions(-) diff --git a/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/implementation/QueryParamPolicy.java b/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/implementation/QueryParamPolicy.java index d36b9d863f8a..adb600ef708f 100644 --- a/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/implementation/QueryParamPolicy.java +++ b/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/implementation/QueryParamPolicy.java @@ -7,19 +7,15 @@ import java.util.stream.Collectors; import com.azure.core.http.HttpPipelineCallContext; -import com.azure.core.http.HttpPipelineNextPolicy; import com.azure.core.http.HttpRequest; -import com.azure.core.http.HttpResponse; -import com.azure.core.http.policy.HttpPipelinePolicy; +import com.azure.core.http.policy.HttpPipelineSyncPolicy; import com.azure.core.util.logging.ClientLogger; -import reactor.core.publisher.Mono; - -public class QueryParamPolicy implements HttpPipelinePolicy { +public final class QueryParamPolicy extends HttpPipelineSyncPolicy { private static final ClientLogger LOGGER = new ClientLogger(QueryParamPolicy.class); @Override - public Mono process(HttpPipelineCallContext context, HttpPipelineNextPolicy next) { + protected void beforeSendingRequest(HttpPipelineCallContext context) { HttpRequest request = context.getHttpRequest(); try { @@ -50,7 +46,5 @@ public Mono process(HttpPipelineCallContext context, HttpPipelineN + "Request will proceed with original URL. URL: {}, Error: {}", request.getUrl(), e.getMessage(), e); } - - return next.process(); } } diff --git a/sdk/appconfiguration/azure-data-appconfiguration/src/test/java/com/azure/data/appconfiguration/implementation/QueryParamPolicyTest.java b/sdk/appconfiguration/azure-data-appconfiguration/src/test/java/com/azure/data/appconfiguration/implementation/QueryParamPolicyTest.java index 99bbc5188306..8d47e92d9d78 100644 --- a/sdk/appconfiguration/azure-data-appconfiguration/src/test/java/com/azure/data/appconfiguration/implementation/QueryParamPolicyTest.java +++ b/sdk/appconfiguration/azure-data-appconfiguration/src/test/java/com/azure/data/appconfiguration/implementation/QueryParamPolicyTest.java @@ -23,25 +23,6 @@ import reactor.core.publisher.Mono; -/** - * Unit tests for QueryParamPolicy - * - * IMPORTANT BUG IDENTIFIED: - * The QueryParamPolicy currently has a bug where multiple query parameters with the same key - * (like multiple "tags" parameters) get incorrectly merged into comma-separated values - * instead of being preserved as separate parameters. - * - * This is problematic for Azure App Configuration API which expects: - * - Multiple tags parameters: "?tags=tag1%3D&tags=tag2%3D" - * - NOT comma-separated: "?tags=tag1%3D,tag2%3D" - * - * The issue occurs because: - * 1. UrlBuilder.getQuery() merges duplicate keys with comma separation - * 2. QueryParamPolicy uses TreeMap which only stores one value per key - * - * To fix: The policy needs to handle multiple values per key properly, preserving - * separate parameters while still providing lowercase conversion and alphabetical sorting. - */ public class QueryParamPolicyTest { private static final String BASE_URL = "http://localhost:8080"; private static final String ENDPOINT_PATH = "/kv/test"; @@ -155,7 +136,7 @@ public void emptyQueryParametersAreHandledCorrectly() { HttpPipelinePolicy auditorPolicy = (context, next) -> { final String actualUrl = context.getHttpRequest().getUrl().toString(); // The URL should either be cleaned up or preserved as is - assertTrue(actualUrl.equals(BASE_URL + ENDPOINT_PATH) || actualUrl.equals(originalUrl), + assertTrue((BASE_URL + ENDPOINT_PATH).equals(actualUrl) || actualUrl.equals(originalUrl), "Empty query parameters should be handled gracefully"); return next.process(); }; From edbecabf0b016c4afc76c542318b7ba1dd67053e Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Fri, 12 Sep 2025 10:56:29 -0700 Subject: [PATCH 06/24] Update QueryParamPolicyTest.java --- .../implementation/QueryParamPolicyTest.java | 47 +++++++++++-------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/sdk/appconfiguration/azure-data-appconfiguration/src/test/java/com/azure/data/appconfiguration/implementation/QueryParamPolicyTest.java b/sdk/appconfiguration/azure-data-appconfiguration/src/test/java/com/azure/data/appconfiguration/implementation/QueryParamPolicyTest.java index 8d47e92d9d78..7f9043ea55df 100644 --- a/sdk/appconfiguration/azure-data-appconfiguration/src/test/java/com/azure/data/appconfiguration/implementation/QueryParamPolicyTest.java +++ b/sdk/appconfiguration/azure-data-appconfiguration/src/test/java/com/azure/data/appconfiguration/implementation/QueryParamPolicyTest.java @@ -176,19 +176,26 @@ public void queryParameterValuesArePreserved() { } /** - * Test that duplicate query parameter keys are handled correctly + * Test that multiple query parameters with the same key are preserved as separate parameters */ @SyncAsyncTest - public void duplicateQueryParameterKeysAreHandled() { + public void multipleParametersWithSameKeyArePreserved() { final String originalUrl = BASE_URL + ENDPOINT_PATH + "?filter=condition1&select=field1&filter=condition2"; QueryParamPolicy queryParamPolicy = new QueryParamPolicy(); HttpPipelinePolicy auditorPolicy = (context, next) -> { final String actualUrl = context.getHttpRequest().getUrl().toString(); - // The policy should handle duplicates gracefully (TreeMap behavior) - assertTrue(actualUrl.contains("filter="), "Filter parameter should be present"); - assertTrue(actualUrl.contains("select="), "Select parameter should be present"); + + // Count how many filter parameters exist + int filterCount = (actualUrl.length() - actualUrl.replace("filter=", "").length()) / "filter=".length(); + + // The policy should preserve both filter parameters as separate entries + assertEquals(2, filterCount, "Both filter parameters should be preserved separately"); + assertTrue(actualUrl.contains("filter=condition1"), "First filter parameter should be preserved"); + assertTrue(actualUrl.contains("filter=condition2"), "Second filter parameter should be preserved"); + assertTrue(actualUrl.contains("select=field1"), "Select parameter should be preserved"); + return next.process(); }; @@ -364,7 +371,7 @@ public void multipleTagsParametersFixedBehavior() { } /** - * Test that demonstrates the correct behavior for multiple tags parameters + * Test that validates the correct behavior for multiple tags parameters * * This test verifies that the policy correctly preserves multiple tags parameters * as separate parameters instead of merging them into comma-separated values. @@ -377,7 +384,7 @@ public void multipleTagsParametersCorrectBehavior() { final String originalUrl = BASE_URL + ENDPOINT_PATH + "?api-version=2023-11-01&key=*&label=dev&tags=tag1%3D&tags=tag2%3D"; - // This is what the URL SHOULD look like after processing + // The URL should preserve multiple tags parameters after processing (with alphabetical sorting) final String expectedUrl = BASE_URL + ENDPOINT_PATH + "?api-version=2023-11-01&key=*&label=dev&tags=tag1%3D&tags=tag2%3D"; @@ -386,14 +393,13 @@ public void multipleTagsParametersCorrectBehavior() { HttpPipelinePolicy auditorPolicy = (context, next) -> { final String actualUrl = context.getHttpRequest().getUrl().toString(); - // THIS ASSERTION WILL FAIL with current implementation - // Multiple tags parameters should be preserved as separate parameters + // Verify that multiple tags parameters are preserved as separate parameters int tagsCount = (actualUrl.length() - actualUrl.replace("tags=", "").length()) / "tags=".length(); - assertEquals(2, tagsCount, "CORRECT BEHAVIOR: Multiple tags parameters should be preserved separately. " + assertEquals(2, tagsCount, "Multiple tags parameters should be preserved separately. " + "Expected 2 separate tags parameters, but got " + tagsCount); - // The URL should remain unchanged (except for sorting) when multiple tags exist - assertEquals(expectedUrl, actualUrl, "Multiple tags parameters should be preserved in their original form"); + // The URL should preserve multiple tags parameters in their original form + assertEquals(expectedUrl, actualUrl, "Multiple tags parameters should be preserved with proper ordering"); return next.process(); }; @@ -406,14 +412,6 @@ public void multipleTagsParametersCorrectBehavior() { () -> sendRequest(pipeline, originalUrl)); } - private Mono sendRequest(HttpPipeline pipeline, String url) { - return pipeline.send(new HttpRequest(HttpMethod.GET, url), Context.NONE); - } - - private HttpResponse sendRequestSync(HttpPipeline pipeline, String url) { - return pipeline.send(new HttpRequest(HttpMethod.GET, url), Context.NONE).block(); - } - /** * Test single tags parameter is handled correctly */ @@ -607,4 +605,13 @@ public void tagsParametersWithSpecialCharacters() { SyncAsyncExtension.execute(() -> sendRequestSync(pipeline, originalUrl), () -> sendRequest(pipeline, originalUrl)); } + + private Mono sendRequest(HttpPipeline pipeline, String url) { + return pipeline.send(new HttpRequest(HttpMethod.GET, url), Context.NONE); + } + + private HttpResponse sendRequestSync(HttpPipeline pipeline, String url) { + return pipeline.send(new HttpRequest(HttpMethod.GET, url), Context.NONE).block(); + } + } From 858ae3c6f5af7341d5baf4045a090d142eb40710 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Fri, 12 Sep 2025 11:07:25 -0700 Subject: [PATCH 07/24] Update QueryParamPolicyTest.java --- .../appconfiguration/implementation/QueryParamPolicyTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/appconfiguration/azure-data-appconfiguration/src/test/java/com/azure/data/appconfiguration/implementation/QueryParamPolicyTest.java b/sdk/appconfiguration/azure-data-appconfiguration/src/test/java/com/azure/data/appconfiguration/implementation/QueryParamPolicyTest.java index 7f9043ea55df..d1149b18b9aa 100644 --- a/sdk/appconfiguration/azure-data-appconfiguration/src/test/java/com/azure/data/appconfiguration/implementation/QueryParamPolicyTest.java +++ b/sdk/appconfiguration/azure-data-appconfiguration/src/test/java/com/azure/data/appconfiguration/implementation/QueryParamPolicyTest.java @@ -416,7 +416,7 @@ public void multipleTagsParametersCorrectBehavior() { * Test single tags parameter is handled correctly */ @SyncAsyncTest - public void singleTagsParameterIsHandledCorrectly() { + public void singleTagsParameter() { final String originalUrl = BASE_URL + ENDPOINT_PATH + "?api-version=2023-11-01&key=*&tags=environment%3Dprod"; QueryParamPolicy queryParamPolicy = new QueryParamPolicy(); From 387731f5af8d390e454e1b97dab94a62b76fb5c0 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Fri, 12 Sep 2025 11:21:21 -0700 Subject: [PATCH 08/24] Update QueryParamPolicyTest.java --- .../implementation/QueryParamPolicyTest.java | 214 ++---------------- 1 file changed, 25 insertions(+), 189 deletions(-) diff --git a/sdk/appconfiguration/azure-data-appconfiguration/src/test/java/com/azure/data/appconfiguration/implementation/QueryParamPolicyTest.java b/sdk/appconfiguration/azure-data-appconfiguration/src/test/java/com/azure/data/appconfiguration/implementation/QueryParamPolicyTest.java index d1149b18b9aa..412e205ac4ce 100644 --- a/sdk/appconfiguration/azure-data-appconfiguration/src/test/java/com/azure/data/appconfiguration/implementation/QueryParamPolicyTest.java +++ b/sdk/appconfiguration/azure-data-appconfiguration/src/test/java/com/azure/data/appconfiguration/implementation/QueryParamPolicyTest.java @@ -246,119 +246,15 @@ public void malformedUrlsAreHandledGracefully() { public void comprehensiveQueryParameterNormalization() { final String originalUrl = BASE_URL + ENDPOINT_PATH + "?$TOP=10&API-Version=2023-10-01&$select=key,value&label=prod&$filter=startsWith(key,'app')&maxItems=100"; + final String expectedUrl = BASE_URL + ENDPOINT_PATH + + "?$filter=startsWith(key,'app')&$select=key,value&$top=10&api-version=2023-10-01&label=prod&maxitems=100"; QueryParamPolicy queryParamPolicy = new QueryParamPolicy(); HttpPipelinePolicy auditorPolicy = (context, next) -> { final String actualUrl = context.getHttpRequest().getUrl().toString(); - - // Verify alphabetical ordering and lowercase conversion - String[] expectedOrder = { "$filter", "$select", "$top", "api-version", "label", "maxitems" }; - String queryString = actualUrl.substring(actualUrl.indexOf('?') + 1); - String[] actualParams = queryString.split("&"); - - for (int i = 0; i < expectedOrder.length && i < actualParams.length; i++) { - String actualKey = actualParams[i].split("=")[0]; - assertEquals(expectedOrder[i], actualKey, - "Parameter at position " + i + " should be " + expectedOrder[i]); - } - - return next.process(); - }; - - final HttpPipeline pipeline = new HttpPipelineBuilder().httpClient(new NoOpHttpClient()) - .policies(queryParamPolicy, auditorPolicy) - .build(); - - SyncAsyncExtension.execute(() -> sendRequestSync(pipeline, originalUrl), - () -> sendRequest(pipeline, originalUrl)); - } - - /** - * Test debugging multiple tags parameters to see actual URL transformation - * This test shows what actually happens with the current implementation - */ - @SyncAsyncTest - public void debugMultipleTagsParametersActualBehavior() { - final String originalUrl = BASE_URL + ENDPOINT_PATH - + "?api-version=2023-11-01&key=*&label=dev&tags=tag1%3Dvalue1&tags=tag2%3Dvalue2"; - - QueryParamPolicy queryParamPolicy = new QueryParamPolicy(); - - HttpPipelinePolicy auditorPolicy = (context, next) -> { - final String actualUrl = context.getHttpRequest().getUrl().toString(); - - // Output for debugging to understand current behavior - System.out.println("Original URL: " + originalUrl); - System.out.println("Transformed URL: " + actualUrl); - - // Basic validations that should always pass - assertTrue(actualUrl.contains("api-version=2023-11-01"), "API version should be preserved"); - assertTrue(actualUrl.contains("key=*"), "Key parameter should be preserved"); - assertTrue(actualUrl.contains("label=dev"), "Label parameter should be preserved"); - assertTrue(actualUrl.contains("tags="), "At least one tags parameter should be present"); - - // Verify alphabetical ordering is maintained - int apiVersionPos = actualUrl.indexOf("api-version="); - int keyPos = actualUrl.indexOf("key="); - int labelPos = actualUrl.indexOf("label="); - int tagsPos = actualUrl.indexOf("tags="); - - assertTrue(apiVersionPos < keyPos, "api-version should come before key"); - assertTrue(keyPos < labelPos, "key should come before label"); - assertTrue(labelPos < tagsPos, "label should come before tags"); - - return next.process(); - }; - - final HttpPipeline pipeline = new HttpPipelineBuilder().httpClient(new NoOpHttpClient()) - .policies(queryParamPolicy, auditorPolicy) - .build(); - - SyncAsyncExtension.execute(() -> sendRequestSync(pipeline, originalUrl), - () -> sendRequest(pipeline, originalUrl)); - } - - /** - * Test that multiple tags parameters are correctly preserved as separate parameters - * - * This test verifies that multiple tags parameters with the same key are preserved - * as separate query parameters instead of being merged into comma-separated values. - * - * Expected behavior: - * "?tags=tag1%3D&tags=tag2%3D" remains as two separate parameters - * - * This is the correct behavior for Azure App Configuration API which expects - * multiple tags parameters, not comma-separated values. - */ - @SyncAsyncTest - public void multipleTagsParametersFixedBehavior() { - final String originalUrl - = BASE_URL + ENDPOINT_PATH + "?api-version=2023-11-01&key=*&label=dev&tags=tag1%3D&tags=tag2%3D"; - - QueryParamPolicy queryParamPolicy = new QueryParamPolicy(); - - HttpPipelinePolicy auditorPolicy = (context, next) -> { - final String actualUrl = context.getHttpRequest().getUrl().toString(); - - // Count how many separate tags parameters exist - int tagsCount = (actualUrl.length() - actualUrl.replace("tags=", "").length()) / "tags=".length(); - - // Verify that 2 separate tags parameters are preserved - assertTrue(tagsCount == 2, "Multiple tags parameters should be preserved as separate parameters. Found " - + tagsCount + " tags parameter(s), expected 2 separate tags parameters."); - - // Verify both tags parameters are preserved separately - if (tagsCount == 2) { - // Both original tag values should be present as separate parameters - assertTrue(actualUrl.contains("tag1%3D") && actualUrl.contains("tag2%3D"), - "Both tags parameters should be preserved separately with original values"); - } - - System.out.println("Original URL: " + originalUrl); - System.out.println("Transformed URL: " + actualUrl); - System.out.println("Tags parameters found: " + tagsCount); - + assertEquals(expectedUrl, actualUrl, + "Complex query parameters should be normalized, sorted, and lowercased"); return next.process(); }; @@ -380,7 +276,7 @@ public void multipleTagsParametersFixedBehavior() { * the Azure App Configuration API requirement for separate tags parameters. */ @SyncAsyncTest - public void multipleTagsParametersCorrectBehavior() { + public void multipleTagsParameters() { final String originalUrl = BASE_URL + ENDPOINT_PATH + "?api-version=2023-11-01&key=*&label=dev&tags=tag1%3D&tags=tag2%3D"; @@ -416,27 +312,15 @@ public void multipleTagsParametersCorrectBehavior() { * Test single tags parameter is handled correctly */ @SyncAsyncTest - public void singleTagsParameter() { + public void singleTagsParameterPreserved() { final String originalUrl = BASE_URL + ENDPOINT_PATH + "?api-version=2023-11-01&key=*&tags=environment%3Dprod"; + final String expectedUrl = BASE_URL + ENDPOINT_PATH + "?api-version=2023-11-01&key=*&tags=environment%3Dprod"; QueryParamPolicy queryParamPolicy = new QueryParamPolicy(); HttpPipelinePolicy auditorPolicy = (context, next) -> { final String actualUrl = context.getHttpRequest().getUrl().toString(); - - // Verify alphabetical ordering: api-version, key, tags - assertTrue(actualUrl.contains("api-version=2023-11-01"), "API version should be preserved"); - assertTrue(actualUrl.contains("key=*"), "Key parameter should be preserved"); - assertTrue(actualUrl.contains("tags=environment%3Dprod"), "Tags parameter should be preserved"); - - // Verify order: api-version should come before key, key should come before tags - int apiVersionPos = actualUrl.indexOf("api-version="); - int keyPos = actualUrl.indexOf("key="); - int tagsPos = actualUrl.indexOf("tags="); - - assertTrue(apiVersionPos < keyPos, "api-version should come before key"); - assertTrue(keyPos < tagsPos, "key should come before tags"); - + assertEquals(expectedUrl, actualUrl, "Single tags parameter should be preserved with proper ordering"); return next.process(); }; @@ -458,33 +342,15 @@ public void singleTagsParameter() { public void multipleTagsParametersWithOrderingVerification() { final String originalUrl = BASE_URL + ENDPOINT_PATH + "?api-version=2023-11-01&key=*&label=dev&tags=tag1%3D&tags=tag2%3D"; + final String expectedUrl + = BASE_URL + ENDPOINT_PATH + "?api-version=2023-11-01&key=*&label=dev&tags=tag1%3D&tags=tag2%3D"; QueryParamPolicy queryParamPolicy = new QueryParamPolicy(); HttpPipelinePolicy auditorPolicy = (context, next) -> { final String actualUrl = context.getHttpRequest().getUrl().toString(); - - // Verify alphabetical ordering: api-version, key, label, tags - assertTrue(actualUrl.contains("api-version=2023-11-01"), "API version should be preserved"); - assertTrue(actualUrl.contains("key=*"), "Key parameter should be preserved"); - assertTrue(actualUrl.contains("label=dev"), "Label parameter should be preserved"); - - // Verify that the policy correctly preserves multiple tags as separate parameters - // This is the correct behavior for Azure App Configuration API - int tagsCount = (actualUrl.length() - actualUrl.replace("tags=", "").length()) / "tags=".length(); - assertEquals(2, tagsCount, "Multiple tags parameters should be preserved as separate parameters. " - + "Expected " + 2 + " separate tags parameters, but found " + tagsCount + "."); - - // Verify parameters are in alphabetical order - int apiVersionPos = actualUrl.indexOf("api-version="); - int keyPos = actualUrl.indexOf("key="); - int labelPos = actualUrl.indexOf("label="); - int tagsPos = actualUrl.indexOf("tags="); - - assertTrue(apiVersionPos < keyPos, "api-version should come before key"); - assertTrue(keyPos < labelPos, "key should come before label"); - assertTrue(labelPos < tagsPos, "label should come before tags"); - + assertEquals(expectedUrl, actualUrl, + "Multiple tags parameters should be preserved with proper alphabetical ordering"); return next.process(); }; @@ -503,21 +369,14 @@ public void multipleTagsParametersWithOrderingVerification() { public void tagsParametersWithComplexValues() { final String originalUrl = BASE_URL + ENDPOINT_PATH + "?tags=environment%3Dproduction&tags=team%3Dbackend&api-version=2023-11-01"; + final String expectedUrl + = BASE_URL + ENDPOINT_PATH + "?api-version=2023-11-01&tags=environment%3Dproduction&tags=team%3Dbackend"; QueryParamPolicy queryParamPolicy = new QueryParamPolicy(); HttpPipelinePolicy auditorPolicy = (context, next) -> { final String actualUrl = context.getHttpRequest().getUrl().toString(); - - // Verify parameters are sorted alphabetically - assertTrue(actualUrl.contains("api-version=2023-11-01"), "API version should be preserved"); - assertTrue(actualUrl.contains("tags="), "Tags parameter should be present"); - - // Verify alphabetical order: api-version comes before tags - int apiVersionPos = actualUrl.indexOf("api-version="); - int tagsPos = actualUrl.indexOf("tags="); - assertTrue(apiVersionPos < tagsPos, "api-version should come before tags"); - + assertEquals(expectedUrl, actualUrl, "Tags parameters with complex values should be sorted and preserved"); return next.process(); }; @@ -536,33 +395,18 @@ public void tagsParametersWithComplexValues() { public void tagsParametersMixedWithOtherParameters() { final String originalUrl = BASE_URL + ENDPOINT_PATH + "?$select=key,value&tags=feature%3Dauth&label=*&api-version=2023-11-01&$filter=startsWith(key,'app')&tags=env%3Dtest"; + final String expectedUrl = BASE_URL + ENDPOINT_PATH + + "?$filter=startsWith(key,'app')&$select=key,value&api-version=2023-11-01&label=*&tags=env%3Dtest&tags=feature%3Dauth"; QueryParamPolicy queryParamPolicy = new QueryParamPolicy(); HttpPipelinePolicy auditorPolicy = (context, next) -> { final String actualUrl = context.getHttpRequest().getUrl().toString(); - - // Verify we have the expected parameters - assertTrue(actualUrl.contains("$filter="), "$filter parameter should be present"); - assertTrue(actualUrl.contains("$select="), "$select parameter should be present"); - assertTrue(actualUrl.contains("api-version="), "api-version parameter should be present"); - assertTrue(actualUrl.contains("label="), "label parameter should be present"); - assertTrue(actualUrl.contains("tags="), "tags parameter should be present"); - - // Verify alphabetical ordering of the first occurrence of each parameter type - int filterPos = actualUrl.indexOf("$filter="); - int selectPos = actualUrl.indexOf("$select="); - int apiVersionPos = actualUrl.indexOf("api-version="); - int labelPos = actualUrl.indexOf("label="); - int tagsPos = actualUrl.indexOf("tags="); - - assertTrue(filterPos < selectPos, "$filter should come before $select"); - assertTrue(selectPos < apiVersionPos, "$select should come before api-version"); - assertTrue(apiVersionPos < labelPos, "api-version should come before label"); - assertTrue(labelPos < tagsPos, "label should come before tags"); - + assertEquals(expectedUrl, actualUrl, + "Tags parameters mixed with other parameters should be sorted correctly"); return next.process(); }; + final HttpPipeline pipeline = new HttpPipelineBuilder().httpClient(new NoOpHttpClient()) .policies(queryParamPolicy, auditorPolicy) .build(); @@ -578,23 +422,15 @@ public void tagsParametersMixedWithOtherParameters() { public void tagsParametersWithSpecialCharacters() { final String originalUrl = BASE_URL + ENDPOINT_PATH + "?TAGS=Priority%3DHigh&api-version=2023-11-01&Tags=Status%3DActive"; + final String expectedUrl + = BASE_URL + ENDPOINT_PATH + "?api-version=2023-11-01&tags=Priority%3DHigh&tags=Status%3DActive"; QueryParamPolicy queryParamPolicy = new QueryParamPolicy(); HttpPipelinePolicy auditorPolicy = (context, next) -> { final String actualUrl = context.getHttpRequest().getUrl().toString(); - - // Verify that tag keys are converted to lowercase - assertTrue(actualUrl.contains("tags="), "Tags parameter key should be lowercase"); - // Should not contain uppercase versions - assertTrue(!actualUrl.contains("TAGS=") || actualUrl.indexOf("tags=") >= 0, - "Uppercase TAGS should be converted to lowercase"); - assertTrue(!actualUrl.contains("Tags=") || actualUrl.indexOf("tags=") >= 0, - "Mixed case Tags should be converted to lowercase"); - - // Verify values are preserved with their encoding - assertTrue(actualUrl.contains("%3D"), "Special characters in values should be preserved"); - + assertEquals(expectedUrl, actualUrl, + "Tags parameters with special characters should be normalized and sorted"); return next.process(); }; @@ -605,7 +441,7 @@ public void tagsParametersWithSpecialCharacters() { SyncAsyncExtension.execute(() -> sendRequestSync(pipeline, originalUrl), () -> sendRequest(pipeline, originalUrl)); } - + private Mono sendRequest(HttpPipeline pipeline, String url) { return pipeline.send(new HttpRequest(HttpMethod.GET, url), Context.NONE); } From d73ca61c20b2d64b42736f340bf6d4533bd130f9 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Thu, 9 Oct 2025 09:10:05 -0700 Subject: [PATCH 09/24] Update assets.json --- sdk/appconfiguration/azure-data-appconfiguration/assets.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/appconfiguration/azure-data-appconfiguration/assets.json b/sdk/appconfiguration/azure-data-appconfiguration/assets.json index b8dd5af55e36..af4c3760cd34 100644 --- a/sdk/appconfiguration/azure-data-appconfiguration/assets.json +++ b/sdk/appconfiguration/azure-data-appconfiguration/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "java", "TagPrefix": "java/appconfiguration/azure-data-appconfiguration", - "Tag": "java/appconfiguration/azure-data-appconfiguration_5e00bac278" + "Tag": "java/appconfiguration/azure-data-appconfiguration_90b5086be3" } From 0efdeb4d8898931083ef76e4b7d57a264d2c7000 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Thu, 9 Oct 2025 12:16:51 -0700 Subject: [PATCH 10/24] review items --- .../implementation/QueryParamPolicyTest.java | 16 +++--- .../implementation/SyncTokenPolicyTest.java | 52 ++++++++++++++++--- 2 files changed, 52 insertions(+), 16 deletions(-) diff --git a/sdk/appconfiguration/azure-data-appconfiguration/src/test/java/com/azure/data/appconfiguration/implementation/QueryParamPolicyTest.java b/sdk/appconfiguration/azure-data-appconfiguration/src/test/java/com/azure/data/appconfiguration/implementation/QueryParamPolicyTest.java index 412e205ac4ce..37974d93eb6b 100644 --- a/sdk/appconfiguration/azure-data-appconfiguration/src/test/java/com/azure/data/appconfiguration/implementation/QueryParamPolicyTest.java +++ b/sdk/appconfiguration/azure-data-appconfiguration/src/test/java/com/azure/data/appconfiguration/implementation/QueryParamPolicyTest.java @@ -277,12 +277,12 @@ public void comprehensiveQueryParameterNormalization() { */ @SyncAsyncTest public void multipleTagsParameters() { - final String originalUrl - = BASE_URL + ENDPOINT_PATH + "?api-version=2023-11-01&key=*&label=dev&tags=tag1%3D&tags=tag2%3D"; + final String originalUrl = BASE_URL + ENDPOINT_PATH + + "?api-version=2023-11-01&key=*&label=dev&tags=environment%3Ddev&tags=team%3Dfrontend"; // The URL should preserve multiple tags parameters after processing (with alphabetical sorting) - final String expectedUrl - = BASE_URL + ENDPOINT_PATH + "?api-version=2023-11-01&key=*&label=dev&tags=tag1%3D&tags=tag2%3D"; + final String expectedUrl = BASE_URL + ENDPOINT_PATH + + "?api-version=2023-11-01&key=*&label=dev&tags=environment%3Ddev&tags=team%3Dfrontend"; QueryParamPolicy queryParamPolicy = new QueryParamPolicy(); @@ -340,10 +340,10 @@ public void singleTagsParameterPreserved() { */ @SyncAsyncTest public void multipleTagsParametersWithOrderingVerification() { - final String originalUrl - = BASE_URL + ENDPOINT_PATH + "?api-version=2023-11-01&key=*&label=dev&tags=tag1%3D&tags=tag2%3D"; - final String expectedUrl - = BASE_URL + ENDPOINT_PATH + "?api-version=2023-11-01&key=*&label=dev&tags=tag1%3D&tags=tag2%3D"; + final String originalUrl = BASE_URL + ENDPOINT_PATH + + "?api-version=2023-11-01&key=*&label=dev&tags=version%3D1.2.0&tags=region%3Deast"; + final String expectedUrl = BASE_URL + ENDPOINT_PATH + + "?api-version=2023-11-01&key=*&label=dev&tags=region%3Deast&tags=version%3D1.2.0"; QueryParamPolicy queryParamPolicy = new QueryParamPolicy(); diff --git a/sdk/appconfiguration/azure-data-appconfiguration/src/test/java/com/azure/data/appconfiguration/implementation/SyncTokenPolicyTest.java b/sdk/appconfiguration/azure-data-appconfiguration/src/test/java/com/azure/data/appconfiguration/implementation/SyncTokenPolicyTest.java index 16f18cb627de..7b63ef7523b5 100644 --- a/sdk/appconfiguration/azure-data-appconfiguration/src/test/java/com/azure/data/appconfiguration/implementation/SyncTokenPolicyTest.java +++ b/sdk/appconfiguration/azure-data-appconfiguration/src/test/java/com/azure/data/appconfiguration/implementation/SyncTokenPolicyTest.java @@ -3,6 +3,15 @@ package com.azure.data.appconfiguration.implementation; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.net.MalformedURLException; +import java.net.URL; + +import org.junit.jupiter.api.Test; + import com.azure.core.http.HttpHeaders; import com.azure.core.http.HttpMethod; import com.azure.core.http.HttpPipeline; @@ -15,15 +24,8 @@ import com.azure.core.test.http.MockHttpResponse; import com.azure.core.test.http.NoOpHttpClient; import com.azure.core.util.Context; -import org.junit.jupiter.api.Test; -import reactor.core.publisher.Mono; -import java.net.MalformedURLException; -import java.net.URL; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; +import reactor.core.publisher.Mono; /** * Unit tests for Sync Token @@ -203,6 +205,40 @@ public void externalSyncTokensFollowRulesWhenAddedTest() throws MalformedURLExce SyncAsyncExtension.execute(() -> pipeline.sendSync(request, Context.NONE), () -> pipeline.send(request)); } + @SyncAsyncTest + public void syncTokenPolicyWithAfterParameterTest() throws MalformedURLException { + final SyncTokenPolicy syncTokenPolicy = new SyncTokenPolicy(); + final String afterValue = "abcdefg"; + final String urlWithAfter = "https://example.azconfig.io/kv?api-version=2023-11-01&After=" + afterValue + + "&tags=tag3%3Dvalue3&key=*&label=dev&$Select=key&tags=tag2%3Dvalue2&tags=tag1%3Dvalue1"; + + syncTokenPolicy.updateSyncToken(SYNC_TOKEN_VALUE + ";sn=1"); + + HttpPipelinePolicy auditorPolicy = (context, next) -> { + final String headerValue = context.getHttpRequest().getHeaders().getValue(SYNC_TOKEN); + final String requestUrl = context.getHttpRequest().getUrl().toString(); + + // Verify sync token is present in header + assertEquals(SYNC_TOKEN_VALUE, headerValue); + // Verify the URL contains the "After" parameter with correct value + assertTrue(requestUrl.contains("After=" + afterValue)); + // Verify the URL contains other expected parameters + assertTrue(requestUrl.contains("api-version=2023-11-01")); + assertTrue(requestUrl.contains("key=*")); + assertTrue(requestUrl.contains("label=dev")); + assertTrue(requestUrl.contains("$Select=key")); + + return next.process(); + }; + + final HttpPipeline pipeline = new HttpPipelineBuilder().httpClient(new NoOpHttpClient()) + .policies(syncTokenPolicy, auditorPolicy) + .build(); + + HttpRequest request = new HttpRequest(HttpMethod.GET, urlWithAfter); + SyncAsyncExtension.execute(() -> pipeline.sendSync(request, Context.NONE), () -> pipeline.send(request)); + } + private void syncTokenEquals(SyncToken syncToken, String id, String value, long sn) { assertEquals(id, syncToken.getId()); assertEquals(value, syncToken.getValue()); From 404b4831b0738dd588e1f93a86cdf34ab95f271d Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Thu, 9 Oct 2025 12:42:03 -0700 Subject: [PATCH 11/24] Update QueryParamPolicy.java --- .../data/appconfiguration/implementation/QueryParamPolicy.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/implementation/QueryParamPolicy.java b/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/implementation/QueryParamPolicy.java index adb600ef708f..90f7e576136b 100644 --- a/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/implementation/QueryParamPolicy.java +++ b/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/implementation/QueryParamPolicy.java @@ -32,7 +32,7 @@ protected void beforeSendingRequest(HttpPipelineCallContext context) { int equalIndex = pair.indexOf('='); return equalIndex != -1 ? pair.substring(0, equalIndex).toLowerCase() + "=" + pair.substring(equalIndex + 1) - : pair.toLowerCase() + "="; + : pair.toLowerCase(); }).sorted().collect(Collectors.joining("&")); String urlWithoutQuery = url.substring(0, queryIndex); String newUrl = urlWithoutQuery + "?" + normalizedQuery; From 3cbc14e38be5192e87ca4a08fe2a6ad9e3f25422 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Mon, 13 Oct 2025 10:55:16 -0700 Subject: [PATCH 12/24] Update QueryParamPolicy.java --- .../implementation/QueryParamPolicy.java | 93 ++++++++++++++----- 1 file changed, 72 insertions(+), 21 deletions(-) diff --git a/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/implementation/QueryParamPolicy.java b/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/implementation/QueryParamPolicy.java index 90f7e576136b..606b864f553f 100644 --- a/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/implementation/QueryParamPolicy.java +++ b/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/implementation/QueryParamPolicy.java @@ -19,32 +19,83 @@ protected void beforeSendingRequest(HttpPipelineCallContext context) { HttpRequest request = context.getHttpRequest(); try { - String url = request.getUrl().toString(); + String originalUrl = request.getUrl().toString(); + String normalizedUrl = normalizeQueryParameters(originalUrl); + + if (normalizedUrl != null && !normalizedUrl.equals(originalUrl)) { + request.setUrl(normalizedUrl); + } + } catch (IllegalArgumentException e) { + // If the constructed URL is invalid when setting it, continue without modification + LOGGER.warning( + "Failed to set normalized URL due to invalid format. " + + "Request will proceed with original URL. URL: {}, Error: {}", + request.getUrl(), e.getMessage(), e); + } + } + + /** + * Normalizes query parameters in a URL by converting parameter names to lowercase and sorting them. + * Preserves URL encoding of parameter values. + * + * @param url the URL to normalize + * @return the normalized URL, or the original URL if no query parameters exist, or null if normalization fails + */ + static String normalizeQueryParameters(String url) { + if (url == null) { + return url; + } + try { // Find the query string manually to preserve URL encoding int queryIndex = url.indexOf('?'); - if (queryIndex != -1) { - String query = url.substring(queryIndex + 1); - - if (!query.isEmpty()) { - String normalizedQuery - = Arrays.stream(query.split("&")).filter(pair -> !pair.isEmpty()).map(pair -> { - int equalIndex = pair.indexOf('='); - return equalIndex != -1 - ? pair.substring(0, equalIndex).toLowerCase() + "=" + pair.substring(equalIndex + 1) - : pair.toLowerCase(); - }).sorted().collect(Collectors.joining("&")); - String urlWithoutQuery = url.substring(0, queryIndex); - String newUrl = urlWithoutQuery + "?" + normalizedQuery; - request.setUrl(newUrl); - } + if (queryIndex == -1) { + return url; } - } catch (Exception e) { - // If URL parsing fails, continue without modification + + // Check for fragment after query string + int fragmentIndex = url.indexOf('#', queryIndex); + String fragment = ""; + String query; + + if (fragmentIndex != -1) { + query = url.substring(queryIndex + 1, fragmentIndex); + fragment = url.substring(fragmentIndex); // Include the '#' + } else { + query = url.substring(queryIndex + 1); + } + + if (query.isEmpty()) { + return url; + } + + String normalizedQuery = Arrays.stream(query.split("&")) + .filter(pair -> !pair.isEmpty()) + .map(pair -> { + int equalIndex = pair.indexOf('='); + return equalIndex != -1 + ? pair.substring(0, equalIndex).toLowerCase() + "=" + pair.substring(equalIndex + 1) + : pair.toLowerCase(); + }) + .sorted() + .collect(Collectors.joining("&")); + + String urlWithoutQuery = url.substring(0, queryIndex); + return urlWithoutQuery + "?" + normalizedQuery + fragment; + } catch (IndexOutOfBoundsException e) { + // If string manipulation fails due to invalid indices, return null to indicate failure LOGGER.warning( - "Failed to parse URL for query parameter normalization. " - + "Request will proceed with original URL. URL: {}, Error: {}", - request.getUrl(), e.getMessage(), e); + "Failed to parse URL for query parameter normalization due to string manipulation error. " + + "URL: {}, Error: {}", + url, e.getMessage(), e); + return null; + } catch (NullPointerException e) { + // If string operations fail on null, return null to indicate failure + LOGGER.warning( + "Failed to parse URL for query parameter normalization due to null value. " + + "Error: {}", + e.getMessage(), e); + return null; } } } From aa6a387f9aa1c9fd1c20f4a7caa2bf40f3c52511 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Mon, 13 Oct 2025 12:06:47 -0700 Subject: [PATCH 13/24] Update QueryParamPolicy.java --- .../implementation/QueryParamPolicy.java | 103 +++++------------- 1 file changed, 28 insertions(+), 75 deletions(-) diff --git a/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/implementation/QueryParamPolicy.java b/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/implementation/QueryParamPolicy.java index 606b864f553f..2c6f1c73e098 100644 --- a/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/implementation/QueryParamPolicy.java +++ b/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/implementation/QueryParamPolicy.java @@ -3,12 +3,17 @@ package com.azure.data.appconfiguration.implementation; -import java.util.Arrays; -import java.util.stream.Collectors; +import java.net.MalformedURLException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; import com.azure.core.http.HttpPipelineCallContext; import com.azure.core.http.HttpRequest; import com.azure.core.http.policy.HttpPipelineSyncPolicy; +import com.azure.core.util.CoreUtils; +import com.azure.core.util.UrlBuilder; import com.azure.core.util.logging.ClientLogger; public final class QueryParamPolicy extends HttpPipelineSyncPolicy { @@ -16,86 +21,34 @@ public final class QueryParamPolicy extends HttpPipelineSyncPolicy { @Override protected void beforeSendingRequest(HttpPipelineCallContext context) { - HttpRequest request = context.getHttpRequest(); + HttpRequest httpRequest = context.getHttpRequest(); try { - String originalUrl = request.getUrl().toString(); - String normalizedUrl = normalizeQueryParameters(originalUrl); - - if (normalizedUrl != null && !normalizedUrl.equals(originalUrl)) { - request.setUrl(normalizedUrl); + UrlBuilder builder = UrlBuilder.parse(httpRequest.getUrl()); + String queryString = builder.getQueryString(); + builder.clearQuery(); + TreeMap> orderedQuery = new TreeMap<>(String::compareTo); + CoreUtils.parseQueryParameters(queryString) + .forEachRemaining(kvp -> orderedQuery.compute(kvp.getKey(), (ignored, values) -> { + if (values == null) { + values = new ArrayList<>(); + } + values.add(kvp.getValue()); + return values; + })); + for (Map.Entry> ordered : orderedQuery.entrySet()) { + for (String val : ordered.getValue()) { + builder.addQueryParameter(ordered.getKey(), val); + } } - } catch (IllegalArgumentException e) { + httpRequest.setUrl(builder.toUrl().toString()); + } catch (IllegalArgumentException | MalformedURLException e) { // If the constructed URL is invalid when setting it, continue without modification LOGGER.warning( "Failed to set normalized URL due to invalid format. " - + "Request will proceed with original URL. URL: {}, Error: {}", - request.getUrl(), e.getMessage(), e); + + "Request will proceed with original URL. URL: {}, Error: {}", + httpRequest.getUrl(), e.getMessage(), e); } } - /** - * Normalizes query parameters in a URL by converting parameter names to lowercase and sorting them. - * Preserves URL encoding of parameter values. - * - * @param url the URL to normalize - * @return the normalized URL, or the original URL if no query parameters exist, or null if normalization fails - */ - static String normalizeQueryParameters(String url) { - if (url == null) { - return url; - } - - try { - // Find the query string manually to preserve URL encoding - int queryIndex = url.indexOf('?'); - if (queryIndex == -1) { - return url; - } - - // Check for fragment after query string - int fragmentIndex = url.indexOf('#', queryIndex); - String fragment = ""; - String query; - - if (fragmentIndex != -1) { - query = url.substring(queryIndex + 1, fragmentIndex); - fragment = url.substring(fragmentIndex); // Include the '#' - } else { - query = url.substring(queryIndex + 1); - } - - if (query.isEmpty()) { - return url; - } - - String normalizedQuery = Arrays.stream(query.split("&")) - .filter(pair -> !pair.isEmpty()) - .map(pair -> { - int equalIndex = pair.indexOf('='); - return equalIndex != -1 - ? pair.substring(0, equalIndex).toLowerCase() + "=" + pair.substring(equalIndex + 1) - : pair.toLowerCase(); - }) - .sorted() - .collect(Collectors.joining("&")); - - String urlWithoutQuery = url.substring(0, queryIndex); - return urlWithoutQuery + "?" + normalizedQuery + fragment; - } catch (IndexOutOfBoundsException e) { - // If string manipulation fails due to invalid indices, return null to indicate failure - LOGGER.warning( - "Failed to parse URL for query parameter normalization due to string manipulation error. " - + "URL: {}, Error: {}", - url, e.getMessage(), e); - return null; - } catch (NullPointerException e) { - // If string operations fail on null, return null to indicate failure - LOGGER.warning( - "Failed to parse URL for query parameter normalization due to null value. " - + "Error: {}", - e.getMessage(), e); - return null; - } - } } From 267caf3879b6f3d02f2489cdd2c1daa79f95e6fa Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Mon, 13 Oct 2025 12:12:31 -0700 Subject: [PATCH 14/24] Update QueryParamPolicy.java --- .../implementation/QueryParamPolicy.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/implementation/QueryParamPolicy.java b/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/implementation/QueryParamPolicy.java index 2c6f1c73e098..d295613ce429 100644 --- a/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/implementation/QueryParamPolicy.java +++ b/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/implementation/QueryParamPolicy.java @@ -28,15 +28,19 @@ protected void beforeSendingRequest(HttpPipelineCallContext context) { String queryString = builder.getQueryString(); builder.clearQuery(); TreeMap> orderedQuery = new TreeMap<>(String::compareTo); - CoreUtils.parseQueryParameters(queryString) - .forEachRemaining(kvp -> orderedQuery.compute(kvp.getKey(), (ignored, values) -> { + CoreUtils.parseQueryParameters(queryString).forEachRemaining(kvp -> { + String lowercaseKey = kvp.getKey().toLowerCase(); + orderedQuery.compute(lowercaseKey, (ignored, values) -> { if (values == null) { values = new ArrayList<>(); } values.add(kvp.getValue()); return values; - })); + }); + }); for (Map.Entry> ordered : orderedQuery.entrySet()) { + // Sort values for each parameter key to ensure consistent ordering + ordered.getValue().sort(String::compareTo); for (String val : ordered.getValue()) { builder.addQueryParameter(ordered.getKey(), val); } From e1907f11925dcbacd63f50dcc9d083b7d7f2c0b3 Mon Sep 17 00:00:00 2001 From: Jair Myree Date: Tue, 21 Oct 2025 18:12:26 -0700 Subject: [PATCH 15/24] Updating assets.json for azure-monitor-query --- sdk/monitor/azure-monitor-query/assets.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/monitor/azure-monitor-query/assets.json b/sdk/monitor/azure-monitor-query/assets.json index 20d798b600b9..164b4cd2f42a 100644 --- a/sdk/monitor/azure-monitor-query/assets.json +++ b/sdk/monitor/azure-monitor-query/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "java", "TagPrefix": "java/monitor/azure-monitor-query", - "Tag": "java/monitor/azure-monitor-query_dba737553b" + "Tag": "java/monitor/azure-monitor-query_6e25f4e453" } From dd05f825d13ba2e4ef9431c74134b93d1459f34d Mon Sep 17 00:00:00 2001 From: Jair Myree Date: Tue, 21 Oct 2025 18:35:53 -0700 Subject: [PATCH 16/24] updating test files to use TME subscription --- .../java/com/azure/monitor/query/MetricsClientTestBase.java | 2 +- .../java/com/azure/monitor/query/MonitorQueryTestUtils.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sdk/monitor/azure-monitor-query/src/test/java/com/azure/monitor/query/MetricsClientTestBase.java b/sdk/monitor/azure-monitor-query/src/test/java/com/azure/monitor/query/MetricsClientTestBase.java index a701354c6a44..b4c543b75979 100644 --- a/sdk/monitor/azure-monitor-query/src/test/java/com/azure/monitor/query/MetricsClientTestBase.java +++ b/sdk/monitor/azure-monitor-query/src/test/java/com/azure/monitor/query/MetricsClientTestBase.java @@ -15,7 +15,7 @@ public class MetricsClientTestBase extends TestProxyTestBase { static final String FAKE_RESOURCE_ID - = "/subscriptions/faa080af-c1d8-40ad-9cce-e1a450ca5b57/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/vm"; + = "/subscriptions/4d042dc6-fe17-4698-a23f-ec6a8d1e98f4/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/vm"; protected String metricEndpoint; protected MetricsClientBuilder clientBuilder; protected ConfigurationClient configClient; diff --git a/sdk/monitor/azure-monitor-query/src/test/java/com/azure/monitor/query/MonitorQueryTestUtils.java b/sdk/monitor/azure-monitor-query/src/test/java/com/azure/monitor/query/MonitorQueryTestUtils.java index 08d69e21ee42..fc83c957b993 100644 --- a/sdk/monitor/azure-monitor-query/src/test/java/com/azure/monitor/query/MonitorQueryTestUtils.java +++ b/sdk/monitor/azure-monitor-query/src/test/java/com/azure/monitor/query/MonitorQueryTestUtils.java @@ -57,7 +57,7 @@ public static String getAdditionalLogWorkspaceId(boolean isPlaybackMode) { public static String getLogResourceId(boolean isPlaybackMode) { if (isPlaybackMode) { - return "/subscriptions/faa080af-c1d8-40ad-9cce-e1a450ca5b57/resourceGroups/rg/providers/Microsoft.OperationalInsights/workspaces/azmonitorlogsws"; + return "/subscriptions/4d042dc6-fe17-4698-a23f-ec6a8d1e98f4/resourceGroups/rg/providers/Microsoft.OperationalInsights/workspaces/azmonitorlogsws"; } else { return LOG_RESOURCE_ID.substring(LOG_RESOURCE_ID.indexOf("/subscriptions")); } @@ -65,7 +65,7 @@ public static String getLogResourceId(boolean isPlaybackMode) { public static String getMetricResourceUri(boolean isPlaybackMode) { if (isPlaybackMode) { - return "/subscriptions/faa080af-c1d8-40ad-9cce-e1a450ca5b57/resourceGroups/rg/providers/Microsoft.Eventhub/Namespaces/eventhub"; + return "/subscriptions/4d042dc6-fe17-4698-a23f-ec6a8d1e98f4/resourceGroups/rg/providers/Microsoft.Eventhub/Namespaces/eventhub"; } else { return METRIC_RESOURCE_URI.substring(METRIC_RESOURCE_URI.indexOf("/subscriptions")); } From b1570ecc6cd3941fbc1a4cf6966e979fcf188265 Mon Sep 17 00:00:00 2001 From: Jair Myree Date: Mon, 27 Oct 2025 11:23:37 -0700 Subject: [PATCH 17/24] Updating assets.json for azure-monitor-query-metrics --- sdk/monitor/azure-monitor-query-metrics/assets.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/monitor/azure-monitor-query-metrics/assets.json b/sdk/monitor/azure-monitor-query-metrics/assets.json index 707a85305859..35cdabbd4950 100644 --- a/sdk/monitor/azure-monitor-query-metrics/assets.json +++ b/sdk/monitor/azure-monitor-query-metrics/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "java", "TagPrefix": "java/monitor/azure-monitor-query-metrics", - "Tag": "java/monitor/azure-monitor-query-metrics_965743715b" + "Tag": "java/monitor/azure-monitor-query-metrics_78077818b6" } \ No newline at end of file From 5d43d7b4841814da93cb478989547224d03a3f5e Mon Sep 17 00:00:00 2001 From: Jair Myree Date: Mon, 27 Oct 2025 12:53:26 -0700 Subject: [PATCH 18/24] Changing monitor app-config dependency to current to pull new changes --- sdk/monitor/azure-monitor-query-metrics/assets.json | 2 +- sdk/monitor/azure-monitor-query-metrics/pom.xml | 2 +- sdk/monitor/azure-monitor-query/assets.json | 2 +- sdk/monitor/azure-monitor-query/pom.xml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/sdk/monitor/azure-monitor-query-metrics/assets.json b/sdk/monitor/azure-monitor-query-metrics/assets.json index 35cdabbd4950..dd47544a6619 100644 --- a/sdk/monitor/azure-monitor-query-metrics/assets.json +++ b/sdk/monitor/azure-monitor-query-metrics/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "java", "TagPrefix": "java/monitor/azure-monitor-query-metrics", - "Tag": "java/monitor/azure-monitor-query-metrics_78077818b6" + "Tag": "java/monitor/azure-monitor-query-metrics_05cd6c2f00" } \ No newline at end of file diff --git a/sdk/monitor/azure-monitor-query-metrics/pom.xml b/sdk/monitor/azure-monitor-query-metrics/pom.xml index edfb2eba8ad7..3be1db223f6a 100644 --- a/sdk/monitor/azure-monitor-query-metrics/pom.xml +++ b/sdk/monitor/azure-monitor-query-metrics/pom.xml @@ -70,7 +70,7 @@ Code generated by Microsoft (R) TypeSpec Code Generator. com.azure azure-data-appconfiguration - 1.8.4 + 1.9.0-beta.1 test diff --git a/sdk/monitor/azure-monitor-query/assets.json b/sdk/monitor/azure-monitor-query/assets.json index 164b4cd2f42a..8831a03963ea 100644 --- a/sdk/monitor/azure-monitor-query/assets.json +++ b/sdk/monitor/azure-monitor-query/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "java", "TagPrefix": "java/monitor/azure-monitor-query", - "Tag": "java/monitor/azure-monitor-query_6e25f4e453" + "Tag": "java/monitor/azure-monitor-query_2e91e5b97e" } diff --git a/sdk/monitor/azure-monitor-query/pom.xml b/sdk/monitor/azure-monitor-query/pom.xml index 7101282d249c..8a2732da7dec 100644 --- a/sdk/monitor/azure-monitor-query/pom.xml +++ b/sdk/monitor/azure-monitor-query/pom.xml @@ -77,7 +77,7 @@ com.azure azure-data-appconfiguration - 1.8.4 + 1.9.0-beta.1 test From 3255ca46bf50089eaaed005eaf8179cf20e2b172 Mon Sep 17 00:00:00 2001 From: Jair Myree Date: Mon, 27 Oct 2025 13:10:23 -0700 Subject: [PATCH 19/24] Changing monitor app-config dependencies --- sdk/monitor/azure-monitor-query-metrics/pom.xml | 2 +- sdk/monitor/azure-monitor-query/pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/monitor/azure-monitor-query-metrics/pom.xml b/sdk/monitor/azure-monitor-query-metrics/pom.xml index 3be1db223f6a..edfb2eba8ad7 100644 --- a/sdk/monitor/azure-monitor-query-metrics/pom.xml +++ b/sdk/monitor/azure-monitor-query-metrics/pom.xml @@ -70,7 +70,7 @@ Code generated by Microsoft (R) TypeSpec Code Generator. com.azure azure-data-appconfiguration - 1.9.0-beta.1 + 1.8.4 test diff --git a/sdk/monitor/azure-monitor-query/pom.xml b/sdk/monitor/azure-monitor-query/pom.xml index 8a2732da7dec..7101282d249c 100644 --- a/sdk/monitor/azure-monitor-query/pom.xml +++ b/sdk/monitor/azure-monitor-query/pom.xml @@ -77,7 +77,7 @@ com.azure azure-data-appconfiguration - 1.9.0-beta.1 + 1.8.4 test From 110b83205699d9dfefd345abb8aca8cc19a5e554 Mon Sep 17 00:00:00 2001 From: Jair Myree Date: Mon, 27 Oct 2025 13:41:25 -0700 Subject: [PATCH 20/24] updating assets.json --- sdk/monitor/azure-monitor-query-metrics/assets.json | 2 +- sdk/monitor/azure-monitor-query/assets.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/monitor/azure-monitor-query-metrics/assets.json b/sdk/monitor/azure-monitor-query-metrics/assets.json index dd47544a6619..efd871231b32 100644 --- a/sdk/monitor/azure-monitor-query-metrics/assets.json +++ b/sdk/monitor/azure-monitor-query-metrics/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "java", "TagPrefix": "java/monitor/azure-monitor-query-metrics", - "Tag": "java/monitor/azure-monitor-query-metrics_05cd6c2f00" + "Tag": "java/monitor/azure-monitor-query-metrics_46867ad5d0" } \ No newline at end of file diff --git a/sdk/monitor/azure-monitor-query/assets.json b/sdk/monitor/azure-monitor-query/assets.json index 8831a03963ea..6f25ecc2c720 100644 --- a/sdk/monitor/azure-monitor-query/assets.json +++ b/sdk/monitor/azure-monitor-query/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "java", "TagPrefix": "java/monitor/azure-monitor-query", - "Tag": "java/monitor/azure-monitor-query_2e91e5b97e" + "Tag": "java/monitor/azure-monitor-query_db0da25cbc" } From 18dd52e1d0e8560465af50d3b2b076f477d2aefc Mon Sep 17 00:00:00 2001 From: Jair Myree Date: Thu, 30 Oct 2025 16:24:23 -0700 Subject: [PATCH 21/24] Changing monitor dependency to unreleased app config --- eng/versioning/version_client.txt | 1 + sdk/monitor/azure-monitor-query-metrics/pom.xml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/eng/versioning/version_client.txt b/eng/versioning/version_client.txt index 953b8def748f..d0918bbbd50a 100644 --- a/eng/versioning/version_client.txt +++ b/eng/versioning/version_client.txt @@ -544,6 +544,7 @@ io.clientcore:optional-dependency-tests;1.0.0-beta.1;1.0.0-beta.1 # In the pom, the version update tag after the version should name the unreleased package and the dependency version: # +unreleased_com.azure:azure-data-appconfiguration;1.9.0-beta.1 unreleased_com.azure.v2:azure-core;2.0.0-beta.1 unreleased_com.azure.v2:azure-identity;2.0.0-beta.1 unreleased_com.azure.v2:azure-data-appconfiguration;2.0.0-beta.1 diff --git a/sdk/monitor/azure-monitor-query-metrics/pom.xml b/sdk/monitor/azure-monitor-query-metrics/pom.xml index a779fb755a21..712aa1766f13 100644 --- a/sdk/monitor/azure-monitor-query-metrics/pom.xml +++ b/sdk/monitor/azure-monitor-query-metrics/pom.xml @@ -70,7 +70,7 @@ Code generated by Microsoft (R) TypeSpec Code Generator. com.azure azure-data-appconfiguration - 1.8.4 + 1.9.0-beta.1 test From 6a1e30994219b156b81ce9d8a928ba66a0c02ef3 Mon Sep 17 00:00:00 2001 From: Jair Myree Date: Fri, 31 Oct 2025 01:57:55 -0700 Subject: [PATCH 22/24] Updating assets.json files --- sdk/monitor/azure-monitor-query-metrics/assets.json | 2 +- sdk/monitor/azure-monitor-query/assets.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/monitor/azure-monitor-query-metrics/assets.json b/sdk/monitor/azure-monitor-query-metrics/assets.json index efd871231b32..5bda6dfb7a63 100644 --- a/sdk/monitor/azure-monitor-query-metrics/assets.json +++ b/sdk/monitor/azure-monitor-query-metrics/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "java", "TagPrefix": "java/monitor/azure-monitor-query-metrics", - "Tag": "java/monitor/azure-monitor-query-metrics_46867ad5d0" + "Tag": "java/monitor/azure-monitor-query-metrics_126778300d" } \ No newline at end of file diff --git a/sdk/monitor/azure-monitor-query/assets.json b/sdk/monitor/azure-monitor-query/assets.json index 6f25ecc2c720..074bac6b1f75 100644 --- a/sdk/monitor/azure-monitor-query/assets.json +++ b/sdk/monitor/azure-monitor-query/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "java", "TagPrefix": "java/monitor/azure-monitor-query", - "Tag": "java/monitor/azure-monitor-query_db0da25cbc" + "Tag": "java/monitor/azure-monitor-query_2231dd407f" } From 6e20621ec382c6bd18c5a83049c8221a02c158fc Mon Sep 17 00:00:00 2001 From: Jair Myree Date: Fri, 31 Oct 2025 02:42:36 -0700 Subject: [PATCH 23/24] updated pom and assets.json --- sdk/monitor/azure-monitor-query/assets.json | 2 +- sdk/monitor/azure-monitor-query/pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/monitor/azure-monitor-query/assets.json b/sdk/monitor/azure-monitor-query/assets.json index 074bac6b1f75..524e8e6a96ca 100644 --- a/sdk/monitor/azure-monitor-query/assets.json +++ b/sdk/monitor/azure-monitor-query/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "java", "TagPrefix": "java/monitor/azure-monitor-query", - "Tag": "java/monitor/azure-monitor-query_2231dd407f" + "Tag": "java/monitor/azure-monitor-query_eedf39b35d" } diff --git a/sdk/monitor/azure-monitor-query/pom.xml b/sdk/monitor/azure-monitor-query/pom.xml index 885e79b35c8f..07d5196535b6 100644 --- a/sdk/monitor/azure-monitor-query/pom.xml +++ b/sdk/monitor/azure-monitor-query/pom.xml @@ -77,7 +77,7 @@ com.azure azure-data-appconfiguration - 1.8.5 + 1.9.0-beta.1 test From 94139bccb74abf850d56e35d1b7a4ae09c68f442 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Mon, 3 Nov 2025 10:52:20 -0800 Subject: [PATCH 24/24] Update QueryParamPolicyTest.java --- .../implementation/QueryParamPolicyTest.java | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/sdk/appconfiguration/azure-data-appconfiguration/src/test/java/com/azure/data/appconfiguration/implementation/QueryParamPolicyTest.java b/sdk/appconfiguration/azure-data-appconfiguration/src/test/java/com/azure/data/appconfiguration/implementation/QueryParamPolicyTest.java index 37974d93eb6b..9e09d2344bb6 100644 --- a/sdk/appconfiguration/azure-data-appconfiguration/src/test/java/com/azure/data/appconfiguration/implementation/QueryParamPolicyTest.java +++ b/sdk/appconfiguration/azure-data-appconfiguration/src/test/java/com/azure/data/appconfiguration/implementation/QueryParamPolicyTest.java @@ -442,6 +442,114 @@ public void tagsParametersWithSpecialCharacters() { () -> sendRequest(pipeline, originalUrl)); } + /** + * Test key and label filters with ampersand (&) character + */ + @SyncAsyncTest + public void keyAndLabelFiltersWithAmpersandCharacter() { + final String originalUrl + = BASE_URL + ENDPOINT_PATH + "?key=app%26config&label=prod%26test&api-version=2023-11-01"; + final String expectedUrl + = BASE_URL + ENDPOINT_PATH + "?api-version=2023-11-01&key=app%26config&label=prod%26test"; + + QueryParamPolicy queryParamPolicy = new QueryParamPolicy(); + + HttpPipelinePolicy auditorPolicy = (context, next) -> { + final String actualUrl = context.getHttpRequest().getUrl().toString(); + assertEquals(expectedUrl, actualUrl, + "Key and label filters with ampersand character should be preserved and sorted"); + return next.process(); + }; + + final HttpPipeline pipeline = new HttpPipelineBuilder().httpClient(new NoOpHttpClient()) + .policies(queryParamPolicy, auditorPolicy) + .build(); + + SyncAsyncExtension.execute(() -> sendRequestSync(pipeline, originalUrl), + () -> sendRequest(pipeline, originalUrl)); + } + + /** + * Test key and label filters with space character + */ + @SyncAsyncTest + public void keyAndLabelFiltersWithSpaceCharacter() { + final String originalUrl + = BASE_URL + ENDPOINT_PATH + "?key=app%20config&label=dev%20environment&api-version=2023-11-01"; + final String expectedUrl + = BASE_URL + ENDPOINT_PATH + "?api-version=2023-11-01&key=app%20config&label=dev%20environment"; + + QueryParamPolicy queryParamPolicy = new QueryParamPolicy(); + + HttpPipelinePolicy auditorPolicy = (context, next) -> { + final String actualUrl = context.getHttpRequest().getUrl().toString(); + assertEquals(expectedUrl, actualUrl, + "Key and label filters with space character should be preserved and sorted"); + return next.process(); + }; + + final HttpPipeline pipeline = new HttpPipelineBuilder().httpClient(new NoOpHttpClient()) + .policies(queryParamPolicy, auditorPolicy) + .build(); + + SyncAsyncExtension.execute(() -> sendRequestSync(pipeline, originalUrl), + () -> sendRequest(pipeline, originalUrl)); + } + + /** + * Test key and label filters with hash (#) character + */ + @SyncAsyncTest + public void keyAndLabelFiltersWithHashCharacter() { + final String originalUrl + = BASE_URL + ENDPOINT_PATH + "?key=app%23config&label=version%23v1&api-version=2023-11-01"; + final String expectedUrl + = BASE_URL + ENDPOINT_PATH + "?api-version=2023-11-01&key=app%23config&label=version%23v1"; + + QueryParamPolicy queryParamPolicy = new QueryParamPolicy(); + + HttpPipelinePolicy auditorPolicy = (context, next) -> { + final String actualUrl = context.getHttpRequest().getUrl().toString(); + assertEquals(expectedUrl, actualUrl, + "Key and label filters with hash character should be preserved and sorted"); + return next.process(); + }; + + final HttpPipeline pipeline = new HttpPipelineBuilder().httpClient(new NoOpHttpClient()) + .policies(queryParamPolicy, auditorPolicy) + .build(); + + SyncAsyncExtension.execute(() -> sendRequestSync(pipeline, originalUrl), + () -> sendRequest(pipeline, originalUrl)); + } + + /** + * Test key and label filters with multiple special characters combined + */ + @SyncAsyncTest + public void keyAndLabelFiltersWithMixedSpecialCharacters() { + final String originalUrl = BASE_URL + ENDPOINT_PATH + + "?key=app%26config%20test%23v1&label=prod%20%26%20test%23env&api-version=2023-11-01"; + final String expectedUrl = BASE_URL + ENDPOINT_PATH + + "?api-version=2023-11-01&key=app%26config%20test%23v1&label=prod%20%26%20test%23env"; + + QueryParamPolicy queryParamPolicy = new QueryParamPolicy(); + + HttpPipelinePolicy auditorPolicy = (context, next) -> { + final String actualUrl = context.getHttpRequest().getUrl().toString(); + assertEquals(expectedUrl, actualUrl, + "Key and label filters with mixed special characters should be preserved and sorted"); + return next.process(); + }; + + final HttpPipeline pipeline = new HttpPipelineBuilder().httpClient(new NoOpHttpClient()) + .policies(queryParamPolicy, auditorPolicy) + .build(); + + SyncAsyncExtension.execute(() -> sendRequestSync(pipeline, originalUrl), + () -> sendRequest(pipeline, originalUrl)); + } + private Mono sendRequest(HttpPipeline pipeline, String url) { return pipeline.send(new HttpRequest(HttpMethod.GET, url), Context.NONE); }