Skip to content

Commit

Permalink
fix: [TRX-354] add proper composition for href URL #889
Browse files Browse the repository at this point in the history
  • Loading branch information
ds-jhartmann authored and ds-lcapellino committed Jan 14, 2025
1 parent c33c8c7 commit e5f4126
Show file tree
Hide file tree
Showing 6 changed files with 304 additions and 15 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@ _**For better traceability add the corresponding GitHub issue number in each cha
### Removed
- Removed subjectId from AssetAdministrationShellDescriptor object

### Fixed

- Fixed URI composition of href URL and configurable submodel suffix to append the path at the correct position #889

### Added

- Added api key authentication for edc notification requests

## [5.4.1] - 2024-08-19
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
********************************************************************************/
package org.eclipse.tractusx.irs.edc.client;

import java.net.URISyntaxException;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
Expand All @@ -37,6 +38,7 @@
import org.eclipse.tractusx.irs.edc.client.model.notification.EdcNotification;
import org.eclipse.tractusx.irs.edc.client.model.notification.EdcNotificationResponse;
import org.eclipse.tractusx.irs.edc.client.model.notification.NotificationContent;
import org.eclipse.tractusx.irs.edc.client.util.UriPathJoiner;

/**
* Public API Facade for submodel domain
Expand All @@ -56,7 +58,8 @@ public class EdcSubmodelFacade {
public SubmodelDescriptor getSubmodelPayload(final String connectorEndpoint, final String submodelDataplaneUrl,
final String assetId, final String bpn) throws EdcClientException {
try {
final String fullSubmodelDataplaneUrl = submodelDataplaneUrl + config.getSubmodel().getSubmodelSuffix();
final String fullSubmodelDataplaneUrl = getFullSubmodelDataplaneUrl(submodelDataplaneUrl);

log.debug("Requesting Submodel for URL: '{}'", fullSubmodelDataplaneUrl);
return client.getSubmodelPayload(connectorEndpoint, fullSubmodelDataplaneUrl, assetId, bpn)
.get(config.getAsyncTimeoutMillis(), TimeUnit.MILLISECONDS);
Expand All @@ -76,6 +79,14 @@ public SubmodelDescriptor getSubmodelPayload(final String connectorEndpoint, fin
}
}

private String getFullSubmodelDataplaneUrl(final String submodelDataplaneUrl) throws EdcClientException {
try {
return UriPathJoiner.appendPath(submodelDataplaneUrl, config.getSubmodel().getSubmodelSuffix());
} catch (URISyntaxException e) {
throw new EdcClientException("Invalid href URL '%s'".formatted(submodelDataplaneUrl), e);
}
}

@SuppressWarnings("PMD.PreserveStackTrace")
public EdcNotificationResponse sendNotification(final String submodelEndpointAddress, final String assetId,
final EdcNotification<NotificationContent> notification, final String bpn) throws EdcClientException {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/********************************************************************************
* Copyright (c) 2022,2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
* Copyright (c) 2021,2024 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Apache License, Version 2.0 which is available at
* https://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 CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* SPDX-License-Identifier: Apache-2.0
********************************************************************************/
package org.eclipse.tractusx.irs.edc.client.util;

import java.net.URI;
import java.net.URISyntaxException;

import lombok.AccessLevel;
import lombok.NoArgsConstructor;

/**
* A utility class for joining paths and appending paths to URLs.
* <p>This class provides methods for joining paths together and appending paths to URLs. The methods ensure that
* the resulting paths and URLs are correctly formatted and handle any necessary encoding or special characters.
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class UriPathJoiner {

/**
* Appends a given path to an existing URL.
* <p>This method takes a base URL and a path to append, and returns a new URL that is the result of appending the path
* to the base URL. The method ensures that the resulting URL is correctly formatted by joining the paths together and
* handling any necessary encoding or special characters.
*
* @param url The base URL to which the path will be appended.
* @param pathToAppend The path to append to the base URL.
* @return A new URL representing the base URL with the appended path.
* @throws URISyntaxException If the base URL or the appended path is not a valid URL.
*/
public static String appendPath(final String url, final String pathToAppend) throws URISyntaxException {
if (url == null || url.isEmpty()) {
throw new URISyntaxException(String.valueOf(url), "Base URL cannot be null or empty");
}

final URI uri = new URI(url);
final String pathWithAppendix = joinPaths(uri.getPath(), pathToAppend);
final URI uriWithAppendix = new URI(uri.getScheme(), uri.getUserInfo(), uri.getHost(), uri.getPort(),
pathWithAppendix, uri.getQuery(), uri.getFragment());

return uriWithAppendix.toString();
}

