Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CIF-2097 - Consume the configured custom HTTP headers in MagentoGraphqlClient #596

Merged
merged 10 commits into from
Jun 17, 2021
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* Copyright 2021 Adobe. All rights reserved.
*
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/

package com.adobe.cq.commerce.core.components.client;

import java.util.Set;

import com.google.common.collect.ImmutableSet;

public interface DeniedHttpHeaders {
dplaton marked this conversation as resolved.
Show resolved Hide resolved
/**
* A list of HTTP headers that cannot be overriden when configuring a list of custom HTTP headers
*/
final Set<String> DENYLIST = ImmutableSet.of(org.apache.http.HttpHeaders.ACCEPT,
org.apache.http.HttpHeaders.ACCEPT_CHARSET,
org.apache.http.HttpHeaders.ACCEPT_ENCODING,
org.apache.http.HttpHeaders.ACCEPT_LANGUAGE,
org.apache.http.HttpHeaders.ACCEPT_RANGES,
org.apache.http.HttpHeaders.AGE,
org.apache.http.HttpHeaders.ALLOW,
org.apache.http.HttpHeaders.AUTHORIZATION,
org.apache.http.HttpHeaders.CACHE_CONTROL,
org.apache.http.HttpHeaders.CONNECTION,
org.apache.http.HttpHeaders.CONTENT_ENCODING,
org.apache.http.HttpHeaders.CONTENT_LANGUAGE,
org.apache.http.HttpHeaders.CONTENT_LENGTH,
org.apache.http.HttpHeaders.CONTENT_LOCATION,
org.apache.http.HttpHeaders.CONTENT_MD5,
org.apache.http.HttpHeaders.CONTENT_RANGE,
org.apache.http.HttpHeaders.CONTENT_TYPE,
org.apache.http.HttpHeaders.DATE,
org.apache.http.HttpHeaders.DAV,
org.apache.http.HttpHeaders.DEPTH,
org.apache.http.HttpHeaders.DESTINATION,
org.apache.http.HttpHeaders.ETAG,
org.apache.http.HttpHeaders.EXPECT,
org.apache.http.HttpHeaders.EXPIRES,
org.apache.http.HttpHeaders.FROM,
org.apache.http.HttpHeaders.HOST,
org.apache.http.HttpHeaders.IF,
org.apache.http.HttpHeaders.IF_MATCH,
org.apache.http.HttpHeaders.IF_MODIFIED_SINCE,
org.apache.http.HttpHeaders.IF_NONE_MATCH,
org.apache.http.HttpHeaders.IF_RANGE,
org.apache.http.HttpHeaders.IF_UNMODIFIED_SINCE,
org.apache.http.HttpHeaders.LAST_MODIFIED,
org.apache.http.HttpHeaders.LOCATION,
org.apache.http.HttpHeaders.LOCK_TOKEN,
org.apache.http.HttpHeaders.MAX_FORWARDS,
org.apache.http.HttpHeaders.OVERWRITE,
org.apache.http.HttpHeaders.PRAGMA,
org.apache.http.HttpHeaders.PROXY_AUTHENTICATE,
org.apache.http.HttpHeaders.PROXY_AUTHORIZATION,
org.apache.http.HttpHeaders.RANGE,
org.apache.http.HttpHeaders.REFERER,
org.apache.http.HttpHeaders.RETRY_AFTER,
org.apache.http.HttpHeaders.SERVER,
org.apache.http.HttpHeaders.STATUS_URI,
org.apache.http.HttpHeaders.TE,
org.apache.http.HttpHeaders.TIMEOUT,
org.apache.http.HttpHeaders.TRAILER,
org.apache.http.HttpHeaders.TRANSFER_ENCODING,
org.apache.http.HttpHeaders.UPGRADE,
org.apache.http.HttpHeaders.USER_AGENT,
org.apache.http.HttpHeaders.VARY,
org.apache.http.HttpHeaders.VIA,
org.apache.http.HttpHeaders.WARNING,
org.apache.http.HttpHeaders.WWW_AUTHENTICATE,
"Store",
"Preview-Version");
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,13 @@

import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.TimeZone;
import java.util.stream.Collectors;

import javax.servlet.http.Cookie;

Expand Down Expand Up @@ -69,6 +73,8 @@ public class MagentoGraphqlClient {

private RequestOptions requestOptions;

private List<Header> httpHeaders;

/**
* Instantiates and returns a new MagentoGraphqlClient.
* This method returns <code>null</code> if the client cannot be instantiated.<br>
Expand Down Expand Up @@ -150,6 +156,7 @@ private MagentoGraphqlClient(Resource resource, Page page, SlingHttpServletReque
graphqlClient = configurationResource.adaptTo(GraphqlClient.class);
} else {
LOGGER.debug("Crafting a configuration resource and attempting to get a GraphQL client from it...");

// The Context-Aware Configuration API does return a ValueMap with all the collected properties from /conf and /libs,
// but if you ask it for a resource via ConfigurationResourceResolver#getConfigurationResource() you get the resource that
// resolves first (e.g. /conf/.../settings/cloudonfigs/commerce). This resource might not contain the properties
Expand All @@ -171,7 +178,7 @@ private MagentoGraphqlClient(Resource resource, Page page, SlingHttpServletReque
.withDataFetchingPolicy(DataFetchingPolicy.CACHE_FIRST);
requestOptions.withCachingStrategy(cachingStrategy);

List<Header> headers = new ArrayList<>();
List<Header> headers = configuration.size() > 0 ? getCustomHttpHeaders(configuration) : new ArrayList<>();

String storeCode;
if (configuration.size() > 0) {
Expand Down Expand Up @@ -215,6 +222,30 @@ private MagentoGraphqlClient(Resource resource, Page page, SlingHttpServletReque
if (!headers.isEmpty()) {
requestOptions.withHeaders(headers);
}

this.httpHeaders = headers;
}

private List<Header> getCustomHttpHeaders(ComponentsConfiguration configuration) {
List<Header> headers = new ArrayList<>();
buuhuu marked this conversation as resolved.
Show resolved Hide resolved

String[] customHeaders = configuration.get("httpHeaders", String[].class);

if (customHeaders != null) {
headers = Arrays.stream(customHeaders)
.map(headerConfig -> {
String name = headerConfig.substring(0, headerConfig.indexOf('='));
if (DeniedHttpHeaders.DENYLIST.stream().noneMatch(name::equalsIgnoreCase)) {
String value = headerConfig.substring(headerConfig.indexOf('=') + 1, headerConfig.length());
return new BasicHeader(name, value);
}
return null;
})
.filter(Objects::nonNull)
.collect(Collectors.toList());
}

return headers;
}

private Long getTimeWarpEpoch(SlingHttpServletRequest request) {
Expand Down Expand Up @@ -272,6 +303,15 @@ public GraphqlClientConfiguration getConfiguration() {
return graphqlClient.getConfiguration();
}

/**
* Returns the list of custom HTTP headers used by the GraphQL client.
*
* @return a {@link Map} with header names as keys and header values as values
*/
public Map<String, String> getHttpHeaders() {
return httpHeaders.stream().collect(Collectors.toMap(Header::getName, Header::getValue));
}

private String readFallBackConfiguration(Resource resource, String propertyName) {

InheritanceValueMap properties;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
*
******************************************************************************/

@Version("1.6.0")
@Version("1.7.0")
package com.adobe.cq.commerce.core.components.client;

import org.osgi.annotation.versioning.Version;
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

package com.adobe.cq.commerce.core.components.internal.models.v1.storeconfigexporter;

import java.util.Map;

import javax.annotation.PostConstruct;
import javax.inject.Inject;

Expand All @@ -31,6 +33,9 @@
import com.adobe.cq.commerce.graphql.client.GraphqlClientConfiguration;
import com.adobe.cq.commerce.graphql.client.HttpMethod;
import com.day.cq.wcm.api.Page;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;

@Model(
adaptables = SlingHttpServletRequest.class,
Expand All @@ -56,6 +61,7 @@ public class StoreConfigExporterImpl implements StoreConfigExporter {
private String graphqlEndpoint = "/magento/graphql";
private HttpMethod method = HttpMethod.POST;
private Page storeRootPage;
private Map<String, String> httpHeaders;

@PostConstruct
void initModel() {
Expand All @@ -70,6 +76,7 @@ void initModel() {
if (magentoGraphqlClient != null) {
GraphqlClientConfiguration graphqlClientConfiguration = magentoGraphqlClient.getConfiguration();
method = graphqlClientConfiguration.httpMethod();
httpHeaders = magentoGraphqlClient.getHttpHeaders();
}
}

Expand All @@ -88,6 +95,19 @@ public String getMethod() {
return method.toString();
}

@Override
public String getHttpHeaders() {
ObjectMapper mapper = new ObjectMapper();
ObjectNode objectNode = mapper.createObjectNode();
httpHeaders.entrySet().stream().forEach(entry -> objectNode.put(entry.getKey(), entry.getValue()));
try {
return mapper.writeValueAsString(objectNode);
} catch (JsonProcessingException e) {
LOGGER.error(e.getMessage(), e);
return "{}";
}
}

@Override
public String getStoreRootUrl() {
if (storeRootPage == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,9 @@ public interface StoreConfigExporter {
* @return The URL of the storefront homepage
*/
String getStoreRootUrl();

/**
* @return the list of custom HTTP headers configured in addition to the standard ones. This list is in JSON format.
*/
String getHttpHeaders();
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
*
******************************************************************************/

@Version("2.0.0")
@Version("3.0.0")
package com.adobe.cq.commerce.core.components.models.storeconfigexporter;

import org.osgi.annotation.versioning.Version;
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,34 @@ private void executeAndCheck(boolean withStoreHeader, MagentoGraphqlClient clien
Mockito.verify(graphqlClient).execute(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.argThat(matcher));
}

@Test
public void testCustomHeaders() {

List<Header> expectedHeaders = new ArrayList<>();
expectedHeaders.add(new BasicHeader("Store", "my-store"));
expectedHeaders.add(new BasicHeader("customHeader-1", "value1"));
expectedHeaders.add(new BasicHeader("customHeader-2", "value2"));
expectedHeaders.add(new BasicHeader("customHeader-3", "=value3=3=3=3=3"));

ValueMap MOCK_CONFIGURATION_CUSTOM_HEADERS = new ValueMapDecorator(ImmutableMap.of("cq:graphqlClient", "default", "magentoStore",
"my-store", "httpHeaders", new String[] { "customHeader-1=value1", "customHeader-2=value2", "customHeader-3==value3=3=3=3=3",
"Authorization=099sx8x7v1" }));
ComponentsConfiguration MOCK_CONFIGURATION_OBJECT = new ComponentsConfiguration(MOCK_CONFIGURATION_CUSTOM_HEADERS);

Page pageWithConfig = Mockito.spy(context.pageManager().getPage(PAGE_A));
Resource pageResource = Mockito.spy(pageWithConfig.adaptTo(Resource.class));
when(pageWithConfig.adaptTo(Resource.class)).thenReturn(pageResource);
when(pageResource.adaptTo(GraphqlClient.class)).thenReturn(graphqlClient);
when(pageResource.adaptTo(ComponentsConfiguration.class)).thenReturn(MOCK_CONFIGURATION_OBJECT);

RequestOptionsMatcher matcher = new RequestOptionsMatcher(expectedHeaders, HttpMethod.GET);
MagentoGraphqlClient client = MagentoGraphqlClient.create(pageWithConfig.adaptTo(Resource.class), pageWithConfig);
client.execute("{dummy}", HttpMethod.GET);
graphqlClient.getConfiguration();

Mockito.verify(graphqlClient).execute(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.argThat(matcher));
}

@Test
public void testMagentoStorePropertyWithConfigBuilder() {
Page pageWithConfig = Mockito.spy(context.pageManager().getPage(PAGE_A));
Expand Down Expand Up @@ -318,10 +346,16 @@ public boolean matches(Object obj) {
return false;
}

List<Header> actualHeaders = requestOptions.getHeaders();

if (headers.size() != actualHeaders.size()) {
return false;
}

for (Header header : headers) {
if (!requestOptions.getHeaders()
if (actualHeaders
.stream()
.anyMatch(h -> h.getName()
.noneMatch(h -> h.getName()
.equals(header.getName()) && h.getValue()
.equals(header.getValue()))) {
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

package com.adobe.cq.commerce.core.components.internal.models.v1.storeconfigexporter;

import java.io.IOException;

import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ValueMap;
import org.apache.sling.api.scripting.SlingBindings;
Expand All @@ -31,6 +33,8 @@
import com.adobe.cq.launches.api.Launch;
import com.day.cq.wcm.api.Page;
import com.day.cq.wcm.scripting.WCMBindingsConstants;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Function;
import com.google.common.collect.ImmutableMap;
import io.wcm.testing.mock.aem.junit.AemContext;
Expand All @@ -43,7 +47,7 @@ public class StoreConfigExporterTest {

private static final ValueMap MOCK_CONFIGURATION = new ValueMapDecorator(
ImmutableMap.of("magentoGraphqlEndpoint", "/my/magento/graphql", "magentoStore", "my-magento-store", "cq:graphqlClient",
"my-graphql-client"));
"my-graphql-client", "httpHeaders", new String[] { "customHeader-1=value1", "customHeader-2=value2" }));
private static final ComponentsConfiguration MOCK_CONFIGURATION_OBJECT = new ComponentsConfiguration(MOCK_CONFIGURATION);

@Rule
Expand Down Expand Up @@ -122,6 +126,19 @@ public void testGetStoreRootUrl() {
Assert.assertEquals("/content/pageB.html", storeConfigExporter.getStoreRootUrl());
}

@Test
public void testCustomHttpHeaders() throws IOException {
setupWithPage("/content/pageH", HttpMethod.POST);
StoreConfigExporterImpl storeConfigExporter = context.request().adaptTo(StoreConfigExporterImpl.class);
String expectedHeaders = "{\"Store\":\"my-magento-store\",\"customHeader-1\":\"value1\",\"customHeader-2\":\"value2\"}";

ObjectMapper mapper = new ObjectMapper();
JsonNode actualNode = mapper.readTree(storeConfigExporter.getHttpHeaders());
JsonNode expectedNode = mapper.readTree(expectedHeaders);

Assert.assertEquals("The custom HTTP headers are correctly parsed", expectedNode, actualNode);
}

private void setupWithPage(String pagePath, HttpMethod method) {
Page page = context.pageManager().getPage(pagePath);
SlingBindings slingBindings = (SlingBindings) context.request().getAttribute(SlingBindings.class.getName());
Expand Down
4 changes: 2 additions & 2 deletions react-components/src/components/App/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import useReferrerEvent from '../../utils/useReferrerEvent';
import usePageEvent from '../../utils/usePageEvent';

const App = props => {
const { graphqlEndpoint, storeView = 'default', graphqlMethod = 'POST' } = useConfigContext();
const { graphqlEndpoint, storeView = 'default', graphqlMethod = 'POST', headers = {} } = useConfigContext();
useCustomUrlEvent();
useReferrerEvent();
usePageEvent();
Expand All @@ -35,7 +35,7 @@ const App = props => {
graphqlAuthLink,
new HttpLink({
uri: graphqlEndpoint,
headers: { Store: storeView },
headers: { ...headers, Store: storeView },
useGETForQueries: graphqlMethod === 'GET',
fetch: compressQueryFetch
})
Expand Down
1 change: 1 addition & 0 deletions react-components/src/context/ConfigContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const ConfigContextProvider = props => {
ConfigContextProvider.propTypes = {
config: PropTypes.shape({
storeView: PropTypes.string.isRequired,
headers: PropTypes.object,
graphqlEndpoint: PropTypes.string.isRequired,
graphqlMethod: PropTypes.oneOf(['GET', 'POST']).isRequired,
mountingPoints: PropTypes.shape({
Expand Down
Loading