diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-automation/gravitee-apim-rest-api-automation-rest/src/test/java/io/gravitee/apim/rest/api/automation/spring/ResourceContextConfiguration.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-automation/gravitee-apim-rest-api-automation-rest/src/test/java/io/gravitee/apim/rest/api/automation/spring/ResourceContextConfiguration.java index 4c1c82d5c73..31173d06494 100644 --- a/gravitee-apim-rest-api/gravitee-apim-rest-api-automation/gravitee-apim-rest-api-automation-rest/src/test/java/io/gravitee/apim/rest/api/automation/spring/ResourceContextConfiguration.java +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-automation/gravitee-apim-rest-api-automation-rest/src/test/java/io/gravitee/apim/rest/api/automation/spring/ResourceContextConfiguration.java @@ -101,6 +101,8 @@ import io.gravitee.apim.core.plugin.crud_service.PolicyPluginCrudService; import io.gravitee.apim.core.plugin.domain_service.EndpointConnectorPluginDomainService; import io.gravitee.apim.core.policy.domain_service.PolicyValidationDomainService; +import io.gravitee.apim.core.portal_page.domain_service.CreatePortalNavigationItemValidatorService; +import io.gravitee.apim.core.portal_page.use_case.CreatePortalNavigationItemUseCase; import io.gravitee.apim.core.portal_page.use_case.GetPortalPageContentUseCase; import io.gravitee.apim.core.portal_page.use_case.ListPortalNavigationItemsUseCase; import io.gravitee.apim.core.promotion.service_provider.CockpitPromotionServiceProvider; @@ -856,6 +858,16 @@ public CreatePromotionUseCase createPromotionUseCase() { return mock(CreatePromotionUseCase.class); } + @Bean + public CreatePortalNavigationItemUseCase createPortalNavigationItemUseCase() { + return mock(CreatePortalNavigationItemUseCase.class); + } + + @Bean + public CreatePortalNavigationItemValidatorService createPortalNavigationItemValidatorService() { + return mock(CreatePortalNavigationItemValidatorService.class); + } + @Bean public ListPortalNavigationItemsUseCase listPortalNavigationItemsUseCase() { return mock(ListPortalNavigationItemsUseCase.class); diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-management-v2/gravitee-apim-rest-api-management-v2-rest/src/main/java/io/gravitee/rest/api/management/v2/rest/GraviteeManagementV2Application.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-management-v2/gravitee-apim-rest-api-management-v2-rest/src/main/java/io/gravitee/rest/api/management/v2/rest/GraviteeManagementV2Application.java index a1a4e8debfd..95f4a4d9973 100644 --- a/gravitee-apim-rest-api/gravitee-apim-rest-api-management-v2/gravitee-apim-rest-api-management-v2-rest/src/main/java/io/gravitee/rest/api/management/v2/rest/GraviteeManagementV2Application.java +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-management-v2/gravitee-apim-rest-api-management-v2-rest/src/main/java/io/gravitee/rest/api/management/v2/rest/GraviteeManagementV2Application.java @@ -24,6 +24,7 @@ import io.gravitee.rest.api.management.v2.rest.exceptionMapper.ThrowableMapper; import io.gravitee.rest.api.management.v2.rest.exceptionMapper.UnrecognizedPropertyExceptionMapper; import io.gravitee.rest.api.management.v2.rest.exceptionMapper.ValidationExceptionMapper; +import io.gravitee.rest.api.management.v2.rest.exceptionMapper.domain.ConflictDomainExceptionMapper; import io.gravitee.rest.api.management.v2.rest.exceptionMapper.domain.NotAllowedDomainExceptionMapper; import io.gravitee.rest.api.management.v2.rest.exceptionMapper.domain.NotFoundDomainExceptionMapper; import io.gravitee.rest.api.management.v2.rest.exceptionMapper.domain.TechnicalDomainExceptionMapper; @@ -106,6 +107,7 @@ public GraviteeManagementV2Application() { register(TechnicalDomainExceptionMapper.class); register(NotAllowedDomainExceptionMapper.class); register(NotFoundDomainExceptionMapper.class); + register(ConflictDomainExceptionMapper.class); register(CommaSeparatedQueryParamConverterProvider.class); diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-management-v2/gravitee-apim-rest-api-management-v2-rest/src/main/java/io/gravitee/rest/api/management/v2/rest/exceptionMapper/domain/ConflictDomainExceptionMapper.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-management-v2/gravitee-apim-rest-api-management-v2-rest/src/main/java/io/gravitee/rest/api/management/v2/rest/exceptionMapper/domain/ConflictDomainExceptionMapper.java new file mode 100644 index 00000000000..4f4023135a1 --- /dev/null +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-management-v2/gravitee-apim-rest-api-management-v2-rest/src/main/java/io/gravitee/rest/api/management/v2/rest/exceptionMapper/domain/ConflictDomainExceptionMapper.java @@ -0,0 +1,40 @@ +/* + * 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.rest.api.management.v2.rest.exceptionMapper.domain; + +import io.gravitee.apim.core.exception.ConflictDomainException; +import io.gravitee.rest.api.management.v2.rest.model.Error; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import java.util.Map; + +public class ConflictDomainExceptionMapper extends AbstractDomainExceptionMapper { + + @Override + public Response toResponse(ConflictDomainException exception) { + return Response.status(Response.Status.CONFLICT) + .type(MediaType.APPLICATION_JSON_TYPE) + .entity(conflictDomainError(exception)) + .build(); + } + + private Error conflictDomainError(ConflictDomainException exception) { + return new Error() + .httpStatus(Response.Status.CONFLICT.getStatusCode()) + .message(exception.getMessage()) + .parameters(exception.getId() != null ? Map.of("id", exception.getId()) : null); + } +} diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-management-v2/gravitee-apim-rest-api-management-v2-rest/src/main/java/io/gravitee/rest/api/management/v2/rest/exceptionMapper/domain/NotFoundDomainExceptionMapper.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-management-v2/gravitee-apim-rest-api-management-v2-rest/src/main/java/io/gravitee/rest/api/management/v2/rest/exceptionMapper/domain/NotFoundDomainExceptionMapper.java index 8b7957f18ae..2a51d1f6694 100644 --- a/gravitee-apim-rest-api/gravitee-apim-rest-api-management-v2/gravitee-apim-rest-api-management-v2-rest/src/main/java/io/gravitee/rest/api/management/v2/rest/exceptionMapper/domain/NotFoundDomainExceptionMapper.java +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-management-v2/gravitee-apim-rest-api-management-v2-rest/src/main/java/io/gravitee/rest/api/management/v2/rest/exceptionMapper/domain/NotFoundDomainExceptionMapper.java @@ -33,6 +33,6 @@ private Error notFoundDomainError(NotFoundDomainException nfe) { return new Error() .httpStatus(Response.Status.NOT_FOUND.getStatusCode()) .message(nfe.getMessage()) - .parameters(Map.of("id", nfe.getId())); + .parameters(nfe.getId() != null ? Map.of("id", nfe.getId()) : null); } } diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-management-v2/gravitee-apim-rest-api-management-v2-rest/src/main/java/io/gravitee/rest/api/management/v2/rest/mapper/PortalNavigationItemsMapper.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-management-v2/gravitee-apim-rest-api-management-v2-rest/src/main/java/io/gravitee/rest/api/management/v2/rest/mapper/PortalNavigationItemsMapper.java new file mode 100644 index 00000000000..f83edb660d0 --- /dev/null +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-management-v2/gravitee-apim-rest-api-management-v2-rest/src/main/java/io/gravitee/rest/api/management/v2/rest/mapper/PortalNavigationItemsMapper.java @@ -0,0 +1,107 @@ +/* + * 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.rest.api.management.v2.rest.mapper; + +import io.gravitee.apim.core.exception.TechnicalDomainException; +import io.gravitee.apim.core.portal_page.model.PortalNavigationItemId; +import io.gravitee.rest.api.management.v2.rest.model.BaseCreatePortalNavigationItem; +import io.gravitee.rest.api.management.v2.rest.model.CreatePortalNavigationFolder; +import io.gravitee.rest.api.management.v2.rest.model.CreatePortalNavigationLink; +import io.gravitee.rest.api.management.v2.rest.model.CreatePortalNavigationPage; +import io.gravitee.rest.api.management.v2.rest.model.PortalNavigationItem; +import java.util.List; +import java.util.UUID; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface PortalNavigationItemsMapper { + PortalNavigationItemsMapper INSTANCE = Mappers.getMapper(PortalNavigationItemsMapper.class); + + @Mapping(target = "type", constant = "PAGE") + @Mapping( + target = "configuration", + expression = "java(new io.gravitee.rest.api.management.v2.rest.model.PortalNavigationPageAllOfConfiguration().portalPageContentId(page.getPortalPageContentId().id()))" + ) + io.gravitee.rest.api.management.v2.rest.model.PortalNavigationPage map( + io.gravitee.apim.core.portal_page.model.PortalNavigationPage page + ); + + @Mapping(target = "type", constant = "FOLDER") + io.gravitee.rest.api.management.v2.rest.model.PortalNavigationFolder map( + io.gravitee.apim.core.portal_page.model.PortalNavigationFolder folder + ); + + @Mapping(target = "type", constant = "LINK") + @Mapping( + target = "configuration", + expression = "java(new io.gravitee.rest.api.management.v2.rest.model.PortalNavigationLinkAllOfConfiguration().url(link.getUrl()))" + ) + io.gravitee.rest.api.management.v2.rest.model.PortalNavigationLink map( + io.gravitee.apim.core.portal_page.model.PortalNavigationLink link + ); + + default List map(List items) { + return items.stream().map(this::map).toList(); + } + + default PortalNavigationItem map(io.gravitee.apim.core.portal_page.model.PortalNavigationItem portalNavigationItem) { + return switch (portalNavigationItem) { + case io.gravitee.apim.core.portal_page.model.PortalNavigationFolder folder -> new PortalNavigationItem(map(folder)); + case io.gravitee.apim.core.portal_page.model.PortalNavigationPage page -> new PortalNavigationItem(map(page)); + case io.gravitee.apim.core.portal_page.model.PortalNavigationLink link -> new PortalNavigationItem(map(link)); + }; + } + + @Mapping( + target = "contentId", + expression = "java(page.getContentId() == null ? null : io.gravitee.apim.core.portal_page.model.PortalPageContentId.of(page.getContentId().toString()))" + ) + io.gravitee.apim.core.portal_page.model.CreatePortalNavigationItem map( + io.gravitee.rest.api.management.v2.rest.model.CreatePortalNavigationPage page + ); + + io.gravitee.apim.core.portal_page.model.CreatePortalNavigationItem map( + io.gravitee.rest.api.management.v2.rest.model.CreatePortalNavigationFolder folder + ); + + @Mapping(target = "url", expression = "java(link.getUrl().toString())") + io.gravitee.apim.core.portal_page.model.CreatePortalNavigationItem map( + io.gravitee.rest.api.management.v2.rest.model.CreatePortalNavigationLink link + ); + + default io.gravitee.apim.core.portal_page.model.CreatePortalNavigationItem map( + BaseCreatePortalNavigationItem createPortalNavigationItem + ) { + return switch (createPortalNavigationItem) { + case CreatePortalNavigationFolder folder -> map(folder); + case CreatePortalNavigationPage page -> map(page); + case CreatePortalNavigationLink link -> map(link); + default -> throw new TechnicalDomainException( + String.format("Unknown PortalNavigationItem class %s", createPortalNavigationItem.getClass().getSimpleName()) + ); + }; + } + + default PortalNavigationItemId map(UUID id) { + return id == null ? null : PortalNavigationItemId.of(id.toString()); + } + + default String map(io.gravitee.apim.core.portal_page.model.PortalNavigationItemId id) { + return id != null ? id.json() : null; + } +} diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-management-v2/gravitee-apim-rest-api-management-v2-rest/src/main/java/io/gravitee/rest/api/management/v2/rest/mapper/PortalNavigationMapper.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-management-v2/gravitee-apim-rest-api-management-v2-rest/src/main/java/io/gravitee/rest/api/management/v2/rest/mapper/PortalNavigationMapper.java deleted file mode 100644 index 0d50607f640..00000000000 --- a/gravitee-apim-rest-api/gravitee-apim-rest-api-management-v2/gravitee-apim-rest-api-management-v2-rest/src/main/java/io/gravitee/rest/api/management/v2/rest/mapper/PortalNavigationMapper.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * 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.rest.api.management.v2.rest.mapper; - -import io.gravitee.apim.core.portal_page.model.PortalNavigationFolder; -import io.gravitee.apim.core.portal_page.model.PortalNavigationItem; -import io.gravitee.apim.core.portal_page.model.PortalNavigationLink; -import io.gravitee.apim.core.portal_page.model.PortalNavigationPage; -import java.util.List; -import org.mapstruct.Mapper; -import org.mapstruct.Mapping; -import org.mapstruct.factory.Mappers; - -@Mapper -public interface PortalNavigationMapper { - PortalNavigationMapper INSTANCE = Mappers.getMapper(PortalNavigationMapper.class); - - default List map(List items) { - return items - .stream() - .map(item -> - switch (item) { - case PortalNavigationPage page -> new io.gravitee.rest.api.management.v2.rest.model.PortalNavigationItem(map(page)); - case PortalNavigationFolder folder -> new io.gravitee.rest.api.management.v2.rest.model.PortalNavigationItem( - map(folder) - ); - case PortalNavigationLink link -> new io.gravitee.rest.api.management.v2.rest.model.PortalNavigationItem(map(link)); - } - ) - .toList(); - } - - default String map(io.gravitee.apim.core.portal_page.model.PortalNavigationItemId id) { - return id != null ? id.json() : null; - } - - @Mapping(target = "type", constant = "PAGE") - @Mapping( - target = "configuration", - expression = "java(new io.gravitee.rest.api.management.v2.rest.model.PortalNavigationPageAllOfConfiguration().portalPageContentId(page.getPortalPageContentId().toString()))" - ) - io.gravitee.rest.api.management.v2.rest.model.PortalNavigationPage map(PortalNavigationPage page); - - @Mapping(target = "type", constant = "FOLDER") - io.gravitee.rest.api.management.v2.rest.model.PortalNavigationFolder map(PortalNavigationFolder folder); - - @Mapping(target = "type", constant = "LINK") - @Mapping( - target = "configuration", - expression = "java(new io.gravitee.rest.api.management.v2.rest.model.PortalNavigationLinkAllOfConfiguration().url(link.getUrl()))" - ) - io.gravitee.rest.api.management.v2.rest.model.PortalNavigationLink map(PortalNavigationLink link); -} diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-management-v2/gravitee-apim-rest-api-management-v2-rest/src/main/java/io/gravitee/rest/api/management/v2/rest/resource/environment/PortalNavigationItemsResource.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-management-v2/gravitee-apim-rest-api-management-v2-rest/src/main/java/io/gravitee/rest/api/management/v2/rest/resource/environment/PortalNavigationItemsResource.java index db67b3ff7ba..a58266cd471 100644 --- a/gravitee-apim-rest-api/gravitee-apim-rest-api-management-v2/gravitee-apim-rest-api-management-v2-rest/src/main/java/io/gravitee/rest/api/management/v2/rest/resource/environment/PortalNavigationItemsResource.java +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-management-v2/gravitee-apim-rest-api-management-v2-rest/src/main/java/io/gravitee/rest/api/management/v2/rest/resource/environment/PortalNavigationItemsResource.java @@ -17,9 +17,11 @@ import io.gravitee.apim.core.portal_page.model.PortalArea; import io.gravitee.apim.core.portal_page.model.PortalNavigationItemId; +import io.gravitee.apim.core.portal_page.use_case.CreatePortalNavigationItemUseCase; import io.gravitee.apim.core.portal_page.use_case.ListPortalNavigationItemsUseCase; import io.gravitee.common.http.MediaType; -import io.gravitee.rest.api.management.v2.rest.mapper.PortalNavigationMapper; +import io.gravitee.rest.api.management.v2.rest.mapper.PortalNavigationItemsMapper; +import io.gravitee.rest.api.management.v2.rest.model.BaseCreatePortalNavigationItem; import io.gravitee.rest.api.management.v2.rest.model.PortalNavigationItemsResponse; import io.gravitee.rest.api.management.v2.rest.resource.AbstractResource; import io.gravitee.rest.api.model.permissions.RolePermission; @@ -28,20 +30,32 @@ import io.gravitee.rest.api.rest.annotation.Permissions; import io.gravitee.rest.api.service.common.GraviteeContext; import jakarta.inject.Inject; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import jakarta.ws.rs.Consumes; import jakarta.ws.rs.DefaultValue; import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; import jakarta.ws.rs.Produces; import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.Response; import java.util.Optional; +import lombok.extern.slf4j.Slf4j; /** * @author GraviteeSource Team */ +@Slf4j public class PortalNavigationItemsResource extends AbstractResource { + @Inject + private CreatePortalNavigationItemUseCase createPortalNavigationItemUseCase; + @Inject private ListPortalNavigationItemsUseCase listPortalNavigationItemsUseCase; + private final PortalNavigationItemsMapper mapper = PortalNavigationItemsMapper.INSTANCE; + @GET @Produces(MediaType.APPLICATION_JSON) @Permissions({ @Permission(value = RolePermission.ENVIRONMENT_DOCUMENTATION, acls = RolePermissionAction.READ) }) @@ -59,6 +73,24 @@ public PortalNavigationItemsResponse getPortalNavigationItems( ) ); - return new PortalNavigationItemsResponse().items(PortalNavigationMapper.INSTANCE.map(result.items())); + return new PortalNavigationItemsResponse().items(mapper.map(result.items())); + } + + @POST + @Permissions({ @Permission(value = RolePermission.ENVIRONMENT_DOCUMENTATION, acls = { RolePermissionAction.UPDATE }) }) + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public Response createPortalNavigationItem(@Valid @NotNull final BaseCreatePortalNavigationItem createPortalNavigationItem) { + final var executionContext = GraviteeContext.getExecutionContext(); + + final var output = createPortalNavigationItemUseCase.execute( + new CreatePortalNavigationItemUseCase.Input( + executionContext.getOrganizationId(), + executionContext.getEnvironmentId(), + mapper.map(createPortalNavigationItem) + ) + ); + + return Response.created(this.getLocationHeader(output.item().getId().toString())).entity(mapper.map(output.item())).build(); } } diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-management-v2/gravitee-apim-rest-api-management-v2-rest/src/main/java/io/gravitee/rest/api/management/v2/rest/resource/installation/EnvironmentResource.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-management-v2/gravitee-apim-rest-api-management-v2-rest/src/main/java/io/gravitee/rest/api/management/v2/rest/resource/installation/EnvironmentResource.java index 61df7bae790..f833193d579 100644 --- a/gravitee-apim-rest-api/gravitee-apim-rest-api-management-v2/gravitee-apim-rest-api-management-v2-rest/src/main/java/io/gravitee/rest/api/management/v2/rest/resource/installation/EnvironmentResource.java +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-management-v2/gravitee-apim-rest-api-management-v2-rest/src/main/java/io/gravitee/rest/api/management/v2/rest/resource/installation/EnvironmentResource.java @@ -116,7 +116,7 @@ public InstancesResource getInstancesResource() { } @Path("/portal-navigation-items") - public PortalNavigationItemsResource getPortalNavigationItemResource() { + public PortalNavigationItemsResource getPortalNavigationItemsResource() { return resourceContext.getResource(PortalNavigationItemsResource.class); } diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-management-v2/gravitee-apim-rest-api-management-v2-rest/src/main/resources/openapi/openapi-environments.yaml b/gravitee-apim-rest-api/gravitee-apim-rest-api-management-v2/gravitee-apim-rest-api-management-v2-rest/src/main/resources/openapi/openapi-environments.yaml index abc0a86214a..0c65ba28e80 100644 --- a/gravitee-apim-rest-api/gravitee-apim-rest-api-management-v2/gravitee-apim-rest-api-management-v2-rest/src/main/resources/openapi/openapi-environments.yaml +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-management-v2/gravitee-apim-rest-api-management-v2-rest/src/main/resources/openapi/openapi-environments.yaml @@ -872,6 +872,27 @@ paths: $ref: "#/components/responses/PortalNavigationItemsResponse" default: $ref: "#/components/responses/Error" + post: + tags: + - Portal Navigation Items + summary: Create a portal navigation item + description: User must have the ENVIRONMENT_DOCUMENTATION[update] permission. + operationId: createPortalNavigationItem + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/CreatePortalNavigationItem" + required: true + responses: + "201": + description: Portal navigation item created + content: + application/json: + schema: + $ref: "#/components/schemas/PortalNavigationItem" + default: + $ref: "#/components/responses/Error" components: schemas: @@ -1692,34 +1713,45 @@ components: type: string description: The portal area (used by portal navigation items) enum: [ HOMEPAGE, TOP_NAVBAR ] + example: TOP_NAVBAR + PortalNavigationItemType: + type: string + description: The type of the navigation item + example: FOLDER + enum: [PAGE, FOLDER, LINK] BasePortalNavigationItem: type: object description: Base portal navigation item properties: id: type: string + format: uuid description: The unique ID of the navigation item + example: 00f8c9e7-78fc-4907-b8c9-e778fc790750 organizationId: type: string description: The organization ID + example: DEFAULT environmentId: type: string description: The environment ID + example: DEFAULT title: type: string description: The title of the navigation item type: - type: string - description: The type of the navigation item - enum: [ PAGE, FOLDER, LINK ] + $ref: "#/components/schemas/PortalNavigationItemType" area: $ref: "#/components/schemas/PortalArea" parentId: type: string + format: uuid description: The parent ID of the navigation item + example: 00f8c9e7-78fc-4907-b8c9-e778fc790750 order: type: integer - description: The order of the navigation item + description: The order of the navigation item (zero-based) + example: 2 required: [ id, organizationId, environmentId, title, type, area, order ] discriminator: propertyName: type @@ -1728,6 +1760,7 @@ components: FOLDER: "#/components/schemas/PortalNavigationFolder" LINK: "#/components/schemas/PortalNavigationLink" PortalNavigationPage: + description: Portal navigation item of type PAGE allOf: - $ref: "#/components/schemas/BasePortalNavigationItem" - properties: @@ -1736,12 +1769,17 @@ components: properties: portalPageContentId: type: string + format: uuid description: The UUID of the portal page content + example: 00f8c9e7-78fc-4907-b8c9-e778fc790750 required: [ portalPageContentId ] + required: [configuration] PortalNavigationFolder: + description: Portal navigation item of type FOLDER allOf: - $ref: "#/components/schemas/BasePortalNavigationItem" PortalNavigationLink: + description: Portal navigation item of type LINK allOf: - $ref: "#/components/schemas/BasePortalNavigationItem" - properties: @@ -1751,7 +1789,9 @@ components: url: type: string description: The URL for the link + example: https://example.com required: [ url ] + required: [configuration] PortalNavigationItem: oneOf: - $ref: "#/components/schemas/PortalNavigationPage" @@ -1764,6 +1804,82 @@ components: FOLDER: "#/components/schemas/PortalNavigationFolder" LINK: "#/components/schemas/PortalNavigationLink" + BaseCreatePortalNavigationItem: + type: object + description: Base portal navigation item + properties: + id: + type: string + format: uuid + description: The unique ID of the navigation item + example: 00f8c9e7-78fc-4907-b8c9-e778fc790750 + title: + type: string + description: The title of the navigation item + type: + $ref: "#/components/schemas/PortalNavigationItemType" + area: + $ref: "#/components/schemas/PortalArea" + order: + type: integer + minimum: 0 + description: The order of the navigation item, from 0 to MAX + 1, where MAX is the maximum existing order within the target parent. If not provided or greater than MAX, the new item is appended to the end of the target parent. + example: 2 + parentId: + type: string + format: uuid + description: The parent ID of the navigation item, if not provided item is created at root + example: 00f8c9e7-78fc-4907-b8c9-e778fc790750 + discriminator: + propertyName: type + mapping: + FOLDER: "#/components/schemas/CreatePortalNavigationFolder" + PAGE: "#/components/schemas/CreatePortalNavigationPage" + LINK: "#/components/schemas/CreatePortalNavigationLink" + required: [title, type, area] + CreatePortalNavigationFolder: + type: object + title: "CreatePortalNavigationFolder" + description: Portal navigation folder to create + allOf: + - $ref: "#/components/schemas/BaseCreatePortalNavigationItem" + CreatePortalNavigationPage: + type: object + title: "CreatePortalNavigationPage" + description: Portal navigation page to create + allOf: + - $ref: "#/components/schemas/BaseCreatePortalNavigationItem" + - properties: + contentId: + type: string + format: uuid + description: The UUID of the portal page content + example: 00f8c9e7-78fc-4907-b8c9-e778fc790750 + CreatePortalNavigationLink: + type: object + title: "CreatePortalNavigationLink" + description: Portal navigation link to create + allOf: + - $ref: "#/components/schemas/BaseCreatePortalNavigationItem" + - properties: + url: + type: string + format: uri + description: The URL for the link + example: https://example.com + required: [url] + CreatePortalNavigationItem: + oneOf: + - $ref: "#/components/schemas/CreatePortalNavigationPage" + - $ref: "#/components/schemas/CreatePortalNavigationFolder" + - $ref: "#/components/schemas/CreatePortalNavigationLink" + discriminator: + propertyName: type + mapping: + PAGE: "#/components/schemas/CreatePortalNavigationPage" + FOLDER: "#/components/schemas/CreatePortalNavigationFolder" + LINK: "#/components/schemas/CreatePortalNavigationLink" + PortalPageContent: type: object description: Portal page content diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-management-v2/gravitee-apim-rest-api-management-v2-rest/src/test/java/fixtures/PortalNavigationItemsFixtures.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-management-v2/gravitee-apim-rest-api-management-v2-rest/src/test/java/fixtures/PortalNavigationItemsFixtures.java new file mode 100644 index 00000000000..b6a0916b028 --- /dev/null +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-management-v2/gravitee-apim-rest-api-management-v2-rest/src/test/java/fixtures/PortalNavigationItemsFixtures.java @@ -0,0 +1,78 @@ +/* + * 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; + +import io.gravitee.apim.core.shared_policy_group.model.SharedPolicyGroupCRD; +import io.gravitee.rest.api.management.v2.rest.model.ApiType; +import io.gravitee.rest.api.management.v2.rest.model.BaseCreatePortalNavigationItem; +import io.gravitee.rest.api.management.v2.rest.model.CreatePortalNavigationFolder; +import io.gravitee.rest.api.management.v2.rest.model.CreatePortalNavigationLink; +import io.gravitee.rest.api.management.v2.rest.model.CreatePortalNavigationPage; +import io.gravitee.rest.api.management.v2.rest.model.CreateSharedPolicyGroup; +import io.gravitee.rest.api.management.v2.rest.model.FlowPhase; +import io.gravitee.rest.api.management.v2.rest.model.PortalNavigationItemType; +import io.gravitee.rest.api.management.v2.rest.model.UpdateSharedPolicyGroup; +import java.net.URI; +import java.util.UUID; +import java.util.function.Supplier; + +public class PortalNavigationItemsFixtures { + + private PortalNavigationItemsFixtures() {} + + public static BaseCreatePortalNavigationItem aCreatePortalNavigationPage() { + var title = "My Page"; + var id = java.util.UUID.fromString("00000000-0000-0000-0000-000000000001"); + var parentId = java.util.UUID.fromString("00000000-0000-0000-0000-000000000002"); + var contentId = java.util.UUID.fromString("00000000-0000-0000-0000-000000000003"); + return new CreatePortalNavigationPage() + .contentId(contentId) + .type(PortalNavigationItemType.PAGE) + .id(id) + .title(title) + .area(io.gravitee.rest.api.management.v2.rest.model.PortalArea.TOP_NAVBAR) + .order(1) + .parentId(parentId); + } + + public static BaseCreatePortalNavigationItem aCreatePortalNavigationFolder() { + var title = "My Folder"; + var id = java.util.UUID.fromString("00000000-0000-0000-0000-000000000001"); + var parentId = UUID.fromString("00000000-0000-0000-0000-000000000002"); + return new CreatePortalNavigationFolder() + .type(PortalNavigationItemType.FOLDER) + .id(id) + .title(title) + .area(io.gravitee.rest.api.management.v2.rest.model.PortalArea.TOP_NAVBAR) + .order(2) + .parentId(parentId); + } + + public static BaseCreatePortalNavigationItem aCreatePortalNavigationLink() { + var title = "My Link"; + var id = UUID.fromString("00000000-0000-0000-0000-000000000001"); + var parentId = UUID.fromString("00000000-0000-0000-0000-000000000002"); + var url = "http://example.com"; + return new CreatePortalNavigationLink() + .url(URI.create(url)) + .type(PortalNavigationItemType.LINK) + .id(id) + .title(title) + .area(io.gravitee.rest.api.management.v2.rest.model.PortalArea.TOP_NAVBAR) + .order(3) + .parentId(parentId); + } +} diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-management-v2/gravitee-apim-rest-api-management-v2-rest/src/test/java/io/gravitee/rest/api/management/v2/rest/mapper/PortalNavigationItemsMapperTest.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-management-v2/gravitee-apim-rest-api-management-v2-rest/src/test/java/io/gravitee/rest/api/management/v2/rest/mapper/PortalNavigationItemsMapperTest.java new file mode 100644 index 00000000000..684066c1410 --- /dev/null +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-management-v2/gravitee-apim-rest-api-management-v2-rest/src/test/java/io/gravitee/rest/api/management/v2/rest/mapper/PortalNavigationItemsMapperTest.java @@ -0,0 +1,200 @@ +/* + * 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.rest.api.management.v2.rest.mapper; + +import static org.assertj.core.api.Assertions.assertThat; + +import fixtures.PortalNavigationItemsFixtures; +import fixtures.core.model.PortalNavigationItemFixtures; +import io.gravitee.apim.core.portal_page.model.CreatePortalNavigationItem; +import io.gravitee.apim.core.portal_page.model.PortalArea; +import io.gravitee.apim.core.portal_page.model.PortalNavigationItemId; +import io.gravitee.apim.core.portal_page.model.PortalNavigationLink; +import io.gravitee.rest.api.management.v2.rest.model.BasePortalNavigationItem; +import io.gravitee.rest.api.management.v2.rest.model.CreatePortalNavigationFolder; +import io.gravitee.rest.api.management.v2.rest.model.CreatePortalNavigationLink; +import io.gravitee.rest.api.management.v2.rest.model.CreatePortalNavigationPage; +import io.gravitee.rest.api.management.v2.rest.model.PortalNavigationItemType; +import java.net.URI; +import java.util.UUID; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +/** + * @author GraviteeSource Team + */ +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class PortalNavigationItemsMapperTest { + + private PortalNavigationItemsMapper mapper; + + @BeforeEach + void setUp() { + mapper = PortalNavigationItemsMapper.INSTANCE; + } + + @Nested + class DomainToResource { + + @Test + void should_map_portal_navigation_page() { + UUID pageId = UUID.fromString("12345678-1234-1234-1234-123456789abc"); + var page = PortalNavigationItemFixtures.aPage(pageId.toString(), "My Page", null); + page.setOrder(1); + + var result = mapper.map(page); + + assertThat(result).isInstanceOf(io.gravitee.rest.api.management.v2.rest.model.PortalNavigationPage.class); + assertThat(result.getId()).isEqualTo(pageId); + assertThat(result.getOrganizationId()).isEqualTo("org-id"); + assertThat(result.getEnvironmentId()).isEqualTo("env-id"); + assertThat(result.getTitle()).isEqualTo("My Page"); + assertThat(result.getType()).isEqualTo(PortalNavigationItemType.PAGE); + assertThat(result.getArea()).isEqualTo(io.gravitee.rest.api.management.v2.rest.model.PortalArea.TOP_NAVBAR); + assertThat(result.getOrder()).isEqualTo(1); + assertThat(result.getParentId()).isNull(); + assertThat(result.getConfiguration()).isNotNull(); + assertThat(result.getConfiguration().getPortalPageContentId()).isEqualTo(page.getPortalPageContentId().id()); + } + + @Test + void should_map_portal_navigation_folder() { + UUID folderId = UUID.fromString("87654321-4321-4321-4321-cba987654321"); + var folder = PortalNavigationItemFixtures.aFolder(folderId.toString(), "My Folder"); + folder.setOrder(2); + + var result = mapper.map(folder); + + assertThat(result).isInstanceOf(io.gravitee.rest.api.management.v2.rest.model.PortalNavigationFolder.class); + assertThat(result.getId()).isEqualTo(folderId); + assertThat(result.getOrganizationId()).isEqualTo("org-id"); + assertThat(result.getEnvironmentId()).isEqualTo("env-id"); + assertThat(result.getTitle()).isEqualTo("My Folder"); + assertThat(result.getType()).isEqualTo(PortalNavigationItemType.FOLDER); + assertThat(result.getArea()).isEqualTo(io.gravitee.rest.api.management.v2.rest.model.PortalArea.TOP_NAVBAR); + assertThat(result.getOrder()).isEqualTo(2); + assertThat(result.getParentId()).isNull(); + } + + @Test + void should_map_portal_navigation_link() { + UUID linkId = UUID.fromString("abcd1234-5678-9012-3456-789012345678"); + var link = new PortalNavigationLink( + PortalNavigationItemId.of(linkId.toString()), + "org-id", + "env-id", + "My Link", + PortalArea.TOP_NAVBAR, + 3, + "https://example.com" + ); + + var result = mapper.map(link); + + assertThat(result).isInstanceOf(io.gravitee.rest.api.management.v2.rest.model.PortalNavigationLink.class); + assertThat(result.getId()).isEqualTo(linkId); + assertThat(result.getOrganizationId()).isEqualTo("org-id"); + assertThat(result.getEnvironmentId()).isEqualTo("env-id"); + assertThat(result.getTitle()).isEqualTo("My Link"); + assertThat(result.getType()).isEqualTo(PortalNavigationItemType.LINK); + assertThat(result.getArea()).isEqualTo(io.gravitee.rest.api.management.v2.rest.model.PortalArea.TOP_NAVBAR); + assertThat(result.getOrder()).isEqualTo(3); + assertThat(result.getParentId()).isNull(); + assertThat(result.getConfiguration()).isNotNull(); + assertThat(result.getConfiguration().getUrl()).isEqualTo("https://example.com"); + } + + @Test + void should_map_list_of_portal_navigation_items() { + var items = PortalNavigationItemFixtures.sampleNavigationItems(); + + var result = mapper.map(items); + + assertThat(result).hasSize(9); + // Check that all items are mapped correctly + assertThat( + result + .stream() + .map(i -> (BasePortalNavigationItem) i.getActualInstance()) + .map(BasePortalNavigationItem::getId) + ).containsExactlyInAnyOrder( + UUID.fromString("00000000-0000-0000-0000-000000000001"), + UUID.fromString("00000000-0000-0000-0000-000000000002"), + UUID.fromString("00000000-0000-0000-0000-000000000003"), + UUID.fromString("00000000-0000-0000-0000-000000000004"), + UUID.fromString("00000000-0000-0000-0000-000000000005"), + UUID.fromString("00000000-0000-0000-0000-000000000006"), + UUID.fromString("00000000-0000-0000-0000-000000000007"), + UUID.fromString("00000000-0000-0000-0000-000000000008"), + UUID.fromString("00000000-0000-0000-0000-000000000009") + ); + } + } + + @Nested + class ResourceToDomain { + + @Test + void should_map_create_portal_navigation_page() { + final var page = PortalNavigationItemsFixtures.aCreatePortalNavigationPage(); + + var result = mapper.map(page); + + assertThat(result).isInstanceOf(CreatePortalNavigationItem.class); + assertThat(result.getType()).isEqualTo(io.gravitee.apim.core.portal_page.model.PortalNavigationItemType.PAGE); + assertThat(result.getId()).isNotNull(); + assertThat(result.getTitle()).isEqualTo(page.getTitle()); + assertThat(result.getArea()).isEqualTo(PortalArea.TOP_NAVBAR); + assertThat(result.getOrder()).isEqualTo(1); + assertThat(result.getParentId().id()).isEqualTo(page.getParentId()); + assertThat(result.getContentId().id()).isEqualTo(((CreatePortalNavigationPage) page).getContentId()); + } + + @Test + void should_map_create_portal_navigation_folder() { + final var folder = PortalNavigationItemsFixtures.aCreatePortalNavigationFolder(); + + var result = mapper.map(folder); + + assertThat(result).isInstanceOf(CreatePortalNavigationItem.class); + assertThat(result.getType()).isEqualTo(io.gravitee.apim.core.portal_page.model.PortalNavigationItemType.FOLDER); + assertThat(result.getId()).isNotNull(); + assertThat(result.getTitle()).isEqualTo(folder.getTitle()); + assertThat(result.getArea()).isEqualTo(PortalArea.TOP_NAVBAR); + assertThat(result.getOrder()).isEqualTo(2); + assertThat(result.getParentId().id()).isEqualTo(folder.getParentId()); + } + + @Test + void should_map_create_portal_navigation_link() { + final var link = PortalNavigationItemsFixtures.aCreatePortalNavigationLink(); + + var result = mapper.map(link); + + assertThat(result).isInstanceOf(CreatePortalNavigationItem.class); + assertThat(result.getType()).isEqualTo(io.gravitee.apim.core.portal_page.model.PortalNavigationItemType.LINK); + assertThat(result.getId()).isNotNull(); + assertThat(result.getTitle()).isEqualTo(link.getTitle()); + assertThat(result.getArea()).isEqualTo(PortalArea.TOP_NAVBAR); + assertThat(result.getOrder()).isEqualTo(3); + assertThat(result.getParentId().id()).isEqualTo(link.getParentId()); + assertThat(result.getUrl()).isEqualTo(((CreatePortalNavigationLink) link).getUrl().toString()); + } + } +} diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-management-v2/gravitee-apim-rest-api-management-v2-rest/src/test/java/io/gravitee/rest/api/management/v2/rest/mapper/PortalNavigationMapperTest.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-management-v2/gravitee-apim-rest-api-management-v2-rest/src/test/java/io/gravitee/rest/api/management/v2/rest/mapper/PortalNavigationMapperTest.java deleted file mode 100644 index 8d659d6c061..00000000000 --- a/gravitee-apim-rest-api/gravitee-apim-rest-api-management-v2/gravitee-apim-rest-api-management-v2-rest/src/test/java/io/gravitee/rest/api/management/v2/rest/mapper/PortalNavigationMapperTest.java +++ /dev/null @@ -1,135 +0,0 @@ -/* - * 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.rest.api.management.v2.rest.mapper; - -import static org.assertj.core.api.Assertions.assertThat; - -import fixtures.core.model.PortalNavigationItemFixtures; -import io.gravitee.apim.core.portal_page.model.PortalArea; -import io.gravitee.apim.core.portal_page.model.PortalNavigationItemId; -import io.gravitee.apim.core.portal_page.model.PortalNavigationLink; -import io.gravitee.rest.api.management.v2.rest.model.BasePortalNavigationItem; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayNameGeneration; -import org.junit.jupiter.api.DisplayNameGenerator; -import org.junit.jupiter.api.Test; - -/** - * @author GraviteeSource Team - */ -@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) -class PortalNavigationMapperTest { - - private PortalNavigationMapper mapper; - - @BeforeEach - void setUp() { - mapper = PortalNavigationMapper.INSTANCE; - } - - @Test - void should_map_portal_navigation_page() { - String pageId = "12345678-1234-1234-1234-123456789abc"; - var page = PortalNavigationItemFixtures.aPage(pageId, "My Page", null); - page.setOrder(1); - - var result = mapper.map(page); - - assertThat(result).isInstanceOf(io.gravitee.rest.api.management.v2.rest.model.PortalNavigationPage.class); - assertThat(result.getId()).isEqualTo(pageId); - assertThat(result.getOrganizationId()).isEqualTo("org-id"); - assertThat(result.getEnvironmentId()).isEqualTo("env-id"); - assertThat(result.getTitle()).isEqualTo("My Page"); - assertThat(result.getType()).isEqualTo(BasePortalNavigationItem.TypeEnum.PAGE); - assertThat(result.getArea()).isEqualTo(io.gravitee.rest.api.management.v2.rest.model.PortalArea.TOP_NAVBAR); - assertThat(result.getOrder()).isEqualTo(1); - assertThat(result.getParentId()).isNull(); - assertThat(result.getConfiguration()).isNotNull(); - assertThat(result.getConfiguration().getPortalPageContentId()).isEqualTo(page.getPortalPageContentId().toString()); - } - - @Test - void should_map_portal_navigation_folder() { - String folderId = "87654321-4321-4321-4321-cba987654321"; - var folder = PortalNavigationItemFixtures.aFolder(folderId, "My Folder"); - folder.setOrder(2); - - var result = mapper.map(folder); - - assertThat(result).isInstanceOf(io.gravitee.rest.api.management.v2.rest.model.PortalNavigationFolder.class); - assertThat(result.getId()).isEqualTo(folderId); - assertThat(result.getOrganizationId()).isEqualTo("org-id"); - assertThat(result.getEnvironmentId()).isEqualTo("env-id"); - assertThat(result.getTitle()).isEqualTo("My Folder"); - assertThat(result.getType()).isEqualTo(BasePortalNavigationItem.TypeEnum.FOLDER); - assertThat(result.getArea()).isEqualTo(io.gravitee.rest.api.management.v2.rest.model.PortalArea.TOP_NAVBAR); - assertThat(result.getOrder()).isEqualTo(2); - assertThat(result.getParentId()).isNull(); - } - - @Test - void should_map_portal_navigation_link() { - String linkId = "abcd1234-5678-9012-3456-789012345678"; - var link = new PortalNavigationLink( - PortalNavigationItemId.of(linkId), - "org-id", - "env-id", - "My Link", - PortalArea.TOP_NAVBAR, - "https://example.com" - ); - link.setOrder(3); - - var result = mapper.map(link); - - assertThat(result).isInstanceOf(io.gravitee.rest.api.management.v2.rest.model.PortalNavigationLink.class); - assertThat(result.getId()).isEqualTo(linkId); - assertThat(result.getOrganizationId()).isEqualTo("org-id"); - assertThat(result.getEnvironmentId()).isEqualTo("env-id"); - assertThat(result.getTitle()).isEqualTo("My Link"); - assertThat(result.getType()).isEqualTo(BasePortalNavigationItem.TypeEnum.LINK); - assertThat(result.getArea()).isEqualTo(io.gravitee.rest.api.management.v2.rest.model.PortalArea.TOP_NAVBAR); - assertThat(result.getOrder()).isEqualTo(3); - assertThat(result.getParentId()).isNull(); - assertThat(result.getConfiguration()).isNotNull(); - assertThat(result.getConfiguration().getUrl()).isEqualTo("https://example.com"); - } - - @Test - void should_map_list_of_portal_navigation_items() { - var items = PortalNavigationItemFixtures.sampleNavigationItems(); - - var result = mapper.map(items); - - assertThat(result).hasSize(8); - // Check that all items are mapped correctly - assertThat( - result - .stream() - .map(i -> (BasePortalNavigationItem) i.getActualInstance()) - .map(BasePortalNavigationItem::getId) - ).containsExactlyInAnyOrder( - "00000000-0000-0000-0000-000000000001", - "00000000-0000-0000-0000-000000000002", - "00000000-0000-0000-0000-000000000003", - "00000000-0000-0000-0000-000000000004", - "00000000-0000-0000-0000-000000000005", - "00000000-0000-0000-0000-000000000006", - "00000000-0000-0000-0000-000000000007", - "00000000-0000-0000-0000-000000000008" - ); - } -} diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-management-v2/gravitee-apim-rest-api-management-v2-rest/src/test/java/io/gravitee/rest/api/management/v2/rest/resource/environment/PortalNavigationItemsResource_CreateTest.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-management-v2/gravitee-apim-rest-api-management-v2-rest/src/test/java/io/gravitee/rest/api/management/v2/rest/resource/environment/PortalNavigationItemsResource_CreateTest.java new file mode 100644 index 00000000000..c1220eb68f0 --- /dev/null +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-management-v2/gravitee-apim-rest-api-management-v2-rest/src/test/java/io/gravitee/rest/api/management/v2/rest/resource/environment/PortalNavigationItemsResource_CreateTest.java @@ -0,0 +1,200 @@ +/* + * 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.rest.api.management.v2.rest.resource.environment; + +import static assertions.MAPIAssertions.assertThat; +import static io.gravitee.common.http.HttpStatusCode.CREATED_201; +import static io.gravitee.common.http.HttpStatusCode.FORBIDDEN_403; +import static jakarta.ws.rs.client.Entity.json; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +import fixtures.PortalNavigationItemsFixtures; +import fixtures.core.model.PortalNavigationItemFixtures; +import io.gravitee.apim.core.portal_page.model.PortalNavigationItem; +import io.gravitee.apim.core.portal_page.use_case.CreatePortalNavigationItemUseCase; +import io.gravitee.rest.api.management.v2.rest.mapper.PortalNavigationItemsMapper; +import io.gravitee.rest.api.management.v2.rest.model.CreatePortalNavigationLink; +import io.gravitee.rest.api.management.v2.rest.model.CreatePortalNavigationPage; +import io.gravitee.rest.api.management.v2.rest.model.PortalNavigationItemType; +import io.gravitee.rest.api.management.v2.rest.model.PortalNavigationLinkAllOfConfiguration; +import io.gravitee.rest.api.management.v2.rest.model.PortalNavigationPageAllOfConfiguration; +import io.gravitee.rest.api.management.v2.rest.resource.AbstractResourceTest; +import io.gravitee.rest.api.model.EnvironmentEntity; +import io.gravitee.rest.api.model.permissions.RolePermission; +import io.gravitee.rest.api.model.permissions.RolePermissionAction; +import io.gravitee.rest.api.service.EnvironmentService; +import io.gravitee.rest.api.service.common.GraviteeContext; +import jakarta.inject.Inject; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.Response; +import java.util.UUID; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +/** + * @author GraviteeSource Team + */ +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class PortalNavigationItemsResource_CreateTest extends AbstractResourceTest { + + private static final String ENVIRONMENT = "environment-id"; + + @Inject + private CreatePortalNavigationItemUseCase createPortalNavigationItemUseCase; + + @Inject + private EnvironmentService environmentService; + + private WebTarget target; + + @Override + protected String contextPath() { + return "/environments/" + ENVIRONMENT + "/portal-navigation-items"; + } + + @BeforeEach + public void setUp() { + target = rootTarget(); + + EnvironmentEntity environmentEntity = EnvironmentEntity.builder().id(ENVIRONMENT).organizationId(ORGANIZATION).build(); + when(environmentService.findById(ENVIRONMENT)).thenReturn(environmentEntity); + when(environmentService.findByOrgAndIdOrHrid(ORGANIZATION, ENVIRONMENT)).thenReturn(environmentEntity); + + GraviteeContext.setCurrentEnvironment(ENVIRONMENT); + GraviteeContext.setCurrentOrganization(ORGANIZATION); + + when( + permissionService.hasPermission( + GraviteeContext.getExecutionContext(), + RolePermission.ENVIRONMENT_DOCUMENTATION, + ENVIRONMENT, + RolePermissionAction.UPDATE + ) + ).thenReturn(true); + } + + @AfterEach + public void tearDown() { + GraviteeContext.cleanContext(); + } + + @Test + void should_not_create_portal_navigation_items_when_no_permission() { + // Given + final var folder = PortalNavigationItemFixtures.aFolder(UUID.randomUUID().toString(), "My Folder"); + when( + permissionService.hasPermission( + GraviteeContext.getExecutionContext(), + RolePermission.ENVIRONMENT_DOCUMENTATION, + ENVIRONMENT, + RolePermissionAction.UPDATE + ) + ).thenReturn(false); + + // When + Response response = target.request().post(json(folder)); + + // Then + assertThat(response).hasStatus(FORBIDDEN_403); + } + + @Test + void should_create_portal_navigation_page() { + // Given + final var page = PortalNavigationItemsFixtures.aCreatePortalNavigationPage(); + + final var output = PortalNavigationItem.from(PortalNavigationItemsMapper.INSTANCE.map(page), ENVIRONMENT, ORGANIZATION); + when(createPortalNavigationItemUseCase.execute(any())).thenReturn(new CreatePortalNavigationItemUseCase.Output(output)); + + // When + Response response = target.request().post(json(page)); + + // Then + assertThat(response).hasStatus(CREATED_201); + + final var item = response.readEntity(io.gravitee.rest.api.management.v2.rest.model.PortalNavigationPage.class); + assertThat(item) + .isNotNull() + .hasFieldOrPropertyWithValue("id", page.getId()) + .hasFieldOrPropertyWithValue("title", page.getTitle()) + .hasFieldOrPropertyWithValue("type", PortalNavigationItemType.PAGE) + .hasFieldOrPropertyWithValue( + "configuration", + new PortalNavigationPageAllOfConfiguration().portalPageContentId(((CreatePortalNavigationPage) page).getContentId()) + ) + .hasFieldOrPropertyWithValue("parentId", page.getParentId()) + .hasFieldOrPropertyWithValue("order", page.getOrder()) + .hasFieldOrPropertyWithValue("area", io.gravitee.rest.api.management.v2.rest.model.PortalArea.TOP_NAVBAR); + } + + @Test + void should_create_portal_navigation_folder() { + // Given + final var folder = PortalNavigationItemsFixtures.aCreatePortalNavigationFolder(); + + final var output = PortalNavigationItem.from(PortalNavigationItemsMapper.INSTANCE.map(folder), ENVIRONMENT, ORGANIZATION); + when(createPortalNavigationItemUseCase.execute(any())).thenReturn(new CreatePortalNavigationItemUseCase.Output(output)); + + // When + Response response = target.request().post(json(folder)); + + // Then + assertThat(response).hasStatus(CREATED_201); + + final var item = response.readEntity(io.gravitee.rest.api.management.v2.rest.model.PortalNavigationFolder.class); + assertThat(item) + .isNotNull() + .hasFieldOrPropertyWithValue("id", folder.getId()) + .hasFieldOrPropertyWithValue("title", folder.getTitle()) + .hasFieldOrPropertyWithValue("type", PortalNavigationItemType.FOLDER) + .hasFieldOrPropertyWithValue("parentId", folder.getParentId()) + .hasFieldOrPropertyWithValue("order", folder.getOrder()) + .hasFieldOrPropertyWithValue("area", io.gravitee.rest.api.management.v2.rest.model.PortalArea.TOP_NAVBAR); + } + + @Test + void should_create_portal_navigation_link() { + // Given + final var link = PortalNavigationItemsFixtures.aCreatePortalNavigationLink(); + + final var output = PortalNavigationItem.from(PortalNavigationItemsMapper.INSTANCE.map(link), ENVIRONMENT, ORGANIZATION); + when(createPortalNavigationItemUseCase.execute(any())).thenReturn(new CreatePortalNavigationItemUseCase.Output(output)); + + // When + Response response = target.request().post(json(link)); + + // Then + assertThat(response).hasStatus(CREATED_201); + + final var item = response.readEntity(io.gravitee.rest.api.management.v2.rest.model.PortalNavigationLink.class); + assertThat(item) + .isNotNull() + .hasFieldOrPropertyWithValue("id", link.getId()) + .hasFieldOrPropertyWithValue("title", link.getTitle()) + .hasFieldOrPropertyWithValue("type", PortalNavigationItemType.LINK) + .hasFieldOrPropertyWithValue( + "configuration", + new PortalNavigationLinkAllOfConfiguration().url(((CreatePortalNavigationLink) link).getUrl().toString()) + ) + .hasFieldOrPropertyWithValue("parentId", link.getParentId()) + .hasFieldOrPropertyWithValue("order", link.getOrder()) + .hasFieldOrPropertyWithValue("area", io.gravitee.rest.api.management.v2.rest.model.PortalArea.TOP_NAVBAR); + } +} diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-management-v2/gravitee-apim-rest-api-management-v2-rest/src/test/java/io/gravitee/rest/api/management/v2/rest/resource/environment/PortalNavigationItemsResourceTest.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-management-v2/gravitee-apim-rest-api-management-v2-rest/src/test/java/io/gravitee/rest/api/management/v2/rest/resource/environment/PortalNavigationItemsResource_GetTest.java similarity index 95% rename from gravitee-apim-rest-api/gravitee-apim-rest-api-management-v2/gravitee-apim-rest-api-management-v2-rest/src/test/java/io/gravitee/rest/api/management/v2/rest/resource/environment/PortalNavigationItemsResourceTest.java rename to gravitee-apim-rest-api/gravitee-apim-rest-api-management-v2/gravitee-apim-rest-api-management-v2-rest/src/test/java/io/gravitee/rest/api/management/v2/rest/resource/environment/PortalNavigationItemsResource_GetTest.java index 60b0cec9b85..6644a1e9f5d 100644 --- a/gravitee-apim-rest-api/gravitee-apim-rest-api-management-v2/gravitee-apim-rest-api-management-v2-rest/src/test/java/io/gravitee/rest/api/management/v2/rest/resource/environment/PortalNavigationItemsResourceTest.java +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-management-v2/gravitee-apim-rest-api-management-v2-rest/src/test/java/io/gravitee/rest/api/management/v2/rest/resource/environment/PortalNavigationItemsResource_GetTest.java @@ -16,12 +16,15 @@ package io.gravitee.rest.api.management.v2.rest.resource.environment; import static assertions.MAPIAssertions.assertThat; +import static io.gravitee.common.http.HttpStatusCode.CREATED_201; import static io.gravitee.common.http.HttpStatusCode.FORBIDDEN_403; import static io.gravitee.common.http.HttpStatusCode.OK_200; +import static jakarta.ws.rs.client.Entity.json; import static org.mockito.Mockito.when; import fixtures.core.model.PortalNavigationItemFixtures; import io.gravitee.apim.core.portal_page.model.PortalArea; +import io.gravitee.apim.core.portal_page.use_case.CreatePortalNavigationItemUseCase; import io.gravitee.apim.core.portal_page.use_case.ListPortalNavigationItemsUseCase; import io.gravitee.rest.api.management.v2.rest.model.PortalNavigationItemsResponse; import io.gravitee.rest.api.management.v2.rest.resource.AbstractResourceTest; @@ -33,6 +36,7 @@ import jakarta.inject.Inject; import jakarta.ws.rs.client.WebTarget; import jakarta.ws.rs.core.Response; +import java.util.UUID; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayNameGeneration; @@ -44,10 +48,13 @@ * @author GraviteeSource Team */ @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) -class PortalNavigationItemsResourceTest extends AbstractResourceTest { +class PortalNavigationItemsResource_GetTest extends AbstractResourceTest { private static final String ENVIRONMENT = "environment-id"; + @Inject + private CreatePortalNavigationItemUseCase createPortalNavigationItemUseCase; + @Inject private ListPortalNavigationItemsUseCase listPortalNavigationItemsUseCase; @@ -106,7 +113,7 @@ void should_return_portal_navigation_items() { assertThat(response) .hasStatus(OK_200) .asEntity(PortalNavigationItemsResponse.class) - .satisfies(entity -> assertThat(entity.getItems()).hasSize(8)); + .satisfies(entity -> assertThat(entity.getItems()).hasSize(9)); var capturedInput = inputCaptor.getValue(); assertThat(capturedInput.environmentId()).isEqualTo(ENVIRONMENT); @@ -239,7 +246,7 @@ void should_return_portal_navigation_items_with_parent_id_and_loading_children() assertThat(response) .hasStatus(OK_200) .asEntity(PortalNavigationItemsResponse.class) - .satisfies(entity -> assertThat(entity.getItems()).hasSize(8)); + .satisfies(entity -> assertThat(entity.getItems()).hasSize(9)); var capturedInput = inputCaptor.getValue(); assertThat(capturedInput.environmentId()).isEqualTo(ENVIRONMENT); @@ -250,7 +257,7 @@ void should_return_portal_navigation_items_with_parent_id_and_loading_children() } @Test - void should_return_forbidden_when_no_permission() { + void should_not_return_portal_navigation_items_when_no_permission() { // Given when( permissionService.hasPermission( diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-management-v2/gravitee-apim-rest-api-management-v2-rest/src/test/java/io/gravitee/rest/api/management/v2/rest/spring/ResourceContextConfiguration.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-management-v2/gravitee-apim-rest-api-management-v2-rest/src/test/java/io/gravitee/rest/api/management/v2/rest/spring/ResourceContextConfiguration.java index fb36a3735d5..df265b234be 100644 --- a/gravitee-apim-rest-api/gravitee-apim-rest-api-management-v2/gravitee-apim-rest-api-management-v2-rest/src/test/java/io/gravitee/rest/api/management/v2/rest/spring/ResourceContextConfiguration.java +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-management-v2/gravitee-apim-rest-api-management-v2-rest/src/test/java/io/gravitee/rest/api/management/v2/rest/spring/ResourceContextConfiguration.java @@ -109,6 +109,8 @@ import io.gravitee.apim.core.plugin.crud_service.PolicyPluginCrudService; import io.gravitee.apim.core.plugin.domain_service.EndpointConnectorPluginDomainService; import io.gravitee.apim.core.policy.domain_service.PolicyValidationDomainService; +import io.gravitee.apim.core.portal_page.domain_service.CreatePortalNavigationItemValidatorService; +import io.gravitee.apim.core.portal_page.use_case.CreatePortalNavigationItemUseCase; import io.gravitee.apim.core.portal_page.use_case.GetPortalPageContentUseCase; import io.gravitee.apim.core.portal_page.use_case.ListPortalNavigationItemsUseCase; import io.gravitee.apim.core.promotion.service_provider.CockpitPromotionServiceProvider; @@ -859,6 +861,16 @@ public CreatePromotionUseCase createPromotionUseCase() { return mock(CreatePromotionUseCase.class); } + @Bean + public CreatePortalNavigationItemUseCase createPortalNavigationItemUseCase() { + return mock(CreatePortalNavigationItemUseCase.class); + } + + @Bean + public CreatePortalNavigationItemValidatorService createPortalNavigationItemValidatorService() { + return mock(CreatePortalNavigationItemValidatorService.class); + } + @Bean public ListPortalNavigationItemsUseCase listPortalNavigationItemsUseCase() { return mock(ListPortalNavigationItemsUseCase.class); diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-management/gravitee-apim-rest-api-management-rest/src/test/java/io/gravitee/rest/api/management/rest/spring/ResourceContextConfiguration.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-management/gravitee-apim-rest-api-management-rest/src/test/java/io/gravitee/rest/api/management/rest/spring/ResourceContextConfiguration.java index 4c82dfb247a..ab5335f4c3d 100644 --- a/gravitee-apim-rest-api/gravitee-apim-rest-api-management/gravitee-apim-rest-api-management-rest/src/test/java/io/gravitee/rest/api/management/rest/spring/ResourceContextConfiguration.java +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-management/gravitee-apim-rest-api-management-rest/src/test/java/io/gravitee/rest/api/management/rest/spring/ResourceContextConfiguration.java @@ -86,6 +86,8 @@ import io.gravitee.apim.core.plugin.crud_service.PolicyPluginCrudService; import io.gravitee.apim.core.plugin.domain_service.EndpointConnectorPluginDomainService; import io.gravitee.apim.core.policy.domain_service.PolicyValidationDomainService; +import io.gravitee.apim.core.portal_page.domain_service.CreatePortalNavigationItemValidatorService; +import io.gravitee.apim.core.portal_page.use_case.CreatePortalNavigationItemUseCase; import io.gravitee.apim.core.portal_page.use_case.GetPortalPageContentUseCase; import io.gravitee.apim.core.portal_page.use_case.ListPortalNavigationItemsUseCase; import io.gravitee.apim.core.promotion.service_provider.CockpitPromotionServiceProvider; @@ -995,6 +997,16 @@ public CreatePromotionUseCase createPromotionUseCase() { return mock(CreatePromotionUseCase.class); } + @Bean + public CreatePortalNavigationItemUseCase createPortalNavigationItemUseCase() { + return mock(CreatePortalNavigationItemUseCase.class); + } + + @Bean + public CreatePortalNavigationItemValidatorService createPortalNavigationItemValidatorService() { + return mock(CreatePortalNavigationItemValidatorService.class); + } + @Bean public ListPortalNavigationItemsUseCase listPortalNavigationItemsUseCase() { return mock(ListPortalNavigationItemsUseCase.class); diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-portal/gravitee-apim-rest-api-portal-rest/src/test/java/io/gravitee/rest/api/portal/rest/spring/ResourceContextConfiguration.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-portal/gravitee-apim-rest-api-portal-rest/src/test/java/io/gravitee/rest/api/portal/rest/spring/ResourceContextConfiguration.java index 88a09f6607a..54279468a87 100644 --- a/gravitee-apim-rest-api/gravitee-apim-rest-api-portal/gravitee-apim-rest-api-portal-rest/src/test/java/io/gravitee/rest/api/portal/rest/spring/ResourceContextConfiguration.java +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-portal/gravitee-apim-rest-api-portal-rest/src/test/java/io/gravitee/rest/api/portal/rest/spring/ResourceContextConfiguration.java @@ -85,6 +85,8 @@ import io.gravitee.apim.core.plugin.crud_service.PolicyPluginCrudService; import io.gravitee.apim.core.plugin.domain_service.EndpointConnectorPluginDomainService; import io.gravitee.apim.core.policy.domain_service.PolicyValidationDomainService; +import io.gravitee.apim.core.portal_page.domain_service.CreatePortalNavigationItemValidatorService; +import io.gravitee.apim.core.portal_page.use_case.CreatePortalNavigationItemUseCase; import io.gravitee.apim.core.portal_page.use_case.GetPortalPageContentUseCase; import io.gravitee.apim.core.portal_page.use_case.ListPortalNavigationItemsUseCase; import io.gravitee.apim.core.promotion.service_provider.CockpitPromotionServiceProvider; @@ -956,6 +958,16 @@ public CreatePromotionUseCase createPromotionUseCase() { return mock(CreatePromotionUseCase.class); } + @Bean + public CreatePortalNavigationItemUseCase createPortalNavigationItemUseCase() { + return mock(CreatePortalNavigationItemUseCase.class); + } + + @Bean + public CreatePortalNavigationItemValidatorService createPortalNavigationItemValidatorService() { + return mock(CreatePortalNavigationItemValidatorService.class); + } + @Bean public ListPortalNavigationItemsUseCase listPortalNavigationItemsUseCase() { return mock(ListPortalNavigationItemsUseCase.class); diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/exception/ConflictDomainException.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/exception/ConflictDomainException.java new file mode 100644 index 00000000000..f2fc99d4696 --- /dev/null +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/exception/ConflictDomainException.java @@ -0,0 +1,35 @@ +/* + * 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.exception; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class ConflictDomainException extends AbstractDomainException { + + private String id; + + public ConflictDomainException(String message) { + super(message); + } + + public ConflictDomainException(String message, String id) { + super(message); + this.setId(id); + } +} diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/portal_page/crud_service/PortalNavigationItemCrudService.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/portal_page/crud_service/PortalNavigationItemCrudService.java new file mode 100644 index 00000000000..807c2514878 --- /dev/null +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/portal_page/crud_service/PortalNavigationItemCrudService.java @@ -0,0 +1,27 @@ +/* + * 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.portal_page.crud_service; + +import io.gravitee.apim.core.portal_page.model.PortalNavigationItem; +import io.gravitee.apim.core.portal_page.model.PortalNavigationItemId; + +public interface PortalNavigationItemCrudService { + PortalNavigationItem create(PortalNavigationItem portalNavigationItem); + + PortalNavigationItem update(PortalNavigationItem portalNavigationItem); + + void delete(PortalNavigationItemId portalNavigationItemId); +} diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/portal_page/domain_service/CreatePortalNavigationItemValidatorService.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/portal_page/domain_service/CreatePortalNavigationItemValidatorService.java new file mode 100644 index 00000000000..3a8253b5f8f --- /dev/null +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/portal_page/domain_service/CreatePortalNavigationItemValidatorService.java @@ -0,0 +1,85 @@ +/* + * 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.portal_page.domain_service; + +import io.gravitee.apim.core.DomainService; +import io.gravitee.apim.core.portal_page.exception.HomepageAlreadyExistsException; +import io.gravitee.apim.core.portal_page.exception.ItemAlreadyExistsException; +import io.gravitee.apim.core.portal_page.exception.ParentAreaMismatchException; +import io.gravitee.apim.core.portal_page.exception.ParentNotFoundException; +import io.gravitee.apim.core.portal_page.exception.ParentTypeMismatchException; +import io.gravitee.apim.core.portal_page.model.CreatePortalNavigationItem; +import io.gravitee.apim.core.portal_page.model.PortalArea; +import io.gravitee.apim.core.portal_page.model.PortalNavigationFolder; +import io.gravitee.apim.core.portal_page.model.PortalNavigationItemType; +import io.gravitee.apim.core.portal_page.model.PortalPageContentId; +import io.gravitee.apim.core.portal_page.query_service.PortalNavigationItemsQueryService; +import lombok.RequiredArgsConstructor; + +@DomainService +@RequiredArgsConstructor +public class CreatePortalNavigationItemValidatorService { + + private final PortalNavigationItemsQueryService queryService; + + public void validate(CreatePortalNavigationItem item, String environmentId) { + validateItem(item, environmentId); + validateParent(item, environmentId); + } + + private void validateItem(CreatePortalNavigationItem item, String environmentId) { + final var itemId = item.getId(); + if (itemId != null) { + final var existingItem = this.queryService.findByIdAndEnvironmentId(environmentId, itemId); + if (existingItem != null) { + throw new ItemAlreadyExistsException(itemId.toString()); + } + } + + if (item.getArea().equals(PortalArea.HOMEPAGE)) { + final var existingHomepage = this.queryService.findTopLevelItemsByEnvironmentIdAndPortalArea(environmentId, item.getArea()); + if (!existingHomepage.isEmpty()) { + throw new HomepageAlreadyExistsException(); + } + } + + if (item.getType() == PortalNavigationItemType.PAGE) { + final var contentId = item.getContentId(); + // TODO check if content exists and create one otherwise, currently assigning a random id to avoid repo level errors + if (contentId == null) { + item.setContentId(PortalPageContentId.random()); + } + } + } + + private void validateParent(CreatePortalNavigationItem item, String environmentId) { + final var parentId = item.getParentId(); + if (parentId == null) { + return; + } + + final var parentItem = this.queryService.findByIdAndEnvironmentId(environmentId, parentId); + if (parentItem == null) { + throw new ParentNotFoundException(parentId.toString()); + } + if (!(parentItem instanceof PortalNavigationFolder)) { + throw new ParentTypeMismatchException(parentId.toString()); + } + if (!parentItem.getArea().equals(item.getArea())) { + throw new ParentAreaMismatchException(parentId.toString()); + } + } +} diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/portal_page/exception/PortalPageSpecificationException.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/portal_page/exception/HomepageAlreadyExistsException.java similarity index 74% rename from gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/portal_page/exception/PortalPageSpecificationException.java rename to gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/portal_page/exception/HomepageAlreadyExistsException.java index ce92ad8fd7f..be1be306eda 100644 --- a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/portal_page/exception/PortalPageSpecificationException.java +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/portal_page/exception/HomepageAlreadyExistsException.java @@ -15,11 +15,11 @@ */ package io.gravitee.apim.core.portal_page.exception; -import io.gravitee.apim.core.exception.AbstractDomainException; +import io.gravitee.apim.core.exception.ConflictDomainException; -public class PortalPageSpecificationException extends AbstractDomainException { +public class HomepageAlreadyExistsException extends ConflictDomainException { - public PortalPageSpecificationException(String message) { - super(message); + public HomepageAlreadyExistsException() { + super("Homepage already exists"); } } diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/portal_page/exception/ItemAlreadyExistsException.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/portal_page/exception/ItemAlreadyExistsException.java new file mode 100644 index 00000000000..5d6753f233f --- /dev/null +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/portal_page/exception/ItemAlreadyExistsException.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.portal_page.exception; + +import io.gravitee.apim.core.exception.ConflictDomainException; + +public class ItemAlreadyExistsException extends ConflictDomainException { + + public ItemAlreadyExistsException(String itemId) { + super("Item with provided id already exists", itemId); + } +} diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/portal_page/exception/ParentAreaMismatchException.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/portal_page/exception/ParentAreaMismatchException.java new file mode 100644 index 00000000000..1aca6a5b5d1 --- /dev/null +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/portal_page/exception/ParentAreaMismatchException.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.portal_page.exception; + +import io.gravitee.apim.core.exception.ValidationDomainException; + +public class ParentAreaMismatchException extends ValidationDomainException { + + public ParentAreaMismatchException(String parentId) { + super(String.format("Parent item with id %s belongs to a different area than the child item", parentId)); + } +} diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/portal_page/exception/ParentNotFoundException.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/portal_page/exception/ParentNotFoundException.java new file mode 100644 index 00000000000..ee1a5e74912 --- /dev/null +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/portal_page/exception/ParentNotFoundException.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.portal_page.exception; + +import io.gravitee.apim.core.exception.NotFoundDomainException; + +public class ParentNotFoundException extends NotFoundDomainException { + + public ParentNotFoundException(String id) { + super("Parent item with provided id does not exist", id); + } +} diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/portal_page/exception/ParentTypeMismatchException.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/portal_page/exception/ParentTypeMismatchException.java new file mode 100644 index 00000000000..e77d5d392b3 --- /dev/null +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/portal_page/exception/ParentTypeMismatchException.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.portal_page.exception; + +import io.gravitee.apim.core.exception.ValidationDomainException; + +public class ParentTypeMismatchException extends ValidationDomainException { + + public ParentTypeMismatchException(String parentId) { + super(String.format("Parent item with id %s is not a folder", parentId)); + } +} diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/portal_page/model/CreatePortalNavigationItem.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/portal_page/model/CreatePortalNavigationItem.java new file mode 100644 index 00000000000..e0217846bf4 --- /dev/null +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/portal_page/model/CreatePortalNavigationItem.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.portal_page.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder(toBuilder = true) +public final class CreatePortalNavigationItem { + + private PortalNavigationItemId id; + private String title; + private PortalArea area; + private Integer order; + private PortalNavigationItemType type; + private PortalNavigationItemId parentId; + private PortalPageContentId contentId; + private String url; +} diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/portal_page/model/PortalNavigationFolder.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/portal_page/model/PortalNavigationFolder.java index a50b8326a3b..a862ec11010 100644 --- a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/portal_page/model/PortalNavigationFolder.java +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/portal_page/model/PortalNavigationFolder.java @@ -24,8 +24,9 @@ public PortalNavigationFolder( @Nonnull String organizationId, @Nonnull String environmentId, @Nonnull String title, - @Nonnull PortalArea area + @Nonnull PortalArea area, + @Nonnull Integer order ) { - super(id, organizationId, environmentId, title, area); + super(id, organizationId, environmentId, title, area, order); } } diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/portal_page/model/PortalNavigationItem.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/portal_page/model/PortalNavigationItem.java index 84e27f153e7..7e4592ea775 100644 --- a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/portal_page/model/PortalNavigationItem.java +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/portal_page/model/PortalNavigationItem.java @@ -17,6 +17,7 @@ import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; +import java.util.Optional; import lombok.Getter; import lombok.Setter; @@ -43,7 +44,7 @@ public abstract sealed class PortalNavigationItem permits PortalNavigationPage, private PortalArea area; @Setter - @Nullable + @Nonnull private Integer order; @Setter @@ -55,13 +56,15 @@ protected PortalNavigationItem( @Nonnull String organizationId, @Nonnull String environmentId, @Nonnull String title, - @Nonnull PortalArea area + @Nonnull PortalArea area, + @Nonnull Integer order ) { this.id = id; this.organizationId = organizationId; this.environmentId = environmentId; this.title = title; this.area = area; + this.order = order; } @Override @@ -81,4 +84,23 @@ public int hashCode() { public String toString() { return "PortalNavigationItem[id=" + id + ", title=" + title + "]"; } + + public static PortalNavigationItem from(CreatePortalNavigationItem item, String organizationId, String environmentId) { + final var id = Optional.ofNullable(item.getId()).orElse(PortalNavigationItemId.random()); + final var title = item.getTitle(); + final var area = item.getArea(); + final var parentId = item.getParentId(); + final var contentId = item.getContentId(); + final var url = item.getUrl(); + final var order = item.getOrder(); + + final var newItem = switch (item.getType()) { + case FOLDER -> new PortalNavigationFolder(id, organizationId, environmentId, title, area, order); + case PAGE -> new PortalNavigationPage(id, organizationId, environmentId, title, area, order, contentId); + case LINK -> new PortalNavigationLink(id, organizationId, environmentId, title, area, order, url); + }; + newItem.setParentId(parentId); + + return newItem; + } } diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/portal_page/model/PortalNavigationItemType.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/portal_page/model/PortalNavigationItemType.java new file mode 100644 index 00000000000..04eb1b85264 --- /dev/null +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/portal_page/model/PortalNavigationItemType.java @@ -0,0 +1,22 @@ +/* + * 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.portal_page.model; + +public enum PortalNavigationItemType { + PAGE, + FOLDER, + LINK, +} diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/portal_page/model/PortalNavigationLink.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/portal_page/model/PortalNavigationLink.java index f503586d61c..1149f62deaa 100644 --- a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/portal_page/model/PortalNavigationLink.java +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/portal_page/model/PortalNavigationLink.java @@ -32,9 +32,10 @@ public PortalNavigationLink( @Nonnull String environmentId, @Nonnull String title, @Nonnull PortalArea area, + @Nonnull Integer order, @Nonnull String url ) { - super(id, organizationId, environmentId, title, area); + super(id, organizationId, environmentId, title, area, order); this.url = url; } } diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/portal_page/model/PortalNavigationPage.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/portal_page/model/PortalNavigationPage.java index da5871d272e..07cc050c8e6 100644 --- a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/portal_page/model/PortalNavigationPage.java +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/portal_page/model/PortalNavigationPage.java @@ -32,9 +32,10 @@ public PortalNavigationPage( @Nonnull String environmentId, @Nonnull String title, @Nonnull PortalArea area, + @Nonnull Integer order, @Nonnull PortalPageContentId portalPageContentId ) { - super(id, organizationId, environmentId, title, area); + super(id, organizationId, environmentId, title, area, order); this.portalPageContentId = portalPageContentId; } } diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/portal_page/query_service/PortalNavigationItemsQueryService.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/portal_page/query_service/PortalNavigationItemsQueryService.java index ae73b793340..4ca0b865686 100644 --- a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/portal_page/query_service/PortalNavigationItemsQueryService.java +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/portal_page/query_service/PortalNavigationItemsQueryService.java @@ -21,6 +21,8 @@ import java.util.List; public interface PortalNavigationItemsQueryService { + PortalNavigationItem findByIdAndEnvironmentId(String environmentId, PortalNavigationItemId id); + List findByParentIdAndEnvironmentId(String environmentId, PortalNavigationItemId id); List findTopLevelItemsByEnvironmentIdAndPortalArea(String environmentId, PortalArea portalArea); diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/portal_page/use_case/CreatePortalNavigationItemUseCase.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/portal_page/use_case/CreatePortalNavigationItemUseCase.java new file mode 100644 index 00000000000..4d7729d3c85 --- /dev/null +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/portal_page/use_case/CreatePortalNavigationItemUseCase.java @@ -0,0 +1,78 @@ +/* + * 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.portal_page.use_case; + +import io.gravitee.apim.core.UseCase; +import io.gravitee.apim.core.portal_page.crud_service.PortalNavigationItemCrudService; +import io.gravitee.apim.core.portal_page.domain_service.CreatePortalNavigationItemValidatorService; +import io.gravitee.apim.core.portal_page.model.CreatePortalNavigationItem; +import io.gravitee.apim.core.portal_page.model.PortalArea; +import io.gravitee.apim.core.portal_page.model.PortalNavigationItem; +import io.gravitee.apim.core.portal_page.model.PortalNavigationItemId; +import io.gravitee.apim.core.portal_page.query_service.PortalNavigationItemsQueryService; +import java.util.List; +import java.util.Objects; +import lombok.Builder; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@UseCase +public class CreatePortalNavigationItemUseCase { + + private final PortalNavigationItemCrudService crudService; + private final PortalNavigationItemsQueryService queryService; + private final CreatePortalNavigationItemValidatorService validatorService; + + public Output execute(Input input) { + final CreatePortalNavigationItem itemToCreate = input.item(); + final var organizationId = input.organizationId(); + final var environmentId = input.environmentId(); + + validatorService.validate(itemToCreate, environmentId); + + final var order = itemToCreate.getOrder(); + // Order is zero based, so new max order == size() + final var newMaxOrder = this.retrieveSiblingItems(itemToCreate.getParentId(), environmentId, itemToCreate.getArea()).size(); + // Limit the new item's order to at most new max order + itemToCreate.setOrder(order == null ? newMaxOrder : Math.min(order, newMaxOrder)); + + final var newItem = PortalNavigationItem.from(itemToCreate, organizationId, environmentId); + final var output = new Output(this.crudService.create(newItem)); + + // Update orders of all following sibling items + this.retrieveSiblingItems(newItem.getParentId(), newItem.getEnvironmentId(), itemToCreate.getArea()) + .stream() + .filter(item -> !Objects.equals(item.getId(), newItem.getId())) + .filter(sibling -> sibling.getOrder() >= newItem.getOrder()) + .forEach(followingSibling -> { + followingSibling.setOrder(followingSibling.getOrder() + 1); + this.crudService.update(followingSibling); + }); + + return output; + } + + @Builder + public record Input(String organizationId, String environmentId, CreatePortalNavigationItem item) {} + + public record Output(PortalNavigationItem item) {} + + private List retrieveSiblingItems(PortalNavigationItemId parentId, String environmentId, PortalArea area) { + return parentId != null + ? queryService.findByParentIdAndEnvironmentId(environmentId, parentId) + : queryService.findTopLevelItemsByEnvironmentIdAndPortalArea(environmentId, area); + } +} diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/infra/query_service/portal_page/PortalNavigationItemsCrudServiceImpl.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/infra/query_service/portal_page/PortalNavigationItemsCrudServiceImpl.java new file mode 100644 index 00000000000..d0f946bfaa4 --- /dev/null +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/infra/query_service/portal_page/PortalNavigationItemsCrudServiceImpl.java @@ -0,0 +1,85 @@ +/* + * 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.infra.query_service.portal_page; + +import io.gravitee.apim.core.exception.TechnicalDomainException; +import io.gravitee.apim.core.portal_page.crud_service.PortalNavigationItemCrudService; +import io.gravitee.apim.core.portal_page.model.PortalNavigationItem; +import io.gravitee.apim.core.portal_page.model.PortalNavigationItemId; +import io.gravitee.apim.infra.adapter.PortalNavigationItemAdapter; +import io.gravitee.repository.exceptions.TechnicalException; +import io.gravitee.repository.management.api.PortalNavigationItemRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; + +@Service +public class PortalNavigationItemsCrudServiceImpl implements PortalNavigationItemCrudService { + + private final PortalNavigationItemRepository portalNavigationItemRepository; + private final PortalNavigationItemAdapter portalNavigationItemAdapter = PortalNavigationItemAdapter.INSTANCE; + private static final Logger logger = LoggerFactory.getLogger(PortalNavigationItemsCrudServiceImpl.class); + + public PortalNavigationItemsCrudServiceImpl(@Lazy final PortalNavigationItemRepository portalNavigationItemRepository) { + this.portalNavigationItemRepository = portalNavigationItemRepository; + } + + @Override + public PortalNavigationItem create(PortalNavigationItem portalNavigationItem) { + try { + final var repoItem = portalNavigationItemAdapter.toRepository(portalNavigationItem); + portalNavigationItemRepository.create(repoItem); + return portalNavigationItem; + } catch (TechnicalException e) { + final var errorMessage = String.format( + "An error occurred while creating portal navigation item with id %s and environmentId %s", + portalNavigationItem.getId(), + portalNavigationItem.getEnvironmentId() + ); + throw new TechnicalDomainException(errorMessage, e); + } + } + + @Override + public PortalNavigationItem update(PortalNavigationItem portalNavigationItem) { + try { + final var repoItem = portalNavigationItemAdapter.toRepository(portalNavigationItem); + portalNavigationItemRepository.update(repoItem); + return portalNavigationItem; + } catch (TechnicalException e) { + final var errorMessage = String.format( + "An error occurred while updating portal navigation item with id %s and environmentId %s", + portalNavigationItem.getId(), + portalNavigationItem.getEnvironmentId() + ); + throw new TechnicalDomainException(errorMessage, e); + } + } + + @Override + public void delete(PortalNavigationItemId portalNavigationItemId) { + try { + portalNavigationItemRepository.delete(portalNavigationItemId.toString()); + } catch (TechnicalException e) { + final var errorMessage = String.format( + "An error occurred while deleting portal navigation item with id %s", + portalNavigationItemId + ); + throw new TechnicalDomainException(errorMessage, e); + } + } +} diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/infra/query_service/portal_page/PortalNavigationItemsQueryServiceImpl.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/infra/query_service/portal_page/PortalNavigationItemsQueryServiceImpl.java index 3810bf0eecf..3797b45db5b 100644 --- a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/infra/query_service/portal_page/PortalNavigationItemsQueryServiceImpl.java +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/infra/query_service/portal_page/PortalNavigationItemsQueryServiceImpl.java @@ -38,6 +38,24 @@ public PortalNavigationItemsQueryServiceImpl(@Lazy final PortalNavigationItemRep this.portalNavigationItemRepository = portalNavigationItemRepository; } + @Override + public PortalNavigationItem findByIdAndEnvironmentId(String environmentId, PortalNavigationItemId id) { + try { + var result = portalNavigationItemRepository.findById(id.json()); + if (result.isPresent() && result.get().getEnvironmentId().equals(environmentId)) { + return portalNavigationItemAdapter.toEntity(result.get()); + } + return null; + } catch (TechnicalException e) { + String errorMessage = String.format( + "An error occurred while finding portal navigation item by id %s and environmentId %s", + id, + environmentId + ); + throw new TechnicalDomainException(errorMessage, e); + } + } + @Override public List findByParentIdAndEnvironmentId(String environmentId, PortalNavigationItemId parentId) { try { diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/fixtures/core/model/PortalNavigationItemFixtures.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/fixtures/core/model/PortalNavigationItemFixtures.java index 9b3a49e4ade..04cfdf1c547 100644 --- a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/fixtures/core/model/PortalNavigationItemFixtures.java +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/fixtures/core/model/PortalNavigationItemFixtures.java @@ -19,14 +19,15 @@ import io.gravitee.apim.core.portal_page.model.PortalNavigationFolder; import io.gravitee.apim.core.portal_page.model.PortalNavigationItem; import io.gravitee.apim.core.portal_page.model.PortalNavigationItemId; +import io.gravitee.apim.core.portal_page.model.PortalNavigationLink; import io.gravitee.apim.core.portal_page.model.PortalNavigationPage; import io.gravitee.apim.core.portal_page.model.PortalPageContentId; import java.util.List; public class PortalNavigationItemFixtures { - private static final String ORG_ID = "org-id"; - private static final String ENV_ID = "env-id"; + public static final String ORG_ID = "org-id"; + public static final String ENV_ID = "env-id"; public static final String APIS_ID = "00000000-0000-0000-0000-000000000001"; private static final String GUIDES_ID = "00000000-0000-0000-0000-000000000002"; @@ -34,15 +35,16 @@ public class PortalNavigationItemFixtures { private static final String OVERVIEW_ID = "00000000-0000-0000-0000-000000000004"; private static final String GETTING_STARTED_ID = "00000000-0000-0000-0000-000000000005"; private static final String CATEGORY1_ID = "00000000-0000-0000-0000-000000000006"; - private static final String PAGE11_ID = "00000000-0000-0000-0000-000000000007"; + public static final String PAGE11_ID = "00000000-0000-0000-0000-000000000007"; private static final String PAGE12_ID = "00000000-0000-0000-0000-000000000008"; + private static final String LINK1_ID = "00000000-0000-0000-0000-000000000009"; public static PortalNavigationFolder aFolder(String id, String title) { return aFolder(id, title, null); } public static PortalNavigationFolder aFolder(String id, String title, PortalNavigationItemId parentId) { - var folder = new PortalNavigationFolder(PortalNavigationItemId.of(id), ORG_ID, ENV_ID, title, PortalArea.TOP_NAVBAR); + var folder = new PortalNavigationFolder(PortalNavigationItemId.of(id), ORG_ID, ENV_ID, title, PortalArea.TOP_NAVBAR, 0); folder.setParentId(parentId); return folder; } @@ -54,24 +56,49 @@ public static PortalNavigationPage aPage(String id, String title, PortalNavigati ENV_ID, title, PortalArea.TOP_NAVBAR, + 0, PortalPageContentId.random() ); page.setParentId(parentId); return page; } + public static PortalNavigationLink aLink(String id, String title, PortalNavigationItemId parentId) { + var link = new PortalNavigationLink( + PortalNavigationItemId.of(id), + ORG_ID, + ENV_ID, + title, + PortalArea.TOP_NAVBAR, + 0, + "http://example.com" + ); + link.setParentId(parentId); + return link; + } + public static List sampleNavigationItems() { var apis = aFolder(APIS_ID, "APIs"); + apis.setOrder(0); var guides = aFolder(GUIDES_ID, "Guides"); + guides.setOrder(1); var support = aPage(SUPPORT_ID, "Support", null); + support.setOrder(2); + var link1 = aLink(LINK1_ID, "Example Link", null); + support.setOrder(3); var overview = aPage(OVERVIEW_ID, "Overview", apis.getId()); + overview.setOrder(0); var gettingStarted = aPage(GETTING_STARTED_ID, "Getting Started", apis.getId()); + gettingStarted.setOrder(1); var category1 = aFolder(CATEGORY1_ID, "Category1", apis.getId()); + category1.setOrder(2); var page11 = aPage(PAGE11_ID, "page11", category1.getId()); + page11.setOrder(0); var page12 = aPage(PAGE12_ID, "page12", category1.getId()); + page12.setOrder(1); - return List.of(apis, guides, support, overview, gettingStarted, category1, page11, page12); + return List.of(apis, guides, support, overview, gettingStarted, category1, page11, page12, link1); } } diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/inmemory/PortalNavigationItemsCrudServiceInMemory.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/inmemory/PortalNavigationItemsCrudServiceInMemory.java new file mode 100644 index 00000000000..f03346f0638 --- /dev/null +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/inmemory/PortalNavigationItemsCrudServiceInMemory.java @@ -0,0 +1,74 @@ +/* + * 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.portal_page.crud_service.PortalNavigationItemCrudService; +import io.gravitee.apim.core.portal_page.model.PortalNavigationItem; +import io.gravitee.apim.core.portal_page.model.PortalNavigationItemId; +import java.util.ArrayList; +import java.util.List; +import java.util.OptionalInt; + +public class PortalNavigationItemsCrudServiceInMemory + implements InMemoryAlternative, PortalNavigationItemCrudService { + + private ArrayList storage; + + public PortalNavigationItemsCrudServiceInMemory() { + this.storage = new ArrayList<>(); + } + + public PortalNavigationItemsCrudServiceInMemory(ArrayList storage) { + this.storage = storage; + } + + @Override + public PortalNavigationItem create(PortalNavigationItem portalNavigationItem) { + storage.add(portalNavigationItem); + return portalNavigationItem; + } + + @Override + public PortalNavigationItem update(PortalNavigationItem portalNavigationItem) { + OptionalInt index = this.findIndex(storage, item -> item.getId().equals(portalNavigationItem.getId())); + if (index.isPresent()) { + storage.set(index.getAsInt(), portalNavigationItem); + return portalNavigationItem; + } + throw new IllegalStateException("Item not found"); + } + + @Override + public void delete(PortalNavigationItemId portalNavigationItemId) { + storage.removeIf(page -> page.getId().equals(portalNavigationItemId)); + } + + @Override + public void initWith(List items) { + storage.clear(); + storage.addAll(items); + } + + @Override + public void reset() { + storage.clear(); + } + + @Override + public List storage() { + return storage; + } +} diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/inmemory/PortalNavigationItemsQueryServiceInMemory.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/inmemory/PortalNavigationItemsQueryServiceInMemory.java index 59bd787b9a2..6e0a71ace7e 100644 --- a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/inmemory/PortalNavigationItemsQueryServiceInMemory.java +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/inmemory/PortalNavigationItemsQueryServiceInMemory.java @@ -25,7 +25,24 @@ public class PortalNavigationItemsQueryServiceInMemory implements InMemoryAlternative, PortalNavigationItemsQueryService { - ArrayList storage = new ArrayList<>(); + ArrayList storage; + + public PortalNavigationItemsQueryServiceInMemory() { + this.storage = new ArrayList<>(); + } + + public PortalNavigationItemsQueryServiceInMemory(ArrayList storage) { + this.storage = storage; + } + + @Override + public PortalNavigationItem findByIdAndEnvironmentId(String environmentId, PortalNavigationItemId id) { + return storage + .stream() + .filter(item -> environmentId.equals(item.getEnvironmentId()) && id.equals(item.getId())) + .findFirst() + .orElse(null); + } @Override public List findByParentIdAndEnvironmentId(String environmentId, PortalNavigationItemId parentId) { diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/apim/core/portal_page/domain_service/CreatePortalNavigationItemValidatorServiceTest.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/apim/core/portal_page/domain_service/CreatePortalNavigationItemValidatorServiceTest.java new file mode 100644 index 00000000000..87f46c6eb34 --- /dev/null +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/apim/core/portal_page/domain_service/CreatePortalNavigationItemValidatorServiceTest.java @@ -0,0 +1,168 @@ +/* + * 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.portal_page.domain_service; + +import static fixtures.core.model.PortalNavigationItemFixtures.APIS_ID; +import static fixtures.core.model.PortalNavigationItemFixtures.ENV_ID; +import static fixtures.core.model.PortalNavigationItemFixtures.ORG_ID; +import static fixtures.core.model.PortalNavigationItemFixtures.PAGE11_ID; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertThrows; + +import fixtures.core.model.PortalNavigationItemFixtures; +import inmemory.PortalNavigationItemsCrudServiceInMemory; +import inmemory.PortalNavigationItemsQueryServiceInMemory; +import io.gravitee.apim.core.portal_page.exception.HomepageAlreadyExistsException; +import io.gravitee.apim.core.portal_page.exception.ItemAlreadyExistsException; +import io.gravitee.apim.core.portal_page.exception.ParentAreaMismatchException; +import io.gravitee.apim.core.portal_page.exception.ParentNotFoundException; +import io.gravitee.apim.core.portal_page.exception.ParentTypeMismatchException; +import io.gravitee.apim.core.portal_page.model.CreatePortalNavigationItem; +import io.gravitee.apim.core.portal_page.model.PortalArea; +import io.gravitee.apim.core.portal_page.model.PortalNavigationItem; +import io.gravitee.apim.core.portal_page.model.PortalNavigationItemId; +import io.gravitee.apim.core.portal_page.model.PortalNavigationItemType; +import java.util.ArrayList; +import org.junit.function.ThrowingRunnable; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class CreatePortalNavigationItemValidatorServiceTest { + + private PortalNavigationItemsQueryServiceInMemory queryService; + private CreatePortalNavigationItemValidatorService validatorService; + + @BeforeEach + void setUp() { + final var storage = new ArrayList(); + final PortalNavigationItemsCrudServiceInMemory crudService = new PortalNavigationItemsCrudServiceInMemory(storage); + + queryService = new PortalNavigationItemsQueryServiceInMemory(storage); + validatorService = new CreatePortalNavigationItemValidatorService(queryService); + queryService.initWith(PortalNavigationItemFixtures.sampleNavigationItems()); + } + + @Nested + class ValidateItem { + + @Test + void should_fail_when_item_with_provided_id_already_exists() { + // Given + final var createPortalNavigationItem = CreatePortalNavigationItem.builder() + .id(PortalNavigationItemId.of(APIS_ID)) + .type(PortalNavigationItemType.FOLDER) + .title("title") + .area(PortalArea.TOP_NAVBAR) + .order(0) + .build(); + + // When + final ThrowingRunnable throwing = () -> validatorService.validate(createPortalNavigationItem, ENV_ID); + + // Then + Exception exception = assertThrows(ItemAlreadyExistsException.class, throwing); + assertThat(exception.getMessage()).isEqualTo("Item with provided id already exists"); + } + + @Test + void should_fail_when_homepage_already_exists() { + // Given + final var createPortalNavigationItem = CreatePortalNavigationItem.builder() + .type(PortalNavigationItemType.FOLDER) + .title("title") + .area(PortalArea.HOMEPAGE) + .order(0) + .build(); + queryService.storage().add(PortalNavigationItem.from(createPortalNavigationItem, ORG_ID, ENV_ID)); + + // When + final ThrowingRunnable throwing = () -> validatorService.validate(createPortalNavigationItem, ENV_ID); + + // Then + Exception exception = assertThrows(HomepageAlreadyExistsException.class, throwing); + assertThat(exception.getMessage()).isEqualTo("Homepage already exists"); + } + } + + @Nested + class ValidateParent { + + @Test + void should_fail_when_parent_is_not_found() { + // Given + final String NON_EXISTENT_ID = "6c2c004d-c4f2-4a2b-b2c3-857a4dfcc842"; + final var createPortalNavigationItem = CreatePortalNavigationItem.builder() + .id(PortalNavigationItemId.random()) + .type(PortalNavigationItemType.FOLDER) + .title("title") + .area(PortalArea.TOP_NAVBAR) + .order(0) + .build(); + createPortalNavigationItem.setParentId(PortalNavigationItemId.of(NON_EXISTENT_ID)); + + // When + final ThrowingRunnable throwing = () -> validatorService.validate(createPortalNavigationItem, ENV_ID); + + // Then + Exception exception = assertThrows(ParentNotFoundException.class, throwing); + assertThat(exception.getMessage()).isEqualTo("Parent item with provided id does not exist"); + } + + @Test + void should_fail_when_parent_is_not_folder() { + // Given + final var createPortalNavigationItem = CreatePortalNavigationItem.builder() + .id(PortalNavigationItemId.random()) + .type(PortalNavigationItemType.FOLDER) + .title("title") + .area(PortalArea.TOP_NAVBAR) + .order(0) + .build(); + createPortalNavigationItem.setParentId(PortalNavigationItemId.of(PAGE11_ID)); + + // When + final ThrowingRunnable throwing = () -> validatorService.validate(createPortalNavigationItem, ENV_ID); + + // Then + Exception exception = assertThrows(ParentTypeMismatchException.class, throwing); + assertThat(exception.getMessage()).isEqualTo("Parent item with id %s is not a folder", PAGE11_ID); + } + + @Test + void should_fail_when_parent_is_in_different_area() { + // Given + final var createPortalNavigationItem = CreatePortalNavigationItem.builder() + .id(PortalNavigationItemId.random()) + .type(PortalNavigationItemType.FOLDER) + .title("title") + .area(PortalArea.HOMEPAGE) + .order(0) + .build(); + createPortalNavigationItem.setParentId(PortalNavigationItemId.of(APIS_ID)); + + // When + final ThrowingRunnable throwing = () -> validatorService.validate(createPortalNavigationItem, ENV_ID); + + // Then + Exception exception = assertThrows(ParentAreaMismatchException.class, throwing); + assertThat(exception.getMessage()).isEqualTo("Parent item with id %s belongs to a different area than the child item", APIS_ID); + } + } +} diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/apim/core/portal_page/model/PortalNavigationItemComparatorTest.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/apim/core/portal_page/model/PortalNavigationItemComparatorTest.java index 6a59c6d241d..fc38f4b7266 100644 --- a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/apim/core/portal_page/model/PortalNavigationItemComparatorTest.java +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/apim/core/portal_page/model/PortalNavigationItemComparatorTest.java @@ -28,14 +28,14 @@ class PortalNavigationItemComparatorTest { @Test - void should_order_items_by_parentId_nulls_first_then_by_order_nulls_last() { + void should_order_items_by_parentId_nulls_first_then_by_order() { var parent = PortalNavigationItemId.random(); var itemWithNullParent = PortalNavigationItemFixtures.aFolder(PortalNavigationItemId.random().toString(), "A"); itemWithNullParent.setOrder(2); - var itemWithParentOrderNull = PortalNavigationItemFixtures.aFolder(PortalNavigationItemId.random().toString(), "B", parent); - itemWithParentOrderNull.setOrder(null); + var itemWithParentOrder = PortalNavigationItemFixtures.aFolder(PortalNavigationItemId.random().toString(), "B", parent); + itemWithParentOrder.setOrder(0); var itemWithParentOrder1 = PortalNavigationItemFixtures.aFolder(PortalNavigationItemId.random().toString(), "C", parent); itemWithParentOrder1.setOrder(1); @@ -44,13 +44,13 @@ void should_order_items_by_parentId_nulls_first_then_by_order_nulls_last() { itemWithParentOrder2.setOrder(2); List items = new ArrayList<>( - List.of(itemWithParentOrder2, itemWithNullParent, itemWithParentOrder1, itemWithParentOrderNull) + List.of(itemWithParentOrder2, itemWithNullParent, itemWithParentOrder1, itemWithParentOrder) ); items.sort(PortalNavigationItemComparator.byNullableParentIdThenNullableOrder()); assertThat(items.getFirst()).isEqualTo(itemWithNullParent); // null parent first - // then items with same parent ordered by order (nulls last -> itemWithParentOrder1 (1), itemWithParentOrder2 (2), itemWithParentOrderNull (null)) - assertThat(items.subList(1, 4)).containsExactly(itemWithParentOrder1, itemWithParentOrder2, itemWithParentOrderNull); + // then items with same parent ordered by order (itemWithParentOrder (0), itemWithParentOrder1 (1), itemWithParentOrder2 (2)) + assertThat(items.subList(1, 4)).containsExactly(itemWithParentOrder, itemWithParentOrder1, itemWithParentOrder2); } } diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/apim/core/portal_page/use_case/CreatePortalNavigationItemUseCaseTest.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/apim/core/portal_page/use_case/CreatePortalNavigationItemUseCaseTest.java new file mode 100644 index 00000000000..e4f384099b4 --- /dev/null +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/apim/core/portal_page/use_case/CreatePortalNavigationItemUseCaseTest.java @@ -0,0 +1,222 @@ +/* + * 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.portal_page.use_case; + +import static fixtures.core.model.PortalNavigationItemFixtures.APIS_ID; +import static fixtures.core.model.PortalNavigationItemFixtures.ENV_ID; +import static fixtures.core.model.PortalNavigationItemFixtures.ORG_ID; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; + +import fixtures.core.model.PortalNavigationItemFixtures; +import inmemory.PortalNavigationItemsCrudServiceInMemory; +import inmemory.PortalNavigationItemsQueryServiceInMemory; +import io.gravitee.apim.core.portal_page.domain_service.CreatePortalNavigationItemValidatorService; +import io.gravitee.apim.core.portal_page.model.CreatePortalNavigationItem; +import io.gravitee.apim.core.portal_page.model.PortalArea; +import io.gravitee.apim.core.portal_page.model.PortalNavigationItem; +import io.gravitee.apim.core.portal_page.model.PortalNavigationItemId; +import io.gravitee.apim.core.portal_page.model.PortalNavigationItemType; +import java.util.ArrayList; +import java.util.stream.Collectors; +import org.junit.function.ThrowingRunnable; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class CreatePortalNavigationItemUseCaseTest { + + private CreatePortalNavigationItemUseCase useCase; + private PortalNavigationItemsCrudServiceInMemory crudService; + private PortalNavigationItemsQueryServiceInMemory queryService; + private CreatePortalNavigationItemValidatorService validatorService; + + @BeforeEach + void setUp() { + final var storage = new ArrayList(); + + crudService = new PortalNavigationItemsCrudServiceInMemory(storage); + queryService = new PortalNavigationItemsQueryServiceInMemory(storage); + validatorService = new CreatePortalNavigationItemValidatorService(queryService); + useCase = new CreatePortalNavigationItemUseCase(crudService, queryService, validatorService); + queryService.initWith(PortalNavigationItemFixtures.sampleNavigationItems()); + } + + @Test + void should_create_item_if_validation_succeeds() { + // Given + final var createPortalNavigationItem = CreatePortalNavigationItem.builder() + .id(PortalNavigationItemId.random()) + .type(PortalNavigationItemType.FOLDER) + .title("title") + .area(PortalArea.TOP_NAVBAR) + .order(0) + .build(); + createPortalNavigationItem.setParentId(PortalNavigationItemId.of(APIS_ID)); + + // When + useCase.execute(new CreatePortalNavigationItemUseCase.Input(ORG_ID, ENV_ID, createPortalNavigationItem)); + + // Then + final var result = queryService.findByParentIdAndEnvironmentId(ENV_ID, PortalNavigationItemId.of(APIS_ID)); + assertThat(result).extracting(PortalNavigationItem::getId).contains(createPortalNavigationItem.getId()); + } + + @Test + void should_not_create_item_if_validation_fails() { + // Given + final CreatePortalNavigationItemValidatorService spyValidator = mock(CreatePortalNavigationItemValidatorService.class); + doThrow(new RuntimeException("Custom exception from validator")) + .when(spyValidator) + .validate(any(CreatePortalNavigationItem.class), anyString()); + + crudService = new PortalNavigationItemsCrudServiceInMemory(new ArrayList<>(queryService.storage())); + useCase = new CreatePortalNavigationItemUseCase(crudService, queryService, spyValidator); + final var numberOfItems = queryService.storage().size(); + + // When + final var createPortalNavigationItem = CreatePortalNavigationItem.builder() + .id(PortalNavigationItemId.random()) + .title("title") + .area(PortalArea.TOP_NAVBAR) + .order(0) + .build(); + + final ThrowingRunnable throwing = () -> + useCase.execute(new CreatePortalNavigationItemUseCase.Input(ORG_ID, ENV_ID, createPortalNavigationItem)); + + // Then + Exception exception = assertThrows(RuntimeException.class, throwing); + assertThat(exception.getMessage()).isEqualTo("Custom exception from validator"); + assertThat(queryService.storage()).hasSize(numberOfItems); + } + + @Nested + class UpdateSiblingsOrder { + + @ParameterizedTest(name = "Order = {1} ({0})") + @CsvSource( + { + "New item at the beginning -> update all siblings, 0", + "New item in the middle -> update some siblings, 1", + "New item at the end -> update no siblings, 2", + "New item at the end -> update no siblings and limit order to max possible, 99999999", + } + ) + void should_update_order_of_siblings_top_level(String label, Integer order) { + // Given + final var createPortalNavigationItem = CreatePortalNavigationItem.builder() + .type(PortalNavigationItemType.FOLDER) + .id(PortalNavigationItemId.random()) + .title("title") + .area(PortalArea.TOP_NAVBAR) + .order(order) + .build(); + + final var existingSiblingOrdersById = queryService + .findTopLevelItemsByEnvironmentIdAndPortalArea(ENV_ID, PortalArea.TOP_NAVBAR) + .stream() + .collect(Collectors.toMap(PortalNavigationItem::getId, PortalNavigationItem::getOrder)); + + // When + useCase.execute(new CreatePortalNavigationItemUseCase.Input(ORG_ID, ENV_ID, createPortalNavigationItem)); + + // Then + final var items = queryService.findTopLevelItemsByEnvironmentIdAndPortalArea(ENV_ID, PortalArea.TOP_NAVBAR); + + final var createdItem = items + .stream() + .filter(item -> item.getId().equals(createPortalNavigationItem.getId())) + .findFirst() + .orElse(null); + assertThat(createdItem).isNotNull(); + final var targetOrder = Math.min(order, items.size() - 1); + assertThat(createdItem.getOrder()).isEqualTo(targetOrder); + + final var updatedSiblings = items.stream().filter(item -> existingSiblingOrdersById.keySet().contains(item.getId())); + assertThat(updatedSiblings).allSatisfy(item -> { + final var oldSiblingOrder = existingSiblingOrdersById.get(item.getId()); + final var updatedSiblingOrder = item.getOrder(); + if (existingSiblingOrdersById.get(item.getId()) < order) { + assertThat(updatedSiblingOrder).isEqualTo(oldSiblingOrder); + } else { + assertThat(updatedSiblingOrder).isEqualTo(oldSiblingOrder + 1); + } + }); + } + + @ParameterizedTest(name = "Order = {1} ({0})") + @CsvSource( + { + "New item at the beginning -> update all siblings, 0", + "New item in the middle -> update some siblings, 1", + "New item at the end -> update no siblings, 2", + "New item at the end -> update no siblings and limit order to max possible, 99999999", + } + ) + void should_update_order_of_siblings_mid_level(String label, Integer order) { + // Given + final var createPortalNavigationItem = CreatePortalNavigationItem.builder() + .type(PortalNavigationItemType.FOLDER) + .id(PortalNavigationItemId.random()) + .title("title") + .area(PortalArea.TOP_NAVBAR) + .order(order) + .build(); + createPortalNavigationItem.setParentId(PortalNavigationItemId.of(APIS_ID)); + + final var existingSiblingOrdersById = queryService + .findByParentIdAndEnvironmentId(ENV_ID, createPortalNavigationItem.getParentId()) + .stream() + .collect(Collectors.toMap(PortalNavigationItem::getId, PortalNavigationItem::getOrder)); + + // When + useCase.execute(new CreatePortalNavigationItemUseCase.Input(ORG_ID, ENV_ID, createPortalNavigationItem)); + + // Then + final var items = queryService.findByParentIdAndEnvironmentId(ENV_ID, createPortalNavigationItem.getParentId()); + + final var createdItem = items + .stream() + .filter(item -> item.getId().equals(createPortalNavigationItem.getId())) + .findFirst() + .orElse(null); + assertThat(createdItem).isNotNull(); + final var targetOrder = Math.min(order, items.size() - 1); + assertThat(createdItem.getOrder()).isEqualTo(targetOrder); + + final var updatedSiblings = items.stream().filter(item -> existingSiblingOrdersById.keySet().contains(item.getId())); + assertThat(updatedSiblings).allSatisfy(item -> { + final var oldSiblingOrder = existingSiblingOrdersById.get(item.getId()); + final var updatedSiblingOrder = item.getOrder(); + if (existingSiblingOrdersById.get(item.getId()) < order) { + assertThat(updatedSiblingOrder).isEqualTo(oldSiblingOrder); + } else { + assertThat(updatedSiblingOrder).isEqualTo(oldSiblingOrder + 1); + } + }); + } + } +} diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/apim/core/portal_page/use_case/ListPortalNavigationItemsUseCaseTest.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/apim/core/portal_page/use_case/ListPortalNavigationItemsUseCaseTest.java index ca05929dcf8..699fa049533 100644 --- a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/apim/core/portal_page/use_case/ListPortalNavigationItemsUseCaseTest.java +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/apim/core/portal_page/use_case/ListPortalNavigationItemsUseCaseTest.java @@ -54,9 +54,9 @@ void should_return_top_level_navigation_items_when_parent_id_is_null_and_load_ch // Then assertThat(result.items()) - .hasSize(8) + .hasSize(9) .extracting(PortalNavigationItem::getTitle) - .containsExactly("APIs", "Guides", "Support", "Overview", "Getting Started", "Category1", "page11", "page12"); + .containsExactly("APIs", "Example Link", "Guides", "Support", "Overview", "Getting Started", "Category1", "page11", "page12"); } @Test diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/apim/infra/adapter/PortalNavigationItemAdapterTest.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/apim/infra/adapter/PortalNavigationItemAdapterTest.java index cc01da9da7d..5923e43af1c 100644 --- a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/apim/infra/adapter/PortalNavigationItemAdapterTest.java +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/apim/infra/adapter/PortalNavigationItemAdapterTest.java @@ -213,10 +213,10 @@ void should_map_folder_to_repository() { "org-id", "env-id", "My Folder", - PortalArea.TOP_NAVBAR + PortalArea.TOP_NAVBAR, + 1 ); entity.setParentId(PortalNavigationItemId.of("550e8400-e29b-41d4-a716-446655440011")); - entity.setOrder(1); // When var repositoryItem = adapter.toRepository(entity); @@ -242,9 +242,9 @@ void should_map_page_to_repository() { "env-id", "My Page", PortalArea.HOMEPAGE, + 2, PortalPageContentId.of("550e8400-e29b-41d4-a716-446655440013") ); - entity.setOrder(2); // When var repositoryItem = adapter.toRepository(entity); @@ -269,9 +269,9 @@ void should_map_link_to_repository() { "env-id", "My Link", PortalArea.TOP_NAVBAR, + 3, "https://example.com" ); - entity.setOrder(3); // When var repositoryItem = adapter.toRepository(entity); @@ -295,7 +295,8 @@ void should_handle_null_parent_id() { "org-id", "env-id", "My Folder", - PortalArea.TOP_NAVBAR + PortalArea.TOP_NAVBAR, + 0 ); // When diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/apim/infra/query_service/portal_page/PortalNavigationItemsCrudServiceImplTest.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/apim/infra/query_service/portal_page/PortalNavigationItemsCrudServiceImplTest.java new file mode 100644 index 00000000000..f55ad7e00cd --- /dev/null +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/apim/infra/query_service/portal_page/PortalNavigationItemsCrudServiceImplTest.java @@ -0,0 +1,306 @@ +/* + * 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.infra.query_service.portal_page; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import io.gravitee.apim.core.exception.TechnicalDomainException; +import io.gravitee.apim.core.portal_page.model.PortalArea; +import io.gravitee.apim.core.portal_page.model.PortalNavigationFolder; +import io.gravitee.apim.core.portal_page.model.PortalNavigationItemId; +import io.gravitee.apim.core.portal_page.model.PortalNavigationLink; +import io.gravitee.apim.core.portal_page.model.PortalNavigationPage; +import io.gravitee.apim.core.portal_page.model.PortalPageContentId; +import io.gravitee.repository.exceptions.TechnicalException; +import io.gravitee.repository.management.api.PortalNavigationItemRepository; +import io.gravitee.repository.management.model.PortalNavigationItem; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class PortalNavigationItemsCrudServiceImplTest { + + @Mock + PortalNavigationItemRepository repository; + + PortalNavigationItemsCrudServiceImpl service; + + @Captor + ArgumentCaptor captor; + + @BeforeEach + void setUp() { + service = new PortalNavigationItemsCrudServiceImpl(repository); + } + + @Nested + class CreatePortalNavigationItem { + + @Test + void should_create_a_folder() throws TechnicalException { + final var itemId = PortalNavigationItemId.random(); + final var item = new PortalNavigationFolder(itemId, "organizationId", "environmentId", "title", PortalArea.TOP_NAVBAR, 0); + + service.create(item); + + final var expectedItem = io.gravitee.repository.management.model.PortalNavigationItem.builder() + .id(itemId.toString()) + .title("title") + .organizationId("organizationId") + .environmentId("environmentId") + .type(PortalNavigationItem.Type.FOLDER) + .area(io.gravitee.repository.management.model.PortalNavigationItem.Area.TOP_NAVBAR) + .order(0) + .parentId(null) + .configuration("{}") + .build(); + + verify(repository).create(captor.capture()); + assertThat(captor.getValue()).usingRecursiveComparison().isEqualTo(expectedItem); + } + + @Test + void should_create_a_page() throws TechnicalException { + final var itemId = PortalNavigationItemId.random(); + final var contentId = PortalPageContentId.random(); + final var item = new PortalNavigationPage( + itemId, + "organizationId", + "environmentId", + "title", + PortalArea.TOP_NAVBAR, + 0, + contentId + ); + + service.create(item); + + final var expectedItem = io.gravitee.repository.management.model.PortalNavigationItem.builder() + .id(itemId.toString()) + .title("title") + .organizationId("organizationId") + .environmentId("environmentId") + .type(PortalNavigationItem.Type.PAGE) + .area(io.gravitee.repository.management.model.PortalNavigationItem.Area.TOP_NAVBAR) + .order(0) + .parentId(null) + .configuration("{\"portalPageContentId\":\"" + contentId.toString() + "\"}") + .build(); + + verify(repository).create(captor.capture()); + assertThat(captor.getValue()).usingRecursiveComparison().isEqualTo(expectedItem); + } + + @Test + void should_create_a_link() throws TechnicalException { + final var itemId = PortalNavigationItemId.random(); + final var url = "http://example.com"; + final var item = new PortalNavigationLink(itemId, "organizationId", "environmentId", "title", PortalArea.TOP_NAVBAR, 0, url); + + service.create(item); + + final var expectedItem = io.gravitee.repository.management.model.PortalNavigationItem.builder() + .id(itemId.toString()) + .title("title") + .organizationId("organizationId") + .environmentId("environmentId") + .type(PortalNavigationItem.Type.LINK) + .area(io.gravitee.repository.management.model.PortalNavigationItem.Area.TOP_NAVBAR) + .order(0) + .parentId(null) + .configuration("{\"url\":\"" + url + "\"}") + .build(); + + verify(repository).create(captor.capture()); + assertThat(captor.getValue()).usingRecursiveComparison().isEqualTo(expectedItem); + } + + @Test + void should_throw_technical_domain_exception_when_repository_throws_technical_exception() throws TechnicalException { + // Given + final var itemId = PortalNavigationItemId.of("00000000-0000-0000-0000-000000000001"); + final var contentId = PortalPageContentId.random(); + final var item = new PortalNavigationPage( + itemId, + "organizationId", + "environmentId", + "title", + PortalArea.TOP_NAVBAR, + 0, + contentId + ); + when(repository.create(any())).thenThrow(new TechnicalException("Database error")); + + // When & Then + assertThatThrownBy(() -> service.create(item)) + .isInstanceOf(TechnicalDomainException.class) + .hasMessage( + "An error occurred while creating portal navigation item with id 00000000-0000-0000-0000-000000000001 and environmentId environmentId" + ) + .hasCauseInstanceOf(TechnicalException.class); + } + } + + @Nested + class UpdatePortalNavigationItem { + + @Test + void should_update_a_folder() throws TechnicalException { + final var itemId = PortalNavigationItemId.random(); + final var item = new PortalNavigationFolder(itemId, "organizationId", "environmentId", "title", PortalArea.TOP_NAVBAR, 0); + + service.update(item); + + final var expectedItem = io.gravitee.repository.management.model.PortalNavigationItem.builder() + .id(itemId.toString()) + .title("title") + .organizationId("organizationId") + .environmentId("environmentId") + .type(PortalNavigationItem.Type.FOLDER) + .area(io.gravitee.repository.management.model.PortalNavigationItem.Area.TOP_NAVBAR) + .order(0) + .parentId(null) + .configuration("{}") + .build(); + + verify(repository).update(captor.capture()); + assertThat(captor.getValue()).usingRecursiveComparison().isEqualTo(expectedItem); + } + + @Test + void should_update_a_page() throws TechnicalException { + final var itemId = PortalNavigationItemId.random(); + final var contentId = PortalPageContentId.random(); + final var item = new PortalNavigationPage( + itemId, + "organizationId", + "environmentId", + "title", + PortalArea.TOP_NAVBAR, + 0, + contentId + ); + + service.update(item); + + final var expectedItem = io.gravitee.repository.management.model.PortalNavigationItem.builder() + .id(itemId.toString()) + .title("title") + .organizationId("organizationId") + .environmentId("environmentId") + .type(PortalNavigationItem.Type.PAGE) + .area(io.gravitee.repository.management.model.PortalNavigationItem.Area.TOP_NAVBAR) + .order(0) + .parentId(null) + .configuration("{\"portalPageContentId\":\"" + contentId.toString() + "\"}") + .build(); + + verify(repository).update(captor.capture()); + assertThat(captor.getValue()).usingRecursiveComparison().isEqualTo(expectedItem); + } + + @Test + void should_update_a_link() throws TechnicalException { + final var itemId = PortalNavigationItemId.random(); + final var url = "http://example.com"; + final var item = new PortalNavigationLink(itemId, "organizationId", "environmentId", "title", PortalArea.TOP_NAVBAR, 0, url); + + service.update(item); + + final var expectedItem = io.gravitee.repository.management.model.PortalNavigationItem.builder() + .id(itemId.toString()) + .title("title") + .organizationId("organizationId") + .environmentId("environmentId") + .type(PortalNavigationItem.Type.LINK) + .area(io.gravitee.repository.management.model.PortalNavigationItem.Area.TOP_NAVBAR) + .order(0) + .parentId(null) + .configuration("{\"url\":\"" + url + "\"}") + .build(); + + verify(repository).update(captor.capture()); + assertThat(captor.getValue()).usingRecursiveComparison().isEqualTo(expectedItem); + } + + @Test + void should_throw_technical_domain_exception_when_repository_throws_technical_exception() throws TechnicalException { + // Given + final var itemId = PortalNavigationItemId.of("00000000-0000-0000-0000-000000000001"); + final var contentId = PortalPageContentId.random(); + final var item = new PortalNavigationPage( + itemId, + "organizationId", + "environmentId", + "title", + PortalArea.TOP_NAVBAR, + 0, + contentId + ); + when(repository.update(any())).thenThrow(new TechnicalException("Database error")); + + // When & Then + assertThatThrownBy(() -> service.update(item)) + .isInstanceOf(TechnicalDomainException.class) + .hasMessage( + "An error occurred while updating portal navigation item with id 00000000-0000-0000-0000-000000000001 and environmentId environmentId" + ) + .hasCauseInstanceOf(TechnicalException.class); + } + } + + @Nested + class DeletePortalNavigationItem { + + @Test + void should_delete_an_item() throws TechnicalException { + final var itemId = PortalNavigationItemId.random(); + + service.delete(itemId); + + final var captor = ArgumentCaptor.forClass(String.class); + verify(repository).delete(captor.capture()); + assertThat(captor.getValue()).isEqualTo(itemId.toString()); + } + + @Test + void should_throw_technical_domain_exception_when_repository_throws_technical_exception() throws TechnicalException { + // Given + final var itemId = PortalNavigationItemId.of("00000000-0000-0000-0000-000000000001"); + doThrow(new TechnicalException("Database error")).when(repository).delete(any()); + + // When & Then + assertThatThrownBy(() -> service.delete(itemId)) + .isInstanceOf(TechnicalDomainException.class) + .hasMessage("An error occurred while deleting portal navigation item with id 00000000-0000-0000-0000-000000000001") + .hasCauseInstanceOf(TechnicalException.class); + } + } +} diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/apim/infra/query_service/portal_page/PortalNavigationItemsQueryServiceImplTest.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/apim/infra/query_service/portal_page/PortalNavigationItemsQueryServiceImplTest.java index 7f1fa28b716..8308dfc7ae7 100644 --- a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/apim/infra/query_service/portal_page/PortalNavigationItemsQueryServiceImplTest.java +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/apim/infra/query_service/portal_page/PortalNavigationItemsQueryServiceImplTest.java @@ -21,10 +21,12 @@ import io.gravitee.apim.core.exception.TechnicalDomainException; import io.gravitee.apim.core.portal_page.model.PortalArea; +import io.gravitee.apim.core.portal_page.model.PortalNavigationItem; import io.gravitee.apim.core.portal_page.model.PortalNavigationItemId; import io.gravitee.repository.exceptions.TechnicalException; import io.gravitee.repository.management.api.PortalNavigationItemRepository; import java.util.List; +import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; @@ -48,6 +50,79 @@ void setUp() { service = new PortalNavigationItemsQueryServiceImpl(repository); } + @Nested + class FindByIdAndEnvironmentId { + + @Test + void should_return_item_when_found_and_environment_matches() throws TechnicalException { + // Given + var itemId = "00000000-0000-0000-0000-000000000001"; + var environmentId = "env-id"; + var repoItem = new io.gravitee.repository.management.model.PortalNavigationItem(); + repoItem.setId(itemId); + repoItem.setEnvironmentId(environmentId); + repoItem.setTitle("Test Item"); + repoItem.setType(io.gravitee.repository.management.model.PortalNavigationItem.Type.FOLDER); + repoItem.setArea(io.gravitee.repository.management.model.PortalNavigationItem.Area.TOP_NAVBAR); + when(repository.findById(itemId)).thenReturn(Optional.of(repoItem)); + + // When + var result = service.findByIdAndEnvironmentId(environmentId, PortalNavigationItemId.of(itemId)); + + // Then + assertThat(result).isNotNull(); + assertThat(result.getTitle()).isEqualTo("Test Item"); + assertThat(result).isInstanceOf(PortalNavigationItem.class); + } + + @Test + void should_return_null_when_item_not_found() throws TechnicalException { + // Given + var itemId = "00000000-0000-0000-0000-000000000001"; + var environmentId = "env-id"; + when(repository.findById(itemId)).thenReturn(Optional.empty()); + + // When + var result = service.findByIdAndEnvironmentId(environmentId, PortalNavigationItemId.of(itemId)); + + // Then + assertThat(result).isNull(); + } + + @Test + void should_return_null_when_environment_does_not_match() throws TechnicalException { + // Given + var itemId = "00000000-0000-0000-0000-000000000001"; + var environmentId = "env-id"; + var repoItem = new io.gravitee.repository.management.model.PortalNavigationItem(); + repoItem.setId(itemId); + repoItem.setEnvironmentId("different-env"); + when(repository.findById(itemId)).thenReturn(Optional.of(repoItem)); + + // When + var result = service.findByIdAndEnvironmentId(environmentId, PortalNavigationItemId.of(itemId)); + + // Then + assertThat(result).isNull(); + } + + @Test + void should_throw_technical_domain_exception_when_repository_throws_technical_exception() throws TechnicalException { + // Given + var itemId = "00000000-0000-0000-0000-000000000001"; + var environmentId = "env-id"; + when(repository.findById(itemId)).thenThrow(new TechnicalException("Database error")); + + // When & Then + assertThatThrownBy(() -> service.findByIdAndEnvironmentId(environmentId, PortalNavigationItemId.of(itemId))) + .isInstanceOf(TechnicalDomainException.class) + .hasMessage( + "An error occurred while finding portal navigation item by id 00000000-0000-0000-0000-000000000001 and environmentId env-id" + ) + .hasCauseInstanceOf(TechnicalException.class); + } + } + @Nested class FindByParentIdAndEnvironmentId {