diff --git a/openapi/openapi-component_provisioner-v1.0.0.yaml b/openapi/openapi-component_provisioner-v1.0.0.yaml index 83581de..33a9d7a 100644 --- a/openapi/openapi-component_provisioner-v1.0.0.yaml +++ b/openapi/openapi-component_provisioner-v1.0.0.yaml @@ -172,6 +172,10 @@ paths: type: string description: The base64 encoded path for the catalogItem. Mind that it may include branch reference. example: "cHJvamVjdHMvQ0FURVNUL3JlcG9zL3VzZXItYWN0aW9ucy1pdGVtL3Jhdy9DYXRhbG9nSXRlbS55YW1sP2F0PXJlZnMvaGVhZHMvbWFzdGVy" + catalogItemSlug: + type: string + description: The slug for the provisioned component. + example: "myproject_repo_name" componentUrl: type: string description: The bitbucket repository url for the provisioned component. diff --git a/src/main/java/org/opendevstack/component_provisioner/config/ControllerExceptionHandler.java b/src/main/java/org/opendevstack/component_provisioner/config/ControllerExceptionHandler.java index a7bc597..57db907 100644 --- a/src/main/java/org/opendevstack/component_provisioner/config/ControllerExceptionHandler.java +++ b/src/main/java/org/opendevstack/component_provisioner/config/ControllerExceptionHandler.java @@ -1,11 +1,6 @@ package org.opendevstack.component_provisioner.config; -import org.opendevstack.component_provisioner.server.controllers.exceptions.BadRequestException; -import org.opendevstack.component_provisioner.server.controllers.exceptions.InvalidRestEntityException; -import org.opendevstack.component_provisioner.server.controllers.exceptions.ProjectComponentAlreadyProvisionedException; -import org.opendevstack.component_provisioner.server.controllers.exceptions.ProjectConfigurationException; -import org.opendevstack.component_provisioner.server.controllers.exceptions.RestEntityNotFoundException; -import org.opendevstack.component_provisioner.server.controllers.exceptions.UserNotAllowedException; +import org.opendevstack.component_provisioner.server.controllers.exceptions.*; import org.opendevstack.component_provisioner.server.model.RestErrorMessage; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; @@ -61,6 +56,11 @@ public ResponseEntity handleUserNotAllowedException(UserNotAll return defaultErrResponse(ex, HttpStatus.FORBIDDEN); } + @ExceptionHandler(SlugNotFoundException.class) + public ResponseEntity handleSlugNotFoundException(SlugNotFoundException ex) { + return defaultErrResponse(ex, HttpStatus.NOT_FOUND); + } + private static ResponseEntity defaultErrResponse(Exception ex, HttpStatus errStatus) { // Explicitly setting MediaType.APPLICATION_JSON contentType is required, // due to clients sending miscellaneous Accept headers on the request, diff --git a/src/main/java/org/opendevstack/component_provisioner/server/controllers/ProvisionResultsApiController.java b/src/main/java/org/opendevstack/component_provisioner/server/controllers/ProvisionResultsApiController.java index cabcf37..e542815 100644 --- a/src/main/java/org/opendevstack/component_provisioner/server/controllers/ProvisionResultsApiController.java +++ b/src/main/java/org/opendevstack/component_provisioner/server/controllers/ProvisionResultsApiController.java @@ -1,5 +1,6 @@ package org.opendevstack.component_provisioner.server.controllers; +import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.opendevstack.component_provisioner.server.api.ProvisionResultsApi; import org.opendevstack.component_provisioner.server.controllers.model.ProjectComponentStatus; @@ -16,30 +17,26 @@ @Controller @RequestMapping("${openapi.componentProvisionerREST.base-path:/v1}") @Slf4j +@AllArgsConstructor public class ProvisionResultsApiController implements ProvisionResultsApi { private final AuthenticationProvider authenticationProvider; private final ProvisionResultsApiFacade provisionResultsApiFacade; - public ProvisionResultsApiController(AuthenticationProvider authenticationProvider, ProvisionResultsApiFacade provisionResultsApiFacade) { - this.authenticationProvider = authenticationProvider; - this.provisionResultsApiFacade = provisionResultsApiFacade; - } - @Override public ResponseEntity notifyProvisioningStatusUpdate(String projectKey, String status, NotifyProvisioningStatusUpdateRequest notifyProvisioningCompletedRequest) { log.debug("Notifying provision status update. ProjectKey: {}, Status: {}, notifyProvisioningCompletedRequest: {}", projectKey, status, notifyProvisioningCompletedRequest); var accessToken = authenticationProvider.getAccessToken(); - provisionResultsApiFacade.validate(projectKey, status); + provisionResultsApiFacade.validate(projectKey, status, notifyProvisioningCompletedRequest); - provisionResultsApiFacade.notifyProvisioningStatusUpdate(projectKey, + provisionResultsApiFacade.notifyProvisioningStatusUpdate( + projectKey, ProjectComponentStatus.valueOf(status), - notifyProvisioningCompletedRequest.getComponentId(), - notifyProvisioningCompletedRequest.getCatalogItemId(), - notifyProvisioningCompletedRequest.getComponentUrl(), - accessToken); + notifyProvisioningCompletedRequest, + accessToken + ); return ResponseEntity.ok().build(); } @@ -58,7 +55,8 @@ public ResponseEntity deleteProvisioningStatus(String projectKey, Provisio @Override public ResponseEntity createIncident(String projectKey, String componentId, CreateIncidentAction createIncidentAction) { log.debug("Creating incident. ProjectKey: {}, componentId: {}, CreateIncidentAction: {}", projectKey, componentId, createIncidentAction); - + NotifyProvisioningStatusUpdateRequest notifyProvisioningStatusUpdateRequest = new NotifyProvisioningStatusUpdateRequest(); + notifyProvisioningStatusUpdateRequest.setComponentId(componentId); var accessToken = authenticationProvider.getAccessToken(); provisionResultsApiFacade.validate(projectKey, componentId, createIncidentAction); @@ -75,10 +73,8 @@ public ResponseEntity createIncident(String projectKey, provisionResultsApiFacade.notifyProvisioningStatusUpdate(projectKey, ProjectComponentStatus.DELETING, - componentId, - null, - null, - accessToken); + notifyProvisioningStatusUpdateRequest, + null); log.debug("Creating incident via AWX"); diff --git a/src/main/java/org/opendevstack/component_provisioner/server/controllers/exceptions/SlugNotFoundException.java b/src/main/java/org/opendevstack/component_provisioner/server/controllers/exceptions/SlugNotFoundException.java new file mode 100644 index 0000000..73f1b6c --- /dev/null +++ b/src/main/java/org/opendevstack/component_provisioner/server/controllers/exceptions/SlugNotFoundException.java @@ -0,0 +1,7 @@ +package org.opendevstack.component_provisioner.server.controllers.exceptions; + +public class SlugNotFoundException extends RuntimeException { + public SlugNotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/org/opendevstack/component_provisioner/server/facade/ProvisionResultsApiFacade.java b/src/main/java/org/opendevstack/component_provisioner/server/facade/ProvisionResultsApiFacade.java index 2ff022d..732f1d7 100644 --- a/src/main/java/org/opendevstack/component_provisioner/server/facade/ProvisionResultsApiFacade.java +++ b/src/main/java/org/opendevstack/component_provisioner/server/facade/ProvisionResultsApiFacade.java @@ -3,22 +3,22 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.util.Strings; +import org.opendevstack.component_provisioner.client.component_catalog.v1.model.CatalogItem; import org.opendevstack.component_provisioner.server.controllers.exceptions.InvalidRestEntityException; import org.opendevstack.component_provisioner.server.controllers.exceptions.ProjectConfigurationException; +import org.opendevstack.component_provisioner.server.controllers.exceptions.SlugNotFoundException; import org.opendevstack.component_provisioner.server.controllers.model.ProjectComponentStatus; import org.opendevstack.component_provisioner.server.controllers.model.awx.AwxResponse; import org.opendevstack.component_provisioner.server.controllers.validators.ParameterType; import org.opendevstack.component_provisioner.server.mappers.EntitiesMapper; import org.opendevstack.component_provisioner.server.model.CreateIncidentAction; import org.opendevstack.component_provisioner.server.model.CreateIncidentParameter; -import org.opendevstack.component_provisioner.server.services.AuthenticationProvider; -import org.opendevstack.component_provisioner.server.services.AwxService; -import org.opendevstack.component_provisioner.server.services.ComponentCatalogService; -import org.opendevstack.component_provisioner.server.services.ProjectsInfoService; -import org.opendevstack.component_provisioner.server.services.ProvisionService; +import org.opendevstack.component_provisioner.server.model.NotifyProvisioningStatusUpdateRequest; +import org.opendevstack.component_provisioner.server.services.*; import org.opendevstack.component_provisioner.server.services.awx.AwxWorkflowJobLaunch; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +import org.springframework.web.client.RestClientException; import java.util.Arrays; @@ -77,15 +77,51 @@ public AwxResponse requestProvisionToAwx(String projectKey, String componentId, .build(); } - public void notifyProvisioningStatusUpdate(String projectKey, ProjectComponentStatus status, String componentId, - String catalogItemId, String componentUrl, String accessToken) { - provisionService.notifyProvisioningStatusUpdate(projectKey, status, componentId, catalogItemId, componentUrl, accessToken); + public void notifyProvisioningStatusUpdate(String projectKey, + ProjectComponentStatus status, + NotifyProvisioningStatusUpdateRequest notifyProvisioningStatusUpdateRequest, + String accessToken) { + String resolvedCatalogItemId = resolveCatalogItemId(accessToken, + notifyProvisioningStatusUpdateRequest.getCatalogItemId(), + notifyProvisioningStatusUpdateRequest.getCatalogItemSlug()); + + provisionService.notifyProvisioningStatusUpdate(projectKey, + status, + notifyProvisioningStatusUpdateRequest.getComponentId(), + resolvedCatalogItemId, + notifyProvisioningStatusUpdateRequest.getComponentUrl(), + accessToken); + } + + private String resolveCatalogItemId(String accessToken, + String catalogItemId, + String catalogItemSlug) { + String resolvedCatalogItemId = catalogItemId; + if (StringUtils.isNotBlank(catalogItemSlug) && StringUtils.isBlank(catalogItemId)) { + log.debug("Resolving catalogItemId for catalogItemSlug: {}", catalogItemSlug); + CatalogItem catalogItem; + try { + catalogItem = componentCatalogService.getCatalogItemBySlug(accessToken, catalogItemSlug); + } catch (RestClientException e) { + throw new SlugNotFoundException("Catalog item slug not found: " + catalogItemSlug); + } + resolvedCatalogItemId = catalogItem.getId(); + log.debug("Resolved catalogItemSlug {} to catalogItemId: {}", catalogItemSlug, resolvedCatalogItemId); + } + return resolvedCatalogItemId; } public void deleteProvisioningStatus(String projectKey, String componentId, String accessToken) { provisionService.deleteProvisioningStatus(projectKey, componentId, accessToken); } + public void validate(String projectKey, String status, NotifyProvisioningStatusUpdateRequest request) { + validate(projectKey, status); + if (StringUtils.isNotBlank(request.getCatalogItemId()) && StringUtils.isNotBlank(request.getCatalogItemSlug())) { + throw new InvalidRestEntityException("Both catalogItemId and catalogItemSlug cannot be defined at the same time."); + } + } + public void validate(String projectKey, String status) { var mainParamsAreEmpty = StringUtils.isBlank(projectKey) || StringUtils.isBlank(status); diff --git a/src/main/java/org/opendevstack/component_provisioner/server/services/ComponentCatalogService.java b/src/main/java/org/opendevstack/component_provisioner/server/services/ComponentCatalogService.java index f800143..26c8c59 100644 --- a/src/main/java/org/opendevstack/component_provisioner/server/services/ComponentCatalogService.java +++ b/src/main/java/org/opendevstack/component_provisioner/server/services/ComponentCatalogService.java @@ -149,6 +149,18 @@ public CatalogItem getCatalogItem(String accessToken, String catalogItemId, Stri return catalogItem; } + @Cacheable + public CatalogItem getCatalogItemBySlug(String accessToken, String slug) { + var apiClient = apiClientsBuilder.componentCatalogApiClient(accessToken, componentCatalogServiceProps.getBaseRestUrl().toString()); + var catalogItemsApi = apiClientsBuilder.catalogItemsApi(apiClient); + + var catalogItem = catalogItemsApi.getCatalogItemBySlug(slug); + + log.debug("Retrieved catalog item with slug {}: {}", slug, catalogItem); + + return catalogItem; + } + private Map> obfuscateParameters(Map> parameters) { if (parameters == null) { return Collections.emptyMap(); diff --git a/src/test/java/org/opendevstack/component_provisioner/server/controllers/ProvisionResultsApiControllerTest.java b/src/test/java/org/opendevstack/component_provisioner/server/controllers/ProvisionResultsApiControllerTest.java index ba61f88..4629b61 100644 --- a/src/test/java/org/opendevstack/component_provisioner/server/controllers/ProvisionResultsApiControllerTest.java +++ b/src/test/java/org/opendevstack/component_provisioner/server/controllers/ProvisionResultsApiControllerTest.java @@ -9,6 +9,7 @@ import org.opendevstack.component_provisioner.server.controllers.model.ProjectComponentStatus; import org.opendevstack.component_provisioner.server.controllers.model.awx.AwxResponse; import org.opendevstack.component_provisioner.server.facade.ProvisionResultsApiFacade; +import org.opendevstack.component_provisioner.server.model.CreateIncidentAction; import org.opendevstack.component_provisioner.server.model.CreateIncidentActionMother; import org.opendevstack.component_provisioner.server.model.NotifyProvisioningStatusUpdateRequest; import org.opendevstack.component_provisioner.server.model.ProvisionActionResponse; @@ -43,12 +44,14 @@ void givenAProvisionService_whenNotifyProvisioningCompletedIsCalled_thenReturnsO var status = ProjectComponentStatus.CREATED; var componentId = "componentId"; var catalogItemId = "catalogItemId"; + var catalogItemSlug = "catalogItemSlug"; var componentUrl = "componentUrl"; var accessToken = "accessToken"; var request = new NotifyProvisioningStatusUpdateRequest(); request.setComponentId(componentId); request.setCatalogItemId(catalogItemId); + request.setCatalogItemSlug(catalogItemSlug); request.setComponentUrl(componentUrl); when(authenticationProvider.getAccessToken()).thenReturn(accessToken); @@ -58,12 +61,12 @@ void givenAProvisionService_whenNotifyProvisioningCompletedIsCalled_thenReturnsO // then assertEquals(HttpStatus.OK, response.getStatusCode()); - verify(provisionResultsApiFacade).notifyProvisioningStatusUpdate(projectKey, status, componentId, catalogItemId, componentUrl, accessToken); - verify(provisionResultsApiFacade).validate(projectKey, status.name()); + verify(provisionResultsApiFacade).notifyProvisioningStatusUpdate(projectKey, status, request, accessToken); + verify(provisionResultsApiFacade).validate(projectKey, status.name(), request); } @Test - void givenAProjectKey_AndAComponentId_whenDeleteProvisioningStatus_thenReturnsOk() { + void givenAProjectKeyAndAComponentId_whenDeleteProvisioningStatusIsCalled_thenReturnsOk() { // given var projectKey = "project-key"; var componentId = "componentId"; @@ -82,7 +85,7 @@ void givenAProjectKey_AndAComponentId_whenDeleteProvisioningStatus_thenReturnsOk } @Test - void givenAProjectKey_AndAComponentId_AndCreateIncidentAction_whenCreateIncident_thenReturnsOk() { + void givenAProjectKeyAndAComponentIdAndCreateIncidentAction_whenCreateIncidentIsCalled_thenReturnsOk() { // given var projectKey = "project-key"; var componentId = "componentId"; @@ -104,18 +107,18 @@ void givenAProjectKey_AndAComponentId_AndCreateIncidentAction_whenCreateIncident verify(provisionResultsApiFacade).validate(projectKey, componentId, createIncidentAction); verify(provisionResultsApiFacade).addSystemParametersToAction(projectKey, createIncidentAction); verify(provisionResultsApiFacade).requestProvisionToAwx(projectKey, componentId, createIncidentAction); - verify(provisionResultsApiFacade).notifyProvisioningStatusUpdate(eq(projectKey), eq(ProjectComponentStatus.DELETING), eq(componentId), isNull(), isNull(), anyString()); + verify(provisionResultsApiFacade).notifyProvisioningStatusUpdate(eq(projectKey), eq(ProjectComponentStatus.DELETING), any(NotifyProvisioningStatusUpdateRequest.class), isNull()); } @Test - void givenInvalidComponentId_whenCreateIncident_thenThrowsInvalidRestEntityException() { + void givenInvalidComponentId_whenCreateIncidentIsCalled_thenThrowsInvalidRestEntityException() { // given String projectKey = "PRJ"; String componentId = ""; var action = CreateIncidentActionMother.of(); - doThrow(new InvalidRestEntityException("project_key, component_id are required.")).when(provisionResultsApiFacade).validate(any(), any(), any()); + doThrow(new InvalidRestEntityException("project_key, component_id are required.")).when(provisionResultsApiFacade).validate(any(String.class), any(String.class), any(CreateIncidentAction.class)); // when / then var ex = assertThrows(InvalidRestEntityException.class, () -> provisionResultsApiController.createIncident(projectKey, componentId, action)); @@ -123,7 +126,7 @@ void givenInvalidComponentId_whenCreateIncident_thenThrowsInvalidRestEntityExcep } @Test - void givenAProjectKey_AndAComponentId_AndCreateIncidentAction_whenCreateIncident_AndComponentAlreadyInDeletingState_thenReturnsOk_andIgnoreAWXCall() { + void givenAProjectKeyAndAComponentIdAndCreateIncidentAction_whenCreateIncidentIsCalledAndComponentAlreadyInDeletingState_thenReturnsOkAndIgnoreAWXCall() { // given var projectKey = "project-key"; var componentId = "componentId"; @@ -143,7 +146,7 @@ void givenAProjectKey_AndAComponentId_AndCreateIncidentAction_whenCreateIncident } @Test - void givenInvalidStatus_whenNotifyProvisioningStatusUpdate_then400OrInvalidRestEntityException() { + void givenInvalidStatus_whenNotifyProvisioningStatusUpdateIsCalled_thenThrowsInvalidRestEntityException() { // given var projectKey = "project-key"; var invalidStatus = "NOT_A_STATUS"; @@ -152,7 +155,7 @@ void givenInvalidStatus_whenNotifyProvisioningStatusUpdate_then400OrInvalidRestE request.setCatalogItemId("cat-1"); request.setComponentUrl("http://example"); - doThrow(new InvalidRestEntityException(exceptionMsg)).when(provisionResultsApiFacade).validate(any(String.class), any(String.class)); + doThrow(new InvalidRestEntityException(exceptionMsg)).when(provisionResultsApiFacade).validate(any(String.class), any(String.class), any(NotifyProvisioningStatusUpdateRequest.class)); // when / then var exception = assertThrows(InvalidRestEntityException.class, () -> provisionResultsApiController.notifyProvisioningStatusUpdate(projectKey, invalidStatus, request)); @@ -161,7 +164,7 @@ void givenInvalidStatus_whenNotifyProvisioningStatusUpdate_then400OrInvalidRestE } @Test - void givenLowercaseStatus_whenNotifyProvisioningStatusUpdate_thenEitherOkOrReject() { + void givenLowercaseStatus_whenNotifyProvisioningStatusUpdateIsCalled_thenThrowsInvalidRestEntityException() { // given var projectKey = "project-key"; var statusLowercase = "created"; @@ -170,7 +173,7 @@ void givenLowercaseStatus_whenNotifyProvisioningStatusUpdate_thenEitherOkOrRejec request.setCatalogItemId("cat-1"); request.setComponentUrl("http://example"); - doThrow(new InvalidRestEntityException(exceptionMsg)).when(provisionResultsApiFacade).validate(any(String.class), any(String.class)); + doThrow(new InvalidRestEntityException(exceptionMsg)).when(provisionResultsApiFacade).validate(any(String.class), any(String.class), any(NotifyProvisioningStatusUpdateRequest.class)); // when / then var exception = assertThrows(InvalidRestEntityException.class, () -> provisionResultsApiController.notifyProvisioningStatusUpdate(projectKey, statusLowercase, request)); diff --git a/src/test/java/org/opendevstack/component_provisioner/server/facade/ProvisionResultsApiFacadeTest.java b/src/test/java/org/opendevstack/component_provisioner/server/facade/ProvisionResultsApiFacadeTest.java index 8cd1835..792fb96 100644 --- a/src/test/java/org/opendevstack/component_provisioner/server/facade/ProvisionResultsApiFacadeTest.java +++ b/src/test/java/org/opendevstack/component_provisioner/server/facade/ProvisionResultsApiFacadeTest.java @@ -11,6 +11,10 @@ import org.opendevstack.component_provisioner.client.component_catalog.v1.model.ProjectComponentInfo; import org.opendevstack.component_provisioner.client.component_catalog.v1.model.ProjectComponentInfoMother; import org.opendevstack.component_provisioner.server.controllers.exceptions.InvalidRestEntityException; +import org.opendevstack.component_provisioner.client.component_catalog.v1.model.CatalogItem; +import org.opendevstack.component_provisioner.server.controllers.exceptions.SlugNotFoundException; +import org.opendevstack.component_provisioner.server.model.NotifyProvisioningStatusUpdateRequest; +import org.springframework.web.client.RestClientException; import org.opendevstack.component_provisioner.server.controllers.exceptions.ProjectConfigurationException; import org.opendevstack.component_provisioner.server.controllers.model.ProjectComponentStatus; import org.opendevstack.component_provisioner.server.mappers.EntitiesMapper; @@ -65,7 +69,8 @@ void init() { } @Test - void requestProvisionToAwx_mapsResponseCorrectly() { + void givenAProjectKeyAndAComponentId_whenRequestProvisionToAwxIsCalled_thenMapsResponseCorrectly() { + // given var action = CreateIncidentActionMother.of(); var launch = new AwxWorkflowJobLaunch(); var job = new AwxWorkflowJob(); @@ -75,72 +80,105 @@ void requestProvisionToAwx_mapsResponseCorrectly() { when(awxService.triggerWorkflowJob("CREATE_INCIDENT", launch)).thenReturn(Pair.of(HttpStatus.OK, Optional.of(job))); when(entitiesMapper.asProvisionActionResponse(job)).thenReturn(response); + // when var result = facade.requestProvisionToAwx("PRJ", "CID", action); + // then assertEquals(HttpStatus.OK, result.httpStatusCode()); assertEquals(response, result.awxResponseBody()); } @Test - void isInDeletingState_returnsTrueWhenMatchingComponentFound() { + void givenAProjectKeyAndAComponentId_whenIsInDeletingStateIsCalled_thenReturnsTrueWhenMatchingComponentFound() { + // given var action = CreateIncidentActionMother.of(); var accessToken = action.getParameters().stream().filter(p -> p.getName().equals("access_token")).map(CreateIncidentParameter::getValue).map(Object::toString).findFirst().orElseThrow(); ProjectComponentInfo pc = ProjectComponentInfoMother.of(ProjectComponentStatus.DELETING); when(componentCatalogService.getProjectComponents("PRJ", accessToken)).thenReturn(List.of(pc)); + // when var result = facade.isInDeletingState("PRJ", "componentId", accessToken); + + // then assertThat(result).isTrue(); } @Test - void validate_status_throwsOnInvalid() { - var ex = assertThrows(InvalidRestEntityException.class, () -> facade.validate("PRJ", "invalid")); + void givenAnInvalidStatus_whenValidateIsCalled_thenThrowsInvalidRestEntityException() { + // given + var projectKey = "PRJ"; + var status = "invalid"; + + // when / then + var ex = assertThrows(InvalidRestEntityException.class, () -> facade.validate(projectKey, status)); assertThat(ex.getMessage()).contains("Status is not valid"); } @Test - void validate_throwsOnMissingProjectKeyOrStatus() { - assertThrows(InvalidRestEntityException.class, () -> facade.validate(null, "CREATED")); + void givenAMissingProjectKeyOrStatus_whenValidateIsCalled_thenThrowsInvalidRestEntityException() { + // given + var projectKey = (String) null; + var status = "CREATED"; + + // when / then + assertThrows(InvalidRestEntityException.class, () -> facade.validate(projectKey, status)); assertThrows(InvalidRestEntityException.class, () -> facade.validate("PRJ", null)); } @Test - void validate_createIncident_throwsOnMissingMainParams() { + void givenAMissingMainParams_whenValidateIsCalled_thenThrowsInvalidRestEntityException() { + // given var action = CreateIncidentActionMother.of(); + + // when / then assertThrows(InvalidRestEntityException.class, () -> facade.validate(null, "CID", action)); assertThrows(InvalidRestEntityException.class, () -> facade.validate("PRJ", null, action)); } @Test - void getParameterString_returnsEmptyOnMissing() { + void givenAMissingParameter_whenGetParameterStringIsCalled_thenReturnsEmptyString() { + // given var action = CreateIncidentAction.builder().parameters(new ArrayList<>()).build(); - assertThat(facade.getParameterString(action, "missing")).isEmpty(); + + // when + var result = facade.getParameterString(action, "missing"); + + // then + assertThat(result).isEmpty(); } @Test - void isInDeletingState_returnsFalseWhenComponentNotFound() { + void givenAProjectKeyAndAComponentId_whenIsInDeletingStateIsCalled_thenReturnsFalseWhenComponentNotFound() { + // given var action = CreateIncidentActionMother.of(); String accessToken = facade.getParameterString(action, "access_token"); when(componentCatalogService.getProjectComponents("PRJ", accessToken)).thenReturn(Collections.emptyList()); + // when var result = facade.isInDeletingState("PRJ", "componentId", accessToken); + + // then assertThat(result).isFalse(); } @Test - void isInDeletingState_returnsFalseWhenComponentNotDeleting() { + void givenAProjectKeyAndAComponentId_whenIsInDeletingStateIsCalled_thenReturnsFalseWhenComponentNotDeleting() { + // given var action = CreateIncidentActionMother.of(); String accessToken = facade.getParameterString(action, "access_token"); ProjectComponentInfo pc = ProjectComponentInfoMother.of(ProjectComponentStatus.CREATED); pc.setComponentId("componentId"); when(componentCatalogService.getProjectComponents("PRJ", accessToken)).thenReturn(List.of(pc)); + // when var result = facade.isInDeletingState("PRJ", "componentId", accessToken); + + // then assertThat(result).isFalse(); } @Test - void validate_status_doesNotThrowWhenValid() { + void givenAValidStatus_whenValidateIsCalled_thenDoesNotThrow() { // given var projectKey = "PRJ"; var status = ProjectComponentStatus.CREATED.name(); @@ -150,7 +188,7 @@ void validate_status_doesNotThrowWhenValid() { } @Test - void validate_createIncident_throwsOnMissingExtraParams() { + void givenAMissingExtraParams_whenValidateIsCalled_thenThrowsInvalidRestEntityException() { // given var action = CreateIncidentAction.builder().parameters(new ArrayList<>()).build(); @@ -160,7 +198,7 @@ void validate_createIncident_throwsOnMissingExtraParams() { } @Test - void requestProvisionToAwx_returnsNullBodyWhenAwxResponseIsEmpty() { + void givenAnEmptyAwxResponse_whenRequestProvisionToAwxIsCalled_thenReturnsNullBody() { // given var action = CreateIncidentActionMother.of(); var launch = new AwxWorkflowJobLaunch(); @@ -177,24 +215,124 @@ void requestProvisionToAwx_returnsNullBodyWhenAwxResponseIsEmpty() { } @Test - void notifyProvisioningStatusUpdate_delegatesToProvisionService() { + void givenAProvisionService_whenNotifyProvisioningStatusUpdateIsCalled_thenDelegatesToProvisionService() { // given var projectKey = "PRJ"; var status = ProjectComponentStatus.CREATED; var componentId = "CID"; var catalogItemId = "CAT"; + var catalogItemSlug = "SLUG"; var componentUrl = "http://example.com"; var accessToken = "token"; + var request = new NotifyProvisioningStatusUpdateRequest(); + request.setComponentId(componentId); + request.setCatalogItemId(catalogItemId); + request.setCatalogItemSlug(catalogItemSlug); + request.setComponentUrl(componentUrl); + // when - facade.notifyProvisioningStatusUpdate(projectKey, status, componentId, catalogItemId, componentUrl, accessToken); + facade.notifyProvisioningStatusUpdate(projectKey, status, request, accessToken); // then verify(provisionService).notifyProvisioningStatusUpdate(projectKey, status, componentId, catalogItemId, componentUrl, accessToken); } @Test - void deleteProvisioningStatus_delegatesToProvisionService() { + void givenACatalogItemSlug_whenNotifyProvisioningStatusUpdateIsCalled_thenResolvesSlugToId() { + // given + var projectKey = "PRJ"; + var status = ProjectComponentStatus.CREATED; + var componentId = "CID"; + var catalogItemSlug = "SLUG"; + var resolvedCatalogItemId = "RESOLVED_ID"; + var componentUrl = "http://example.com"; + var accessToken = "token"; + + var request = new NotifyProvisioningStatusUpdateRequest(); + request.setComponentId(componentId); + request.setCatalogItemSlug(catalogItemSlug); + request.setComponentUrl(componentUrl); + + var catalogItem = new CatalogItem(); + catalogItem.setId(resolvedCatalogItemId); + + when(componentCatalogService.getCatalogItemBySlug(accessToken, catalogItemSlug)).thenReturn(catalogItem); + + // when + facade.notifyProvisioningStatusUpdate(projectKey, status, request, accessToken); + + // then + verify(provisionService).notifyProvisioningStatusUpdate(projectKey, status, componentId, resolvedCatalogItemId, componentUrl, accessToken); + } + + @Test + void givenAnInvalidCatalogItemSlug_whenNotifyProvisioningStatusUpdateIsCalled_thenThrowsSlugNotFoundException() { + // given + var projectKey = "PRJ"; + var status = ProjectComponentStatus.CREATED; + var componentId = "CID"; + var catalogItemSlug = "INVALID_SLUG"; + var componentUrl = "http://example.com"; + var accessToken = "token"; + + var request = new NotifyProvisioningStatusUpdateRequest(); + request.setComponentId(componentId); + request.setCatalogItemSlug(catalogItemSlug); + request.setComponentUrl(componentUrl); + + when(componentCatalogService.getCatalogItemBySlug(accessToken, catalogItemSlug)).thenThrow(new RestClientException("Not found")); + + // when / then + assertThrows(SlugNotFoundException.class, () -> facade.notifyProvisioningStatusUpdate(projectKey, status, request, accessToken)); + } + + @Test + void givenBothCatalogItemIdAndSlug_whenValidateIsCalled_thenThrowsInvalidRestEntityException() { + // given + var request = new NotifyProvisioningStatusUpdateRequest(); + request.setCatalogItemId("ID"); + request.setCatalogItemSlug("SLUG"); + + // when / then + var ex = assertThrows(InvalidRestEntityException.class, () -> facade.validate("PRJ", "CREATED", request)); + assertThat(ex.getMessage()).contains("Both catalogItemId and catalogItemSlug cannot be defined at the same time"); + } + + @Test + void givenOnlyCatalogItemId_whenValidateIsCalled_thenDoesNotThrow() { + // given + var request = new NotifyProvisioningStatusUpdateRequest(); + request.setCatalogItemId("ID"); + + // when / then + assertDoesNotThrow(() -> facade.validate("PRJ", "CREATED", request)); + } + + @Test + void givenOnlyCatalogItemSlug_whenValidateIsCalled_thenDoesNotThrow() { + // given + var request = new NotifyProvisioningStatusUpdateRequest(); + request.setCatalogItemSlug("SLUG"); + + // when / then + assertDoesNotThrow(() -> facade.validate("PRJ", "CREATED", request)); + } + + @Test + void givenAValidStatusAndRequest_whenValidateIsCalled_thenDoesNotThrow() { + // given + var projectKey = "PRJ"; + var status = ProjectComponentStatus.CREATED.name(); + var request = new NotifyProvisioningStatusUpdateRequest(); + request.setCatalogItemId("ID"); + + // when / then + assertDoesNotThrow(() -> facade.validate(projectKey, status, request)); + } + + @Test + void givenAProjectKeyAndAComponentId_whenDeleteProvisioningStatusIsCalled_thenDelegatesToProvisionService() { // given var projectKey = "PRJ"; var componentId = "CID"; @@ -208,7 +346,7 @@ void deleteProvisioningStatus_delegatesToProvisionService() { } @Test - void addSystemParametersToAction_addsClusterAndCallerToAction() { + void givenAProjectKeyAndAnAction_whenAddSystemParametersToActionIsCalled_thenAddsClusterAndCallerToAction() { // given var projectKey = "PRJ"; var accessToken = "token123"; @@ -232,7 +370,7 @@ void addSystemParametersToAction_addsClusterAndCallerToAction() { } @Test - void addSystemParametersToAction_throwsWhenClustersEmpty() { + void givenAProjectWithNoClusters_whenAddSystemParametersToActionIsCalled_thenThrowsProjectConfigurationException() { // given var projectKey = "PRJ"; var accessToken = "token123"; diff --git a/src/test/java/org/opendevstack/component_provisioner/server/services/ApiClientsBuilderTest.java b/src/test/java/org/opendevstack/component_provisioner/server/services/ApiClientsBuilderTest.java index 18d7724..40ec768 100644 --- a/src/test/java/org/opendevstack/component_provisioner/server/services/ApiClientsBuilderTest.java +++ b/src/test/java/org/opendevstack/component_provisioner/server/services/ApiClientsBuilderTest.java @@ -24,7 +24,7 @@ class ApiClientsBuilderTest { private ApiClientsBuilder builder; @Test - void givenAccessTokenAndBaseUrl_whenProjectsInfoServiceApiClient_thenClientConfiguredCorrectly() { + void givenAccessTokenAndBaseUrl_whenProjectsInfoServiceApiClientIsCalled_thenClientIsConfiguredCorrectly() { // given String accessToken = "test-token"; String baseUrl = "http://example.com"; @@ -41,7 +41,7 @@ void givenAccessTokenAndBaseUrl_whenProjectsInfoServiceApiClient_thenClientConfi } @Test - void givenBearerTokenAndBaseUrl_whenComponentCatalogApiClient_thenClientConfiguredCorrectly() { + void givenBearerTokenAndBaseUrl_whenComponentCatalogApiClientIsCalled_thenClientIsConfiguredCorrectly() { // given String accessToken = "test-token"; String baseUrl = "http://component-catalog"; @@ -58,7 +58,7 @@ void givenBearerTokenAndBaseUrl_whenComponentCatalogApiClient_thenClientConfigur } @Test - void givenApiClient_whenProjectsApi_thenReturnProjectsApiInstance() { + void givenApiClient_whenProjectsApiIsCalled_thenReturnsProjectsApiInstance() { // given ApiClient client = new ApiClient(); @@ -71,7 +71,7 @@ void givenApiClient_whenProjectsApi_thenReturnProjectsApiInstance() { } @Test - void givenApiClient_whenAzureGroupsApi_thenReturnAzureGroupsApiInstance() { + void givenApiClient_whenAzureGroupsApiIsCalled_thenReturnsAzureGroupsApiInstance() { // given ApiClient client = new ApiClient(); @@ -84,7 +84,7 @@ void givenApiClient_whenAzureGroupsApi_thenReturnAzureGroupsApiInstance() { } @Test - void givenApiClient_whenCatalogItemsApi_thenReturnCatalogItemsApiInstance() { + void givenApiClient_whenCatalogItemsApiIsCalled_thenReturnsCatalogItemsApiInstance() { // given var client = new org.opendevstack.component_provisioner.client.component_catalog.v1.ApiClient(); @@ -97,7 +97,7 @@ void givenApiClient_whenCatalogItemsApi_thenReturnCatalogItemsApiInstance() { } @Test - void givenApiClient_whenProvisionerActionsApi_thenReturnProvisionerActionsApiInstance() { + void givenAccessTokenAndBaseUrl_whenProvisionerActionsApiIsCalled_thenReturnsProvisionerActionsApiInstance() { // given String accessToken = "test-token"; String baseUrl = "http://component-catalog"; diff --git a/src/test/java/org/opendevstack/component_provisioner/server/services/ComponentCatalogServiceTest.java b/src/test/java/org/opendevstack/component_provisioner/server/services/ComponentCatalogServiceTest.java index df02a82..bcee390 100644 --- a/src/test/java/org/opendevstack/component_provisioner/server/services/ComponentCatalogServiceTest.java +++ b/src/test/java/org/opendevstack/component_provisioner/server/services/ComponentCatalogServiceTest.java @@ -67,7 +67,7 @@ class ComponentCatalogServiceTest { private ComponentCatalogService componentCatalogService; @Test - void givenValidInputs_whenGetCatalogItemUserActionMessageDefinition_thenReturnsBodyAndStatus() { + void givenValidInputs_whenGetCatalogItemUserActionMessageDefinitionIsCalled_thenReturnsBodyAndStatus() { //given String catalogItemId = "cat-123"; String userActionId = "ua-456"; @@ -96,7 +96,7 @@ void givenValidInputs_whenGetCatalogItemUserActionMessageDefinition_thenReturnsB } @Test - void givenNullBody_whenGetCatalogItemUserActionMessageDefinition_thenReturnsEmptyOptionalAndStatus() { + void givenNullBody_whenGetCatalogItemUserActionMessageDefinitionIsCalled_thenReturnsEmptyOptionalAndStatus() { //given String catalogItemId = "cat-123"; String userActionId = "ua-456"; @@ -125,7 +125,7 @@ void givenNullBody_whenGetCatalogItemUserActionMessageDefinition_thenReturnsEmpt } @Test - void givenApiReturns404_whenGetCatalogItemUserActionMessageDefinition_thenReturnsStatusAndEmptyOptional() { + void givenApiReturns404_whenGetCatalogItemUserActionMessageDefinitionIsCalled_thenReturnsStatusAndEmptyOptional() { //given String catalogItemId = "cat-123"; String userActionId = "ua-456"; @@ -151,7 +151,7 @@ void givenApiReturns404_whenGetCatalogItemUserActionMessageDefinition_thenReturn } @Test - void givenRestClientException_whenGetCatalogItemUserActionMessageDefinition_thenThrowsCatalogClientException() { + void givenRestClientException_whenGetCatalogItemUserActionMessageDefinitionIsCalled_thenThrowsCatalogClientException() { //given String catalogItemId = "cat-123"; String userActionId = "ua-456"; @@ -174,7 +174,7 @@ void givenRestClientException_whenGetCatalogItemUserActionMessageDefinition_then } @Test - void givenValidInput_whenNotifyComponentCatalogProvisionStarts_thenInvokesProvisionerActionsApiWithCreating() throws MalformedURLException { + void givenValidInput_whenNotifyComponentCatalogProvisionStartsIsCalled_thenInvokesProvisionerActionsApiWithCreating() throws MalformedURLException { //given String projectKey = "PRJ-KEY"; String componentId = "CMP-001"; @@ -226,7 +226,7 @@ void givenValidInput_whenNotifyComponentCatalogProvisionStarts_thenInvokesProvis } @Test - void givenNullParameters_whenNotifyComponentCatalogProvisionStarts_thenEmptyMapIsUsed() throws MalformedURLException { + void givenNullParameters_whenNotifyComponentCatalogProvisionStartsIsCalled_thenEmptyMapIsUsed() throws MalformedURLException { //given String projectKey = "PRJ-KEY"; String componentId = "CMP-001"; @@ -248,7 +248,7 @@ void givenNullParameters_whenNotifyComponentCatalogProvisionStarts_thenEmptyMapI } @Test - void givenEmptyBlacklist_whenObfuscateParameters_thenNoParametersAreMasked() { + void givenEmptyBlacklist_whenObfuscateParametersIsCalled_thenNoParametersAreMasked() { // given when(parametersProps.getBlacklist()).thenReturn(new String[0]); Map> input = Map.of("key", List.of("value")); @@ -268,7 +268,7 @@ void givenEmptyBlacklist_whenObfuscateParameters_thenNoParametersAreMasked() { } @Test - void givenNullBlacklist_whenObfuscateParameters_thenNoParametersAreMasked() { + void givenNullBlacklist_whenObfuscateParametersIsCalled_thenNoParametersAreMasked() { // given when(parametersProps.getBlacklist()).thenReturn(null); Map> input = Map.of("key", List.of("value")); @@ -288,7 +288,7 @@ void givenNullBlacklist_whenObfuscateParameters_thenNoParametersAreMasked() { } @Test - void givenParameters_whenMaskParameters_thenCorrectParametersAreMasked() { + void givenParameters_whenMaskParametersIsCalled_thenCorrectParametersAreMasked() { // given when(parametersProps.getBlacklist()).thenReturn(new String[]{"password", "token"}); Map> input = Map.of( @@ -317,7 +317,7 @@ void givenParameters_whenMaskParameters_thenCorrectParametersAreMasked() { } @Test - void givenValidInput_whenGetCatalogItem_thenCatalogItemIsReturned() throws MalformedURLException { + void givenValidInput_whenGetCatalogItemIsCalled_thenCatalogItemIsReturned() throws MalformedURLException { // given String accessToken = "access-token"; String catalogItemId = "CAT-123"; @@ -359,7 +359,7 @@ void givenValidInput_whenGetCatalogItem_thenCatalogItemIsReturned() throws Malfo } @Test - void givenValidInput_whenGetProjectComponents_thenProjectComponentsAreReturned() { + void givenValidInput_whenGetProjectComponentsIsCalled_thenProjectComponentsAreReturned() { // given String projectKey = "PRJ-1"; String accessToken = "access-token"; @@ -378,4 +378,29 @@ void givenValidInput_whenGetProjectComponents_thenProjectComponentsAreReturned() verify(auth).setBearerToken(accessToken); verify(projectComponentsApi).getProjectComponents(projectKey, accessToken); } + + @Test + void givenValidInput_whenGetCatalogItemBySlugIsCalled_thenCatalogItemIsReturned() throws MalformedURLException { + // given + String accessToken = "access-token"; + String slug = "myproject_repo-name"; + URL baseUrl = URI.create("http://component-catalog").toURL(); + CatalogItem expectedCatalogItem = new CatalogItem(); + expectedCatalogItem.setId("CAT-123"); + + when(componentCatalogServiceProps.getBaseRestUrl()).thenReturn(baseUrl); + when(apiClientsBuilder.componentCatalogApiClient(accessToken, baseUrl.toString())) + .thenReturn(componentCatalogApiClient); + when(apiClientsBuilder.catalogItemsApi(componentCatalogApiClient)) + .thenReturn(catalogItemsApi); + when(catalogItemsApi.getCatalogItemBySlug(slug)) + .thenReturn(expectedCatalogItem); + + // when + CatalogItem result = componentCatalogService.getCatalogItemBySlug(accessToken, slug); + + // then + assertThat(result).isSameAs(expectedCatalogItem); + verify(catalogItemsApi).getCatalogItemBySlug(slug); + } } \ No newline at end of file diff --git a/src/test/java/org/opendevstack/component_provisioner/server/services/ProvisionServiceTest.java b/src/test/java/org/opendevstack/component_provisioner/server/services/ProvisionServiceTest.java new file mode 100644 index 0000000..efa30de --- /dev/null +++ b/src/test/java/org/opendevstack/component_provisioner/server/services/ProvisionServiceTest.java @@ -0,0 +1,83 @@ +package org.opendevstack.component_provisioner.server.services; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.opendevstack.component_provisioner.client.component_catalog.v1.api.ProvisionerActionsApi; +import org.opendevstack.component_provisioner.client.component_catalog.v1.model.ProvisioningDeleteRequest; +import org.opendevstack.component_provisioner.client.component_catalog.v1.model.ProvisioningStatusUpdateRequest; +import org.opendevstack.component_provisioner.config.ApplicationPropertiesConfiguration; +import org.opendevstack.component_provisioner.server.controllers.model.ProjectComponentStatus; + +import java.net.URL; + +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class ProvisionServiceTest { + + @Mock + private ApiClientsBuilder apiClientsBuilder; + + @Mock + private ApplicationPropertiesConfiguration.ComponentCatalogServiceProps componentCatalogServiceProps; + + @Mock + private ProvisionerActionsApi provisionerActionsApi; + + @InjectMocks + private ProvisionService provisionService; + + @Test + void givenAProjectKeyAndStatusAndComponentIdAndCatalogItemIdAndComponentUrlAndAccessToken_whenNotifyProvisioningStatusUpdateIsCalled_thenInvokesProvisionerActionsApi() throws Exception { + // given + var projectKey = "PRJ"; + var status = ProjectComponentStatus.CREATED; + var componentId = "CID"; + var catalogItemId = "CAT"; + var componentUrl = "http://example.com"; + var accessToken = "token"; + var baseUrl = "http://catalog.example.com"; + + when(componentCatalogServiceProps.getBaseRestUrl()).thenReturn(new URL(baseUrl)); + when(apiClientsBuilder.provisionerActionsApi(accessToken, baseUrl)).thenReturn(provisionerActionsApi); + + // when + provisionService.notifyProvisioningStatusUpdate(projectKey, status, componentId, catalogItemId, componentUrl, accessToken); + + // then + var expectedRequest = ProvisioningStatusUpdateRequest.builder() + .componentId(componentId) + .catalogItemId(catalogItemId) + .componentUrl(componentUrl) + .accessToken(accessToken) + .build(); + + verify(provisionerActionsApi).notifyProvisioningStatusUpdatePartially(projectKey, status.name(), expectedRequest); + } + + @Test + void givenAProjectKeyAndComponentIdAndAccessToken_whenDeleteProvisioningStatusIsCalled_thenInvokesProvisionerActionsApi() throws Exception { + // given + var projectKey = "PRJ"; + var componentId = "CID"; + var accessToken = "token"; + var baseUrl = "http://catalog.example.com"; + + when(componentCatalogServiceProps.getBaseRestUrl()).thenReturn(new URL(baseUrl)); + when(apiClientsBuilder.provisionerActionsApi(accessToken, baseUrl)).thenReturn(provisionerActionsApi); + + // when + provisionService.deleteProvisioningStatus(projectKey, componentId, accessToken); + + // then + var expectedRequest = ProvisioningDeleteRequest.builder() + .componentId(componentId) + .build(); + + verify(provisionerActionsApi).deleteProvisioningStatus(projectKey, expectedRequest); + } +}