diff --git a/.gitignore b/.gitignore index 5183737..c765529 100644 --- a/.gitignore +++ b/.gitignore @@ -95,5 +95,7 @@ api-project-platform/src/main/java/org/opendevstack/apiservice/projectplatform/a api-project-platform/src/main/java/org/opendevstack/apiservice/projectplatform/model api-project/src/main/java/org/opendevstack/apiservice/project/api api-project/src/main/java/org/opendevstack/apiservice/project/model +api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/api +api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/model **/.openapi-generator diff --git a/api-project-component-v0/openapi/api-project-component-v0.yaml b/api-project-component-v0/openapi/api-project-component-v0.yaml new file mode 100644 index 0000000..dcd3912 --- /dev/null +++ b/api-project-component-v0/openapi/api-project-component-v0.yaml @@ -0,0 +1,166 @@ +openapi: 3.0.3 +info: + title: ODS API Server + description: API documentation for ODS (Open DevStack) API Service + contact: + name: ODS Team + version: v0.0.1 +servers: + - url: http://{baseurl}/api/pub/v0 + variables: + baseurl: + default: localhost:8080 + description: Development environment +tags: + - name: ProjectComponents + description: API for managing project components + +paths: + /projects/{projectId}/components/: + post: + tags: + - projectComponents + summary: Create a component in a project + operationId: createProjectComponent + description: Retrieves information about a specific component + parameters: + - name: projectId + in: path + required: true + description: Project id + schema: + type: string + requestBody: + description: Component data + content: + application/json: + schema: + $ref: '#/components/schemas/CreateComponentRequest' + responses: + '201': + description: Created + headers: + Location: + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/CreateComponentResponse' + '400': + description: Bad Request + '401': + $ref: '#/components/responses/UnauthorizedError' + '403': + description: Forbidden + '404': + description: Endpoint not found / Project not found / Product not found + '500': + description: Internal Server Error + + /projects/{projectId}/components/{componentId}: + get: + tags: + - projectComponents + summary: Get component information + operationId: getProjectComponent + description: Retrieves information about a specific component + parameters: + - name: projectId + in: path + required: true + description: Project id + schema: + type: string + - name: componentId + in: path + required: true + description: Component id + schema: + type: string + responses: + '200': + description: Component information + content: + application/json: + schema: + $ref: '#/components/schemas/Component' + +components: + securitySchemes: + basicAuth: + type: http + scheme: basic + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + responses: + UnauthorizedError: + description: Authentication information is missing or invalid + headers: + WWW_Authenticate: + schema: + type: string + schemas: + Component: + type: object + properties: + id: + type: string + description: Component ID + name: + type: string + description: Name of the component + productDescription: + type: string + description: Description of the product + productName: + type: string + description: Name of the product (e.g. Docker plain) + productId: + type: string + description: Product ID + environment: + type: string + description: Environment (e.g. DEV) + status: + type: string + description: Status of the component (e.g. READY, NOT_READY) + resultTraceback: + type: string + description: Traceback information in case of error + repositoryURL: + type: string + description: URL of the repository (for ODS products) + params: + type: object + description: Additional parameters (key-value pairs) + component-type: + type: string + description: Type of component (ods|awx) + CreateComponentResponse: + type: object + properties: + errorCode: + type: integer + field: + type: string + message: + type: string + location: + type: string + format: url + CreateComponentRequest: + type: object + properties: + name: + type: string + description: component name + productId: + type: string + description: product id + params: + type: object + additionalProperties: + type: string \ No newline at end of file diff --git a/api-project-component-v0/pom.xml b/api-project-component-v0/pom.xml new file mode 100644 index 0000000..a3f50df --- /dev/null +++ b/api-project-component-v0/pom.xml @@ -0,0 +1,163 @@ + + 4.0.0 + + + org.opendevstack.apiservice + devstack-api-service + 0.0.3 + + + api-project-component-v0 + API Project Components V0 + API module for managing EDP project components + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + + org.springframework.boot + spring-boot-starter-validation + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + + + + org.opendevstack.apiservice + service-projects + ${project.version} + + + + io.jsonwebtoken + jjwt-api + 0.12.3 + + + + io.jsonwebtoken + jjwt-impl + 0.12.3 + runtime + + + + io.jsonwebtoken + jjwt-jackson + 0.12.3 + runtime + + + + org.projectlombok + lombok + provided + + + + org.mapstruct + mapstruct + 1.6.3 + + + + org.openapitools + jackson-databind-nullable + ${jackson-databind-nullable.version} + + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-core + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.projectlombok + lombok + ${lombok.version} + + + org.mapstruct + mapstruct-processor + 1.6.3 + + + + + + org.openapitools + openapi-generator-maven-plugin + + + generate-api-project-component-v0 + + generate + + + spring + ${project.basedir} + spring-boot + ${project.basedir}/openapi/api-project-component-v0.yaml + org.opendevstack.apiservice.project.api + org.opendevstack.apiservice.project.model + org.opendevstack.apiservice.project + false + false + false + false + false + false + + true + true + springdoc + true + true + true + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + true + + + + + + \ No newline at end of file diff --git a/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/controller/ComponentsResponseFactory.java b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/controller/ComponentsResponseFactory.java new file mode 100644 index 0000000..c3ef127 --- /dev/null +++ b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/controller/ComponentsResponseFactory.java @@ -0,0 +1,25 @@ +package org.opendevstack.apiservice.project.controller; + +import org.opendevstack.apiservice.project.model.CreateComponentResponse; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +public class ComponentsResponseFactory { + + private ComponentsResponseFactory() { + } + + public static CreateComponentResponse error(String projectId) { + CreateComponentResponse response = new CreateComponentResponse(); + response.setErrorCode(HttpStatus.INTERNAL_SERVER_ERROR.value()); + response.setMessage("Failed to create component for project '" + projectId + "'"); + return response; + } + + public static CreateComponentResponse entityCreated(String projectId, String componentName) { + CreateComponentResponse response = new CreateComponentResponse(); + response.setErrorCode(HttpStatus.CREATED.value()); + response.setMessage(componentName + " component created successfully in project " + projectId); + return response; + } +} diff --git a/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/controller/ProjectComponentsController.java b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/controller/ProjectComponentsController.java new file mode 100644 index 0000000..2a49194 --- /dev/null +++ b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/controller/ProjectComponentsController.java @@ -0,0 +1,52 @@ +package org.opendevstack.apiservice.project.controller; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.opendevstack.apiservice.project.api.ProjectComponentsApi; +import org.opendevstack.apiservice.project.mapper.ComponentResponseMapper; +import org.opendevstack.apiservice.project.model.Component; +import org.opendevstack.apiservice.project.model.CreateComponentRequest; +import org.opendevstack.apiservice.project.model.CreateComponentResponse; +import org.opendevstack.apiservice.project.service.ComponentsService; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@AllArgsConstructor +@Slf4j +public class ProjectComponentsController implements ProjectComponentsApi { + + private final ComponentsService componentsService; + + private final ComponentResponseMapper componentResponseMapper; + + @Override + public ResponseEntity createProjectComponent(String projectId, CreateComponentRequest createComponentRequest) { + try { + Component component = componentsService.createProjectComponent(projectId, createComponentRequest); + if (component == null) { + log.error("Failed to create component for project '{}'", projectId); + return componentResponseMapper.toResponseEntity(ComponentsResponseFactory.error(projectId)); + } + return componentResponseMapper.toResponseEntity(ComponentsResponseFactory.entityCreated(projectId, component.getName())); + } catch (Exception e) { + log.error("Error while trying to create component for project '" + projectId + "': " + e.getMessage(), e); + return componentResponseMapper.toResponseEntity(ComponentsResponseFactory.error(projectId)); + } + } + + @Override + public ResponseEntity getProjectComponent(String projectId, String componentId) { + try { + Component component = componentsService.getProjectComponent(projectId, componentId); + if (component == null) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).build(); + } + return ResponseEntity.status(HttpStatus.OK).body(component); + } catch (Exception e) { + log.error("Error retrieving component '{}' for project '{}': {}", componentId, projectId, e.getMessage(), e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } +} diff --git a/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/mapper/ComponentResponseMapper.java b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/mapper/ComponentResponseMapper.java new file mode 100644 index 0000000..4c3198b --- /dev/null +++ b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/mapper/ComponentResponseMapper.java @@ -0,0 +1,15 @@ +package org.opendevstack.apiservice.project.mapper; + +import org.mapstruct.Mapper; +import org.opendevstack.apiservice.project.model.CreateComponentResponse; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +@Mapper(componentModel = "spring") +public interface ComponentResponseMapper { + + default ResponseEntity toResponseEntity(CreateComponentResponse response) { + return new ResponseEntity<>(response, HttpStatus.valueOf(response.getErrorCode())); + } + +} diff --git a/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/service/ComponentsService.java b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/service/ComponentsService.java new file mode 100644 index 0000000..959127a --- /dev/null +++ b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/service/ComponentsService.java @@ -0,0 +1,43 @@ +package org.opendevstack.apiservice.project.service; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.opendevstack.apiservice.externalservice.api.ExternalService; +import org.opendevstack.apiservice.project.model.Component; +import org.opendevstack.apiservice.project.model.CreateComponentRequest; +import org.springframework.stereotype.Service; + +@Service +@AllArgsConstructor +@Slf4j +public class ComponentsService { + + private final MarketplaceExternalServicePlaceholder marketplaceExternalService; + + public Component getProjectComponent(String projectId, String componentId) { + return marketplaceExternalService.getProjectComponent(projectId, componentId); + } + + public Component createProjectComponent(String projectId, CreateComponentRequest createComponentRequest) { + return marketplaceExternalService.createProjectComponent(projectId, createComponentRequest); + } + + @Service + class MarketplaceExternalServicePlaceholder implements ExternalService { + + @Override + public boolean isHealthy() { + return false; + } + + public Component getProjectComponent(String projectId, String componentId) { + log.info("Get component with id '" + componentId + "' for project '" + projectId + "'"); + return null; + } + + public Component createProjectComponent(String projectId, CreateComponentRequest createComponentRequest) { + log.info("Creating component for project '" + projectId + "'" + " with request: " + createComponentRequest); + return null; + } + } +} diff --git a/api-project-component-v0/src/test/java/org/opendevstack/apiservice/project/controller/ProjectComponentsControllerTest.java b/api-project-component-v0/src/test/java/org/opendevstack/apiservice/project/controller/ProjectComponentsControllerTest.java new file mode 100644 index 0000000..4d46cbb --- /dev/null +++ b/api-project-component-v0/src/test/java/org/opendevstack/apiservice/project/controller/ProjectComponentsControllerTest.java @@ -0,0 +1,100 @@ +package org.opendevstack.apiservice.project.controller; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mapstruct.factory.Mappers; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.opendevstack.apiservice.project.mapper.ComponentResponseMapper; +import org.opendevstack.apiservice.project.model.Component; +import org.opendevstack.apiservice.project.model.CreateComponentRequest; +import org.opendevstack.apiservice.project.model.CreateComponentResponse; +import org.opendevstack.apiservice.project.service.ComponentsService; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; +import static org.opendevstack.apiservice.project.util.TestHelper.*; + +@ExtendWith(MockitoExtension.class) +class ProjectComponentsControllerTest { + + @Mock + private ComponentsService componentsService; + + private final ComponentResponseMapper componentResponseMapper = Mappers.getMapper(ComponentResponseMapper.class); + + private ProjectComponentsController projectComponentsController; + + @BeforeEach + void setup() { + projectComponentsController = new ProjectComponentsController(componentsService, componentResponseMapper); + } + + @Test + void testCreateProjectComponent_whenSuccess_thenReturnOk() throws Exception { + Component testComponent = buildTestComponent(); + String testProjectId = "testProjectId"; + CreateComponentRequest testCreateComponentRequest = buildTestCreateComponentRequest(); + CreateComponentResponse testServiceResponseSuccess = buildTestCreateComponentResponseSuccess(testComponent.getName(), + testProjectId); + + when(componentsService.createProjectComponent(anyString(), any(CreateComponentRequest.class))) + .thenReturn(testComponent); + + ResponseEntity response = projectComponentsController.createProjectComponent(testProjectId, + testCreateComponentRequest); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody()).isEqualTo(testServiceResponseSuccess); + } + + @Test + void testCreateProjectComponent_whenFailure_thenReturnErrorResponse() throws Exception { + CreateComponentRequest testCreateComponentRequest = buildTestCreateComponentRequest(); + String testProjectId = "testProjectId"; + CreateComponentResponse testServiceResponseFailure = buildTestCreateComponentResponseFailure(testProjectId); + + when(componentsService.createProjectComponent(anyString(), any(CreateComponentRequest.class))) + .thenReturn(null); + + ResponseEntity response = projectComponentsController.createProjectComponent(testProjectId, + testCreateComponentRequest); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody()).isEqualTo(testServiceResponseFailure); + } + + @Test + void testGetProjectComponent_whenSuccess_thenReturnOk() throws Exception { + Component testComponent = buildTestComponent(); + + when(componentsService.getProjectComponent(anyString(), anyString())) + .thenReturn(testComponent); + + ResponseEntity response = projectComponentsController.getProjectComponent("projectId", + "testId"); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody()).isEqualTo(testComponent); + } + + @Test + void testGetProjectComponent_whenFailure_thenReturnErrorResponse() throws Exception { + when(componentsService.getProjectComponent(anyString(), anyString())) + .thenReturn(null); + + ResponseEntity response = projectComponentsController.getProjectComponent("projectId", + "testId"); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + assertThat(response.getBody()).isNull(); + } +} \ No newline at end of file diff --git a/api-project-component-v0/src/test/java/org/opendevstack/apiservice/project/service/ComponentsServiceTest.java b/api-project-component-v0/src/test/java/org/opendevstack/apiservice/project/service/ComponentsServiceTest.java new file mode 100644 index 0000000..1ce3b3d --- /dev/null +++ b/api-project-component-v0/src/test/java/org/opendevstack/apiservice/project/service/ComponentsServiceTest.java @@ -0,0 +1,74 @@ +package org.opendevstack.apiservice.project.service; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.opendevstack.apiservice.project.model.Component; +import org.opendevstack.apiservice.project.model.CreateComponentRequest; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; +import static org.opendevstack.apiservice.project.util.TestHelper.buildTestComponent; +import static org.opendevstack.apiservice.project.util.TestHelper.buildTestCreateComponentRequest; + +@ExtendWith(MockitoExtension.class) +class ComponentsServiceTest { + + @Mock + private ComponentsService.MarketplaceExternalServicePlaceholder marketPlaceExternalServicePlaceholder; + + private ComponentsService componentsService; + + @BeforeEach + void setup() { + componentsService = new ComponentsService(marketPlaceExternalServicePlaceholder); + } + + @Test + void testGetProjectComponent_whenSuccess_thenReturnCorrectComponent() throws Exception { + Component testComponent = buildTestComponent(); + + when(marketPlaceExternalServicePlaceholder.getProjectComponent(anyString(), eq("testId"))) + .thenReturn(testComponent); + + Component retrievedComponent = componentsService.getProjectComponent("testId", "testId"); + assertThat(retrievedComponent).isEqualTo(testComponent); + } + + @Test + void testGetProjectComponent_whenNoComponentFound_thenReturnNull() throws Exception { + when(marketPlaceExternalServicePlaceholder.getProjectComponent(anyString(), eq("testId"))) + .thenReturn(null); + + Component retrievedComponent = componentsService.getProjectComponent("testId", "testId"); + assertThat(retrievedComponent).isNull(); + } + + @Test + void testCreateProjectComponent_whenSuccess_thenReturnCorrectComponent() throws Exception { + Component testComponent = buildTestComponent(); + CreateComponentRequest testRequest = buildTestCreateComponentRequest(); + + when(marketPlaceExternalServicePlaceholder.createProjectComponent(anyString(), eq(testRequest))) + .thenReturn(testComponent); + + Component retrievedComponent = componentsService.createProjectComponent("testId", testRequest); + assertThat(retrievedComponent).isEqualTo(testComponent); + } + + + @Test + void testCreateProjectComponent_whenFailure_thenReturnNull() throws Exception { + CreateComponentRequest testRequest = buildTestCreateComponentRequest(); + + when(marketPlaceExternalServicePlaceholder.createProjectComponent(anyString(), eq(testRequest))) + .thenReturn(null); + + Component retrievedComponent = componentsService.createProjectComponent("testId", testRequest); + assertThat(retrievedComponent).isNull(); + } +} \ No newline at end of file diff --git a/api-project-component-v0/src/test/java/org/opendevstack/apiservice/project/util/TestHelper.java b/api-project-component-v0/src/test/java/org/opendevstack/apiservice/project/util/TestHelper.java new file mode 100644 index 0000000..40c0ac6 --- /dev/null +++ b/api-project-component-v0/src/test/java/org/opendevstack/apiservice/project/util/TestHelper.java @@ -0,0 +1,42 @@ +package org.opendevstack.apiservice.project.util; + +import org.opendevstack.apiservice.project.model.Component; +import org.opendevstack.apiservice.project.model.CreateComponentRequest; +import org.opendevstack.apiservice.project.model.CreateComponentResponse; +import org.springframework.http.HttpStatus; + +public class TestHelper { + + private TestHelper() { + } + + public static Component buildTestComponent() { + Component component = new Component(); + component.setId("testId"); + component.setName("testComponentName"); + component.environment("testEnv"); + component.setComponentType("testComponentType"); + return component; + } + + public static CreateComponentRequest buildTestCreateComponentRequest() { + CreateComponentRequest request = new CreateComponentRequest(); + request.setName("testComponentName"); + request.setProductId("testProductId"); + return request; + } + + public static CreateComponentResponse buildTestCreateComponentResponseSuccess(String componentName, String projectId) { + CreateComponentResponse response = new CreateComponentResponse(); + response.setErrorCode(HttpStatus.CREATED.value()); + response.setMessage(componentName + " component created successfully in project " + projectId); + return response; + } + + public static CreateComponentResponse buildTestCreateComponentResponseFailure(String projectId) { + CreateComponentResponse response = new CreateComponentResponse(); + response.setErrorCode(HttpStatus.INTERNAL_SERVER_ERROR.value()); + response.setMessage("Failed to create component for project '" + projectId + "'"); + return response; + } +} diff --git a/pom.xml b/pom.xml index c825aa0..6382c31 100644 --- a/pom.xml +++ b/pom.xml @@ -57,6 +57,7 @@ api-project-users api-project-platform api-project + api-project-component-v0