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

fix: [TRX-354] add proper composition for href URL https://github.com… #908

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading