diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/appconfiguration/config/web/implementation/AppConfigurationEndpoint.java b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/appconfiguration/config/web/implementation/AppConfigurationEndpoint.java index 35dacb154598..bcfa48a494dc 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/appconfiguration/config/web/implementation/AppConfigurationEndpoint.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/appconfiguration/config/web/implementation/AppConfigurationEndpoint.java @@ -5,11 +5,10 @@ import static com.azure.spring.cloud.appconfiguration.config.web.implementation.AppConfigurationWebConstants.DATA; import static com.azure.spring.cloud.appconfiguration.config.web.implementation.AppConfigurationWebConstants.SYNC_TOKEN; import static com.azure.spring.cloud.appconfiguration.config.web.implementation.AppConfigurationWebConstants.VALIDATION_CODE_KEY; -import static com.azure.spring.cloud.appconfiguration.config.web.implementation.AppConfigurationWebConstants.VALIDATION_TOPIC; import java.io.IOException; +import java.net.URI; import java.util.List; -import java.util.Locale; import java.util.Map; import java.util.stream.Collectors; @@ -18,7 +17,6 @@ import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationStoreMonitoring.AccessToken; import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationStoreMonitoring.PushNotification; import com.azure.spring.cloud.appconfiguration.config.implementation.properties.ConfigStore; - import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; @@ -26,12 +24,10 @@ * Common class for authenticating refresh requests. */ public class AppConfigurationEndpoint { + + private static final String CONFIG_STORE_SUBJECT = "subject"; - private static final String CONFIG_STORE_TOPIC = "configurationstores"; - - private final String endpoint; - - private final String store; + private final URI endpoint; private final List configStores; @@ -75,12 +71,10 @@ public AppConfigurationEndpoint(HttpServletRequest request, List co validationResponse = requestBody.findValue(VALIDATION_CODE_KEY); - JsonNode requestTopic = requestBody.findValue(VALIDATION_TOPIC); - if (requestTopic != null) { - String topic = requestTopic.asText(); - store = topic.substring( - topic.toLowerCase(Locale.ROOT).indexOf(CONFIG_STORE_TOPIC) + CONFIG_STORE_TOPIC.length() + 1); - endpoint = String.format("https://%s", store); + JsonNode requestSubject = requestBody.findValue(CONFIG_STORE_SUBJECT); + if (requestSubject != null) { + String subject = requestSubject.asText(); + endpoint = URI.create(subject); } else { throw new IllegalArgumentException("Refresh request missing topic field."); } @@ -94,7 +88,7 @@ public AppConfigurationEndpoint(HttpServletRequest request, List co */ public boolean authenticate() { for (ConfigStore configStore : configStores) { - if (configStore.getEndpoint().startsWith(endpoint)) { + if (configStore.containsEndpoint(getEndpoint())) { PushNotification pushNotification = configStore.getMonitoring().getPushNotification(); // One of these need to be set @@ -129,7 +123,7 @@ private boolean isTokenMatch(AccessToken token) { */ public boolean triggerRefresh() { for (ConfigStore configStore : configStores) { - if (configStore.getEndpoint().startsWith(endpoint) && configStore.getMonitoring().isEnabled()) { + if (configStore.containsEndpoint(getEndpoint()) && configStore.getMonitoring().isEnabled()) { return true; } } @@ -155,14 +149,6 @@ public JsonNode getValidationResponse() { * @return the endpoint */ public String getEndpoint() { - return endpoint; - } - - /** - * @return the store - */ - public String getStore() { - return store; + return endpoint.getScheme() + "://" + endpoint.getHost(); } - } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/appconfiguration/config/web/implementation/AppConfigurationEndpointTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/appconfiguration/config/web/implementation/AppConfigurationEndpointTest.java index b6ec7c77fec4..69b3bfe12d0d 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/appconfiguration/config/web/implementation/AppConfigurationEndpointTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/appconfiguration/config/web/implementation/AppConfigurationEndpointTest.java @@ -67,15 +67,13 @@ public void validationParsing() throws JsonGenerationException, JsonMappingExcep Map allRequestParams = new HashMap(); AppConfigurationEndpoint endpoint = new AppConfigurationEndpoint(request, configStores, allRequestParams); - assertEquals("https://testConfig", endpoint.getEndpoint()); - assertEquals("testConfig", endpoint.getStore()); + assertEquals("https://fake.test.azconfig.io", endpoint.getEndpoint()); requestBody = mapper.readValue(new File(GET_TEST_REFRESH), JsonNode.class).toString(); when(lines.collect(Mockito.any())).thenReturn(requestBody); endpoint = new AppConfigurationEndpoint(request, configStores, allRequestParams); - assertEquals("https://testConfig", endpoint.getEndpoint()); - assertEquals("testConfig", endpoint.getStore()); + assertEquals("https://testconfig.azconfig.io", endpoint.getEndpoint()); } @Test @@ -109,7 +107,7 @@ public void authenticate() throws JsonParseException, JsonMappingException, IOEx assertFalse(endpoint.authenticate()); ConfigStore validConfigStore = new ConfigStore(); - validConfigStore.setEndpoint("https://testConfig.azconfig.io"); + validConfigStore.setEndpoint("https://fake.test.azconfig.io"); configStores.add(validConfigStore); endpoint = new AppConfigurationEndpoint(request, configStores, allRequestParams); // Valid Config Store, no secrets @@ -174,7 +172,7 @@ public void triggerRefresh() throws JsonParseException, JsonMappingException, IO assertFalse(endpoint.authenticate()); ConfigStore validConfigStore = new ConfigStore(); - validConfigStore.setEndpoint("https://testConfig.azconfig.io"); + validConfigStore.setEndpoint("https://fake.test.azconfig.io"); configStores.add(validConfigStore); endpoint = new AppConfigurationEndpoint(request, configStores, allRequestParams); diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/appconfiguration/config/web/implementation/pushbusrefresh/AppConfigurationBusRefreshEndpointTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/appconfiguration/config/web/implementation/pushbusrefresh/AppConfigurationBusRefreshEndpointTest.java index eaece1de3c87..6aff460c6233 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/appconfiguration/config/web/implementation/pushbusrefresh/AppConfigurationBusRefreshEndpointTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/appconfiguration/config/web/implementation/pushbusrefresh/AppConfigurationBusRefreshEndpointTest.java @@ -7,6 +7,9 @@ import static com.azure.spring.cloud.appconfiguration.config.web.implementation.TestConstants.TRIGGER_LABEL; import static com.azure.spring.cloud.appconfiguration.config.web.implementation.TestConstants.VALIDATION_URL; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.io.BufferedReader; @@ -84,6 +87,7 @@ public void setup() throws IOException { configStores = new ArrayList<>(); configStore.setMonitoring(monitoring); configStore.setEndpoint("https://fake.test.azconfig.io"); + configStore.validateAndInit(); configStores.add(configStore); when(request.getReader()).thenReturn(reader); @@ -110,10 +114,11 @@ public void webHookValidation() throws IOException { when(lines.collect(Mockito.any())).thenReturn("[{\r\n" + " \"id\": \"2d1781af-3a4c-4d7c-bd0c-e34b19da4e66\",\r\n" + " \"topic\":" + TOPIC + ",\r\n" - + " \"subject\": \"\",\r\n" + + " \"subject\": \"https://fake.test.azconfig.io/kv/Foo?label=FizzBuzz\",\r\n" + " \"data\": {\r\n" + " \"validationCode\": \"512d38b6-c7b8-40c8-89fe-f46f9e9622b6\",\r\n" - + " \"validationUrl\":" + VALIDATION_URL + "\r\n" + + " \"validationUrl\":" + VALIDATION_URL + ",\r\n" + + " \"syncToken\": \"zAJw6V16=MzoyMCMyODA3Mzc3;sn=2807377\"\r\n" + " },\r\n" + " \"eventType\": \"Microsoft.EventGrid.SubscriptionValidationEvent\",\r\n" + " \"eventTime\": \"2018-01-25T22:12:19.4556811Z\",\r\n" @@ -140,6 +145,11 @@ public void webHookRefresh() throws IOException { when(lines.collect(Mockito.any())).thenReturn(getResetNotification()); assertEquals(HttpStatus.OK.getReasonPhrase(), endpoint.refresh(request, response, allRequestParams)); + verify(publisher, times(1)).publishEvent(argThat(refreshEvent -> { + boolean hasEndpoint = ((AppConfigurationBusRefreshEvent) refreshEvent).getEndpoint().equals("https://fake.test.azconfig.io"); + boolean hasSyncToken = ((AppConfigurationBusRefreshEvent) refreshEvent).getSyncToken().equals("zAJw6V16=MzoyMCMyODA3Mzc3;sn=2807377"); + return hasEndpoint && hasSyncToken; + })); } @Test @@ -255,7 +265,8 @@ private String getResetNotification() { + " \"data\" : {\r\n" + " \"key\" : \"trigger_key\",\r\n" + " \"label\" : \"trigger_label\",\r\n" - + " \"etag\" : \"r05tB2hfMQs0vo6ITcXu7ScIOhR\"\r\n" + + " \"etag\" : \"r05tB2hfMQs0vo6ITcXu7ScIOhR\",\r\n" + + " \"syncToken\": \"zAJw6V16=MzoyMCMyODA3Mzc3;sn=2807377\"\r\n" + " },\r\n" + " \"eventType\" : \"Microsoft.AppConfiguration.KeyValueModified\",\r\n" + " \"dataVersion\" : \"1\",\r\n" diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/appconfiguration/config/web/implementation/pushrefresh/AppConfigurationRefreshEndpointTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/appconfiguration/config/web/implementation/pushrefresh/AppConfigurationRefreshEndpointTest.java index af449330df34..becc434610c0 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/appconfiguration/config/web/implementation/pushrefresh/AppConfigurationRefreshEndpointTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/appconfiguration/config/web/implementation/pushrefresh/AppConfigurationRefreshEndpointTest.java @@ -7,6 +7,9 @@ import static com.azure.spring.cloud.appconfiguration.config.web.implementation.TestConstants.TRIGGER_LABEL; import static com.azure.spring.cloud.appconfiguration.config.web.implementation.TestConstants.VALIDATION_URL; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.io.BufferedReader; @@ -61,6 +64,8 @@ public class AppConfigurationRefreshEndpointTest { private ArrayList triggers; private AppConfigurationStoreMonitoring monitoring; + + private String endpoint = "https://fake.test.azconfig.io"; private String tokenName = "token"; @@ -112,10 +117,11 @@ public void webHookValidation() throws IOException { when(lines.collect(Mockito.any())).thenReturn("[{\r\n" + " \"id\": \"2d1781af-3a4c-4d7c-bd0c-e34b19da4e66\",\r\n" + " \"topic\":" + TOPIC + ",\r\n" - + " \"subject\": \"\",\r\n" + + " \"subject\": \"https://fake.test.azconfig.io/kv/Foo?label=FizzBuzz\",\r\n" + " \"data\": {\r\n" + " \"validationCode\": \"512d38b6-c7b8-40c8-89fe-f46f9e9622b6\",\r\n" - + " \"validationUrl\":" + VALIDATION_URL + "\r\n" + + " \"validationUrl\":" + VALIDATION_URL + ",\r\n" + + " \"syncToken\": \"zAJw6V16=MzoyMCMyODA3Mzc3;sn=2807377\"\r\n" + " },\r\n" + " \"eventType\": \"Microsoft.EventGrid.SubscriptionValidationEvent\",\r\n" + " \"eventTime\": \"2018-01-25T22:12:19.4556811Z\",\r\n" @@ -141,6 +147,11 @@ public void webHookRefresh() throws IOException { when(lines.collect(Mockito.any())).thenReturn(getResetNotification()); assertEquals(HttpStatus.OK.getReasonPhrase(), endpoint.refresh(request, response, allRequestParams)); + verify(publisher, times(1)).publishEvent(argThat(refreshEvent -> { + boolean hasEndpoint = ((AppConfigurationRefreshEvent) refreshEvent).getEndpoint().equals("https://fake.test.azconfig.io"); + boolean hasSyncToken = ((AppConfigurationRefreshEvent) refreshEvent).getSyncToken().equals("zAJw6V16=MzoyMCMyODA3Mzc3;sn=2807377"); + return hasEndpoint && hasSyncToken; + })); } @Test diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/test/resources/webHookValidation.json b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/test/resources/webHookValidation.json index ae1d570b8cd4..58f33d2424a4 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/test/resources/webHookValidation.json +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/test/resources/webHookValidation.json @@ -2,7 +2,7 @@ { "id": "2d1781af-3a4c-4d7c-bd0c-e34b19da4e66", "topic": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/testGroup/providers/microsoft.appconfiguration/configurationstores/testConfig", - "subject": "", + "subject": "https://fake.test.azconfig.io/kv/Foo?label=FizzBuzz", "data": { "validationCode": "512d38b6-c7b8-40c8-89fe-f46f9e9622b8", "validationUrl": "https://rp-eastus2.eventgrid.azure.net:553/eventsubscriptions/estest/validate?id=512d38b6-c7b8-40c8-89fe-f46f9e9622b6&t=2018-04-26T20:30:54.4538837Z&apiVersion=2018-05-01-preview&token=1A1A1A1A" diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/ConfigStore.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/ConfigStore.java index 684067151ffe..372e17e07225 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/ConfigStore.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/ConfigStore.java @@ -2,6 +2,7 @@ // Licensed under the MIT License. package com.azure.spring.cloud.appconfiguration.config.implementation.properties; +import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; @@ -20,7 +21,7 @@ public final class ConfigStore { private static final String DEFAULT_KEYS = "/application/"; - private String endpoint; // Config store endpoint + private String endpoint = ""; // Config store endpoint private List endpoints = new ArrayList<>(); @@ -165,6 +166,13 @@ public void setFeatureFlags(FeatureFlagStore featureFlags) { this.featureFlags = featureFlags; } + public boolean containsEndpoint(String endpoint) { + if (this.endpoint.startsWith(endpoint)) { + return true; + } + return endpoints.stream().anyMatch(storeEndpoint -> storeEndpoint.startsWith(endpoint)); + } + /** * @throws IllegalStateException Connection String URL endpoint is invalid */ @@ -193,11 +201,11 @@ public void validateAndInit() { String endpoint = (AppConfigurationReplicaClientsBuilder.getEndpointFromConnectionString(connection)); try { // new URI is used to validate the endpoint as a valid URI - new URI(endpoint); + new URI(endpoint).toURL(); if (!StringUtils.hasText(this.endpoint)) { this.endpoint = endpoint; } - } catch (URISyntaxException e) { + } catch (MalformedURLException | URISyntaxException | IllegalArgumentException e) { throw new IllegalStateException("Endpoint in connection string is not a valid URI.", e); } } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/stores/ConfigStoreTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/stores/ConfigStoreTest.java index f941b16b3053..c1be6025ed69 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/stores/ConfigStoreTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/stores/ConfigStoreTest.java @@ -4,7 +4,9 @@ import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.ArrayList; import java.util.List; @@ -75,5 +77,47 @@ public void getLabelsTest() { configStore.setSelects(selects); assertEquals("\0", configStore.getSelects().get(0).getLabelFilter(new ArrayList<>())[0]); } + + @Test + public void testContainsEndpoint() { + ConfigStore store = new ConfigStore(); + store.setEndpoint("endpoint"); + store.validateAndInit(); + assertTrue(store.containsEndpoint("endpoint")); + assertFalse(store.containsEndpoint("invalidEndpoint")); + + store = new ConfigStore(); + List endpoints = new ArrayList<>(); + endpoints.add("endpoint"); + endpoints.add("secondEndpoint"); + store.setEndpoints(endpoints); + store.validateAndInit(); + assertTrue(store.containsEndpoint("endpoint")); + assertTrue(store.containsEndpoint("secondEndpoint")); + assertFalse(store.containsEndpoint("invalidEndpoint")); + } + + @Test + public void testValidateConnectionString() { + ConfigStore store = new ConfigStore(); + store.setConnectionString("Endpoint=https://endpoint.io;Id=identifier;Secret=secret="); + store.validateAndInit(); + + store = new ConfigStore(); + List connectionStrings = new ArrayList<>(); + connectionStrings.add("Endpoint=https://endpoint.io;Id=identifier;Secret=secret="); + connectionStrings.add("Endpoint=https://endpoint2.io;Id=identifier;Secret=secret="); + store.setConnectionStrings(connectionStrings); + store.validateAndInit(); + } + + @Test + public void testValidateConnectionStringInvalid() { + ConfigStore store = new ConfigStore(); + List connectionStrings = new ArrayList<>(); + connectionStrings.add("Endpoint=endpoint;Id=identifier;Secret=secret="); + store.setConnectionStrings(connectionStrings); + assertThrows(IllegalStateException.class, () -> store.validateAndInit()); + } }