/**
* Joins two paths together, ensuring that the resulting path is correctly formatted.
* <p>This method takes two strings representing paths and returns a new string that is the result of joining
* them together. The paths are joined by appending the second path to the first, ensuring that there is exactly
* one forward slash ("/") between them. If either of the input paths is null or empty, the method returns the
* non-null, non-empty path.
*
* @param firstPath The first path to join.
* @param secondPath The second path to join.
* @return A new string representing the joined paths.
*/
public static String joinPaths(final String firstPath, final String secondPath) {
final String joinedPath;
if (firstPath == null || firstPath.isEmpty()) {
joinedPath = secondPath != null ? secondPath : "";
} else if (secondPath == null || secondPath.isEmpty()) {
joinedPath = firstPath;
} else {

final String trimmedFirstPath = firstPath.endsWith("/")
? firstPath.substring(0, firstPath.length() - 1)
: firstPath;
final String trimmedSecondPath = secondPath.startsWith("/") ? secondPath.substring(1) : secondPath;

joinedPath = trimmedFirstPath + "/" + trimmedSecondPath;
}
return joinedPath.startsWith("/") ? joinedPath : "/" + joinedPath;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import java.util.List;
Expand All @@ -45,22 +47,26 @@
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.mockito.Answers;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

@ExtendWith(MockitoExtension.class)
class EdcSubmodelFacadeTest {

private final static String CONNECTOR_ENDPOINT = "https://connector.endpoint.com";
private final static String SUBMODEL_SUFIX = "/shells/{aasIdentifier}/submodels/{submodelIdentifier}/submodel";
private final static String DATAPLANE_URL = "https://edc.dataplane.test/public/submodel";
private final static String ASSET_ID = "9300395e-c0a5-4e88-bc57-a3973fec4c26";

private EdcSubmodelFacade testee;

@Mock
private EdcSubmodelClient client;

private final EdcConfiguration config = new EdcConfiguration();
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private EdcConfiguration config;

@BeforeEach
public void beforeEach() {
Expand All @@ -79,8 +85,8 @@ void shouldThrowExecutionExceptionForSubmodel() throws EdcClientException {
when(client.getSubmodelPayload(any(), any(), any(), any())).thenReturn(future);

// act
ThrowableAssert.ThrowingCallable action = () -> testee.getSubmodelPayload(CONNECTOR_ENDPOINT,
SUBMODEL_SUFIX, ASSET_ID, "bpn");
ThrowableAssert.ThrowingCallable action = () -> testee.getSubmodelPayload(CONNECTOR_ENDPOINT, DATAPLANE_URL,
ASSET_ID, "bpn");

// assert
assertThatThrownBy(action).isInstanceOf(EdcClientException.class);
Expand All @@ -93,8 +99,8 @@ void shouldThrowEdcClientExceptionForSubmodel() throws EdcClientException {
when(client.getSubmodelPayload(any(), any(), any(), any())).thenThrow(e);

// act
ThrowableAssert.ThrowingCallable action = () -> testee.getSubmodelPayload(CONNECTOR_ENDPOINT,
SUBMODEL_SUFIX, ASSET_ID, "bpn");
ThrowableAssert.ThrowingCallable action = () -> testee.getSubmodelPayload(CONNECTOR_ENDPOINT, DATAPLANE_URL,
ASSET_ID, "bpn");

// assert
assertThatThrownBy(action).isInstanceOf(EdcClientException.class);
Expand All @@ -106,16 +112,73 @@ void shouldRestoreInterruptOnInterruptExceptionForSubmodel()
// arrange
final CompletableFuture<SubmodelDescriptor> future = mock(CompletableFuture.class);
final InterruptedException e = new InterruptedException();
when(config.getAsyncTimeoutMillis()).thenReturn(1000L);
when(future.get(config.getAsyncTimeoutMillis(), TimeUnit.MILLISECONDS)).thenThrow(e);
when(client.getSubmodelPayload(any(), any(), any(), any())).thenReturn(future);

// act
testee.getSubmodelPayload(CONNECTOR_ENDPOINT, SUBMODEL_SUFIX, ASSET_ID, "bpn");
testee.getSubmodelPayload(CONNECTOR_ENDPOINT, DATAPLANE_URL, ASSET_ID, "bpn");

// assert
assertThat(Thread.currentThread().isInterrupted()).isTrue();
}

@Test
void shouldAppendSubmodelSuffixPathCorrectly() throws EdcClientException {
// Arrange
final String connectorEndpoint = "https://connector.endpoint.com";
final String dataplaneUrl = "https://edc.dataplane.test/public/submodel?content=value&extent=withBlobValue";
final CompletableFuture<SubmodelDescriptor> future = mock(CompletableFuture.class);
when(client.getSubmodelPayload(any(), any(), any(), any())).thenReturn(future);
when(config.getSubmodel().getSubmodelSuffix()).thenReturn("/$value");
when(config.getAsyncTimeoutMillis()).thenReturn(1000L);

// Act
testee.getSubmodelPayload(connectorEndpoint, dataplaneUrl, ASSET_ID, "bpn");

// Assert
final String expectedDataplaneUrl = "https://edc.dataplane.test/public/submodel/$value?content=value&extent=withBlobValue";
verify(client, times(1)).getSubmodelPayload(connectorEndpoint, expectedDataplaneUrl, ASSET_ID, "bpn");
}

@ParameterizedTest
@CsvSource(
{ "'https://edc.dataplane.test/public/submodel', '/$value', 'https://edc.dataplane.test/public/submodel/$value'",
"'https://edc.dataplane.test/public/submodel', '$value', 'https://edc.dataplane.test/public/submodel/$value'",
"'https://edc.dataplane.test/public/submodel/', '/$value', 'https://edc.dataplane.test/public/submodel/$value'",
"'https://edc.dataplane.test/public/submodel/', '$value', 'https://edc.dataplane.test/public/submodel/$value'",
"'https://edc.test/submodel?content=value&extent=withBlobValue', '/$value', 'https://edc.test/submodel/$value?content=value&extent=withBlobValue'",
"'https://edc.test/submodel?content=value&extent=withBlobValue', '$value', 'https://edc.test/submodel/$value?content=value&extent=withBlobValue'",
"'https://edc.test/submodel/?content=value&extent=withBlobValue', '/$value', 'https://edc.test/submodel/$value?content=value&extent=withBlobValue'",
"'https://edc.test/submodel/?content=value&extent=withBlobValue', '$value', 'https://edc.test/submodel/$value?content=value&extent=withBlobValue'"
})
void shouldAppendSubmodelSuffixPathCorrectly(final String dataplaneUrlFromHref, final String submodelSuffix,
final String expectedDataplaneUrl) throws EdcClientException {
// Arrange
final String connectorEndpoint = "https://connector.endpoint.com";
final CompletableFuture<SubmodelDescriptor> future = mock(CompletableFuture.class);
when(client.getSubmodelPayload(any(), any(), any(), any())).thenReturn(future);
when(config.getSubmodel().getSubmodelSuffix()).thenReturn(submodelSuffix);
when(config.getAsyncTimeoutMillis()).thenReturn(1000L);

// Act
testee.getSubmodelPayload(connectorEndpoint, dataplaneUrlFromHref, ASSET_ID, "bpn");

// Assert
verify(client, times(1)).getSubmodelPayload(connectorEndpoint, expectedDataplaneUrl, ASSET_ID, "bpn");
}

@Test
void shouldThrowEdcClientExceptionForInvalidHrefUrl() {
// Arrange
final String connectorEndpoint = "https://connector.endpoint.com";
final String invalidDataplaneUrl = "://example.com";

// Act & Assert
assertThatThrownBy(() -> testee.getSubmodelPayload(connectorEndpoint, invalidDataplaneUrl, ASSET_ID,
"bpn")).isInstanceOf(EdcClientException.class)
.hasMessage("Invalid href URL '%s'".formatted(invalidDataplaneUrl));
}
}

@Nested
Expand All @@ -128,6 +191,7 @@ void shouldRestoreInterruptOnInterruptExceptionForNotification()
// arrange
final CompletableFuture<EdcNotificationResponse> future = mock(CompletableFuture.class);
final InterruptedException e = new InterruptedException();
when(config.getAsyncTimeoutMillis()).thenReturn(1000L);
when(future.get(config.getAsyncTimeoutMillis(), TimeUnit.MILLISECONDS)).thenThrow(e);
when(client.sendNotification(any(), any(), any(), any())).thenReturn(future);

Expand All @@ -146,7 +210,8 @@ void shouldThrowExecutionExceptionForNotification() throws EdcClientException {
when(client.sendNotification(any(), any(), any(), any())).thenReturn(future);

// act
ThrowableAssert.ThrowingCallable action = () -> testee.sendNotification("", "notify-request-asset", null, "bpn");
ThrowableAssert.ThrowingCallable action = () -> testee.sendNotification("", "notify-request-asset", null,
"bpn");

// assert
assertThatThrownBy(action).isInstanceOf(EdcClientException.class);
Expand All @@ -159,7 +224,8 @@ void shouldThrowEdcClientExceptionForNotification() throws EdcClientException {
when(client.sendNotification(any(), any(), any(), any())).thenThrow(e);

// act
ThrowableAssert.ThrowingCallable action = () -> testee.sendNotification("", "notify-request-asset", null, "bpn");
ThrowableAssert.ThrowingCallable action = () -> testee.sendNotification("", "notify-request-asset", null,
"bpn");

// assert
assertThatThrownBy(action).isInstanceOf(EdcClientException.class);
Expand Down Expand Up @@ -191,7 +257,8 @@ void shouldReturnFailedFuture() throws EdcClientException {
List.of(CompletableFuture.failedFuture(new EdcClientException("test"))));

// act
final List<CompletableFuture<EndpointDataReference>> results = testee.getEndpointReferencesForRegistryAsset("", "");
final List<CompletableFuture<EndpointDataReference>> results = testee.getEndpointReferencesForRegistryAsset(
"", "");

// assert
assertThat(results).hasSize(1);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
@ExtendWith(MockitoExtension.class)
class SubmodelExponentialRetryTest {

public static final String SUBMODEL_DATAPLANE_URL = "https://edc.dataplane.test/public/submodel";
private final RetryRegistry retryRegistry = new InMemoryRetryRegistry();
@Mock
private RestTemplate restTemplate;
Expand Down Expand Up @@ -98,8 +99,7 @@ void shouldRetryExecutionOfGetSubmodelOnClientMaxAttemptTimes() {
new EndpointDataReferenceStatus(null, EndpointDataReferenceStatus.TokenStatus.REQUIRED_NEW));

// Act
assertThatThrownBy(() -> testee.getSubmodelPayload("https://connector.endpoint.com",
"/shells/{aasIdentifier}/submodels/{submodelIdentifier}/submodel",
assertThatThrownBy(() -> testee.getSubmodelPayload("https://connector.endpoint.com", SUBMODEL_DATAPLANE_URL,
"9300395e-c0a5-4e88-bc57-a3973fec4c26", "bpn")).hasCauseInstanceOf(HttpServerErrorException.class);

// Assert
Expand All @@ -117,8 +117,7 @@ void shouldRetryOnAnyRuntimeException() {
new EndpointDataReferenceStatus(null, EndpointDataReferenceStatus.TokenStatus.REQUIRED_NEW));

// Act
assertThatThrownBy(() -> testee.getSubmodelPayload("https://connector.endpoint.com",
"/shells/{aasIdentifier}/submodels/{submodelIdentifier}/submodel",
assertThatThrownBy(() -> testee.getSubmodelPayload("https://connector.endpoint.com", SUBMODEL_DATAPLANE_URL,
"9300395e-c0a5-4e88-bc57-a3973fec4c26", "bpn")).hasCauseInstanceOf(RuntimeException.class);

// Assert
Expand Down
Loading

0 comments on commit e5f4126

Please sign in to comment.