From 1e7f71eb75d437fa454536f323e9b6a50dbae4d2 Mon Sep 17 00:00:00 2001 From: Julien Giovaresco Date: Thu, 28 Mar 2024 09:00:30 +0100 Subject: [PATCH] feat: implement DiscoverIntegrationAssetUseCase Implement business rules for the Federation Discovery feature. It is currently not "callable" as we don't have any endpoint on the REST API or an actual implementation of IntegrationAgent. However, it should contain everything necessary for the creation of the new type of API https://gravitee.atlassian.net/browse/APIM-4204 --- .../definition/model/DefinitionVersion.java | 3 +- .../CreateApiDomainService.java | 50 +- .../ValidateFederatedApiDomainService.java | 37 ++ .../IntegrationNotFoundException.java | 25 + .../apim/core/integration/model/Asset.java | 21 + .../core/integration/model/Integration.java | 3 + .../integration/spi/IntegrationAgent.java | 24 + .../DiscoverIntegrationAssetUseCase.java | 111 +++++ .../java/fixtures/core/model/ApiFixtures.java | 10 + .../core/model/IntegrationAssetFixtures.java | 37 ++ .../inmemory/IntegrationAgentInMemory.java | 49 ++ ...ValidateFederatedApiDomainServiceTest.java | 60 +++ .../DiscoverIntegrationAssetUseCaseTest.java | 461 ++++++++++++++++++ .../main/java/fixtures/ApiModelFixtures.java | 12 + 14 files changed, 890 insertions(+), 13 deletions(-) create mode 100644 gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/api/domain_service/ValidateFederatedApiDomainService.java create mode 100644 gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/integration/exception/IntegrationNotFoundException.java create mode 100644 gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/integration/model/Asset.java create mode 100644 gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/integration/spi/IntegrationAgent.java create mode 100644 gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/integration/use_case/DiscoverIntegrationAssetUseCase.java create mode 100644 gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/fixtures/core/model/IntegrationAssetFixtures.java create mode 100644 gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/inmemory/IntegrationAgentInMemory.java create mode 100644 gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/apim/core/api/domain_service/ValidateFederatedApiDomainServiceTest.java create mode 100644 gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/apim/core/integration/use_case/DiscoverIntegrationAssetUseCaseTest.java diff --git a/gravitee-apim-definition/gravitee-apim-definition-model/src/main/java/io/gravitee/definition/model/DefinitionVersion.java b/gravitee-apim-definition/gravitee-apim-definition-model/src/main/java/io/gravitee/definition/model/DefinitionVersion.java index 0320e2a1029..dcad7856d2f 100644 --- a/gravitee-apim-definition/gravitee-apim-definition-model/src/main/java/io/gravitee/definition/model/DefinitionVersion.java +++ b/gravitee-apim-definition/gravitee-apim-definition-model/src/main/java/io/gravitee/definition/model/DefinitionVersion.java @@ -32,7 +32,8 @@ public enum DefinitionVersion { @JsonEnumDefaultValue V1("1.0.0"), V2("2.0.0"), - V4("4.0.0"); + V4("4.0.0"), + FEDERATED("FEDERATED"); private static final Map BY_LABEL = new HashMap<>(); diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/api/domain_service/CreateApiDomainService.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/api/domain_service/CreateApiDomainService.java index 861f4edea8f..50438445135 100644 --- a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/api/domain_service/CreateApiDomainService.java +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/api/domain_service/CreateApiDomainService.java @@ -34,9 +34,11 @@ import io.gravitee.apim.core.parameters.query_service.ParametersQueryService; import io.gravitee.apim.core.search.Indexer; import io.gravitee.apim.core.workflow.crud_service.WorkflowCrudService; +import io.gravitee.definition.model.v4.flow.Flow; import io.gravitee.rest.api.model.parameters.Key; import io.gravitee.rest.api.model.parameters.ParameterReferenceType; import java.util.Collections; +import java.util.List; import java.util.function.UnaryOperator; @DomainService @@ -98,13 +100,13 @@ public ApiWithFlows create(Api api, PrimaryOwnerEntity primaryOwner, AuditInfo a apiPrimaryOwnerDomainService.createApiPrimaryOwnerMembership(created.getId(), primaryOwner, auditInfo); - createDefaultMailNotification(created.getId()); + createDefaultMailNotification(created); - apiMetadataDomainService.createDefaultApiMetadata(created.getId(), auditInfo); + createDefaultMetadata(created, auditInfo); - flowCrudService.saveApiFlows(api.getId(), api.getApiDefinitionV4().getFlows()); + var createdFlows = saveApiFlows(api); - if (isApiReviewEnabled(auditInfo.organizationId(), auditInfo.environmentId())) { + if (isApiReviewEnabled(created, auditInfo.organizationId(), auditInfo.environmentId())) { workflowCrudService.create(newApiReviewWorkflow(api.getId(), auditInfo.actor().userId())); } @@ -113,7 +115,7 @@ public ApiWithFlows create(Api api, PrimaryOwnerEntity primaryOwner, AuditInfo a created, primaryOwner ); - return new ApiWithFlows(created, api.getApiDefinitionV4().getFlows()); + return new ApiWithFlows(created, createdFlows); } private void createAuditLog(Api created, AuditInfo auditInfo) { @@ -132,14 +134,38 @@ private void createAuditLog(Api created, AuditInfo auditInfo) { ); } - private void createDefaultMailNotification(String apiId) { - notificationConfigCrudService.create(NotificationConfig.defaultMailNotificationConfigFor(apiId)); + private void createDefaultMailNotification(Api api) { + switch (api.getDefinitionVersion()) { + case V4 -> notificationConfigCrudService.create(NotificationConfig.defaultMailNotificationConfigFor(api.getId())); + case V1, V2, FEDERATED -> { + // nothing to do + } + } } - private boolean isApiReviewEnabled(String organizationId, String environmentId) { - return parametersQueryService.findAsBoolean( - Key.API_REVIEW_ENABLED, - new ParameterContext(environmentId, organizationId, ParameterReferenceType.ENVIRONMENT) - ); + private void createDefaultMetadata(Api api, AuditInfo auditInfo) { + switch (api.getDefinitionVersion()) { + case V4 -> apiMetadataDomainService.createDefaultApiMetadata(api.getId(), auditInfo); + case V1, V2, FEDERATED -> { + // nothing to do + } + } + } + + private List saveApiFlows(Api api) { + return switch (api.getDefinitionVersion()) { + case V4 -> flowCrudService.saveApiFlows(api.getId(), api.getApiDefinitionV4().getFlows()); + case V1, V2, FEDERATED -> null; + }; + } + + private boolean isApiReviewEnabled(Api api, String organizationId, String environmentId) { + return switch (api.getDefinitionVersion()) { + case V1, V2, V4 -> parametersQueryService.findAsBoolean( + Key.API_REVIEW_ENABLED, + new ParameterContext(environmentId, organizationId, ParameterReferenceType.ENVIRONMENT) + ); + case FEDERATED -> false; + }; } } diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/api/domain_service/ValidateFederatedApiDomainService.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/api/domain_service/ValidateFederatedApiDomainService.java new file mode 100644 index 00000000000..7082af5e004 --- /dev/null +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/api/domain_service/ValidateFederatedApiDomainService.java @@ -0,0 +1,37 @@ +/* + * Copyright © 2015 The Gravitee team (http://gravitee.io) + * + * Licensed 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 CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.gravitee.apim.core.api.domain_service; + +import io.gravitee.apim.core.DomainService; +import io.gravitee.apim.core.api.model.Api; +import io.gravitee.apim.core.exception.ValidationDomainException; +import io.gravitee.definition.model.DefinitionVersion; +import io.gravitee.rest.api.service.exceptions.InvalidDataException; + +@DomainService +public class ValidateFederatedApiDomainService { + + public Api validateAndSanitizeForCreation(final Api api) { + if (api.getDefinitionVersion() != DefinitionVersion.FEDERATED) { + throw new ValidationDomainException("Definition version is unsupported, should be FEDERATED"); + } + + // Reset lifecycle state as Federated API are not deployed on Gateway + api.setLifecycleState(null); + + return api; + } +} diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/integration/exception/IntegrationNotFoundException.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/integration/exception/IntegrationNotFoundException.java new file mode 100644 index 00000000000..b26c5f45089 --- /dev/null +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/integration/exception/IntegrationNotFoundException.java @@ -0,0 +1,25 @@ +/* + * Copyright © 2015 The Gravitee team (http://gravitee.io) + * + * Licensed 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 CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.gravitee.apim.core.integration.exception; + +import io.gravitee.apim.core.exception.NotFoundDomainException; + +public class IntegrationNotFoundException extends NotFoundDomainException { + + public IntegrationNotFoundException(String id) { + super("Integration not found.", id); + } +} diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/integration/model/Asset.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/integration/model/Asset.java new file mode 100644 index 00000000000..26f798a7fd4 --- /dev/null +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/integration/model/Asset.java @@ -0,0 +1,21 @@ +/* + * Copyright © 2015 The Gravitee team (http://gravitee.io) + * + * Licensed 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 CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.gravitee.apim.core.integration.model; + +import lombok.Builder; + +@Builder(toBuilder = true) +public record Asset(String integrationId, String id, String name, String description, String version) {} diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/integration/model/Integration.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/integration/model/Integration.java index 938f4e2c192..d0f7d3a0c67 100644 --- a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/integration/model/Integration.java +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/integration/model/Integration.java @@ -20,6 +20,7 @@ import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import lombok.With; /** * @author Remi Baptiste (remi.baptiste at graviteesource.com) @@ -31,7 +32,9 @@ @AllArgsConstructor public class Integration { + @With String id; + String name; String description; String provider; diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/integration/spi/IntegrationAgent.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/integration/spi/IntegrationAgent.java new file mode 100644 index 00000000000..77040b4a8a3 --- /dev/null +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/integration/spi/IntegrationAgent.java @@ -0,0 +1,24 @@ +/* + * Copyright © 2015 The Gravitee team (http://gravitee.io) + * + * Licensed 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 CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.gravitee.apim.core.integration.spi; + +import io.gravitee.apim.core.integration.model.Asset; +import io.gravitee.apim.core.integration.model.Integration; +import io.reactivex.rxjava3.core.Flowable; + +public interface IntegrationAgent { + Flowable fetchAllAssets(Integration integration); +} diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/integration/use_case/DiscoverIntegrationAssetUseCase.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/integration/use_case/DiscoverIntegrationAssetUseCase.java new file mode 100644 index 00000000000..7b80dbccaf1 --- /dev/null +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/integration/use_case/DiscoverIntegrationAssetUseCase.java @@ -0,0 +1,111 @@ +/* + * Copyright © 2015 The Gravitee team (http://gravitee.io) + * + * Licensed 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 CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.gravitee.apim.core.integration.use_case; + +import io.gravitee.apim.core.UseCase; +import io.gravitee.apim.core.api.domain_service.CreateApiDomainService; +import io.gravitee.apim.core.api.domain_service.ValidateFederatedApiDomainService; +import io.gravitee.apim.core.api.model.Api; +import io.gravitee.apim.core.audit.model.AuditInfo; +import io.gravitee.apim.core.integration.crud_service.IntegrationCrudService; +import io.gravitee.apim.core.integration.exception.IntegrationNotFoundException; +import io.gravitee.apim.core.integration.model.Asset; +import io.gravitee.apim.core.integration.model.Integration; +import io.gravitee.apim.core.integration.spi.IntegrationAgent; +import io.gravitee.apim.core.membership.domain_service.ApiPrimaryOwnerFactory; +import io.gravitee.common.utils.TimeProvider; +import io.gravitee.definition.model.DefinitionVersion; +import io.gravitee.rest.api.service.common.UuidString; +import io.reactivex.rxjava3.core.Completable; +import io.reactivex.rxjava3.core.Single; +import lombok.extern.slf4j.Slf4j; + +@UseCase +@Slf4j +public class DiscoverIntegrationAssetUseCase { + + private final IntegrationCrudService integrationCrudService; + private final ApiPrimaryOwnerFactory apiPrimaryOwnerFactory; + private final ValidateFederatedApiDomainService validateFederatedApi; + private final CreateApiDomainService createApiDomainService; + private final IntegrationAgent integrationAgent; + + public DiscoverIntegrationAssetUseCase( + IntegrationCrudService integrationCrudService, + ApiPrimaryOwnerFactory apiPrimaryOwnerFactory, + ValidateFederatedApiDomainService validateFederatedApi, + CreateApiDomainService apiCrudService, + IntegrationAgent integrationAgent + ) { + this.integrationCrudService = integrationCrudService; + this.apiPrimaryOwnerFactory = apiPrimaryOwnerFactory; + this.validateFederatedApi = validateFederatedApi; + this.createApiDomainService = apiCrudService; + this.integrationAgent = integrationAgent; + } + + public Completable execute(Input input) { + var integrationId = input.integrationId; + var auditInfo = input.auditInfo; + var organizationId = auditInfo.organizationId(); + var environmentId = auditInfo.environmentId(); + + var primaryOwner = apiPrimaryOwnerFactory.createForNewApi(organizationId, environmentId, input.auditInfo.actor().userId()); + + return Single + .fromCallable(() -> + integrationCrudService + .findById(integrationId) + .filter(integration -> integration.getEnvironmentId().equals(environmentId)) + .orElseThrow(() -> new IntegrationNotFoundException(integrationId)) + ) + .flatMapPublisher(integration -> + integrationAgent + .fetchAllAssets(integration) + .doOnNext(asset -> { + try { + createApiDomainService.create( + adaptAssetToApi(asset, integration), + primaryOwner, + auditInfo, + validateFederatedApi::validateAndSanitizeForCreation + ); + } catch (Exception e) { + log.warn("An error occurred while importing asset {}", asset, e); + } + }) + ) + .ignoreElements(); + } + + public record Input(String integrationId, AuditInfo auditInfo) {} + + public Api adaptAssetToApi(Asset asset, Integration integration) { + var now = TimeProvider.now(); + return Api + .builder() + .id(UuidString.generateRandom()) + .version(asset.version()) + .definitionVersion(DefinitionVersion.FEDERATED) + .name(asset.name()) + .description(asset.description()) + .createdAt(now) + .updatedAt(now) + .environmentId(integration.getEnvironmentId()) + .lifecycleState(null) + .build(); + } +} diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/fixtures/core/model/ApiFixtures.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/fixtures/core/model/ApiFixtures.java index 80067dc2e96..ae68a7d0c44 100644 --- a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/fixtures/core/model/ApiFixtures.java +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/fixtures/core/model/ApiFixtures.java @@ -265,4 +265,14 @@ public static Api aTcpApiV4(List hosts) { ) .build(); } + + public static Api aFederatedApi() { + return BASE + .get() + .definitionVersion(DefinitionVersion.FEDERATED) + .lifecycleState(null) + .apiDefinitionV4(null) + .apiDefinition(null) + .build(); + } } diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/fixtures/core/model/IntegrationAssetFixtures.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/fixtures/core/model/IntegrationAssetFixtures.java new file mode 100644 index 00000000000..07335e36a77 --- /dev/null +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/fixtures/core/model/IntegrationAssetFixtures.java @@ -0,0 +1,37 @@ +/* + * Copyright © 2015 The Gravitee team (http://gravitee.io) + * + * Licensed 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 CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package fixtures.core.model; + +import io.gravitee.apim.core.integration.model.Asset; +import java.util.function.Supplier; + +public class IntegrationAssetFixtures { + + private IntegrationAssetFixtures() {} + + private static final Supplier BASE = () -> + Asset + .builder() + .integrationId("integration-id") + .id("asset-id") + .name("An alien API") + .description("An alien API description") + .version("1.0.0"); + + public static Asset anAssetForIntegration(String integrationId) { + return BASE.get().integrationId(integrationId).build(); + } +} diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/inmemory/IntegrationAgentInMemory.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/inmemory/IntegrationAgentInMemory.java new file mode 100644 index 00000000000..d9bbf2d2b7f --- /dev/null +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/inmemory/IntegrationAgentInMemory.java @@ -0,0 +1,49 @@ +/* + * Copyright © 2015 The Gravitee team (http://gravitee.io) + * + * Licensed 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 CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package inmemory; + +import io.gravitee.apim.core.integration.model.Asset; +import io.gravitee.apim.core.integration.model.Integration; +import io.gravitee.apim.core.integration.spi.IntegrationAgent; +import io.reactivex.rxjava3.core.Flowable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class IntegrationAgentInMemory implements IntegrationAgent, InMemoryAlternative { + + List storage = new ArrayList<>(); + + @Override + public Flowable fetchAllAssets(Integration integration) { + return Flowable.fromIterable(storage).filter(asset -> asset.integrationId().equals(integration.getId())); + } + + @Override + public void initWith(List items) { + this.storage.addAll(items); + } + + @Override + public void reset() { + this.storage.clear(); + } + + @Override + public List storage() { + return Collections.unmodifiableList(storage); + } +} diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/apim/core/api/domain_service/ValidateFederatedApiDomainServiceTest.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/apim/core/api/domain_service/ValidateFederatedApiDomainServiceTest.java new file mode 100644 index 00000000000..1d8feb4bb89 --- /dev/null +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/apim/core/api/domain_service/ValidateFederatedApiDomainServiceTest.java @@ -0,0 +1,60 @@ +/* + * Copyright © 2015 The Gravitee team (http://gravitee.io) + * + * Licensed 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 CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.gravitee.apim.core.api.domain_service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; + +import fixtures.core.model.ApiFixtures; +import io.gravitee.apim.core.api.model.Api; +import io.gravitee.apim.core.exception.ValidationDomainException; +import io.gravitee.definition.model.DefinitionVersion; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +class ValidateFederatedApiDomainServiceTest { + + ValidateFederatedApiDomainService service = new ValidateFederatedApiDomainService(); + + @Test + void should_return_the_api_when_valid() { + var api = ApiFixtures.aFederatedApi(); + + var result = service.validateAndSanitizeForCreation(api); + + assertThat(result).isSameAs(api); + } + + @Test + void should_reset_lifecycle_state_when_defined() { + var api = ApiFixtures.aFederatedApi().toBuilder().lifecycleState(Api.LifecycleState.STARTED).build(); + + var result = service.validateAndSanitizeForCreation(api); + + assertThat(result).extracting(Api::getLifecycleState).isNull(); + } + + @ParameterizedTest + @EnumSource(value = DefinitionVersion.class, mode = EnumSource.Mode.EXCLUDE, names = { "FEDERATED" }) + void should_throw_when_definition_version_is_incorrect(DefinitionVersion definitionVersion) { + var api = ApiFixtures.aFederatedApi().toBuilder().definitionVersion(definitionVersion).build(); + + var throwable = catchThrowable(() -> service.validateAndSanitizeForCreation(api)); + + assertThat(throwable).isInstanceOf(ValidationDomainException.class); + } +} diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/apim/core/integration/use_case/DiscoverIntegrationAssetUseCaseTest.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/apim/core/integration/use_case/DiscoverIntegrationAssetUseCaseTest.java new file mode 100644 index 00000000000..949039474d8 --- /dev/null +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/apim/core/integration/use_case/DiscoverIntegrationAssetUseCaseTest.java @@ -0,0 +1,461 @@ +/* + * Copyright © 2015 The Gravitee team (http://gravitee.io) + * + * Licensed 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 CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.gravitee.apim.core.integration.use_case; + +import static fixtures.core.model.RoleFixtures.apiPrimaryOwnerRoleId; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import fixtures.core.model.AuditInfoFixtures; +import fixtures.core.model.IntegrationAssetFixtures; +import fixtures.core.model.IntegrationFixture; +import inmemory.ApiCrudServiceInMemory; +import inmemory.ApiMetadataQueryServiceInMemory; +import inmemory.AuditCrudServiceInMemory; +import inmemory.FlowCrudServiceInMemory; +import inmemory.GroupQueryServiceInMemory; +import inmemory.InMemoryAlternative; +import inmemory.IndexerInMemory; +import inmemory.IntegrationAgentInMemory; +import inmemory.IntegrationCrudServiceInMemory; +import inmemory.MembershipCrudServiceInMemory; +import inmemory.MembershipQueryServiceInMemory; +import inmemory.MetadataCrudServiceInMemory; +import inmemory.NotificationConfigCrudServiceInMemory; +import inmemory.ParametersQueryServiceInMemory; +import inmemory.RoleQueryServiceInMemory; +import inmemory.UserCrudServiceInMemory; +import inmemory.WorkflowCrudServiceInMemory; +import io.gravitee.apim.core.api.domain_service.ApiIndexerDomainService; +import io.gravitee.apim.core.api.domain_service.ApiMetadataDecoderDomainService; +import io.gravitee.apim.core.api.domain_service.ApiMetadataDomainService; +import io.gravitee.apim.core.api.domain_service.CreateApiDomainService; +import io.gravitee.apim.core.api.domain_service.ValidateFederatedApiDomainService; +import io.gravitee.apim.core.api.model.Api; +import io.gravitee.apim.core.audit.domain_service.AuditDomainService; +import io.gravitee.apim.core.audit.model.AuditEntity; +import io.gravitee.apim.core.audit.model.AuditInfo; +import io.gravitee.apim.core.audit.model.event.ApiAuditEvent; +import io.gravitee.apim.core.audit.model.event.MembershipAuditEvent; +import io.gravitee.apim.core.exception.ValidationDomainException; +import io.gravitee.apim.core.integration.exception.IntegrationNotFoundException; +import io.gravitee.apim.core.integration.model.Asset; +import io.gravitee.apim.core.integration.model.Integration; +import io.gravitee.apim.core.membership.domain_service.ApiPrimaryOwnerDomainService; +import io.gravitee.apim.core.membership.domain_service.ApiPrimaryOwnerFactory; +import io.gravitee.apim.core.membership.model.Membership; +import io.gravitee.apim.core.membership.model.PrimaryOwnerEntity; +import io.gravitee.apim.core.policy.domain_service.PolicyValidationDomainService; +import io.gravitee.apim.core.search.model.IndexableApi; +import io.gravitee.apim.core.user.model.BaseUserEntity; +import io.gravitee.apim.infra.json.jackson.JacksonJsonDiffProcessor; +import io.gravitee.apim.infra.template.FreemarkerTemplateProcessor; +import io.gravitee.common.utils.TimeProvider; +import io.gravitee.definition.model.DefinitionVersion; +import io.gravitee.repository.management.model.Parameter; +import io.gravitee.repository.management.model.ParameterReferenceType; +import io.gravitee.rest.api.model.parameters.Key; +import io.gravitee.rest.api.model.settings.ApiPrimaryOwnerMode; +import io.gravitee.rest.api.service.common.UuidString; +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneId; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; +import org.assertj.core.api.Assertions; +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +class DiscoverIntegrationAssetUseCaseTest { + + private static final Instant INSTANT_NOW = Instant.parse("2023-10-22T10:15:30Z"); + private static final String INTEGRATION_ID = "integration-id"; + private static final String ORGANIZATION_ID = "organization-id"; + private static final String ENVIRONMENT_ID = "environment-id"; + private static final String USER_ID = "user-id"; + + private static final AuditInfo AUDIT_INFO = AuditInfoFixtures.anAuditInfo(ORGANIZATION_ID, ENVIRONMENT_ID, USER_ID); + + PolicyValidationDomainService policyValidationDomainService = mock(PolicyValidationDomainService.class); + ApiCrudServiceInMemory apiCrudService = new ApiCrudServiceInMemory(); + AuditCrudServiceInMemory auditCrudService = new AuditCrudServiceInMemory(); + GroupQueryServiceInMemory groupQueryService = new GroupQueryServiceInMemory(); + IntegrationCrudServiceInMemory integrationCrudService = new IntegrationCrudServiceInMemory(); + MembershipCrudServiceInMemory membershipCrudService = new MembershipCrudServiceInMemory(); + MetadataCrudServiceInMemory metadataCrudService = new MetadataCrudServiceInMemory(); + NotificationConfigCrudServiceInMemory notificationConfigCrudService = new NotificationConfigCrudServiceInMemory(); + ParametersQueryServiceInMemory parametersQueryService = new ParametersQueryServiceInMemory(); + RoleQueryServiceInMemory roleQueryService = new RoleQueryServiceInMemory(); + UserCrudServiceInMemory userCrudService = new UserCrudServiceInMemory(); + WorkflowCrudServiceInMemory workflowCrudService = new WorkflowCrudServiceInMemory(); + + IntegrationAgentInMemory integrationAgent = new IntegrationAgentInMemory(); + IndexerInMemory indexer = new IndexerInMemory(); + + ValidateFederatedApiDomainService validateFederatedApiDomainService = spy(new ValidateFederatedApiDomainService()); + DiscoverIntegrationAssetUseCase useCase; + + @BeforeAll + static void beforeAll() { + UuidString.overrideGenerator(() -> "generated-id"); + TimeProvider.overrideClock(Clock.fixed(INSTANT_NOW, ZoneId.systemDefault())); + } + + @AfterAll + static void afterAll() { + UuidString.reset(); + TimeProvider.overrideClock(Clock.systemDefaultZone()); + } + + @BeforeEach + void setUp() { + var auditDomainService = new AuditDomainService(auditCrudService, userCrudService, new JacksonJsonDiffProcessor()); + + var metadataQueryService = new ApiMetadataQueryServiceInMemory(metadataCrudService); + var membershipQueryService = new MembershipQueryServiceInMemory(membershipCrudService); + var apiPrimaryOwnerFactory = new ApiPrimaryOwnerFactory( + groupQueryService, + membershipQueryService, + parametersQueryService, + roleQueryService, + userCrudService + ); + + var createApiDomainService = new CreateApiDomainService( + apiCrudService, + auditDomainService, + new ApiIndexerDomainService( + new ApiMetadataDecoderDomainService(metadataQueryService, new FreemarkerTemplateProcessor()), + indexer + ), + new ApiMetadataDomainService(metadataCrudService, auditDomainService), + new ApiPrimaryOwnerDomainService( + auditDomainService, + groupQueryService, + membershipCrudService, + membershipQueryService, + roleQueryService, + userCrudService + ), + new FlowCrudServiceInMemory(), + notificationConfigCrudService, + parametersQueryService, + workflowCrudService + ); + + useCase = + new DiscoverIntegrationAssetUseCase( + integrationCrudService, + apiPrimaryOwnerFactory, + validateFederatedApiDomainService, + createApiDomainService, + integrationAgent + ); + + enableApiPrimaryOwnerMode(ApiPrimaryOwnerMode.USER); + when(policyValidationDomainService.validateAndSanitizeConfiguration(any(), any())) + .thenAnswer(invocation -> invocation.getArgument(1)); + + roleQueryService.resetSystemRoles(ORGANIZATION_ID); + givenExistingUsers( + List.of(BaseUserEntity.builder().id(USER_ID).firstname("Jane").lastname("Doe").email("jane.doe@gravitee.io").build()) + ); + } + + @AfterEach + void tearDown() { + Stream + .of( + apiCrudService, + auditCrudService, + groupQueryService, + integrationCrudService, + membershipCrudService, + parametersQueryService, + userCrudService + ) + .forEach(InMemoryAlternative::reset); + } + + @Nested + class FederatedApiCreation { + + @Test + void should_create_and_index_a_federated_api() { + // Given + givenAnIntegration(IntegrationFixture.anIntegration(ENVIRONMENT_ID).withId(INTEGRATION_ID)); + givenAssets( + IntegrationAssetFixtures + .anAssetForIntegration(INTEGRATION_ID) + .toBuilder() + .id("asset-1") + .name("api-1") + .description("my description") + .version("1.1.1") + .build() + ); + + // When + useCase.execute(new DiscoverIntegrationAssetUseCase.Input(INTEGRATION_ID, AUDIT_INFO)).test().awaitDone(10, TimeUnit.SECONDS); + + // Then + SoftAssertions.assertSoftly(soft -> { + Api expectedApi = Api + .builder() + .id("generated-id") + .definitionVersion(DefinitionVersion.FEDERATED) + .name("api-1") + .description("my description") + .version("1.1.1") + .createdAt(INSTANT_NOW.atZone(ZoneId.systemDefault())) + .updatedAt(INSTANT_NOW.atZone(ZoneId.systemDefault())) + .environmentId(ENVIRONMENT_ID) + .lifecycleState(null) + .build(); + soft.assertThat(apiCrudService.storage()).containsExactlyInAnyOrder(expectedApi); + soft + .assertThat(indexer.storage()) + .containsExactly( + new IndexableApi( + expectedApi, + new PrimaryOwnerEntity(USER_ID, "jane.doe@gravitee.io", "Jane Doe", PrimaryOwnerEntity.Type.USER), + Map.of() + ) + ); + }); + } + + @Test + void should_create_an_audit() { + // Given + givenAnIntegration(IntegrationFixture.anIntegration(ENVIRONMENT_ID).withId(INTEGRATION_ID)); + givenAssets(IntegrationAssetFixtures.anAssetForIntegration(INTEGRATION_ID)); + + // When + useCase.execute(new DiscoverIntegrationAssetUseCase.Input(INTEGRATION_ID, AUDIT_INFO)).test().awaitDone(10, TimeUnit.SECONDS); + + // Then + assertThat(auditCrudService.storage()) + .usingRecursiveFieldByFieldElementComparatorIgnoringFields("patch") + .containsExactly( + // API Audit + AuditEntity + .builder() + .id("generated-id") + .organizationId(ORGANIZATION_ID) + .environmentId(ENVIRONMENT_ID) + .referenceType(AuditEntity.AuditReferenceType.API) + .referenceId("generated-id") + .user(USER_ID) + .properties(Collections.emptyMap()) + .event(ApiAuditEvent.API_CREATED.name()) + .createdAt(INSTANT_NOW.atZone(ZoneId.systemDefault())) + .build(), + // Membership Audit + AuditEntity + .builder() + .id("generated-id") + .organizationId(ORGANIZATION_ID) + .environmentId(ENVIRONMENT_ID) + .referenceType(AuditEntity.AuditReferenceType.API) + .referenceId("generated-id") + .user(USER_ID) + .properties(Map.of("USER", USER_ID)) + .event(MembershipAuditEvent.MEMBERSHIP_CREATED.name()) + .createdAt(INSTANT_NOW.atZone(ZoneId.systemDefault())) + .build() + ); + } + + @ParameterizedTest + @EnumSource(value = ApiPrimaryOwnerMode.class, mode = EnumSource.Mode.INCLUDE, names = { "USER", "HYBRID" }) + void should_create_primary_owner_membership_when_user_or_hybrid_mode_is_enabled(ApiPrimaryOwnerMode mode) { + // Given + enableApiPrimaryOwnerMode(mode); + givenAnIntegration(IntegrationFixture.anIntegration(ENVIRONMENT_ID).withId(INTEGRATION_ID)); + givenAssets(IntegrationAssetFixtures.anAssetForIntegration(INTEGRATION_ID)); + + // When + useCase.execute(new DiscoverIntegrationAssetUseCase.Input(INTEGRATION_ID, AUDIT_INFO)).test().awaitDone(10, TimeUnit.SECONDS); + + // Then + assertThat(membershipCrudService.storage()) + .contains( + Membership + .builder() + .id("generated-id") + .roleId(apiPrimaryOwnerRoleId(ORGANIZATION_ID)) + .memberId(USER_ID) + .memberType(Membership.Type.USER) + .referenceId("generated-id") + .referenceType(Membership.ReferenceType.API) + .source("system") + .createdAt(INSTANT_NOW.atZone(ZoneId.systemDefault())) + .updatedAt(INSTANT_NOW.atZone(ZoneId.systemDefault())) + .build() + ); + } + + @Test + void should_not_create_default_metadata() { + // Given + givenAnIntegration(IntegrationFixture.anIntegration(ENVIRONMENT_ID).withId(INTEGRATION_ID)); + givenAssets(IntegrationAssetFixtures.anAssetForIntegration(INTEGRATION_ID)); + + // When + useCase.execute(new DiscoverIntegrationAssetUseCase.Input(INTEGRATION_ID, AUDIT_INFO)).test().awaitDone(10, TimeUnit.SECONDS); + + // Then + assertThat(metadataCrudService.storage()).isEmpty(); + } + + @Test + void should_not_create_default_email_notification_configuration() { + // Given + givenAnIntegration(IntegrationFixture.anIntegration(ENVIRONMENT_ID).withId(INTEGRATION_ID)); + givenAssets(IntegrationAssetFixtures.anAssetForIntegration(INTEGRATION_ID)); + + // When + useCase.execute(new DiscoverIntegrationAssetUseCase.Input(INTEGRATION_ID, AUDIT_INFO)).test().awaitDone(10, TimeUnit.SECONDS); + + // Then + assertThat(notificationConfigCrudService.storage()).isEmpty(); + } + + @Test + void should_ignore_api_review_mode() { + // Given + enableApiReview(); + givenAnIntegration(IntegrationFixture.anIntegration(ENVIRONMENT_ID).withId(INTEGRATION_ID)); + givenAssets( + IntegrationAssetFixtures.anAssetForIntegration(INTEGRATION_ID).toBuilder().id("asset-1").name("api-1").build(), + IntegrationAssetFixtures.anAssetForIntegration(INTEGRATION_ID).toBuilder().id("asset-2").name("api-2").build() + ); + + // When + useCase + .execute(new DiscoverIntegrationAssetUseCase.Input(INTEGRATION_ID, AUDIT_INFO)) + .test() + .awaitDone(0, TimeUnit.SECONDS) + .assertComplete(); + + // Then + assertThat(workflowCrudService.storage()).isEmpty(); + } + + private void enableApiReview() { + parametersQueryService.define( + new Parameter(Key.API_REVIEW_ENABLED.key(), ENVIRONMENT_ID, ParameterReferenceType.ENVIRONMENT, "true") + ); + } + } + + @Test + void should_throw_when_no_integration_is_found() { + // When + var obs = useCase.execute(new DiscoverIntegrationAssetUseCase.Input("unknown", AUDIT_INFO)).test(); + + // Then + obs.assertError(IntegrationNotFoundException.class); + } + + @Test + void should_do_nothing_when_no_asset_to_import() { + // Given + givenAnIntegration(IntegrationFixture.anIntegration().withId(INTEGRATION_ID)); + + // When + useCase.execute(new DiscoverIntegrationAssetUseCase.Input(INTEGRATION_ID, AUDIT_INFO)).test().awaitDone(10, TimeUnit.SECONDS); + + // Then + Assertions.assertThat(apiCrudService.storage()).isEmpty(); + } + + @Test + void should_create_a_federated_api_for_each_asset() { + // Given + givenAnIntegration(IntegrationFixture.anIntegration(ENVIRONMENT_ID).withId(INTEGRATION_ID)); + givenAssets( + IntegrationAssetFixtures.anAssetForIntegration(INTEGRATION_ID).toBuilder().id("asset-1").name("api-1").build(), + IntegrationAssetFixtures.anAssetForIntegration(INTEGRATION_ID).toBuilder().id("asset-2").name("api-2").build() + ); + + // When + useCase + .execute(new DiscoverIntegrationAssetUseCase.Input(INTEGRATION_ID, AUDIT_INFO)) + .test() + .awaitDone(0, TimeUnit.SECONDS) + .assertComplete(); + + // Then + Assertions.assertThat(apiCrudService.storage()).extracting(Api::getName).containsExactlyInAnyOrder("api-1", "api-2"); + } + + @Test + void should_skip_creating_federated_api_when_validation_fails() { + // Given + givenAnIntegration(IntegrationFixture.anIntegration(ENVIRONMENT_ID).withId(INTEGRATION_ID)); + givenAssets( + IntegrationAssetFixtures.anAssetForIntegration(INTEGRATION_ID).toBuilder().id("asset-1").name("api-1").build(), + IntegrationAssetFixtures.anAssetForIntegration(INTEGRATION_ID).toBuilder().id("asset-2").name("api-2").build(), + IntegrationAssetFixtures.anAssetForIntegration(INTEGRATION_ID).toBuilder().id("asset-3").name("api-3").build() + ); + when(validateFederatedApiDomainService.validateAndSanitizeForCreation(argThat(api -> api.getName().equals("api-2")))) + .thenThrow(new ValidationDomainException("validation failed")); + + // When + useCase + .execute(new DiscoverIntegrationAssetUseCase.Input(INTEGRATION_ID, AUDIT_INFO)) + .test() + .awaitDone(0, TimeUnit.SECONDS) + .assertComplete(); + + // Then + Assertions.assertThat(apiCrudService.storage()).extracting(Api::getName).containsExactlyInAnyOrder("api-1", "api-3"); + } + + private void givenAnIntegration(Integration integration) { + integrationCrudService.initWith(List.of(integration)); + } + + private void givenAssets(Asset... assets) { + integrationAgent.initWith(List.of(assets)); + } + + private void givenExistingUsers(List users) { + userCrudService.initWith(users); + } + + private void enableApiPrimaryOwnerMode(ApiPrimaryOwnerMode mode) { + parametersQueryService.initWith( + List.of(new Parameter(Key.API_PRIMARY_OWNER_MODE.key(), ENVIRONMENT_ID, ParameterReferenceType.ENVIRONMENT, mode.name())) + ); + } +} diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-test-fixtures/src/main/java/fixtures/ApiModelFixtures.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-test-fixtures/src/main/java/fixtures/ApiModelFixtures.java index c701fbc0b60..a2870d25f41 100644 --- a/gravitee-apim-rest-api/gravitee-apim-rest-api-test-fixtures/src/main/java/fixtures/ApiModelFixtures.java +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-test-fixtures/src/main/java/fixtures/ApiModelFixtures.java @@ -16,6 +16,7 @@ package fixtures; import io.gravitee.common.component.Lifecycle; +import io.gravitee.definition.model.DefinitionVersion; import io.gravitee.definition.model.Rule; import io.gravitee.definition.model.services.Services; import io.gravitee.definition.model.v4.flow.execution.FlowExecution; @@ -104,6 +105,16 @@ private ApiModelFixtures() {} .background("my-background") .backgroundUrl("my-background-url"); + private static final ApiEntity.ApiEntityBuilder BASE_MODEL_API_FEDERATED = ApiEntity + .builder() + .id("my-id") + .name("my-name") + .apiVersion("v1.0") + .definitionVersion(DefinitionVersion.FEDERATED) + .deployedAt(new Date()) + .createdAt(new Date()) + .updatedAt(new Date()); + public static io.gravitee.rest.api.model.api.ApiEntity aModelApiV1() { return BASE_MODEL_API_V1.build(); } @@ -121,6 +132,7 @@ public static GenericApiEntity aGenericApiEntity(final io.gravitee.definition.mo case V1 -> aModelApiV1(); case V2 -> aModelApiV2(); case V4 -> aModelApiV4(); + case FEDERATED -> BASE_MODEL_API_FEDERATED.build(); }; } }