From ea11f7f4daaaa04b801e9aa3b905440bffba19eb Mon Sep 17 00:00:00 2001 From: soriaoli Date: Tue, 17 Feb 2026 09:56:51 +0100 Subject: [PATCH 01/11] [ods-api-to-projecs-info-service] - Add projects info service openApi configuration. --- .../openapi-projects-info-service-v1.0.0.yaml | 264 ++++++++++++++++++ 1 file changed, 264 insertions(+) create mode 100644 external-service-projects-info-service/openapi/openapi-projects-info-service-v1.0.0.yaml diff --git a/external-service-projects-info-service/openapi/openapi-projects-info-service-v1.0.0.yaml b/external-service-projects-info-service/openapi/openapi-projects-info-service-v1.0.0.yaml new file mode 100644 index 0000000..251ab8f --- /dev/null +++ b/external-service-projects-info-service/openapi/openapi-projects-info-service-v1.0.0.yaml @@ -0,0 +1,264 @@ +openapi: 3.0.3 +info: + title: Projects info service REST API + version: '1.0.0' + description: > + The Projects Info Service API allows clients to collect project information for a predefined user. This user will be + Managed thanks to SSO token. + + **NOTES**: + - The OpenAPI specification file is also used to [generate](https://openapi-generator.tech/) REST client(s) and a server REST API. + - Clients and servers generated from the same OpenAPI specification version are guaranteed to be **compatible**. + contact: + name: OpenDevStack + url: https://www.opendevstack.org/ +servers: + - url: http://{baseurl}/v1 + variables: + baseurl: + default: localhost:8080 + description: Default address for a Projects Info Service's backend REST API instance. +security: + - bearerAuth: [] +tags: + - name: AzureGroups + description: Get azure groups operations. + - name: Projects + description: Get projects operations. +paths: + /azure/groups: + get: + tags: + - AzureGroups + summary: Get all azure groups associated to the user. + description: > + This endpoint receives an azure token, and returns all the groups associated to the user. + operationId: getAzureGroups + parameters: + - name: token + in: header + required: true + schema: + type: string + description: Azure token used to get the groups. + responses: + "200": + description: List of azure groups associated to the user. + content: + application/json: + schema: + type: array + items: + type: string + "401": + description: Invalid client token on the request. + content: + application/json: + schema: + $ref: '#/components/schemas/RestErrorMessage' + "403": + description: Insufficient permissions for the client to access the resource. + content: + application/json: + schema: + $ref: '#/components/schemas/RestErrorMessage' + "500": + description: Server error. + content: + application/json: + schema: + $ref: '#/components/schemas/RestErrorMessage' + /projects: + get: + tags: + - Projects + summary: Get all the projects a user get access to. + description: > + Get all the projects a user get access to. For that, first of all it will get all the azure groups associated to the user, + and then it will get all the projects associated to those groups. + operationId: getProjects + parameters: + - name: token + in: header + required: true + schema: + type: string + description: Azure token used to get the groups. + responses: + "200": + description: List of projects the user has access to. + content: + application/json: + schema: + type: array + items: + type: string + "401": + description: Invalid client token on the request. + content: + application/json: + schema: + $ref: '#/components/schemas/RestErrorMessage' + "403": + description: Insufficient permissions for the client to access the resource. + content: + application/json: + schema: + $ref: '#/components/schemas/RestErrorMessage' + "500": + description: Server error. + content: + application/json: + schema: + $ref: '#/components/schemas/RestErrorMessage' + /projects/{projectKey}/clusters: + get: + tags: + - Projects + summary: Get all project info and cluster for a given project key. + description: > + Get all project info and cluster for a given project key. + operationId: getProjectClusters + parameters: + - name: token + in: header + required: true + schema: + type: string + description: Azure token used to get the groups. + - name: projectKey + in: path + required: true + schema: + type: string + description: Project id to get the info and clusters. + responses: + "200": + description: Project info. + content: + application/json: + schema: + $ref: '#/components/schemas/ProjectInfo' + + "401": + description: Invalid client token on the request. + content: + application/json: + schema: + $ref: '#/components/schemas/RestErrorMessage' + "403": + description: Insufficient permissions for the client to access the resource. + content: + application/json: + schema: + $ref: '#/components/schemas/RestErrorMessage' + "500": + description: Server error. + content: + application/json: + schema: + $ref: '#/components/schemas/RestErrorMessage' + + /projects/{projectKey}/platforms: + get: + tags: + - Projects + summary: Get platform information for a given project key. + description: Returns disabled platforms and categorized platform links for the specified project. + operationId: getProjectPlatforms + parameters: + - name: projectKey + in: path + required: true + schema: + type: string + description: Project key to retrieve platform information. + responses: + '200': + description: Platform information for the project. + content: + application/json: + schema: + $ref: '#/components/schemas/ProjectPlatforms' + "401": + description: Invalid client token on the request. + content: + application/json: + schema: + $ref: '#/components/schemas/RestErrorMessage' + "403": + description: Insufficient permissions for the client to access the resource. + content: + application/json: + schema: + $ref: '#/components/schemas/RestErrorMessage' + +components: + securitySchemes: + bearerAuth: + type: http + description: > + Authorization via bearer token. + scheme: bearer + bearerFormat: JWT + schemas: + RestErrorMessage: + properties: + message: + type: string + required: + - message + ProjectInfo: + type: object + properties: + projectKey: + type: string + description: Project KEY. + clusters: + type: array + items: + type: string + description: List of clusters associated to the project. + required: + - projectKey + - projectName + - clusters + Link: + type: object + properties: + label: + type: string + url: + type: string + tooltip: + type: string + type: + type: string + abbreviation: + type: string + disabled: + type: boolean + required: + - label + - url + Section: + type: object + properties: + section: + type: string + tooltip: + type: string + links: + type: array + items: + $ref: '#/components/schemas/Link' + required: + - section + - links + ProjectPlatforms: + type: object + properties: + sections: + type: array + items: + $ref: '#/components/schemas/Section' \ No newline at end of file From 7cc9fa27e3ae2e86d9055dd180d33775abc0d7d5 Mon Sep 17 00:00:00 2001 From: soriaoli Date: Tue, 17 Feb 2026 10:30:44 +0100 Subject: [PATCH 02/11] [ods-api-to-projecs-info-service] - Delete previous code. --- .../facade/impl/ProjectsFacadeImpl.java | 15 +- .../mapper/ProjectPlatformsMapper.java | 77 +---- .../facade/impl/ProjectsFacadeImplTest.java | 22 +- .../mapper/ProjectPlatformsMapperTest.java | 24 +- .../pom.xml | 35 ++ .../annotations/CacheableWithFallback.java | 15 - .../CacheableWithFallbackAspect.java | 173 ---------- .../client/AzureGraphClient.java | 183 ----------- .../client/PlatformsYmlClient.java | 63 ---- .../client/TestingHubClient.java | 97 ------ .../config/CacheConfiguration.java | 63 ---- .../CacheSpecPropertiesConfiguration.java | 23 -- .../config/MockConfiguration.java | 23 -- .../config/OpenshiftClusterConfiguration.java | 17 - .../config/PlatformsConfiguration.java | 18 -- .../config/ProjectFilterConfiguration.java | 22 -- .../config/ProjectsInfoServiceConfig.java | 115 ------- .../ProjectsInfoServiceSslProperties.java | 65 ---- .../projectsinfoservice/dto/Link.java | 302 ----------------- .../projectsinfoservice/dto/ProjectInfo.java | 188 ----------- .../dto/ProjectPlatforms.java | 153 --------- .../projectsinfoservice/dto/Section.java | 225 ------------- .../InvalidConfigurationException.java | 7 - .../InvalidContentProcessException.java | 11 - .../exception/InvalidTokenException.java | 7 - .../ProjectsInfoServiceException.java | 15 - .../UnableToReachAzureException.java | 7 - .../facade/ProjectsFacade.java | 106 ------ .../projectsinfoservice/model/Metadata.java | 12 - .../model/OpenshiftProjectCluster.java | 14 - .../projectsinfoservice/model/Platform.java | 15 - .../model/PlatformLink.java | 15 - .../model/PlatformSection.java | 59 ---- .../model/PlatformSectionLink.java | 69 ---- .../model/PlatformSectionService.java | 16 - .../projectsinfoservice/model/Platforms.java | 38 --- .../model/PlatformsWithTitle.java | 15 - .../model/PlatformsYml.java | 19 -- .../projectsinfoservice/model/Project.java | 12 - .../model/ProjectList.java | 14 - .../model/TestingHubProject.java | 14 - .../service/MockProjectsService.java | 119 ------- .../service/OpenShiftProjectService.java | 82 ----- .../service/PlatformService.java | 243 -------------- .../service/ProjectsInfoService.java | 37 --- .../service/impl/ProjectsInfoServiceImpl.java | 109 ------- .../CacheableWithFallbackAspectTest.java | 256 --------------- .../client/AzureGraphClientTest.java | 167 ---------- .../config/SslConfigurationTest.java | 44 --- .../projectsinfoservice/dto/LinkMother.java | 17 - .../dto/ProjectInfoMother.java | 17 - .../dto/ProjectPlatformsMother.java | 12 - .../dto/SectionMother.java | 14 - .../facade/ProjectsFacadeTest.java | 177 ---------- .../model/OpenshiftProjectClusterMother.java | 11 - .../model/PlatformLinkMother.java | 22 -- .../model/PlatformMother.java | 17 - .../model/PlatformsWithTitleMother.java | 27 -- .../model/TestingHubProjectMother.java | 7 - .../service/MockProjectsServiceTest.java | 32 -- .../service/OpenshiftProjectServiceTest.java | 133 -------- .../service/PlatformServiceTest.java | 304 ------------------ .../impl/ProjectsInfoServiceImplTest.java | 295 ----------------- .../images/application-configuration.png | Bin 161707 -> 0 bytes .../static/images/configuration-tree.png | Bin 75864 -> 0 bytes 65 files changed, 58 insertions(+), 4467 deletions(-) delete mode 100644 external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/annotations/CacheableWithFallback.java delete mode 100644 external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/annotations/CacheableWithFallbackAspect.java delete mode 100644 external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/client/AzureGraphClient.java delete mode 100644 external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/client/PlatformsYmlClient.java delete mode 100644 external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/client/TestingHubClient.java delete mode 100644 external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/config/CacheConfiguration.java delete mode 100644 external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/config/CacheSpecPropertiesConfiguration.java delete mode 100644 external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/config/MockConfiguration.java delete mode 100644 external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/config/OpenshiftClusterConfiguration.java delete mode 100644 external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/config/PlatformsConfiguration.java delete mode 100644 external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/config/ProjectFilterConfiguration.java delete mode 100644 external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/config/ProjectsInfoServiceConfig.java delete mode 100644 external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/config/ProjectsInfoServiceSslProperties.java delete mode 100644 external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/dto/Link.java delete mode 100644 external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/dto/ProjectInfo.java delete mode 100644 external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/dto/ProjectPlatforms.java delete mode 100644 external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/dto/Section.java delete mode 100644 external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/exception/InvalidConfigurationException.java delete mode 100644 external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/exception/InvalidContentProcessException.java delete mode 100644 external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/exception/InvalidTokenException.java delete mode 100644 external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/exception/ProjectsInfoServiceException.java delete mode 100644 external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/exception/UnableToReachAzureException.java delete mode 100644 external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/facade/ProjectsFacade.java delete mode 100644 external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/Metadata.java delete mode 100644 external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/OpenshiftProjectCluster.java delete mode 100644 external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/Platform.java delete mode 100644 external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/PlatformLink.java delete mode 100644 external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/PlatformSection.java delete mode 100644 external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/PlatformSectionLink.java delete mode 100644 external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/PlatformSectionService.java delete mode 100644 external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/Platforms.java delete mode 100644 external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/PlatformsWithTitle.java delete mode 100644 external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/PlatformsYml.java delete mode 100644 external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/Project.java delete mode 100644 external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/ProjectList.java delete mode 100644 external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/TestingHubProject.java delete mode 100644 external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/MockProjectsService.java delete mode 100644 external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/OpenShiftProjectService.java delete mode 100644 external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/PlatformService.java delete mode 100644 external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/ProjectsInfoService.java delete mode 100644 external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/impl/ProjectsInfoServiceImpl.java delete mode 100644 external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/annotations/CacheableWithFallbackAspectTest.java delete mode 100644 external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/client/AzureGraphClientTest.java delete mode 100644 external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/config/SslConfigurationTest.java delete mode 100644 external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/dto/LinkMother.java delete mode 100644 external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/dto/ProjectInfoMother.java delete mode 100644 external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/dto/ProjectPlatformsMother.java delete mode 100644 external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/dto/SectionMother.java delete mode 100644 external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/facade/ProjectsFacadeTest.java delete mode 100644 external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/OpenshiftProjectClusterMother.java delete mode 100644 external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/PlatformLinkMother.java delete mode 100644 external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/PlatformMother.java delete mode 100644 external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/PlatformsWithTitleMother.java delete mode 100644 external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/TestingHubProjectMother.java delete mode 100644 external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/MockProjectsServiceTest.java delete mode 100644 external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/OpenshiftProjectServiceTest.java delete mode 100644 external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/PlatformServiceTest.java delete mode 100644 external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/impl/ProjectsInfoServiceImplTest.java delete mode 100644 external-service-projects-info-service/static/images/application-configuration.png delete mode 100644 external-service-projects-info-service/static/images/configuration-tree.png diff --git a/api-project-platform/src/main/java/org/opendevstack/apiservice/projectplatform/facade/impl/ProjectsFacadeImpl.java b/api-project-platform/src/main/java/org/opendevstack/apiservice/projectplatform/facade/impl/ProjectsFacadeImpl.java index c30f824..67724e3 100644 --- a/api-project-platform/src/main/java/org/opendevstack/apiservice/projectplatform/facade/impl/ProjectsFacadeImpl.java +++ b/api-project-platform/src/main/java/org/opendevstack/apiservice/projectplatform/facade/impl/ProjectsFacadeImpl.java @@ -1,16 +1,21 @@ package org.opendevstack.apiservice.projectplatform.facade.impl; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.exception.ProjectsInfoServiceException; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.model.Platforms; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.service.ProjectsInfoService; import org.opendevstack.apiservice.projectplatform.exception.ProjectPlatformsException; import org.opendevstack.apiservice.projectplatform.facade.ProjectsFacade; -import org.opendevstack.apiservice.projectplatform.mapper.ProjectPlatformsMapper; import org.opendevstack.apiservice.projectplatform.model.ProjectPlatforms; import org.springframework.stereotype.Component; @Component public class ProjectsFacadeImpl implements ProjectsFacade { + @Override + public ProjectPlatforms getProjectPlatforms(String projectKey) throws ProjectPlatformsException { + return null; + } + + + + /* + FIXME: Add real code private final ProjectsInfoService projectsInfoService; private final ProjectPlatformsMapper mapper; @@ -30,4 +35,6 @@ public ProjectPlatforms getProjectPlatforms(String projectKey) throws ProjectPla throw new ProjectPlatformsException("Failed to retrieve project platforms", e); } } + + */ } diff --git a/api-project-platform/src/main/java/org/opendevstack/apiservice/projectplatform/mapper/ProjectPlatformsMapper.java b/api-project-platform/src/main/java/org/opendevstack/apiservice/projectplatform/mapper/ProjectPlatformsMapper.java index de07a84..6cc2156 100644 --- a/api-project-platform/src/main/java/org/opendevstack/apiservice/projectplatform/mapper/ProjectPlatformsMapper.java +++ b/api-project-platform/src/main/java/org/opendevstack/apiservice/projectplatform/mapper/ProjectPlatformsMapper.java @@ -1,88 +1,13 @@ package org.opendevstack.apiservice.projectplatform.mapper; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.model.PlatformSection; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.model.PlatformSectionLink; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.model.Platforms; -import org.opendevstack.apiservice.projectplatform.model.Link; -import org.opendevstack.apiservice.projectplatform.model.ProjectPlatforms; -import org.opendevstack.apiservice.projectplatform.model.Section; import org.springframework.stereotype.Component; -import java.util.stream.Collectors; - /** * Mapper class for converting between external service and API models. */ @Component public class ProjectPlatformsMapper { - /** - * Converts external service ProjectPlatforms to API ProjectPlatforms. - * - * @param externalPlatforms the external service ProjectPlatforms - * @return the API ProjectPlatforms - */ - public ProjectPlatforms toApiModel(Platforms externalPlatforms) { - - if (externalPlatforms == null) { - return null; - } - - ProjectPlatforms apiPlatforms = new ProjectPlatforms(); - - // Map sections - if (externalPlatforms.getSections() != null) { - apiPlatforms.setSections( - externalPlatforms.getSections().stream() - .map(this::toApiSection) - .collect(Collectors.toList()) - ); - } - - return apiPlatforms; - } - - /** - * Converts external service Section to API Section. - * - * @param externalSection the external service ProjectPlatformSection - * @return the API Section - */ - private Section toApiSection(PlatformSection externalSection) { - - if (externalSection == null) { - return null; - } - - Section apiSection = new Section(); - apiSection.setSection(externalSection.getSection()); - apiSection.setTooltip(externalSection.getTooltip()); - - // Map links - if (externalSection.getLinks() != null) { - apiSection.setLinks( - externalSection.getLinks().stream() - .map(this::toApiLink) - .collect(Collectors.toList()) - ); - } - - return apiSection; - } - - /** - * Converts external service Link to API Link. - * - * @param externalLink the external service ProjectPlatformSectionLink - * @return the API Link - */ - private Link toApiLink(PlatformSectionLink externalLink) { - - if (externalLink == null) { - return null; - } - - return new Link(externalLink.getLabel(), externalLink.getUrl(), externalLink.getTooltip(), externalLink.getType(), externalLink.getAbbreviation(), externalLink.getDisabled()); - } + // FIXME: Add proper code } diff --git a/api-project-platform/src/test/java/org/opendevstack/apiservice/projectplatform/facade/impl/ProjectsFacadeImplTest.java b/api-project-platform/src/test/java/org/opendevstack/apiservice/projectplatform/facade/impl/ProjectsFacadeImplTest.java index 9949664..fff4d41 100644 --- a/api-project-platform/src/test/java/org/opendevstack/apiservice/projectplatform/facade/impl/ProjectsFacadeImplTest.java +++ b/api-project-platform/src/test/java/org/opendevstack/apiservice/projectplatform/facade/impl/ProjectsFacadeImplTest.java @@ -1,27 +1,14 @@ package org.opendevstack.apiservice.projectplatform.facade.impl; -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.externalservice.projectsinfoservice.exception.ProjectsInfoServiceException; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.model.Platforms; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.service.ProjectsInfoService; -import org.opendevstack.apiservice.projectplatform.exception.ProjectPlatformsException; -import org.opendevstack.apiservice.projectplatform.mapper.ProjectPlatformsMapper; -import org.opendevstack.apiservice.projectplatform.model.Link; -import org.opendevstack.apiservice.projectplatform.model.ProjectPlatforms; -import org.opendevstack.apiservice.projectplatform.model.Section; - -import java.util.List; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class ProjectsFacadeImplTest { + /* + FIXME: Add proper code + @Mock private ProjectsInfoService projectsInfoService; @@ -123,5 +110,8 @@ private ProjectPlatforms createExpectedProjectPlatforms() { return platforms; } + + + */ } diff --git a/api-project-platform/src/test/java/org/opendevstack/apiservice/projectplatform/mapper/ProjectPlatformsMapperTest.java b/api-project-platform/src/test/java/org/opendevstack/apiservice/projectplatform/mapper/ProjectPlatformsMapperTest.java index 47b2f13..2761f13 100644 --- a/api-project-platform/src/test/java/org/opendevstack/apiservice/projectplatform/mapper/ProjectPlatformsMapperTest.java +++ b/api-project-platform/src/test/java/org/opendevstack/apiservice/projectplatform/mapper/ProjectPlatformsMapperTest.java @@ -1,30 +1,14 @@ package org.opendevstack.apiservice.projectplatform.mapper; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.model.PlatformSection; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.model.PlatformSectionLink; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.model.Platforms; -import org.opendevstack.apiservice.projectplatform.model.ProjectPlatforms; -import org.opendevstack.apiservice.projectplatform.model.Section; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; import org.mockito.junit.jupiter.MockitoExtension; -import java.net.URI; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - @ExtendWith(MockitoExtension.class) class ProjectPlatformsMapperTest { + /* + FIXME: Add proper code + @InjectMocks private ProjectPlatformsMapper mapper; @@ -307,5 +291,7 @@ private Platforms createCompleteExternalProjectPlatforms() { return externalPlatforms; } + + */ } diff --git a/external-service-projects-info-service/pom.xml b/external-service-projects-info-service/pom.xml index 9eafead..a561873 100644 --- a/external-service-projects-info-service/pom.xml +++ b/external-service-projects-info-service/pom.xml @@ -102,6 +102,41 @@ + + org.openapitools + openapi-generator-maven-plugin + + + generate-api-project-users + + generate + + + spring + ${project.basedir} + spring-boot + ${project.basedir}/openapi/openapi-projects-info-service-v1.0.0.yaml + org.opendevstack.apiservice.projects_info_service.api + org.opendevstack.apiservice.projects_info_service.model + org.opendevstack.apiservice.projects_info_service + false + false + false + false + false + false + + true + true + springdoc + true + true + true + + + + + org.springframework.boot spring-boot-maven-plugin diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/annotations/CacheableWithFallback.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/annotations/CacheableWithFallback.java deleted file mode 100644 index a18704f..0000000 --- a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/annotations/CacheableWithFallback.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.opendevstack.apiservice.externalservice.projectsinfoservice.annotations; - -import org.opendevstack.apiservice.externalservice.projectsinfoservice.config.CacheConfiguration; - -import java.lang.annotation.*; - -@Target({ElementType.METHOD}) -@Retention(RetentionPolicy.RUNTIME) -@Documented -public @interface CacheableWithFallback { - String primary(); - String fallback(); - String defaultValue() default ""; // SpEL or literal string - String cacheManager() default CacheConfiguration.CUSTOM_CACHE_MANAGER_NAME; -} diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/annotations/CacheableWithFallbackAspect.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/annotations/CacheableWithFallbackAspect.java deleted file mode 100644 index 13e0638..0000000 --- a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/annotations/CacheableWithFallbackAspect.java +++ /dev/null @@ -1,173 +0,0 @@ -package org.opendevstack.apiservice.externalservice.projectsinfoservice.annotations; - -import lombok.AllArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.aspectj.lang.ProceedingJoinPoint; -import org.aspectj.lang.annotation.Around; -import org.aspectj.lang.annotation.Aspect; -import org.aspectj.lang.reflect.MethodSignature; -import org.springframework.cache.Cache; -import org.springframework.cache.CacheManager; -import org.springframework.cache.interceptor.SimpleKeyGenerator; -import org.springframework.core.annotation.Order; -import org.springframework.expression.ExpressionParser; -import org.springframework.expression.spel.standard.SpelExpressionParser; -import org.springframework.expression.spel.support.StandardEvaluationContext; -import org.springframework.stereotype.Component; - -import java.lang.reflect.Method; - -@AllArgsConstructor -@Slf4j -@Aspect -@Component -@Order(1) -public class CacheableWithFallbackAspect { - - private static final String EMPTY_STRING = ""; - private final CacheManager cacheManager; - - @Around("@annotation(cacheableWithFallback)") - public Object cacheWithFallback(ProceedingJoinPoint pjp, CacheableWithFallback cacheableWithFallback) { - log.debug("CacheWithFallback. pjp: {}, annotation: {}", pjp, cacheableWithFallback); - - String primary = cacheableWithFallback.primary(); - String fallback = cacheableWithFallback.fallback(); - Object defaultValue = getDefaultValue(pjp, cacheableWithFallback); - - Object key = generateKey(pjp); - Cache primaryCache = cacheManager.getCache(primary); - Cache fallbackCache = cacheManager.getCache(fallback); - - Object result = null; - - if (primaryCache != null) { - result = extractValueFromPrimaryCache(primaryCache, key); - } - - if (result == null) { - Object pjpResult = extractValueFromRealMethodCall(pjp); - - if (pjpResult != null) { - result = pjpResult; - - updateCaches(primaryCache, fallbackCache, (String) key, result); - } else if (fallbackCache != null) { - result = extractValueFromFallbackCache(fallbackCache, key, defaultValue); - } else { - result = defaultValue; - - log.debug("No fallback cache configured. Returning default value: {}", defaultValue); - } - } - - log.debug("CacheableWithFallback result: {}", result); - return result; - } - - protected Object getDefaultValue(ProceedingJoinPoint pjp, CacheableWithFallback cacheableWithFallback) { - String defaultValueStr = cacheableWithFallback.defaultValue(); - Method method = ((MethodSignature) pjp.getSignature()).getMethod(); - Class returnType = method.getReturnType(); - - if (defaultValueStr != null && defaultValueStr.equals(EMPTY_STRING)) { - return EMPTY_STRING; - } else { - return resolveDefaultValue(defaultValueStr, returnType); - } - } - - - protected Object resolveDefaultValue(String defaultValue, Class returnType) { - if (defaultValue == null || defaultValue.isEmpty()) return null; - - ExpressionParser parser = new SpelExpressionParser(); - StandardEvaluationContext context = new StandardEvaluationContext(); - - Object result = parser.parseExpression(defaultValue).getValue(context); - - if (result != null && !returnType.isAssignableFrom(result.getClass())) { - throw new IllegalArgumentException("Default value type mismatch: expected " + - returnType.getName() + ", but got " + result.getClass().getName()); - } - - return result; - } - - protected Object generateKey(ProceedingJoinPoint pjp) { - log.debug("Generating key for method: {}", pjp.getSignature().getName()); - - Object generatedKey; - Object[] args = pjp.getArgs(); - - if (args == null || args.length == 0) { - generatedKey = pjp.getSignature().getName(); - } else { - generatedKey = SimpleKeyGenerator.generateKey(pjp.getArgs()); - } - - log.debug("Generated key: {}", generatedKey); - - return generatedKey; - } - - private Object extractValueFromPrimaryCache(Cache primaryCache, Object cacheKey) { - Object result = null; - - Cache.ValueWrapper primaryCachedValue = primaryCache.get(cacheKey); - if (primaryCachedValue != null) { - result = primaryCachedValue.get(); - - log.debug("Returning value from primary cache: {}, key: {}", primaryCache.getName(), cacheKey); - } - - return result; - } - - private Object extractValueFromFallbackCache(Cache fallbackCache, Object cacheKey, Object defaultValue) { - log.debug("There were an issue getting the real value. Trying to get value from fallback cache: {}, key: {}", fallbackCache, cacheKey); - - Object result; - - Cache.ValueWrapper fallbackCachedValue = fallbackCache.get(cacheKey); - if (fallbackCachedValue != null) { - result = fallbackCachedValue.get(); - - log.debug("Returning value from fallback cache: {}, key: {}", fallbackCache, cacheKey); - } else { - result = defaultValue; - - log.debug("No value in fallback cache. Returning default value: {}", defaultValue); - } - - return result; - } - - private Object extractValueFromRealMethodCall(ProceedingJoinPoint pjp) { - Object pjpResult = null; - - try { - log.debug("Calling real method: {}", pjp.getSignature().getName()); - - pjpResult = pjp.proceed(); // call the actual method - } catch (Throwable throwable) { - log.error("There were an error calling real method: {}", pjp.getSignature().getName(), throwable); - } - - return pjpResult; - } - - private void updateCaches(Cache primaryCache, Cache fallbackCache, String key, Object result) { - log.debug("Real method call generated a result. Updating caches: {}, {}, key: {}", primaryCache, fallbackCache, key); - - if (primaryCache != null) { - primaryCache.put(key, result); - } - - if (fallbackCache != null) { - fallbackCache.put(key, result); - } - - log.debug("Caches updated. Primary: {}, Fallback: {}, key: {}", primaryCache, fallbackCache, key); - } -} diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/client/AzureGraphClient.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/client/AzureGraphClient.java deleted file mode 100644 index fe1bde3..0000000 --- a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/client/AzureGraphClient.java +++ /dev/null @@ -1,183 +0,0 @@ -package org.opendevstack.apiservice.externalservice.projectsinfoservice.client; - -import org.opendevstack.apiservice.externalservice.projectsinfoservice.annotations.CacheableWithFallback; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.exception.InvalidContentProcessException; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.exception.InvalidTokenException; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.exception.UnableToReachAzureException; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.cache.annotation.Cacheable; -import org.springframework.http.*; -import org.springframework.stereotype.Service; -import org.springframework.web.client.HttpClientErrorException; -import org.springframework.web.client.RestTemplate; - -import java.io.IOException; -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; - -@Slf4j -@Service -public class AzureGraphClient { - - private static final String USER_INFO_URL = "https://graph.microsoft.com/v1.0/me"; - private static final String MEMBER_OF_URL = "https://graph.microsoft.com/v1.0/me/memberOf"; - public static final String ERROR_WHILE_GETTING_USER_GROUPS = "Error while getting user groups"; - public static final String ERROR_WHILE_PROCESSING_SERVER_RESPONSE = "Error while processing server response"; - public static final String ERROR_WHILE_GETTING_APPLICATION_GROUPS = "Error while getting application groups"; - - private final RestTemplate restTemplate; - private final ObjectMapper mapper; - - @Value("${externalservices.projects-info-service.azure.groups.page-size}") - private Integer pageSize; - - @Value("${externalservices.projects-info-service.azure.access-token}") - private String azureAccessToken; - - @Value("${externalservices.projects-info-service.azure.datahub.group-id}") - private String dataHubGroupId; - - AzureGraphClient(RestTemplate restTemplate) { - this.restTemplate = restTemplate; - this.mapper = new ObjectMapper(); - } - - public Set getUserGroups(String userAccessToken) { - Set groupIds = new HashSet<>(); - String url = MEMBER_OF_URL + "?$top=" + pageSize; // e.g., "https://graph.microsoft.com/v1.0/me/memberOf?$top=100" - - HttpHeaders headers = new HttpHeaders(); - headers.setBearerAuth(userAccessToken); - headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); - HttpEntity entity = new HttpEntity<>(headers); - - try { - while (url != null) { - ResponseEntity response = restTemplate.exchange(url, HttpMethod.GET, entity, String.class); - groupIds.addAll(processAzureResponse(response)); - - // Parse the nextLink if present - JsonNode root = new ObjectMapper().readTree(response.getBody()); - JsonNode nextLinkNode = root.get("@odata.nextLink"); - - if (nextLinkNode != null) { - log.debug("Next link found: {}", nextLinkNode.asText()); - - url = nextLinkNode != null ? nextLinkNode.asText() : null; - } else { - url = null; // No more pages - } - } - } catch (HttpClientErrorException | IOException e) { - log.error(ERROR_WHILE_GETTING_USER_GROUPS, e); - throw new InvalidTokenException(ERROR_WHILE_GETTING_USER_GROUPS, e); - } - - return groupIds; - } - - // TODO: Can not use short circuit at the moment, we will do it when integration is completed - @Cacheable("dataHubGroups") - public Set getDataHubGroups() { - return getApplicationGroups(azureAccessToken, dataHubGroupId); - } - - // TODO: Can not use short circuit at the moment, we will do it when integration is completed - @Cacheable("testingHubGroups") - public Set getTestingHubGroups() { - return getApplicationGroups(azureAccessToken, dataHubGroupId); - } - - protected Set getApplicationGroups(String accessToken, String appObjectId) { - Set groupNames = new HashSet<>(); - String url = "https://graph.microsoft.com/v1.0/servicePrincipals/" + appObjectId + "/appRoleAssignedTo?$top=" + pageSize; - - HttpHeaders headers = new HttpHeaders(); - headers.setBearerAuth(accessToken); - headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); - HttpEntity entity = new HttpEntity<>(headers); - - try { - while (url != null) { - ResponseEntity response = restTemplate.exchange(url, HttpMethod.GET, entity, String.class); - groupNames.addAll(processAzureResponse(response)); - - JsonNode root = mapper.readTree(response.getBody()); - JsonNode nextLinkNode = root.get("@odata.nextLink"); - url = nextLinkNode != null ? nextLinkNode.asText() : null; - } - } catch (HttpClientErrorException e) { - log.error(ERROR_WHILE_GETTING_APPLICATION_GROUPS, e); - - if(e.getStatusCode() == HttpStatus.UNAUTHORIZED || e.getStatusCode() == HttpStatus.FORBIDDEN) { - throw new UnableToReachAzureException(ERROR_WHILE_GETTING_APPLICATION_GROUPS, e); - } else { - log.trace("There were an error when trying to get groups. We continue the process and consider platform as disabled, " + - "in order to not block application usage", e); - } - } catch (IOException e) { - log.error(ERROR_WHILE_PROCESSING_SERVER_RESPONSE, e); - } - - return groupNames; - } - - @CacheableWithFallback(primary = "userEmail", fallback = "userEmail-fallback") - public String getUserEmail(String accessToken) { - HttpHeaders headers = new HttpHeaders(); - headers.setBearerAuth(accessToken); - headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); - - HttpEntity entity = new HttpEntity<>(headers); - - try { - ResponseEntity response = restTemplate.exchange(USER_INFO_URL, HttpMethod.GET, entity, String.class); - JsonNode root = mapper.readTree(response.getBody()); - - // Try to get mail or userPrincipalName - if (root.has("mail") && !root.get("mail").isNull()) { - return root.get("mail").asText(); - } else if (root.has("userPrincipalName")) { - return root.get("userPrincipalName").asText(); - } else { - throw new InvalidContentProcessException("Email not found in user profile"); - } - - } catch (HttpClientErrorException e) { - log.error("Error while getting user email", e); - throw new InvalidTokenException("Error while getting user email", e); - } catch (Exception e) { - log.error("Error while processing user email response", e); - throw new InvalidContentProcessException("Error while processing user email response", e); - } - } - - - private Set processAzureResponse(ResponseEntity response) { - log.trace("Processing Azure response: {}", response.getBody()); - - Set groups = new HashSet<>(); - - try { - JsonNode root = mapper.readTree(response.getBody()); - JsonNode values = root.path("value"); - - for (JsonNode group : values) { - if (group.has("displayName")) { - groups.add(group.get("displayName").asText()); - } - } - } catch (Exception e) { - log.error(ERROR_WHILE_PROCESSING_SERVER_RESPONSE, e); - - throw new InvalidContentProcessException(ERROR_WHILE_PROCESSING_SERVER_RESPONSE, e); - } - - return groups; - } -} - diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/client/PlatformsYmlClient.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/client/PlatformsYmlClient.java deleted file mode 100644 index ac1ab89..0000000 --- a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/client/PlatformsYmlClient.java +++ /dev/null @@ -1,63 +0,0 @@ -package org.opendevstack.apiservice.externalservice.projectsinfoservice.client; - -import org.apache.commons.lang3.tuple.Pair; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.config.PlatformsConfiguration; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.model.Platform; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.model.PlatformSectionService; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.model.PlatformsYml; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; -import lombok.AllArgsConstructor; -import lombok.SneakyThrows; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Service; -import org.springframework.web.client.RestTemplate; - -import java.util.List; - -@Slf4j -@AllArgsConstructor -@Service -public class PlatformsYmlClient { - - private final RestTemplate restTemplate; - private final PlatformsConfiguration platformsConfiguration; - private final ObjectMapper yamlMapper = new ObjectMapper(new YAMLFactory()); - - @SneakyThrows - public List fetchSectionsFromYaml(String url) { - - HttpHeaders headers = new HttpHeaders(); - headers.setBearerAuth(platformsConfiguration.getBearerToken()); // Adds "Authorization: Bearer " - HttpEntity entity = new HttpEntity<>(headers); - - log.debug("Fetching sections from YAML. url={}", url); - - ResponseEntity response = restTemplate.exchange(url, HttpMethod.GET, entity, String.class); - String yamlContent = response.getBody(); - - PlatformsYml root = yamlMapper.readValue(yamlContent, PlatformsYml.class); - return root.getSections(); - } - - @SneakyThrows - public Pair> fetchPlatformsFromYaml(String url) { - - HttpHeaders headers = new HttpHeaders(); - headers.setBearerAuth(platformsConfiguration.getBearerToken()); // Adds "Authorization: Bearer " - HttpEntity entity = new HttpEntity<>(headers); - - log.debug("Fetching sections from YAML. url={}", url); - - ResponseEntity response = restTemplate.exchange(url, HttpMethod.GET, entity, String.class); - String yamlContent = response.getBody(); - - PlatformsYml root = yamlMapper.readValue(yamlContent, PlatformsYml.class); - return Pair.of(root.getTitle(), root.getPlatforms()); - } -} - diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/client/TestingHubClient.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/client/TestingHubClient.java deleted file mode 100644 index 96012fa..0000000 --- a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/client/TestingHubClient.java +++ /dev/null @@ -1,97 +0,0 @@ -package org.opendevstack.apiservice.externalservice.projectsinfoservice.client; - -import org.opendevstack.apiservice.externalservice.projectsinfoservice.model.TestingHubProject; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.tuple.ImmutablePair; -import org.apache.commons.lang3.tuple.Pair; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.*; -import org.springframework.stereotype.Service; -import org.springframework.web.client.RestTemplate; - -import java.util.Collections; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; - -@Slf4j -@Service -public class TestingHubClient { - - @Value("${externalservices.projects-info-service.testing-hub.api.url}") - private String url; - - @Value("${externalservices.projects-info-service.testing-hub.api.token}") - private String token; - - @Value("${externalservices.projects-info-service.testing-hub.api.page-size}") - private String pageSize; - - @Value("#{'${externalservices.projects-info-service.testing-hub.default.projects}'.split(',')}") - public List defaultProjects; - - private final RestTemplate restTemplate; - private final ObjectMapper jacksonMapper; - - public TestingHubClient(RestTemplate restTemplate, ObjectMapper jacksonMapper) { - this.restTemplate = restTemplate; - this.jacksonMapper = jacksonMapper; - } - - public Set getDefaultProjects() { - return defaultProjects.stream() - .map(String::trim) - .map(this::parseKeyAndId) - .map(keyAndId -> new TestingHubProject(keyAndId.getRight(), keyAndId.getLeft())) - .collect(Collectors.toSet()); - } - - // TODO: This method will be used when TestingHub notifies the changes on their API. - public Set getAllProjects() { - HttpHeaders headers = new HttpHeaders(); - headers.set("accept", "application/json"); - headers.set("X-Id-Token", "Bearer " + token); - - HttpEntity entity = new HttpEntity<>(headers); - - // Let's discuss later about pagination handling - var requestUrl = url + "?pageNumber=0&itemsPerPage=" + pageSize; - - ResponseEntity response = restTemplate.exchange( - requestUrl, - HttpMethod.GET, - entity, - String.class - ); - - if (response.getStatusCode() == HttpStatus.OK) { - String responseBody = response.getBody(); - - try { - return jacksonMapper.readValue(responseBody, new TypeReference<>() {}); - } catch (JsonProcessingException e) { - log.error("Failed to deserialize response", e); - - return Collections.emptySet(); - } - } else { - log.error("Failed to fetch projects from Testing Hub. Status code: {}", response.getStatusCode()); - - return Collections.emptySet(); - } - } - - private Pair parseKeyAndId(String keyAndId) { - var parts = keyAndId.split(":"); - - if (parts.length != 2) { - throw new IllegalArgumentException("Invalid project key and id format: " + keyAndId + - ". Expected format: :"); - } - - return new ImmutablePair<>(parts[0], parts[1]); - } -} diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/config/CacheConfiguration.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/config/CacheConfiguration.java deleted file mode 100644 index e6297a6..0000000 --- a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/config/CacheConfiguration.java +++ /dev/null @@ -1,63 +0,0 @@ -package org.opendevstack.apiservice.externalservice.projectsinfoservice.config; - -import com.github.benmanes.caffeine.cache.Caffeine; -import com.github.benmanes.caffeine.cache.RemovalListener; -import lombok.AllArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.cache.Cache; -import org.springframework.cache.CacheManager; -import org.springframework.cache.annotation.EnableCaching; -import org.springframework.cache.caffeine.CaffeineCache; -import org.springframework.cache.support.SimpleCacheManager; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import java.util.List; -import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; - -@Configuration -@EnableCaching -@AllArgsConstructor -@Slf4j -public class CacheConfiguration { - - public static final String CUSTOM_CACHE_MANAGER_NAME = "customCacheManager"; - - private final CacheSpecPropertiesConfiguration cacheSpecProperties; - - @Bean(CUSTOM_CACHE_MANAGER_NAME) - public CacheManager customCacheManager() { - SimpleCacheManager manager = new SimpleCacheManager(); - - List caches = cacheSpecProperties.getSpecs().entrySet().stream() - .map(entry -> createCache(entry.getKey(), entry.getValue())) - .collect(Collectors.toList()); - - manager.setCaches(caches); - return manager; - } - - // Update this method in case we need to support different cache types - private Cache createCache(String name, CacheSpecPropertiesConfiguration.CacheSpec spec) { - RemovalListener listener = (key, value, cause) -> { - if (cause.wasEvicted()) { - log.debug("Eviction from cache '{}': key={}, cause={}", name, key, cause); - // Add custom logic here (e.g., notify, audit, etc.) - } else { - log.debug("Removing from cache '{}': key={}, cause={}", name, key, cause); - } - }; - - return new CaffeineCache( - name, - Caffeine.newBuilder() - .expireAfterWrite(spec.getTtl(), TimeUnit.SECONDS) - .maximumSize(spec.getMaxSize()) - .evictionListener(listener) - .removalListener(listener) - .build() - ); - } - -} diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/config/CacheSpecPropertiesConfiguration.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/config/CacheSpecPropertiesConfiguration.java deleted file mode 100644 index 640da53..0000000 --- a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/config/CacheSpecPropertiesConfiguration.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.opendevstack.apiservice.externalservice.projectsinfoservice.config; - -import lombok.Getter; -import lombok.Setter; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.context.annotation.Configuration; - -import java.util.Map; - -@Configuration -@Getter -@Setter -@ConfigurationProperties(prefix = "externalservices.projects-info-service.custom.cache") -public class CacheSpecPropertiesConfiguration { - private Map specs; - - @Getter - @Setter - public static class CacheSpec { - private long ttl; - private int maxSize; - } -} diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/config/MockConfiguration.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/config/MockConfiguration.java deleted file mode 100644 index fff0536..0000000 --- a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/config/MockConfiguration.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.opendevstack.apiservice.externalservice.projectsinfoservice.config; - -import lombok.Getter; -import lombok.Setter; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Configuration; - -import java.util.List; - -@Configuration -@Getter -@Setter -public class MockConfiguration { - - @Value("#{'${externalservices.projects-info-service.mock.clusters}'.split(',')}") - private List clusters; - - @Value("#{'${externalservices.projects-info-service.mock.projects.default}'.split(',')}") - private List defaultProjects; - - @Value("${externalservices.projects-info-service.mock.projects.users}") - private String usersProjects; -} diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/config/OpenshiftClusterConfiguration.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/config/OpenshiftClusterConfiguration.java deleted file mode 100644 index 18d76ab..0000000 --- a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/config/OpenshiftClusterConfiguration.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.opendevstack.apiservice.externalservice.projectsinfoservice.config; - -import lombok.Getter; -import lombok.Setter; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.context.annotation.Configuration; - -import java.util.Map; - -@Getter -@Setter -@Configuration -@ConfigurationProperties(prefix = "externalservices.projects-info-service.openshift.api") -public class OpenshiftClusterConfiguration { - private Map> clusters; - -} \ No newline at end of file diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/config/PlatformsConfiguration.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/config/PlatformsConfiguration.java deleted file mode 100644 index cf85e06..0000000 --- a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/config/PlatformsConfiguration.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.opendevstack.apiservice.externalservice.projectsinfoservice.config; - -import lombok.Getter; -import lombok.Setter; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.context.annotation.Configuration; - -import java.util.Map; - -@Getter -@Setter -@Configuration -@ConfigurationProperties(prefix = "externalservices.projects-info-service.platforms") -public class PlatformsConfiguration { - private String basePath; - private String bearerToken; - private Map clusters; -} diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/config/ProjectFilterConfiguration.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/config/ProjectFilterConfiguration.java deleted file mode 100644 index 6098827..0000000 --- a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/config/ProjectFilterConfiguration.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.opendevstack.apiservice.externalservice.projectsinfoservice.config; - -import lombok.Getter; -import lombok.Setter; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Configuration; - -import java.util.List; - -@Configuration -@Getter -@Setter -public class ProjectFilterConfiguration { - - @Value("${externalservices.projects-info-service.project.filter.project-roles-group-prefix}") - private String projectRolesGroupPrefix; - - @Value("#{'${externalservices.projects-info-service.project.filter.project-roles-group-suffixes}'.split(',')}") - private List projectRolesGroupSuffixes; - -} - diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/config/ProjectsInfoServiceConfig.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/config/ProjectsInfoServiceConfig.java deleted file mode 100644 index 06c29f0..0000000 --- a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/config/ProjectsInfoServiceConfig.java +++ /dev/null @@ -1,115 +0,0 @@ -package org.opendevstack.apiservice.externalservice.projectsinfoservice.config; - -import lombok.extern.slf4j.Slf4j; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.boot.web.client.RestTemplateBuilder; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.http.client.SimpleClientHttpRequestFactory; -import org.springframework.scheduling.annotation.EnableAsync; -import org.springframework.util.StringUtils; -import org.springframework.web.client.RestTemplate; - -import javax.net.ssl.HostnameVerifier; -import javax.net.ssl.HttpsURLConnection; -import javax.net.ssl.SSLContext; -import javax.net.ssl.TrustManager; -import javax.net.ssl.X509TrustManager; -import java.io.IOException; -import java.net.HttpURLConnection; -import java.security.GeneralSecurityException; -import java.security.cert.X509Certificate; - -/** - * Configuration class for external service components. - */ -@Configuration -@EnableAsync -@EnableConfigurationProperties(ProjectsInfoServiceSslProperties.class) -@Slf4j -public class ProjectsInfoServiceConfig { - - private final ProjectsInfoServiceSslProperties sslProperties; - - public ProjectsInfoServiceConfig(ProjectsInfoServiceSslProperties sslProperties) { - this.sslProperties = sslProperties; - } - - /** - * Creates a RestTemplate bean for HTTP client operations with configurable SSL settings. - * - * @return RestTemplate instance with SSL configuration - */ - @Bean - public RestTemplate projectsInfoServiceRestTemplate(RestTemplateBuilder restTemplateBuilder) { - if (!sslProperties.isVerifyCertificates()) { - log.warn("SSL certificate verification is DISABLED - this should only be used in development environments"); - return createInsecureRestTemplate(); - } else { - log.info("SSL certificate verification is ENABLED"); - return createSecureRestTemplate(); - } - } - - private RestTemplate createInsecureRestTemplate() { - try { - // Create a trust manager that accepts all certificates - // WARNING: This is insecure and should only be used in development environments - TrustManager[] trustAllCerts = new TrustManager[] { - new X509TrustManager() { - public X509Certificate[] getAcceptedIssuers() { - return new X509Certificate[0]; // Return empty array instead of null - } - public void checkClientTrusted(X509Certificate[] certs, String authType) { - // Intentionally empty - accepts all client certificates (insecure) - } - public void checkServerTrusted(X509Certificate[] certs, String authType) { - // Intentionally empty - accepts all server certificates (insecure) - } - } - }; - - // Install the all-trusting trust manager - SSLContext sslContext = SSLContext.getInstance("TLS"); - sslContext.init(null, trustAllCerts, new java.security.SecureRandom()); - - // Create hostname verifier that accepts all hostnames (insecure) - HostnameVerifier allHostsValid = (hostname, session) -> true; - - // Create a custom request factory that uses our SSL configuration - SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory() { - @Override - protected void prepareConnection(HttpURLConnection connection, String httpMethod) throws IOException { - if (connection instanceof HttpsURLConnection httpsConnection) { - httpsConnection.setSSLSocketFactory(sslContext.getSocketFactory()); - httpsConnection.setHostnameVerifier(allHostsValid); - } - super.prepareConnection(connection, httpMethod); - } - }; - - return new RestTemplate(requestFactory); - - } catch (GeneralSecurityException e) { - log.warn("Failed to create insecure RestTemplate, falling back to default: {}", e.getMessage()); - return new RestTemplate(); - } - } - - private RestTemplate createSecureRestTemplate() { - try { - // If custom trust store is provided, configure it - if (StringUtils.hasText(sslProperties.getTrustStorePath())) { - log.info("Custom trust store specified: {} (custom trust store support can be added in future versions)", - sslProperties.getTrustStorePath()); - } - - // Return default RestTemplate with system SSL settings - return new RestTemplate(); - - } catch (Exception e) { - log.warn("Failed to create secure RestTemplate with custom trust store, using default: {}", e.getMessage()); - return new RestTemplate(); - } - } -} diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/config/ProjectsInfoServiceSslProperties.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/config/ProjectsInfoServiceSslProperties.java deleted file mode 100644 index 7463b1e..0000000 --- a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/config/ProjectsInfoServiceSslProperties.java +++ /dev/null @@ -1,65 +0,0 @@ -package org.opendevstack.apiservice.externalservice.projectsinfoservice.config; - -import org.springframework.boot.context.properties.ConfigurationProperties; - -/** - * Configuration properties for SSL settings in external service calls. - */ -@ConfigurationProperties(prefix = "externalservices.projects-info-service.ssl") -public class ProjectsInfoServiceSslProperties { - - /** - * Whether to verify SSL certificates when making external service calls. - * Default is true for security. - */ - private boolean verifyCertificates = true; - - /** - * Path to the trust store file for SSL certificate validation. - * Optional - if not provided, uses system default trust store. - */ - private String trustStorePath; - - /** - * Password for the trust store. - */ - private String trustStorePassword; - - /** - * Type of the trust store (JKS, PKCS12, etc.). - * Default is JKS. - */ - private String trustStoreType = "JKS"; - - public boolean isVerifyCertificates() { - return verifyCertificates; - } - - public void setVerifyCertificates(boolean verifyCertificates) { - this.verifyCertificates = verifyCertificates; - } - - public String getTrustStorePath() { - return trustStorePath; - } - - public void setTrustStorePath(String trustStorePath) { - this.trustStorePath = trustStorePath; - } - - public String getTrustStorePassword() { - return trustStorePassword; - } - - public void setTrustStorePassword(String trustStorePassword) { - this.trustStorePassword = trustStorePassword; - } - - public String getTrustStoreType() { - return trustStoreType; - } - - public void setTrustStoreType(String trustStoreType) { - this.trustStoreType = trustStoreType; - } -} \ No newline at end of file diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/dto/Link.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/dto/Link.java deleted file mode 100644 index e73455b..0000000 --- a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/dto/Link.java +++ /dev/null @@ -1,302 +0,0 @@ -package org.opendevstack.apiservice.externalservice.projectsinfoservice.dto; - -import java.net.URI; -import java.util.Objects; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonCreator; -import org.openapitools.jackson.nullable.JsonNullable; -import java.time.OffsetDateTime; -import jakarta.validation.Valid; -import jakarta.validation.constraints.*; -import io.swagger.v3.oas.annotations.media.Schema; - -import java.util.*; -import jakarta.annotation.Generated; - -/** - * Link - */ - -@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", comments = "Generator version: 7.10.0") -public class Link { - - private String label; - - private String url; - - private String tooltip; - - private String type; - - private String abbreviation; - - private Boolean disabled; - - public Link() { - super(); - } - - /** - * Constructor with only required parameters - */ - public Link(String label, String url) { - this.label = label; - this.url = url; - } - - public Link label(String label) { - this.label = label; - return this; - } - - /** - * Get label - * @return label - */ - @NotNull - @Schema(name = "label", requiredMode = Schema.RequiredMode.REQUIRED) - @JsonProperty("label") - public String getLabel() { - return label; - } - - public void setLabel(String label) { - this.label = label; - } - - public Link url(String url) { - this.url = url; - return this; - } - - /** - * Get url - * @return url - */ - @NotNull - @Schema(name = "url", requiredMode = Schema.RequiredMode.REQUIRED) - @JsonProperty("url") - public String getUrl() { - return url; - } - - public void setUrl(String url) { - this.url = url; - } - - public Link tooltip(String tooltip) { - this.tooltip = tooltip; - return this; - } - - /** - * Get tooltip - * @return tooltip - */ - - @Schema(name = "tooltip", requiredMode = Schema.RequiredMode.NOT_REQUIRED) - @JsonProperty("tooltip") - public String getTooltip() { - return tooltip; - } - - public void setTooltip(String tooltip) { - this.tooltip = tooltip; - } - - public Link type(String type) { - this.type = type; - return this; - } - - /** - * Get type - * @return type - */ - - @Schema(name = "type", requiredMode = Schema.RequiredMode.NOT_REQUIRED) - @JsonProperty("type") - public String getType() { - return type; - } - - public void setType(String type) { - this.type = type; - } - - public Link abbreviation(String abbreviation) { - this.abbreviation = abbreviation; - return this; - } - - /** - * Get abbreviation - * @return abbreviation - */ - - @Schema(name = "abbreviation", requiredMode = Schema.RequiredMode.NOT_REQUIRED) - @JsonProperty("abbreviation") - public String getAbbreviation() { - return abbreviation; - } - - public void setAbbreviation(String abbreviation) { - this.abbreviation = abbreviation; - } - - public Link disabled(Boolean disabled) { - this.disabled = disabled; - return this; - } - - /** - * Get disabled - * @return disabled - */ - - @Schema(name = "disabled", requiredMode = Schema.RequiredMode.NOT_REQUIRED) - @JsonProperty("disabled") - public Boolean getDisabled() { - return disabled; - } - - public void setDisabled(Boolean disabled) { - this.disabled = disabled; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - Link link = (Link) o; - return Objects.equals(this.label, link.label) && - Objects.equals(this.url, link.url) && - Objects.equals(this.tooltip, link.tooltip) && - Objects.equals(this.type, link.type) && - Objects.equals(this.abbreviation, link.abbreviation) && - Objects.equals(this.disabled, link.disabled); - } - - @Override - public int hashCode() { - return Objects.hash(label, url, tooltip, type, abbreviation, disabled); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("class Link {\n"); - sb.append(" label: ").append(toIndentedString(label)).append("\n"); - sb.append(" url: ").append(toIndentedString(url)).append("\n"); - sb.append(" tooltip: ").append(toIndentedString(tooltip)).append("\n"); - sb.append(" type: ").append(toIndentedString(type)).append("\n"); - sb.append(" abbreviation: ").append(toIndentedString(abbreviation)).append("\n"); - sb.append(" disabled: ").append(toIndentedString(disabled)).append("\n"); - sb.append("}"); - return sb.toString(); - } - - /** - * Convert the given object to string with each line indented by 4 spaces - * (except the first line). - */ - private String toIndentedString(Object o) { - if (o == null) { - return "null"; - } - return o.toString().replace("\n", "\n "); - } - - public static class Builder { - - private Link instance; - - public Builder() { - this(new Link()); - } - - protected Builder(Link instance) { - this.instance = instance; - } - - protected Builder copyOf(Link value) { - this.instance.setLabel(value.label); - this.instance.setUrl(value.url); - this.instance.setTooltip(value.tooltip); - this.instance.setType(value.type); - this.instance.setAbbreviation(value.abbreviation); - this.instance.setDisabled(value.disabled); - return this; - } - - public Link.Builder label(String label) { - this.instance.label(label); - return this; - } - - public Link.Builder url(String url) { - this.instance.url(url); - return this; - } - - public Link.Builder tooltip(String tooltip) { - this.instance.tooltip(tooltip); - return this; - } - - public Link.Builder type(String type) { - this.instance.type(type); - return this; - } - - public Link.Builder abbreviation(String abbreviation) { - this.instance.abbreviation(abbreviation); - return this; - } - - public Link.Builder disabled(Boolean disabled) { - this.instance.disabled(disabled); - return this; - } - - /** - * returns a built Link instance. - * - * The builder is not reusable (NullPointerException) - */ - public Link build() { - try { - return this.instance; - } finally { - // ensure that this.instance is not reused - this.instance = null; - } - } - - @Override - public String toString() { - return getClass() + "=(" + instance + ")"; - } - } - - /** - * Create a builder with no initialized field (except for the default values). - */ - public static Link.Builder builder() { - return new Link.Builder(); - } - - /** - * Create a builder with a shallow copy of this instance. - */ - public Link.Builder toBuilder() { - Link.Builder builder = new Link.Builder(); - return builder.copyOf(this); - } - -} - diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/dto/ProjectInfo.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/dto/ProjectInfo.java deleted file mode 100644 index 2723eb7..0000000 --- a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/dto/ProjectInfo.java +++ /dev/null @@ -1,188 +0,0 @@ -package org.opendevstack.apiservice.externalservice.projectsinfoservice.dto; - -import com.fasterxml.jackson.annotation.JsonProperty; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.annotation.Generated; -import jakarta.validation.Valid; -import jakarta.validation.constraints.NotNull; - -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; - -/** - * ProjectInfo - */ - -@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", comments = "Generator version: 7.10.0") -public class ProjectInfo { - - private String projectKey; - - @Valid - private List clusters = new ArrayList<>(); - - public ProjectInfo() { - super(); - } - - /** - * Constructor with only required parameters - */ - public ProjectInfo(String projectKey, List clusters) { - this.projectKey = projectKey; - this.clusters = clusters; - } - - public ProjectInfo projectKey(String projectKey) { - this.projectKey = projectKey; - return this; - } - - /** - * Project KEY. - * @return projectKey - */ - @NotNull - @Schema(name = "projectKey", description = "Project KEY.", requiredMode = Schema.RequiredMode.REQUIRED) - @JsonProperty("projectKey") - public String getProjectKey() { - return projectKey; - } - - public void setProjectKey(String projectKey) { - this.projectKey = projectKey; - } - - public ProjectInfo clusters(List clusters) { - this.clusters = clusters; - return this; - } - - public ProjectInfo addClustersItem(String clustersItem) { - if (this.clusters == null) { - this.clusters = new ArrayList<>(); - } - this.clusters.add(clustersItem); - return this; - } - - /** - * List of clusters associated to the project. - * @return clusters - */ - @NotNull - @Schema(name = "clusters", description = "List of clusters associated to the project.", requiredMode = Schema.RequiredMode.REQUIRED) - @JsonProperty("clusters") - public List getClusters() { - return clusters; - } - - public void setClusters(List clusters) { - this.clusters = clusters; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - ProjectInfo projectInfo = (ProjectInfo) o; - return Objects.equals(this.projectKey, projectInfo.projectKey) && - Objects.equals(this.clusters, projectInfo.clusters); - } - - @Override - public int hashCode() { - return Objects.hash(projectKey, clusters); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("class ProjectInfo {\n"); - sb.append(" projectKey: ").append(toIndentedString(projectKey)).append("\n"); - sb.append(" clusters: ").append(toIndentedString(clusters)).append("\n"); - sb.append("}"); - return sb.toString(); - } - - /** - * Convert the given object to string with each line indented by 4 spaces - * (except the first line). - */ - private String toIndentedString(Object o) { - if (o == null) { - return "null"; - } - return o.toString().replace("\n", "\n "); - } - - public static class Builder { - - private ProjectInfo instance; - - public Builder() { - this(new ProjectInfo()); - } - - protected Builder(ProjectInfo instance) { - this.instance = instance; - } - - protected Builder copyOf(ProjectInfo value) { - this.instance.setProjectKey(value.projectKey); - this.instance.setClusters(value.clusters); - return this; - } - - public Builder projectKey(String projectKey) { - this.instance.projectKey(projectKey); - return this; - } - - public Builder clusters(List clusters) { - this.instance.clusters(clusters); - return this; - } - - /** - * returns a built ProjectInfo instance. - * - * The builder is not reusable (NullPointerException) - */ - public ProjectInfo build() { - try { - return this.instance; - } finally { - // ensure that this.instance is not reused - this.instance = null; - } - } - - @Override - public String toString() { - return getClass() + "=(" + instance + ")"; - } - } - - /** - * Create a builder with no initialized field (except for the default values). - */ - public static Builder builder() { - return new Builder(); - } - - /** - * Create a builder with a shallow copy of this instance. - */ - public Builder toBuilder() { - Builder builder = new Builder(); - return builder.copyOf(this); - } - -} - diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/dto/ProjectPlatforms.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/dto/ProjectPlatforms.java deleted file mode 100644 index 076e64a..0000000 --- a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/dto/ProjectPlatforms.java +++ /dev/null @@ -1,153 +0,0 @@ -package org.opendevstack.apiservice.externalservice.projectsinfoservice.dto; - -import java.net.URI; -import java.util.Objects; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonCreator; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import org.openapitools.jackson.nullable.JsonNullable; -import java.time.OffsetDateTime; -import jakarta.validation.Valid; -import jakarta.validation.constraints.*; -import io.swagger.v3.oas.annotations.media.Schema; - - -import java.util.*; -import jakarta.annotation.Generated; - -/** - * ProjectPlatforms - */ - -@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", comments = "Generator version: 7.10.0") -public class ProjectPlatforms { - - @Valid - private List<@Valid Section> sections = new ArrayList<>(); - - public ProjectPlatforms sections(List<@Valid Section> sections) { - this.sections = sections; - return this; - } - - public ProjectPlatforms addSectionsItem(Section sectionsItem) { - if (this.sections == null) { - this.sections = new ArrayList<>(); - } - this.sections.add(sectionsItem); - return this; - } - - /** - * Get sections - * @return sections - */ - @Valid - @Schema(name = "sections", requiredMode = Schema.RequiredMode.NOT_REQUIRED) - @JsonProperty("sections") - public List<@Valid Section> getSections() { - return sections; - } - - public void setSections(List<@Valid Section> sections) { - this.sections = sections; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - ProjectPlatforms projectPlatforms = (ProjectPlatforms) o; - return Objects.equals(this.sections, projectPlatforms.sections); - } - - @Override - public int hashCode() { - return Objects.hash(sections); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("class ProjectPlatforms {\n"); - sb.append(" sections: ").append(toIndentedString(sections)).append("\n"); - sb.append("}"); - return sb.toString(); - } - - /** - * Convert the given object to string with each line indented by 4 spaces - * (except the first line). - */ - private String toIndentedString(Object o) { - if (o == null) { - return "null"; - } - return o.toString().replace("\n", "\n "); - } - - public static class Builder { - - private ProjectPlatforms instance; - - public Builder() { - this(new ProjectPlatforms()); - } - - protected Builder(ProjectPlatforms instance) { - this.instance = instance; - } - - protected Builder copyOf(ProjectPlatforms value) { - this.instance.setSections(value.sections); - return this; - } - - public ProjectPlatforms.Builder sections(List<@Valid Section> sections) { - this.instance.sections(sections); - return this; - } - - /** - * returns a built ProjectPlatforms instance. - * - * The builder is not reusable (NullPointerException) - */ - public ProjectPlatforms build() { - try { - return this.instance; - } finally { - // ensure that this.instance is not reused - this.instance = null; - } - } - - @Override - public String toString() { - return getClass() + "=(" + instance + ")"; - } - } - - /** - * Create a builder with no initialized field (except for the default values). - */ - public static ProjectPlatforms.Builder builder() { - return new ProjectPlatforms.Builder(); - } - - /** - * Create a builder with a shallow copy of this instance. - */ - public ProjectPlatforms.Builder toBuilder() { - ProjectPlatforms.Builder builder = new ProjectPlatforms.Builder(); - return builder.copyOf(this); - } - -} - diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/dto/Section.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/dto/Section.java deleted file mode 100644 index d0a3b09..0000000 --- a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/dto/Section.java +++ /dev/null @@ -1,225 +0,0 @@ -package org.opendevstack.apiservice.externalservice.projectsinfoservice.dto; - -import java.net.URI; -import java.util.Objects; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonCreator; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import org.openapitools.jackson.nullable.JsonNullable; -import java.time.OffsetDateTime; -import jakarta.validation.Valid; -import jakarta.validation.constraints.*; -import io.swagger.v3.oas.annotations.media.Schema; - - -import java.util.*; -import jakarta.annotation.Generated; - -/** - * Section - */ - -@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", comments = "Generator version: 7.10.0") -public class Section { - - private String section; - - private String tooltip; - - @Valid - private List<@Valid Link> links = new ArrayList<>(); - - public Section() { - super(); - } - - /** - * Constructor with only required parameters - */ - public Section(String section, List<@Valid Link> links) { - this.section = section; - this.links = links; - } - - public Section section(String section) { - this.section = section; - return this; - } - - /** - * Get section - * @return section - */ - @NotNull - @Schema(name = "section", requiredMode = Schema.RequiredMode.REQUIRED) - @JsonProperty("section") - public String getSection() { - return section; - } - - public void setSection(String section) { - this.section = section; - } - - public Section tooltip(String tooltip) { - this.tooltip = tooltip; - return this; - } - - /** - * Get tooltip - * @return tooltip - */ - - @Schema(name = "tooltip", requiredMode = Schema.RequiredMode.NOT_REQUIRED) - @JsonProperty("tooltip") - public String getTooltip() { - return tooltip; - } - - public void setTooltip(String tooltip) { - this.tooltip = tooltip; - } - - public Section links(List<@Valid Link> links) { - this.links = links; - return this; - } - - public Section addLinksItem(Link linksItem) { - if (this.links == null) { - this.links = new ArrayList<>(); - } - this.links.add(linksItem); - return this; - } - - /** - * Get links - * @return links - */ - @NotNull @Valid - @Schema(name = "links", requiredMode = Schema.RequiredMode.REQUIRED) - @JsonProperty("links") - public List<@Valid Link> getLinks() { - return links; - } - - public void setLinks(List<@Valid Link> links) { - this.links = links; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - Section section = (Section) o; - return Objects.equals(this.section, section.section) && - Objects.equals(this.tooltip, section.tooltip) && - Objects.equals(this.links, section.links); - } - - @Override - public int hashCode() { - return Objects.hash(section, tooltip, links); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("class Section {\n"); - sb.append(" section: ").append(toIndentedString(section)).append("\n"); - sb.append(" tooltip: ").append(toIndentedString(tooltip)).append("\n"); - sb.append(" links: ").append(toIndentedString(links)).append("\n"); - sb.append("}"); - return sb.toString(); - } - - /** - * Convert the given object to string with each line indented by 4 spaces - * (except the first line). - */ - private String toIndentedString(Object o) { - if (o == null) { - return "null"; - } - return o.toString().replace("\n", "\n "); - } - - public static class Builder { - - private Section instance; - - public Builder() { - this(new Section()); - } - - protected Builder(Section instance) { - this.instance = instance; - } - - protected Builder copyOf(Section value) { - this.instance.setSection(value.section); - this.instance.setTooltip(value.tooltip); - this.instance.setLinks(value.links); - return this; - } - - public Section.Builder section(String section) { - this.instance.section(section); - return this; - } - - public Section.Builder tooltip(String tooltip) { - this.instance.tooltip(tooltip); - return this; - } - - public Section.Builder links(List<@Valid Link> links) { - this.instance.links(links); - return this; - } - - /** - * returns a built Section instance. - * - * The builder is not reusable (NullPointerException) - */ - public Section build() { - try { - return this.instance; - } finally { - // ensure that this.instance is not reused - this.instance = null; - } - } - - @Override - public String toString() { - return getClass() + "=(" + instance + ")"; - } - } - - /** - * Create a builder with no initialized field (except for the default values). - */ - public static Section.Builder builder() { - return new Section.Builder(); - } - - /** - * Create a builder with a shallow copy of this instance. - */ - public Section.Builder toBuilder() { - Section.Builder builder = new Section.Builder(); - return builder.copyOf(this); - } - -} - diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/exception/InvalidConfigurationException.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/exception/InvalidConfigurationException.java deleted file mode 100644 index 8e09fcd..0000000 --- a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/exception/InvalidConfigurationException.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.opendevstack.apiservice.externalservice.projectsinfoservice.exception; - -public class InvalidConfigurationException extends RuntimeException { - public InvalidConfigurationException(String message) { - super(message); - } -} diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/exception/InvalidContentProcessException.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/exception/InvalidContentProcessException.java deleted file mode 100644 index b914a3c..0000000 --- a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/exception/InvalidContentProcessException.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.opendevstack.apiservice.externalservice.projectsinfoservice.exception; - -public class InvalidContentProcessException extends RuntimeException { - public InvalidContentProcessException(String message) { - super(message); - } - - public InvalidContentProcessException(String message, Exception cause) { - super(message, cause); - } -} diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/exception/InvalidTokenException.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/exception/InvalidTokenException.java deleted file mode 100644 index c58d8c1..0000000 --- a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/exception/InvalidTokenException.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.opendevstack.apiservice.externalservice.projectsinfoservice.exception; - -public class InvalidTokenException extends RuntimeException { - public InvalidTokenException(String message, Throwable cause) { - super(message, cause); - } -} diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/exception/ProjectsInfoServiceException.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/exception/ProjectsInfoServiceException.java deleted file mode 100644 index f206ef8..0000000 --- a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/exception/ProjectsInfoServiceException.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.opendevstack.apiservice.externalservice.projectsinfoservice.exception; - -/** - * Exception thrown when there are issues with projects info service operations. - */ -public class ProjectsInfoServiceException extends Exception { - - public ProjectsInfoServiceException(String message) { - super(message); - } - - public ProjectsInfoServiceException(String message, Throwable cause) { - super(message, cause); - } -} diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/exception/UnableToReachAzureException.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/exception/UnableToReachAzureException.java deleted file mode 100644 index 0225172..0000000 --- a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/exception/UnableToReachAzureException.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.opendevstack.apiservice.externalservice.projectsinfoservice.exception; - -public class UnableToReachAzureException extends RuntimeException { - public UnableToReachAzureException(String message, Throwable cause) { - super(message, cause); - } -} diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/facade/ProjectsFacade.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/facade/ProjectsFacade.java deleted file mode 100644 index 5b83be6..0000000 --- a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/facade/ProjectsFacade.java +++ /dev/null @@ -1,106 +0,0 @@ -package org.opendevstack.apiservice.externalservice.projectsinfoservice.facade; - -import lombok.extern.slf4j.Slf4j; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.client.AzureGraphClient; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.dto.Link; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.dto.ProjectPlatforms; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.dto.Section; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.model.Platform; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.model.PlatformsWithTitle; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.service.MockProjectsService; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.service.OpenShiftProjectService; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.service.PlatformService; -import org.springframework.stereotype.Component; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -@Slf4j -@Component -public class ProjectsFacade { - - private final AzureGraphClient azureGraphClient; - - private final OpenShiftProjectService openShiftProjectService; - - private final MockProjectsService mockProjectsService; - - private final PlatformService platformService; - - private Map clusterMapper; - - public ProjectsFacade(AzureGraphClient azureGraphClient, - OpenShiftProjectService openShiftProjectService, - MockProjectsService mockProjectsService, - PlatformService platformService) { - this.azureGraphClient = azureGraphClient; - this.openShiftProjectService = openShiftProjectService; - this.mockProjectsService = mockProjectsService; - this.platformService = platformService; - } - - public ProjectPlatforms getProjectPlatforms(String projectKey) { - var allEdpProjectsInfo = openShiftProjectService.fetchProjects(); - var mockProjectsAndClusters = mockProjectsService.getDefaultProjectsAndClusters(); - - var edProjectInfo = allEdpProjectsInfo.stream() - .filter(p -> p.getProject().equals(projectKey)) - .findFirst(); - - var mockClusters = mockProjectsAndClusters.entrySet().stream() - .filter(e -> e.getValue().getProjectKey().equals(projectKey)) - .flatMap(e -> e.getValue().getClusters().stream()) - .toList(); - - // If EDP project exists, add its clusters to the front of the list, so we prioritize them - var mergedClusters = edProjectInfo.map(projectInfo -> { - List clusters = new ArrayList<>(); - - clusters.add(projectInfo.getCluster()); - - clusters.addAll(mockClusters); - - return List.copyOf(clusters); // We always prefer immutable lists - }).orElse(mockClusters); - - if (mergedClusters.isEmpty()) { - log.debug("Project not found: {}", projectKey); - - return null; - } else { - log.debug("Project found: {}, returning ProjectPlatforms for clusters: {}.", projectKey, mergedClusters); - - List
sections = new ArrayList<>(platformService.getSections(projectKey, mergedClusters.getFirst())); - var disabledPlatforms = platformService.getDisabledPlatforms(projectKey); - var platformsWithTitle = platformService.getPlatforms(projectKey, mergedClusters.getFirst()); - - var firstSection = componseFirstSection(platformsWithTitle, disabledPlatforms); - - sections.addFirst(firstSection); - - return ProjectPlatforms.builder() - .sections(sections) - .build(); - } - } - - private Section componseFirstSection(PlatformsWithTitle platformsWithTitle, List disabledPlatforms) { - var links = platformsWithTitle.getPlatforms().entrySet().stream() - .map(entry -> Link.builder() - .label(entry.getValue().getLabel()) - .url(entry.getValue().getUrl()) - .type("platform") - .disabled(disabledPlatforms.contains(entry.getKey())) - .abbreviation(entry.getValue().getAbbreviation()) - .build() - ) - .toList(); - - return Section.builder() - .section(platformsWithTitle.getTitle()) - .links(links) - .build(); - } - -} diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/Metadata.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/Metadata.java deleted file mode 100644 index b8a967e..0000000 --- a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/Metadata.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.opendevstack.apiservice.externalservice.projectsinfoservice.model; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import lombok.Getter; -import lombok.Setter; - -@Setter -@Getter -@JsonIgnoreProperties(ignoreUnknown = true) -public class Metadata { - private String name; -} \ No newline at end of file diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/OpenshiftProjectCluster.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/OpenshiftProjectCluster.java deleted file mode 100644 index 7aa8c06..0000000 --- a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/OpenshiftProjectCluster.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.opendevstack.apiservice.externalservice.projectsinfoservice.model; - -import lombok.*; - -@Getter -@Setter -@AllArgsConstructor -@Builder -@EqualsAndHashCode -@ToString -public class OpenshiftProjectCluster { - String project; - String cluster; -} diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/Platform.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/Platform.java deleted file mode 100644 index 7f8bc0e..0000000 --- a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/Platform.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.opendevstack.apiservice.externalservice.projectsinfoservice.model; - -import lombok.*; - -@NoArgsConstructor -@AllArgsConstructor -@Getter -@Setter -@Builder -public class Platform { - private String id; - private String label; - private String url; - private String abbreviation; -} \ No newline at end of file diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/PlatformLink.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/PlatformLink.java deleted file mode 100644 index 1d34a87..0000000 --- a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/PlatformLink.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.opendevstack.apiservice.externalservice.projectsinfoservice.model; - -import lombok.*; - -@NoArgsConstructor -@AllArgsConstructor -@Builder -@Getter -@Setter -public class PlatformLink { - private String label; - private String url; - private String type; - private String tooltip; -} \ No newline at end of file diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/PlatformSection.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/PlatformSection.java deleted file mode 100644 index 948277e..0000000 --- a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/PlatformSection.java +++ /dev/null @@ -1,59 +0,0 @@ -package org.opendevstack.apiservice.externalservice.projectsinfoservice.model; - -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; -import lombok.Data; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.dto.Section; - -/** - * Represents a section of links from a specific platform associated to a project. - */ -@Data -public class PlatformSection { - - private String section; - private String tooltip; - private List links; - - public PlatformSection() { - // default constructor - } - - public PlatformSection(Section section) { - this.section = section.getSection(); - this.tooltip = section.getTooltip(); - this.links = section.getLinks().stream() - .map(PlatformSectionLink::new) - .toList(); - } - - /** - * Creates a ProjectPlatformSection from a raw map. - * - * @param rawSection the raw map containing section data - * @return a new ProjectPlatformSection instance - */ - public static PlatformSection fromMap(Map rawSection) { - PlatformSection section = new PlatformSection(); - - if (rawSection.containsKey("section")) { - section.setSection((String) rawSection.get("section")); - } - - if (rawSection.containsKey("tooltip")) { - section.setTooltip((String) rawSection.get("tooltip")); - } - - if (rawSection.containsKey("links")) { - @SuppressWarnings("unchecked") - List> rawLinks = (List>) rawSection.get("links"); - List links = rawLinks.stream() - .map(PlatformSectionLink::fromMap) - .collect(Collectors.toList()); - section.setLinks(links); - } - - return section; - } -} diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/PlatformSectionLink.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/PlatformSectionLink.java deleted file mode 100644 index 90ea31e..0000000 --- a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/PlatformSectionLink.java +++ /dev/null @@ -1,69 +0,0 @@ -package org.opendevstack.apiservice.externalservice.projectsinfoservice.model; - -import lombok.Data; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.dto.Link; - -import java.util.Map; - -/** - * Represents a link from a specific platform associated to a project. - */ -@Data -public class PlatformSectionLink { - - private String label; - private String url; - private String tooltip; - private String type; - private String abbreviation; - private Boolean disabled; - - public PlatformSectionLink() { - // default constructor - } - - public PlatformSectionLink(Link link) { - this.label = link.getLabel(); - this.url = link.getUrl(); - this.tooltip = link.getTooltip(); - this.type = link.getType(); - this.abbreviation = link.getAbbreviation(); - this.disabled = link.getDisabled(); - } - - /** - * Creates a ProjectPlatformSectionLink from a raw map. - * - * @param rawLink the raw map containing link data - * @return a new ProjectPlatformSectionLink instance - */ - public static PlatformSectionLink fromMap(Map rawLink) { - PlatformSectionLink link = new PlatformSectionLink(); - - if (rawLink.containsKey("label")) { - link.setLabel((String) rawLink.get("label")); - } - - if (rawLink.containsKey("url") && rawLink.get("url") != null) { - link.setUrl((String) rawLink.get("url")); - } - - if (rawLink.containsKey("tooltip")) { - link.setTooltip((String) rawLink.get("tooltip")); - } - - if (rawLink.containsKey("type")) { - link.setType((String) rawLink.get("type")); - } - - if (rawLink.containsKey("abbreviation")) { - link.setAbbreviation((String) rawLink.get("abbreviation")); - } - - if (rawLink.containsKey("disabled")) { - link.setDisabled((Boolean) rawLink.get("disabled")); - } - - return link; - } -} diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/PlatformSectionService.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/PlatformSectionService.java deleted file mode 100644 index b14b2c7..0000000 --- a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/PlatformSectionService.java +++ /dev/null @@ -1,16 +0,0 @@ -package org.opendevstack.apiservice.externalservice.projectsinfoservice.model; - -import lombok.*; - -import java.util.List; - -@NoArgsConstructor -@AllArgsConstructor -@Builder -@Getter -@Setter -public class PlatformSectionService { - private String section; - private String tooltip; - private List links; -} \ No newline at end of file diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/Platforms.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/Platforms.java deleted file mode 100644 index ce5956d..0000000 --- a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/Platforms.java +++ /dev/null @@ -1,38 +0,0 @@ -package org.opendevstack.apiservice.externalservice.projectsinfoservice.model; - -import java.net.URI; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; -import lombok.Data; - -/** - * Represents the platforms information associated to a project. - */ -@Data -public class Platforms { - - private List sections; - - /** - * Creates a ProjectPlatforms from a raw map response. - * - * @param responseBody the raw map containing platforms data - * @return a new ProjectPlatforms instance - */ - public static Platforms fromMap(Map responseBody) { - Platforms platforms = new Platforms(); - - // Map sections - if (responseBody.containsKey("sections")) { - @SuppressWarnings("unchecked") - List> rawSections = (List>) responseBody.get("sections"); - List sections = rawSections.stream() - .map(PlatformSection::fromMap) - .collect(Collectors.toList()); - platforms.setSections(sections); - } - - return platforms; - } -} diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/PlatformsWithTitle.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/PlatformsWithTitle.java deleted file mode 100644 index ee2cd0f..0000000 --- a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/PlatformsWithTitle.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.opendevstack.apiservice.externalservice.projectsinfoservice.model; - -import lombok.*; - -import java.util.Map; - -@NoArgsConstructor -@AllArgsConstructor -@Getter -@Setter -@Builder -public class PlatformsWithTitle { - private String title; - private Map platforms; -} diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/PlatformsYml.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/PlatformsYml.java deleted file mode 100644 index 7ab4836..0000000 --- a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/PlatformsYml.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.opendevstack.apiservice.externalservice.projectsinfoservice.model; - -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -import java.util.List; - - -@NoArgsConstructor -@AllArgsConstructor -@Getter -@Setter -public class PlatformsYml { - private String title; - private List platforms; - private List sections; -} \ No newline at end of file diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/Project.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/Project.java deleted file mode 100644 index e86ef3b..0000000 --- a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/Project.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.opendevstack.apiservice.externalservice.projectsinfoservice.model; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import lombok.Getter; -import lombok.Setter; - -@Setter -@Getter -@JsonIgnoreProperties(ignoreUnknown = true) -public class Project { - private Metadata metadata; -} \ No newline at end of file diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/ProjectList.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/ProjectList.java deleted file mode 100644 index f34e3c3..0000000 --- a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/ProjectList.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.opendevstack.apiservice.externalservice.projectsinfoservice.model; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import lombok.Getter; -import lombok.Setter; - -import java.util.List; - -@Setter -@Getter -@JsonIgnoreProperties(ignoreUnknown = true) -public class ProjectList { - private List items; -} \ No newline at end of file diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/TestingHubProject.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/TestingHubProject.java deleted file mode 100644 index 6bb3411..0000000 --- a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/TestingHubProject.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.opendevstack.apiservice.externalservice.projectsinfoservice.model; - -import lombok.*; - -@NoArgsConstructor -@AllArgsConstructor -@Getter -@Setter -@EqualsAndHashCode -@ToString -public class TestingHubProject { - private String id; - private String name; -} diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/MockProjectsService.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/MockProjectsService.java deleted file mode 100644 index 5a4ba01..0000000 --- a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/MockProjectsService.java +++ /dev/null @@ -1,119 +0,0 @@ -package org.opendevstack.apiservice.externalservice.projectsinfoservice.service; - -import org.opendevstack.apiservice.externalservice.projectsinfoservice.config.MockConfiguration; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.dto.ProjectInfo; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.exception.InvalidContentProcessException; -import lombok.AllArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; - -import java.util.*; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -@Slf4j -@AllArgsConstructor -@Service -public class MockProjectsService { - - private final MockConfiguration mockConfiguration; - - public Map getProjectsAndClusters(String userEmail) { - Map defaultProjects = getDefaultProjectsAndClusters(); - Map userProjects = getUserProjectsAndClusters(userEmail); - - return merge(defaultProjects, userProjects); - } - - public Map getDefaultProjectsAndClusters() { - return mockConfiguration.getDefaultProjects().stream() - .map(String::trim) - .map(this::extractProjectAndCluster) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - } - - private Map getUserProjectsAndClusters(String userEmail) { - return getUserProjects(userEmail).stream() - .map(this::extractProjectAndCluster) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - } - - private Set getUserProjects(String userEmail) { - var projectsByUsers = mockConfiguration.getUsersProjects().replace("{", "") - .replace("}", "") - .split(";"); - - var userConfigurations = Stream.of(projectsByUsers) - .map(String::trim) - .filter(s -> s.startsWith(userEmail)) - .map(s -> s.replace(userEmail + ":", "").trim()) - .toList(); - - return userConfigurations.stream() - .map(this::getSingleUserProjects) - .flatMap(Set::stream) - .collect(Collectors.toSet()); - } - - private Set getSingleUserProjects(String userProjects) { - if (userProjects.equals("[]")) { - return Collections.emptySet(); - } else { - if (!userProjects.startsWith("[") || !userProjects.endsWith("]")) { - log.error("User projects string is not well formatted: {}", userProjects); - - throw new InvalidContentProcessException("User projects string is not well formatted: " + userProjects); - } - - var projects = userProjects.substring(userProjects.indexOf("[") + 1, userProjects.indexOf("]")) - .split(","); - - return Stream.of(projects) - .map(String::trim) - .map(this::extractProjectAndCluster) - .map(Map.Entry::getKey) - .collect(Collectors.toSet()); - } - } - - private Map.Entry extractProjectAndCluster(String projectWithCluster) { - if (projectWithCluster.contains(":")) { - var parts = projectWithCluster.split(":"); - - var key = parts[0].trim(); - var projectInfo = new ProjectInfo(key, List.of(parts[1].trim())); - - return Map.entry(key, projectInfo); - } else { - var key = projectWithCluster.trim(); - var projectInfo = new ProjectInfo(key, mockConfiguration.getClusters().stream() - .toList()); - - return Map.entry(key, projectInfo); - } - } - - private Map merge(Map map1, Map map2) { - Set allKeysSet = Stream.concat(map1.keySet().stream(), map2.keySet().stream()) - .collect(Collectors.toSet()); - - Map result = new HashMap<>(); - - for (String key : allKeysSet) { - ProjectInfo projectInfo1 = map1.getOrDefault(key, new ProjectInfo(key, Collections.emptyList())); - ProjectInfo projectInfo2 = map2.getOrDefault(key, new ProjectInfo(key, Collections.emptyList())); - - List mergedClusters = Stream.concat( - projectInfo1.getClusters().stream(), - projectInfo2.getClusters().stream() - ) - .map(String::trim) - .distinct() - .toList(); - - result.put(key, new ProjectInfo(key, mergedClusters)); - } - - return result; - } -} diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/OpenShiftProjectService.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/OpenShiftProjectService.java deleted file mode 100644 index ccea0a7..0000000 --- a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/OpenShiftProjectService.java +++ /dev/null @@ -1,82 +0,0 @@ -package org.opendevstack.apiservice.externalservice.projectsinfoservice.service; - -import org.opendevstack.apiservice.externalservice.projectsinfoservice.config.OpenshiftClusterConfiguration; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.annotations.CacheableWithFallback; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.model.OpenshiftProjectCluster; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.model.ProjectList; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.*; -import org.springframework.stereotype.Service; -import org.springframework.web.client.*; - -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; - -@Slf4j -@Service -public class OpenShiftProjectService { - @Value("${externalservices.projects-info-service.openshift.api.project.url}") - private String projectApiUrl; - - private final RestTemplate restTemplate; - private final OpenshiftClusterConfiguration openshiftClusterConfig; - - public OpenShiftProjectService(RestTemplate restTemplate, OpenshiftClusterConfiguration openshiftClusterConfig) { - this.restTemplate = restTemplate; - this.openshiftClusterConfig = openshiftClusterConfig; - } - - @CacheableWithFallback(primary = "openshiftProjects", fallback = "openshiftProjects-fallback", defaultValue = "T(java.util.Collections).emptyList()") - public List fetchProjects() { - final List result = new ArrayList<>(); - - openshiftClusterConfig.getClusters().forEach((cluster, clusterValues) -> { - log.debug("Fetching projects for cluster: {}", cluster); - - final HttpHeaders headers = new HttpHeaders(); - headers.set("Authorization", "Bearer " + clusterValues.get("token")); - headers.setAccept(List.of(MediaType.APPLICATION_JSON)); - - final String url = clusterValues.get("url") + projectApiUrl; - final HttpEntity entity = new HttpEntity<>(headers); - - log.debug("Setting headers to request: {} for url {}", headers, url); - - try { - ResponseEntity response = restTemplate.exchange(url, HttpMethod.GET, entity, ProjectList.class); - ProjectList body = response.getBody(); - - if (body != null && body.getItems() != null) { - log.debug("Found {} projects for cluster {}:", body.getItems().size(), cluster); - log.debug("Projects: \n {}", body.getItems().stream() - .map(project -> project.getMetadata().getName()) - .collect(Collectors.joining(", "))); - - List clusterProjects = body.getItems().stream() - .filter(project -> project.getMetadata() != null) - .map(project -> project.getMetadata().getName()) - .filter(projectName -> projectName.endsWith("-cd")) - .map(projectName -> projectName.replace("-cd", "")) - .map(String::toUpperCase) - .map(projectName -> OpenshiftProjectCluster.builder() - .project(projectName) - .cluster(cluster) - .build()) - .toList(); - - result.addAll(clusterProjects); - } - } catch (HttpClientErrorException | HttpServerErrorException e) { - log.error("HTTP error while fetching projects for cluster {}: {} - {}", cluster, e.getStatusCode(), e.getMessage()); - } catch (ResourceAccessException e) { - log.error("Resource access error for cluster {}: {}", cluster, e.getMessage()); - } catch (RestClientException e) { - log.error("Unexpected error while fetching projects for cluster {}: {}", cluster, e.getMessage()); - } - }); - - return result; - } -} diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/PlatformService.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/PlatformService.java deleted file mode 100644 index 818518e..0000000 --- a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/PlatformService.java +++ /dev/null @@ -1,243 +0,0 @@ -package org.opendevstack.apiservice.externalservice.projectsinfoservice.service; - -import org.opendevstack.apiservice.externalservice.projectsinfoservice.config.PlatformsConfiguration; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.config.ProjectFilterConfiguration; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.client.AzureGraphClient; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.client.PlatformsYmlClient; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.client.TestingHubClient; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.dto.Link; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.dto.Section; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.exception.InvalidConfigurationException; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.exception.UnableToReachAzureException; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.model.*; -import lombok.AllArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; - -import java.util.*; -import java.util.function.Function; -import java.util.stream.Collectors; - -@Slf4j -@AllArgsConstructor -@Service -public class PlatformService { - - private static final String PROJECT_KEY_TOKEN = "${projectKey}"; - private static final String LOWER_PROJECT_KEY_TOKEN = "${project_key}"; - private static final String UPPER_PROJECT_KEY_TOKEN = "${PROJECT_KEY}"; - private static final String TESTING_HUB_PROJECT_TOKEN = "${testingHubProject}"; - - PlatformsConfiguration platformsConfiguration; - ProjectFilterConfiguration projectFilterConfiguration; - PlatformsYmlClient platformsYmlClient; - AzureGraphClient azureGraphClient; - TestingHubClient testingHubClient; - - public PlatformsWithTitle getPlatforms(String projectKey, String cluster) { - log.debug("Getting platform links for project {} and cluster {}.", projectKey, cluster); - - if (platformsConfiguration.getClusters().containsKey(cluster)) { - var clusterConfigurationPath = platformsConfiguration.getClusters().get(cluster); - var url = platformsConfiguration.getBasePath() + clusterConfigurationPath; - - var titleAndPlatforms = platformsYmlClient.fetchPlatformsFromYaml(url); - var platforms = titleAndPlatforms.getValue(); - - platforms.forEach(platform -> { - var resolvedUrl = Optional.ofNullable(resolvePlatformUrl(platform.getUrl(), projectKey)).orElse(""); - - platform.setUrl(resolvedUrl); - }); - - var platformsMap = platforms.stream() - .collect(Collectors.toMap( - Platform::getId, - Function.identity(), - (existing, replacement) -> existing, // merge function - LinkedHashMap::new // supplier to preserve order - )); - - return PlatformsWithTitle.builder() - .title(titleAndPlatforms.getKey()) - .platforms(platformsMap) - .build(); - } else { - String errorMessage = "Cluster " + cluster + " is not configured. All valid clusters: " + - platformsConfiguration.getClusters().keySet(); - - throw new InvalidConfigurationException(errorMessage); - } - } - - private String resolvePlatformUrl(String urlTemplate, String projectKey) { - if (urlTemplate.contains(TESTING_HUB_PROJECT_TOKEN)) { - // TODO: Change to getAllProjects TestingHub notifies the changes on their API. - Set testingHubProjects = testingHubClient.getDefaultProjects(); - String testingHubValue = testingHubProjects.stream() - .filter(project -> project.getName().equals(projectKey)) - .map(TestingHubProject::getId) - .findFirst() - .orElse(null); - - // If the project is not found in TestingHub, we cannot resolve the URL so we return null - if (testingHubValue == null) { - return null; - } - - urlTemplate = urlTemplate.replace(TESTING_HUB_PROJECT_TOKEN, testingHubValue); - } - - return replaceTokens(urlTemplate, Map.of( - PROJECT_KEY_TOKEN, projectKey, - UPPER_PROJECT_KEY_TOKEN, projectKey.toUpperCase(), - LOWER_PROJECT_KEY_TOKEN, projectKey.toLowerCase() - )); - } - - public List getDisabledPlatforms(String projectKey) { - List disabledPlatforms = new ArrayList<>(); - - disabledPlatforms.addAll(addDisabledPlatformIfDataHubIsDisabled(projectKey)); - disabledPlatforms.addAll(addDisabledPlatformIfTestingHubIsDisabled(projectKey)); - - return disabledPlatforms; - } - - private List addDisabledPlatformIfDataHubIsDisabled(String projectKey) { - List disabledPlatforms = new ArrayList<>(); - - try { - var dataHubGroups = azureGraphClient.getDataHubGroups(); - - var isDataHubDisabled = checkIfProjectIsEnabledForGroups(projectKey, dataHubGroups); - - if (!isDataHubDisabled) { - disabledPlatforms.add("datahub"); - } - } catch (UnableToReachAzureException e) { - log.error("Unable to reach Azure to get DataHub groups", e); - - // As agreed, if we cannot reach Azure, we consider DataHub as NOT disabled for all projects - } - - - return disabledPlatforms; - } - - private List addDisabledPlatformIfTestingHubIsDisabled(String projectKey) { - List disabledPlatforms = new ArrayList<>(); - - //var testingHubProjects = testingHubClient.getAllProjects(); // TODO: This will be used when TestingHub notifies the changes on their API. - var testingHubDefaultProjects = testingHubClient.getDefaultProjects(); - - var isTestingHubEnabled = false; - - try { - var testingHubProjects = extractIdsFromDefaultProjects(azureGraphClient.getTestingHubGroups(), testingHubDefaultProjects); - - isTestingHubEnabled = testingHubProjects.stream() - .anyMatch(testingHubProject -> testingHubProject.getName().equals(projectKey)); - } catch (UnableToReachAzureException e) { - log.error("Unable to reach Azure to get Testing Hub groups", e); - - isTestingHubEnabled = testingHubDefaultProjects.stream() - .anyMatch(testingHubProject -> testingHubProject.getName().equals(projectKey)); - } - - if (isTestingHubEnabled) { - log.trace("testingHub is enabled"); - } else { - log.trace("testingHub is not enabled"); - - disabledPlatforms.add("testinghub"); - } - - return disabledPlatforms; - } - - private Set extractIdsFromDefaultProjects(Set testingHubGroups, Set testingHubDefaultProjects) { - return testingHubDefaultProjects.stream() - .filter(project -> testingHubGroups.contains(project.getName())) - .collect(Collectors.toSet()); - } - - public List
getSections(String projectKey, String cluster) { - log.debug("Getting sections for project {} and cluster {}.", projectKey, cluster); - - if (platformsConfiguration.getClusters().containsKey(cluster)) { - var clusterConfigurationPath = platformsConfiguration.getClusters().get(cluster); - var url = platformsConfiguration.getBasePath() + clusterConfigurationPath; - - List sections = platformsYmlClient.fetchSectionsFromYaml(url); - - return sections.stream() - .map(section -> replaceTokens(section, Map.of( - PROJECT_KEY_TOKEN, projectKey, - UPPER_PROJECT_KEY_TOKEN, projectKey.toUpperCase(), - LOWER_PROJECT_KEY_TOKEN, projectKey.toLowerCase() - ))) - .toList(); - } else { - String errorMessage = "Cluster " + cluster + " is not configured. All valid clusters: " + - platformsConfiguration.getClusters().keySet(); - - throw new InvalidConfigurationException(errorMessage); - } - } - - private Section replaceTokens(PlatformSectionService section, Map tokens) { - log.debug("Replacing tokens {} for section {}", tokens, section); - - var updatedLinksWithTokens = section.getLinks().stream() - .map(link -> Link.builder() - .url(replaceTokens(link.getUrl(), tokens)) - .label(link.getLabel()) - .type(link.getType()) - .tooltip(link.getTooltip()) - .build() - ).toList(); - - return Section.builder() - .section(section.getSection()) - .tooltip(section.getTooltip()) - .links(updatedLinksWithTokens) - .build(); - } - - private String replaceTokens(String source, Map tokens) { - String result = source; - - for (Map.Entry tokenEntry : tokens.entrySet()) { - result = result.replace(tokenEntry.getKey(), tokenEntry.getValue()); - } - - return result; - } - - private boolean checkIfProjectIsEnabledForGroups(String projectKey, Set applicationGroups) { - var edpAzureProjects = applicationGroups.stream() - .filter( group -> group.startsWith(projectFilterConfiguration.getProjectRolesGroupPrefix()) ) - .filter(group -> projectFilterConfiguration.getProjectRolesGroupSuffixes().stream().anyMatch(group::endsWith)) - .map(group -> group.replaceFirst(projectFilterConfiguration.getProjectRolesGroupPrefix() + "-", "")) - .map( this::removeSuffixes) - .collect(Collectors.toSet()); - - return edpAzureProjects.contains(projectKey); - } - - private String removeSuffixes(String azureGroupName) { - String result = azureGroupName; - - for (String suffix : projectFilterConfiguration.getProjectRolesGroupSuffixes()) { - var suffixWithSeparator = "-" + suffix.trim(); - - if (result.endsWith(suffixWithSeparator)) { - result = result.substring(0, result.length() - suffixWithSeparator.length()); - break; // Assuming only one suffix will match - } - } - - return result; - } -} \ No newline at end of file diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/ProjectsInfoService.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/ProjectsInfoService.java deleted file mode 100644 index 0d1c36f..0000000 --- a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/ProjectsInfoService.java +++ /dev/null @@ -1,37 +0,0 @@ -package org.opendevstack.apiservice.externalservice.projectsinfoservice.service; - -import org.opendevstack.apiservice.externalservice.projectsinfoservice.exception.ProjectsInfoServiceException; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.model.Platforms; - -/** - * Service interface for integrating with projects info service - * This interface provides a generic way to consume the service. - */ -public interface ProjectsInfoService { - - /** - * Retrieves the platforms associated with a given project. - * - * @param projectKey the key of the project you want to check - * @return the platforms details associated with the project - * @throws ProjectsInfoServiceException if workflow execution fails - */ - Platforms getProjectPlatforms(String projectKey) throws ProjectsInfoServiceException; - - /** - * Validates connection to the projects info service. - * - * @return true if connection is valid, false otherwise - */ - boolean validateConnection(); - - /** - * Checks if the projects info service is healthy and reachable. - * This method is used by health indicators and should not throw exceptions. - * - * @return true if the service is healthy, false otherwise - */ - boolean isHealthy(); - - -} diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/impl/ProjectsInfoServiceImpl.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/impl/ProjectsInfoServiceImpl.java deleted file mode 100644 index aa31505..0000000 --- a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/impl/ProjectsInfoServiceImpl.java +++ /dev/null @@ -1,109 +0,0 @@ -package org.opendevstack.apiservice.externalservice.projectsinfoservice.service.impl; - - -import org.opendevstack.apiservice.externalservice.projectsinfoservice.dto.ProjectPlatforms; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.exception.InvalidContentProcessException; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.exception.ProjectsInfoServiceException; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.facade.ProjectsFacade; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.model.PlatformLink; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.model.PlatformSection; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.model.Platforms; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.service.ProjectsInfoService; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.core.ParameterizedTypeReference; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Service; -import org.springframework.web.client.RestClientException; -import org.springframework.web.client.RestTemplate; - -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -/** - * Implementation of ProjectsInfoService. - * This service provides integration with projects info service to retrieve projects details. - */ -@Service -@Slf4j -public class ProjectsInfoServiceImpl implements ProjectsInfoService { - - @Qualifier("projectsInfoServiceRestTemplate") - private final RestTemplate restTemplate; - - @Value("${externalservices.projects-info-service.base-url:http://localhost:8080}") - private String baseUrl; - - private final ProjectsFacade projectsFacade; - - public ProjectsInfoServiceImpl(RestTemplate restTemplate, ProjectsFacade projectsFacade) { - this.restTemplate = restTemplate; - this.projectsFacade = projectsFacade; - } - - @Override - public Platforms getProjectPlatforms(String projectKey) throws ProjectsInfoServiceException { - log.debug("Getting project platforms"); - - var projectPlatforms = projectsFacade.getProjectPlatforms(projectKey); - - if (projectPlatforms == null) { - return null; - } else { - List platformSections = projectPlatforms.getSections().stream() - .map(PlatformSection::new) - .toList(); - - var platforms = new Platforms(); - platforms.setSections(platformSections); - - return platforms; - } - } - - @Override - public boolean validateConnection() { - try { - HttpHeaders headers = createHeaders(); - HttpEntity request = new HttpEntity<>(headers); - - String url = baseUrl + "/actuator/health"; - ResponseEntity> response = restTemplate.exchange(url, HttpMethod.GET, request, new ParameterizedTypeReference<>() {}); - - boolean isValid = response.getStatusCode().is2xxSuccessful(); - log.debug("Connection validation: {}", isValid ? "successful" : "failed"); - return isValid; - - } catch (Exception e) { - log.warn("Connection validation failed: {}", e.getMessage()); - return false; - } - } - - @Override - public boolean isHealthy() { - try { - // Use validateConnection for health checks, but don't log warnings on failure - // as health checks are frequent and failures are expected to be handled by the health indicator - return validateConnection(); - } catch (Exception e) { - log.debug("Health check failed: {}", e.getMessage()); - return false; - } - } - - private HttpHeaders createHeaders() { - HttpHeaders headers = new HttpHeaders(); - headers.set("Content-Type", "application/json"); - return headers; - } -} diff --git a/external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/annotations/CacheableWithFallbackAspectTest.java b/external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/annotations/CacheableWithFallbackAspectTest.java deleted file mode 100644 index 6d5bbff..0000000 --- a/external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/annotations/CacheableWithFallbackAspectTest.java +++ /dev/null @@ -1,256 +0,0 @@ -package org.opendevstack.apiservice.externalservice.projectsinfoservice.annotations; - -import org.aspectj.lang.ProceedingJoinPoint; -import org.aspectj.lang.Signature; -import org.aspectj.lang.reflect.MethodSignature; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.cache.Cache; -import org.springframework.cache.CacheManager; -import org.springframework.cache.interceptor.SimpleKey; - -import java.lang.reflect.Method; -import java.util.List; -import java.util.Map; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; - -@ExtendWith(MockitoExtension.class) -class CacheableWithFallbackAspectTest { - - @Mock - CacheManager cacheManager; - - @InjectMocks - CacheableWithFallbackAspect cacheableWithFallbackAspect; - - @Test - void givenAProceedingJoinPoint_andNoArgsMethod_whenGenerateKey_thenReturnSignatureNameKey() { - // given - var signatureName = "pjp-signature-name"; - - ProceedingJoinPoint pjp = Mockito.mock(ProceedingJoinPoint.class); - Signature signature = Mockito.mock(Signature.class); - - when(pjp.getArgs()).thenReturn(new Object[]{}); - when(pjp.getSignature()).thenReturn(signature); - when(signature.getName()).thenReturn(signatureName); - - // when - var key = cacheableWithFallbackAspect.generateKey(pjp); - - // then - assertThat(key).isEqualTo(signatureName); - } - - @Test - void givenAProceedingJoinPoint_andNoArgsMethod_whenGenerateKey_thenReturnGeneratorGeneratedKey() { - // given - var signatureName = "pjp-signature-name"; - Object[] args = new Object[]{"arg1", 2, 3.0}; - var expectedKey = new SimpleKey(args); - - ProceedingJoinPoint pjp = Mockito.mock(ProceedingJoinPoint.class); - Signature signature = Mockito.mock(Signature.class); - - when(pjp.getSignature()).thenReturn(signature); - when(signature.getName()).thenReturn(signatureName); - when(pjp.getArgs()).thenReturn(args); - - // when - var key = cacheableWithFallbackAspect.generateKey(pjp); - - // then - assertThat(key).isEqualTo(expectedKey); - } - - @Test - void givenAnEmptyResultValue_whenGetDefaultValue_thenReturnNull() { - // given - String defaultValue = ""; - Class returnType = String.class; - - // when - Object resolveDefaultValue = cacheableWithFallbackAspect.resolveDefaultValue(defaultValue, returnType); - - // then - assertThat(resolveDefaultValue).isNull(); - } - - @Test - void givenAnSpelReturnValue_AndTypeMap_whenGetDefaultValue_thenReturnProperType() { - // given - String defaultValue = "T(java.util.Collections).emptyMap()"; - Class returnType = Map.class; - - // when - Object resolveDefaultValue = cacheableWithFallbackAspect.resolveDefaultValue(defaultValue, returnType); - - // then - assertThat(resolveDefaultValue) - .isInstanceOf(Map.class) - .isEqualTo(Map.of()); - } - - @Test - void givenAnSpelReturnValue_AndTypeList_whenGetDefaultValue_andSpelExpectsMap_thenThrowException() { - // given - String defaultValue = "T(java.util.Collections).emptyMap()"; - Class returnType = List.class; - - // when - var exception = assertThrows(IllegalArgumentException.class, () -> cacheableWithFallbackAspect.resolveDefaultValue(defaultValue, returnType)); - - // then - assertThat(exception.getMessage()).isEqualTo("Default value type mismatch: expected java.util.List, but got java.util.Collections$EmptyMap"); - } - - @Test - void givenAProceedingJoinPoint_andACacheableWithFallback_whenCacheWithFallback_andNoValueIsCached_thenCallRealMethod() throws Throwable { - // given - String primary = "primaryCache"; - String fallback = "fallbackCache"; - String defaultValue = ""; - String signatureName = "pjp-signature-name"; - String expectedResult = "real-method-result"; - - ProceedingJoinPoint pjp = initializeProceedingJoinPoint(signatureName); - CacheableWithFallback cacheableWithFallback = initializeCacheableWithFallback(primary, fallback, defaultValue); - - when(cacheManager.getCache(primary)).thenReturn(null); - when(cacheManager.getCache(fallback)).thenReturn(null); - - when(pjp.proceed()).thenReturn(expectedResult); - - // when - var result = cacheableWithFallbackAspect.cacheWithFallback(pjp, cacheableWithFallback); - - // then - assertThat(result).isEqualTo(expectedResult); - } - - @Test - void givenAProceedingJoinPoint_andACacheableWithFallback_whenCacheWithFallback_andResponseIsCachedInPrimary_thenReturnPrimaryCacheValue() { - // given - String primary = "primaryCache"; - String fallback = "fallbackCache"; - String defaultValue = ""; - String signatureName = "pjp-signature-name"; - String primaryCacheResult = "primary-cache-result"; - - ProceedingJoinPoint pjp = initializeProceedingJoinPoint(signatureName); - CacheableWithFallback cacheableWithFallback = initializeCacheableWithFallback(primary, fallback, defaultValue); - - initializeCache(primary, primaryCacheResult); - - // when - var result = cacheableWithFallbackAspect.cacheWithFallback(pjp, cacheableWithFallback); - - // then - assertThat(result).isEqualTo(primaryCacheResult); - } - - @Test - void givenAProceedingJoinPoint_andACacheableWithFallback_whenCacheWithFallback_andPrimaryCacheIsEmpty_thenReturnRealMethod() throws Throwable { - // given - String primary = "primaryCache"; - String fallback = "fallbackCache"; - String defaultValue = ""; - String signatureName = "pjp-signature-name"; - String realMethodResult = "real-method-result"; - - ProceedingJoinPoint pjp = initializeProceedingJoinPoint(signatureName); - CacheableWithFallback cacheableWithFallback = initializeCacheableWithFallback(primary, fallback, defaultValue); - - when(pjp.proceed()).thenReturn(realMethodResult); - - // when - var result = cacheableWithFallbackAspect.cacheWithFallback(pjp, cacheableWithFallback); - - // then - assertThat(result).isEqualTo(realMethodResult); - } - - @Test - void givenAProceedingJoinPoint_andACacheableWithFallback_whenCacheWithFallback_andPrimaryCacheIsEmpty_AndRealCallFails_AndResponseIsCachedInFallback_thenReturnFallbackCacheValue() throws Throwable { - // given - String primary = "primaryCache"; - String fallback = "fallbackCache"; - String defaultValue = ""; - String signatureName = "pjp-signature-name"; - String fallbackCacheResult = "fallback-cache-result"; - - ProceedingJoinPoint pjp = initializeProceedingJoinPoint(signatureName); - CacheableWithFallback cacheableWithFallback = initializeCacheableWithFallback(primary, fallback, defaultValue); - - initializeCache(primary, null); - initializeCache(fallback, fallbackCacheResult); - - when(pjp.proceed()).thenThrow(new RuntimeException("That's an expected exception")); - - // when - var result = cacheableWithFallbackAspect.cacheWithFallback(pjp, cacheableWithFallback); - - // then - assertThat(result).isEqualTo(fallbackCacheResult); - } - - @Test - void givenAProceedingJoinPoint_andACacheableWithFallback_whenCacheWithFallback_andNoValueIsCached_andRealCallFails_thenReturnDefaultValue() throws Throwable { - // given - String primary = "primaryCache"; - String fallback = "fallbackCache"; - String defaultValue = ""; - String signatureName = "pjp-signature-name"; - - ProceedingJoinPoint pjp = initializeProceedingJoinPoint(signatureName); - CacheableWithFallback cacheableWithFallback = initializeCacheableWithFallback(primary, fallback, defaultValue); - - when(pjp.proceed()).thenThrow(new RuntimeException("That's an expected exception")); - - // when - var result = cacheableWithFallbackAspect.cacheWithFallback(pjp, cacheableWithFallback); - - // then - assertThat(result).isEqualTo(defaultValue); - } - - private void initializeCache(String cacheName, Object cacheResult) { - Cache cache = Mockito.mock(Cache.class); - Cache.ValueWrapper cacheValueWrapper = Mockito.mock(Cache.ValueWrapper.class); - - when(cacheManager.getCache(cacheName)).thenReturn(cache); - when(cache.get(any())).thenReturn(cacheValueWrapper); - when(cacheValueWrapper.get()).thenReturn(cacheResult); - } - - private ProceedingJoinPoint initializeProceedingJoinPoint(String signatureName) { - ProceedingJoinPoint pjp = Mockito.mock(ProceedingJoinPoint.class); - MethodSignature signature = Mockito.mock(MethodSignature.class); - Method method = Mockito.mock(Method.class); - - when(pjp.getSignature()).thenReturn(signature); - when(signature.getName()).thenReturn(signatureName); - when(signature.getMethod()).thenReturn(method); - Mockito.doReturn(String.class).when(method).getReturnType(); // Mockito when method is not dealing well with generics - - return pjp; - } - - private CacheableWithFallback initializeCacheableWithFallback(String primary, String fallback, String defaultValue) { - CacheableWithFallback cacheableWithFallback = Mockito.mock(CacheableWithFallback.class); - - when(cacheableWithFallback.primary()).thenReturn(primary); - when(cacheableWithFallback.fallback()).thenReturn(fallback); - when(cacheableWithFallback.defaultValue()).thenReturn(defaultValue); - - return cacheableWithFallback; - } -} diff --git a/external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/client/AzureGraphClientTest.java b/external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/client/AzureGraphClientTest.java deleted file mode 100644 index 2d24dad..0000000 --- a/external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/client/AzureGraphClientTest.java +++ /dev/null @@ -1,167 +0,0 @@ -package org.opendevstack.apiservice.externalservice.projectsinfoservice.client; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.ArgumentCaptor; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.exception.InvalidContentProcessException; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.exception.InvalidTokenException; -import org.springframework.http.*; -import org.springframework.test.util.ReflectionTestUtils; -import org.springframework.web.client.HttpClientErrorException; -import org.springframework.web.client.RestTemplate; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.*; - -@ExtendWith(MockitoExtension.class) -class AzureGraphClientTest { - - @Mock - private RestTemplate restTemplate; - - @InjectMocks - private AzureGraphClient azureGraphClient; - - @BeforeEach - void setUp() { - ReflectionTestUtils.setField(azureGraphClient, "pageSize", 10); - } - - @Test - void givenValidAccessToken_whenGetUserGroups_thenReturnGroups() { - // given - var accessToken = "testAccessToken"; - - ArgumentCaptor> captor = ArgumentCaptor.forClass(HttpEntity.class); - - when(restTemplate.exchange( - eq("https://graph.microsoft.com/v1.0/me/memberOf?$top=10"), - eq(HttpMethod.GET), - captor.capture(), - eq(String.class) - )).thenReturn( // @odata.nextLink - new ResponseEntity<>( - "{\"@odata.nextLink\":\"https://graph.microsoft.com/v1.0/me/memberOf?$top=10\", \"value\":[{\"displayName\":\"Group1\"},{\"displayName\":\"Group2\"}]}", - HttpStatus.OK - )).thenReturn( - new ResponseEntity<>( - "{\"value\":[{\"displayName\":\"Group3\"},{\"displayName\":\"Group4\"}]}", - HttpStatus.OK - )); // No next link, end of pagination - - // when - var userGroups = azureGraphClient.getUserGroups(accessToken); - - // then - assertThat(captor.getValue()).isNotNull(); - assertThat(captor.getValue().getHeaders()).isNotNull(); - - HttpHeaders headers = captor.getValue().getHeaders(); - - assertThat(headers.getFirst(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer " + accessToken); - assertThat(userGroups).contains("Group1", "Group2", "Group3", "Group4"); - - verify(restTemplate, times(2)).exchange(anyString(), eq(HttpMethod.GET), any(HttpEntity.class), eq(String.class)); - } - - @Test - void givenValidAccessToken_whenGetUserGroups_andNoGroups_thenReturnsEmptySet() { - // given - var accessToken = "testAccessToken"; - - when(restTemplate.exchange( - any(String.class), - any(HttpMethod.class), - any(HttpEntity.class), - any(Class.class) - )).thenReturn(new ResponseEntity<>( - "{\"invalid\":\"response\"}", - HttpStatus.OK - )); - - // when - var groups = azureGraphClient.getUserGroups(accessToken); - - // then - assertThat(groups).isEmpty(); - } - - @Test - void givenValidAccessToken_whenGetUserGroups_andResponseIsNotValid_thenThrowsInvalidContentProcessException() { - // given - var accessToken = "testAccessToken"; - - when(restTemplate.exchange( - any(String.class), - any(HttpMethod.class), - any(HttpEntity.class), - any(Class.class) - )).thenReturn(new ResponseEntity<>( - "not a valid json", - HttpStatus.OK - )); - - // when - var invalidContentProcessException = assertThrows(InvalidContentProcessException.class, () -> azureGraphClient.getUserGroups(accessToken)); - - // then - assertThat(invalidContentProcessException.getMessage()).isEqualTo("Error while processing server response"); - } - - @Test - void givenInvalidAccessToken_whenGetUserGroups_thenThrowsInvalidTokenException() { - // given - var accessToken = "testAccessToken"; - - when(restTemplate.exchange( - any(String.class), - any(HttpMethod.class), - any(HttpEntity.class), - any(Class.class) - )).thenThrow(new HttpClientErrorException(HttpStatus.UNAUTHORIZED)); - - // when - var invalidTokenException = assertThrows(InvalidTokenException.class, () -> azureGraphClient.getUserGroups(accessToken)); - - // then - assertThat(invalidTokenException.getMessage()).isEqualTo("Error while getting user groups"); - } - - @Test - void givenAValidAccessToken_whenGetUserEmail_thenReturnEmail() { - // given - var accessToken = "testAccessToken"; - var userEmail = "pepito@example.com"; - - ArgumentCaptor> captor = ArgumentCaptor.forClass(HttpEntity.class); - - when(restTemplate.exchange( - eq("https://graph.microsoft.com/v1.0/me"), - eq(HttpMethod.GET), - captor.capture(), - eq(String.class) - )).thenReturn(new ResponseEntity<>( - "{\"mail\": \"" + userEmail + "\"}", - HttpStatus.OK - )); - - // when - var userEmailResponse = azureGraphClient.getUserEmail(accessToken); - - // then - assertThat(captor.getValue()).isNotNull(); - assertThat(captor.getValue().getHeaders()).isNotNull(); - - HttpHeaders headers = captor.getValue().getHeaders(); - - assertThat(headers.getFirst(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer " + accessToken); - assertThat(userEmailResponse).isEqualTo(userEmail); - } -} diff --git a/external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/config/SslConfigurationTest.java b/external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/config/SslConfigurationTest.java deleted file mode 100644 index 41b7f61..0000000 --- a/external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/config/SslConfigurationTest.java +++ /dev/null @@ -1,44 +0,0 @@ -package org.opendevstack.apiservice.externalservice.projectsinfoservice.config; - -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.*; - -/** - * Test class for SSL configuration functionality. - */ -class SslConfigurationTest { - - @Test - void testSslPropertiesDefaultValues() { - ProjectsInfoServiceSslProperties sslProperties = new ProjectsInfoServiceSslProperties(); - - assertTrue(sslProperties.isVerifyCertificates(), "SSL verification should be enabled by default"); - assertEquals("JKS", sslProperties.getTrustStoreType(), "Default trust store type should be JKS"); - assertNull(sslProperties.getTrustStorePath(), "Trust store path should be null by default"); - assertNull(sslProperties.getTrustStorePassword(), "Trust store password should be null by default"); - } - - @Test - void testSslPropertiesSetters() { - ProjectsInfoServiceSslProperties sslProperties = new ProjectsInfoServiceSslProperties(); - - sslProperties.setVerifyCertificates(false); - sslProperties.setTrustStorePath("/path/to/truststore.jks"); - sslProperties.setTrustStorePassword("password"); - sslProperties.setTrustStoreType("PKCS12"); - - assertFalse(sslProperties.isVerifyCertificates(), "SSL verification should be disabled"); - assertEquals("/path/to/truststore.jks", sslProperties.getTrustStorePath()); - assertEquals("password", sslProperties.getTrustStorePassword()); - assertEquals("PKCS12", sslProperties.getTrustStoreType()); - } - - @Test - void testExternalServiceConfigCreation() { - ProjectsInfoServiceSslProperties sslProperties = new ProjectsInfoServiceSslProperties(); - ProjectsInfoServiceConfig config = new ProjectsInfoServiceConfig(sslProperties); - - assertNotNull(config, "ExternalServiceConfig should be created successfully"); - } -} \ No newline at end of file diff --git a/external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/dto/LinkMother.java b/external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/dto/LinkMother.java deleted file mode 100644 index abd5e49..0000000 --- a/external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/dto/LinkMother.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.opendevstack.apiservice.externalservice.projectsinfoservice.dto; - -public class LinkMother { - - public static Link of() { - return of("label", "https://www.example.com", "general", "some info"); - } - - public static Link of(String label, String url, String type, String tooltip) { - return Link.builder() - .label(label) - .url(url) - .type(type) - .tooltip(tooltip) - .build(); - } -} diff --git a/external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/dto/ProjectInfoMother.java b/external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/dto/ProjectInfoMother.java deleted file mode 100644 index a829554..0000000 --- a/external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/dto/ProjectInfoMother.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.opendevstack.apiservice.externalservice.projectsinfoservice.dto; - -import java.util.List; - -public class ProjectInfoMother { - - public static ProjectInfo of() { - return of("mother-project-key"); - } - - public static ProjectInfo of(String projectKey) { - return ProjectInfo.builder() - .projectKey(projectKey) - .clusters(List.of("mother-cluster-1", "mother-cluster-2")) - .build(); - } -} diff --git a/external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/dto/ProjectPlatformsMother.java b/external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/dto/ProjectPlatformsMother.java deleted file mode 100644 index 2d137e1..0000000 --- a/external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/dto/ProjectPlatformsMother.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.opendevstack.apiservice.externalservice.projectsinfoservice.dto; - -import java.util.List; - -public class ProjectPlatformsMother { - - public static ProjectPlatforms of(List
sections) { - return ProjectPlatforms.builder() - .sections(sections) - .build(); - } -} diff --git a/external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/dto/SectionMother.java b/external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/dto/SectionMother.java deleted file mode 100644 index 021c0f0..0000000 --- a/external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/dto/SectionMother.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.opendevstack.apiservice.externalservice.projectsinfoservice.dto; - -import java.util.List; - -public class SectionMother { - - public static Section of() { - return Section.builder() - .section("section") - .tooltip("tooltip") - .links(List.of(LinkMother.of())) - .build(); - } -} diff --git a/external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/facade/ProjectsFacadeTest.java b/external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/facade/ProjectsFacadeTest.java deleted file mode 100644 index 742658e..0000000 --- a/external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/facade/ProjectsFacadeTest.java +++ /dev/null @@ -1,177 +0,0 @@ -package org.opendevstack.apiservice.externalservice.projectsinfoservice.facade; - -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.apiservice.externalservice.projectsinfoservice.client.AzureGraphClient; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.dto.Link; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.dto.ProjectInfoMother; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.dto.ProjectPlatforms; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.dto.SectionMother; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.model.OpenshiftProjectCluster; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.model.OpenshiftProjectClusterMother; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.model.PlatformMother; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.model.PlatformsWithTitleMother; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.service.MockProjectsService; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.service.OpenShiftProjectService; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.service.PlatformService; - -import java.util.List; -import java.util.Map; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.when; - -@ExtendWith(MockitoExtension.class) -class ProjectsFacadeTest { - - @Mock - private AzureGraphClient azureGraphClient; - - @Mock - private OpenShiftProjectService openShiftProjectService; - - @Mock - private MockProjectsService mockProjectsService; - - @Mock - private PlatformService platformService; - - @InjectMocks - private ProjectsFacade projectsFacade; - - @Test - void givenAProjectKey_whenGetProjectPlatforms_ThenPlatformsAreReturned() { - // given - var openshiftProjectCluster = OpenshiftProjectClusterMother.of(); - var projectKey = openshiftProjectCluster.getProject(); - var cluster = openshiftProjectCluster.getCluster(); - var disabledPlatforms = List.of("platform1", "platform2"); - var expectedSection = SectionMother.of(); - var expectedSections = List.of(expectedSection); - var expectedPlatforms = PlatformsWithTitleMother.of( - Map.of( - "platform1", PlatformMother.of("platform1", "Platform 1 label"), - "platform2", PlatformMother.of("platform2", "Platform 2 label"), - "platform3", PlatformMother.of("platform2", "Platform 3 label") - ) - ); - - List projectClusters = List.of(openshiftProjectCluster); - - when(openShiftProjectService.fetchProjects()).thenReturn(projectClusters); - - when(platformService.getDisabledPlatforms(projectKey)).thenReturn(disabledPlatforms); - when(platformService.getPlatforms(projectKey, cluster)).thenReturn(expectedPlatforms); - when(platformService.getSections(projectKey, cluster)).thenReturn(expectedSections); - - // when - ProjectPlatforms result = projectsFacade.getProjectPlatforms(projectKey); - - // then - assertThat(result).isNotNull(); - - var sections = result.getSections(); - - assertThat(sections).isNotNull() - .hasSize(2); - - assertThat(sections.get(0).getSection()).isEqualTo("Simple title"); - assertThat(sections.get(0).getLinks()).contains(Link.builder() - .label("Platform 1 label") - .url("http://www.example.com/platform1") - .abbreviation("ABRPLATFORM1") - .type("platform") - .disabled(true) - .build()); - assertThat(sections.get(1)).isEqualTo(expectedSection); - - } - - @Test - void givenAProjectKey_whenGetProjectPlatforms_AndProjectNotInOpenshift_ThenReturnNull() { - // given - var projectKey = "sampleProject"; - - List projectClusters = List.of(OpenshiftProjectClusterMother.of()); - - when(openShiftProjectService.fetchProjects()).thenReturn(projectClusters); - - // when - ProjectPlatforms result = projectsFacade.getProjectPlatforms(projectKey); - - // then - assertThat(result).isNull(); - } - - @Test - void givenAProjectKey_whenGetProjectPlatforms_AndProjectNotInOpenshift_butMockedProjects_ThenReturnMockProject() { - // given - var projectKey = "mock-project-key"; - var projectInfo = ProjectInfoMother.of(projectKey); - - List projectClusters = List.of(OpenshiftProjectClusterMother.of()); - var disabledPlatforms = List.of("datahub", "testinghub"); - var expectedSection = SectionMother.of(); - var expectedSections = List.of(expectedSection); - - when(openShiftProjectService.fetchProjects()).thenReturn(projectClusters); - when(mockProjectsService.getDefaultProjectsAndClusters()).thenReturn(Map.of(projectKey, projectInfo)); - - when(platformService.getDisabledPlatforms(projectKey)).thenReturn(disabledPlatforms); - when(platformService.getSections(projectKey, projectInfo.getClusters().getFirst())).thenReturn(expectedSections); - when(platformService.getPlatforms(projectKey, projectInfo.getClusters().getFirst())).thenReturn(PlatformsWithTitleMother.of()); - - // when - ProjectPlatforms result = projectsFacade.getProjectPlatforms(projectKey); - - // then - assertThat(result).isNotNull(); - - var sections = result.getSections(); - - assertThat(sections).isNotNull() - .hasSize(2); - - assertThat(sections.get(0).getSection()).isEqualTo("Simple title"); - assertThat(sections.get(1)).isEqualTo(expectedSection); - - } - - @Test - void givenAProjectKey_andProjectExistsInOpenshift_andThereAreMockedProjectWithSameKey_whenGetProjectPlatforms_ThenOpenshiftProjectClustersArePrioritized() { - // given - var openshiftProjectCluster = OpenshiftProjectClusterMother.of(); - var projectKey = openshiftProjectCluster.getProject(); - var cluster = openshiftProjectCluster.getCluster(); - var projectInfo = ProjectInfoMother.of(projectKey); - var disabledPlatforms = List.of("datahub", "testinghub"); - var expectedSection = SectionMother.of(); - var expectedSections = List.of(expectedSection); - - List projectClusters = List.of(openshiftProjectCluster); - - when(openShiftProjectService.fetchProjects()).thenReturn(projectClusters); - when(mockProjectsService.getDefaultProjectsAndClusters()).thenReturn(Map.of(projectKey, projectInfo)); - - when(platformService.getDisabledPlatforms(projectKey)).thenReturn(disabledPlatforms); - when(platformService.getSections(projectKey, cluster)).thenReturn(expectedSections); - when(platformService.getPlatforms(projectKey, cluster)).thenReturn(PlatformsWithTitleMother.of()); - - // when - ProjectPlatforms result = projectsFacade.getProjectPlatforms(projectKey); - - // then - assertThat(result).isNotNull(); - - var sections = result.getSections(); - - assertThat(sections).isNotNull() - .hasSize(2); - - assertThat(sections.get(0).getSection()).isEqualTo("Simple title"); - assertThat(sections.get(1)).isEqualTo(expectedSection); - } -} diff --git a/external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/OpenshiftProjectClusterMother.java b/external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/OpenshiftProjectClusterMother.java deleted file mode 100644 index 78f16c0..0000000 --- a/external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/OpenshiftProjectClusterMother.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.opendevstack.apiservice.externalservice.projectsinfoservice.model; - -public class OpenshiftProjectClusterMother { - - public static OpenshiftProjectCluster of() { - return OpenshiftProjectCluster.builder() - .project("mother-project-key") - .cluster("mother-cluster") - .build(); - } -} diff --git a/external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/PlatformLinkMother.java b/external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/PlatformLinkMother.java deleted file mode 100644 index 26e24fc..0000000 --- a/external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/PlatformLinkMother.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.opendevstack.apiservice.externalservice.projectsinfoservice.model; - -public class PlatformLinkMother { - - public static PlatformLink of() { - return PlatformLink.builder() - .label("label") - .url("https://google.com") - .type("general") - .tooltip("Some help here") - .build(); - } - - public static PlatformLink of(String label, String url, String type, String tooltip) { - return PlatformLink.builder() - .label(label) - .url(url) - .type(type) - .tooltip(tooltip) - .build(); - } -} diff --git a/external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/PlatformMother.java b/external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/PlatformMother.java deleted file mode 100644 index e9d38d8..0000000 --- a/external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/PlatformMother.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.opendevstack.apiservice.externalservice.projectsinfoservice.model; - -public class PlatformMother { - - public static Platform of() { - return new Platform(); - } - - public static Platform of(String id, String label) { - return Platform.builder() - .id(id) - .label(label) - .url("http://www.example.com/" + id) - .abbreviation("ABR" + id.toUpperCase()) - .build(); - } -} diff --git a/external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/PlatformsWithTitleMother.java b/external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/PlatformsWithTitleMother.java deleted file mode 100644 index 3b021d5..0000000 --- a/external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/PlatformsWithTitleMother.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.opendevstack.apiservice.externalservice.projectsinfoservice.model; - -import java.util.Collections; -import java.util.Map; - -public class PlatformsWithTitleMother { - - public static final String DEFAULT_TITLE = "Simple title"; - - public static PlatformsWithTitle of() { - return of(DEFAULT_TITLE); - } - - public static PlatformsWithTitle of(String title) { - return PlatformsWithTitle.builder() - .title(title) - .platforms(Collections.emptyMap()) - .build(); - } - - public static PlatformsWithTitle of(Map platforms) { - return PlatformsWithTitle.builder() - .title(DEFAULT_TITLE) - .platforms(platforms) - .build(); - } -} diff --git a/external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/TestingHubProjectMother.java b/external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/TestingHubProjectMother.java deleted file mode 100644 index 2787270..0000000 --- a/external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/TestingHubProjectMother.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.opendevstack.apiservice.externalservice.projectsinfoservice.model; - -public class TestingHubProjectMother { - public static TestingHubProject of(String key, String id) { - return new TestingHubProject(id, key); - } -} diff --git a/external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/MockProjectsServiceTest.java b/external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/MockProjectsServiceTest.java deleted file mode 100644 index e768c09..0000000 --- a/external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/MockProjectsServiceTest.java +++ /dev/null @@ -1,32 +0,0 @@ -package org.opendevstack.apiservice.externalservice.projectsinfoservice.service; - -import org.junit.jupiter.api.Test; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.config.MockConfiguration; - -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; - - -class MockProjectsServiceTest { - - @Test - void givenAMockConfiguration_whenGetProjectsAndClusters_thenReturnExpectedProjects() { - // Given - MockConfiguration mockConfiguration = new MockConfiguration(); - mockConfiguration.setClusters(List.of("US-TEST", "eu", "CN")); - mockConfiguration.setDefaultProjects(List.of("DEFAULT1", "DEFAULT2:cn")); - mockConfiguration.setUsersProjects("{PEPE:[PROJECT-3, PROJECT-4:US-TEST]; PPT:[PROJECT-3, PROJECT-5]}"); - - var userEmail = "PEPE"; - - MockProjectsService mockProjectsService = new MockProjectsService(mockConfiguration); - - // when - var projects = mockProjectsService.getProjectsAndClusters(userEmail); - - // then - assertThat(projects.size()).isEqualTo(4); - assertThat(projects.keySet()).containsExactlyInAnyOrder("DEFAULT1", "DEFAULT2", "PROJECT-3", "PROJECT-4"); - } -} diff --git a/external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/OpenshiftProjectServiceTest.java b/external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/OpenshiftProjectServiceTest.java deleted file mode 100644 index 0438999..0000000 --- a/external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/OpenshiftProjectServiceTest.java +++ /dev/null @@ -1,133 +0,0 @@ -package org.opendevstack.apiservice.externalservice.projectsinfoservice.service; - -import org.junit.jupiter.api.BeforeEach; -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.apiservice.externalservice.projectsinfoservice.config.OpenshiftClusterConfiguration; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.model.Metadata; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.model.OpenshiftProjectCluster; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.model.Project; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.model.ProjectList; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpMethod; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.test.util.ReflectionTestUtils; -import org.springframework.web.client.RestTemplate; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.*; -import static org.mockito.Mockito.when; - -@ExtendWith(MockitoExtension.class) -class OpenshiftProjectServiceTest { - - @Mock - private RestTemplate mockRestTemplate; - - @InjectMocks - private OpenShiftProjectService openshiftProjectService; - - @BeforeEach - void setUp() { - Map> clusters = new HashMap<>(); - Map euCluster = new HashMap<>(); - Map usTest = new HashMap<>(); - euCluster.put("url", "https://cluster1.example.com"); - euCluster.put("token", "mytoken"); - usTest.put("url", "https://cluster2.example.com"); - usTest.put("token", "mytoken"); - clusters.put("cluster1", euCluster); - clusters.put("cluster2", usTest); - - OpenshiftClusterConfiguration openshiftClusterConfig = new OpenshiftClusterConfiguration(); - openshiftClusterConfig.setClusters(clusters); - - // Manually inject the config since it's not a Spring bean - openshiftProjectService = new OpenShiftProjectService(mockRestTemplate, openshiftClusterConfig); - - - // Set the @Value field using ReflectionTestUtils - ReflectionTestUtils.setField(openshiftProjectService, "projectApiUrl", - "/apis/project.openshift.io/v1/projects"); - } - - @Test - void givenTwoProjectsInDifferentClusters_whenFetchProjects_thenReturnTwoProjectsWithTheirClusters() { - // Mock project list response - Project project1 = new Project(); - Metadata metadata1 = new Metadata(); - metadata1.setName("myapp-cd"); - project1.setMetadata(metadata1); - - Project project2 = new Project(); - Metadata metadata2 = new Metadata(); - metadata2.setName("anotherapp"); // Should be filtered out - project2.setMetadata(metadata2); - - ProjectList projectList = new ProjectList(); - projectList.setItems(List.of(project1, project2)); - - ResponseEntity responseEntity = new ResponseEntity<>(projectList, HttpStatus.OK); - - when(mockRestTemplate.exchange( - eq("https://cluster1.example.com" + "/apis/project.openshift.io/v1/projects"), - any(HttpMethod.class), - any(HttpEntity.class), - eq(ProjectList.class) - )).thenReturn(responseEntity); - - when(mockRestTemplate.exchange( - eq("https://cluster2.example.com" + "/apis/project.openshift.io/v1/projects"), - any(HttpMethod.class), - any(HttpEntity.class), - eq(ProjectList.class) - )).thenReturn(responseEntity); - - // Execute - List result = openshiftProjectService.fetchProjects(); - - // Verify - assertEquals(2, result.size()); // One for each cluster - OpenshiftProjectCluster projectCluster = result.getFirst(); - assertEquals("MYAPP", projectCluster.getProject()); - // Cluster name should be either "cluster1" or "cluster2" - assertTrue(List.of("cluster1", "cluster2").contains(projectCluster.getCluster())); - } - - @Test - void givenProjectsWithLowercaseNames_whenFetchProjects_thenReturnProjectsWithUppercaseNames() { - Project project1 = new Project(); - Metadata metadata1 = new Metadata(); - metadata1.setName("myapp-cd"); - project1.setMetadata(metadata1); - - Project project2 = new Project(); - Metadata metadata2 = new Metadata(); - metadata2.setName("anotherapp-cd"); - project2.setMetadata(metadata2); - - ProjectList projectList = new ProjectList(); - projectList.setItems(List.of(project1, project2)); - - ResponseEntity responseEntity = new ResponseEntity<>(projectList, HttpStatus.OK); - - when(mockRestTemplate.exchange(anyString(), any(HttpMethod.class), any(HttpEntity.class), eq(ProjectList.class))) - .thenReturn(responseEntity); - - List result = openshiftProjectService.fetchProjects(); - - assertEquals(4, result.size()); - result.forEach(projectCluster -> - assertEquals(projectCluster.getProject(), projectCluster.getProject().toUpperCase()) - ); - } -} diff --git a/external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/PlatformServiceTest.java b/external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/PlatformServiceTest.java deleted file mode 100644 index c27325f..0000000 --- a/external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/PlatformServiceTest.java +++ /dev/null @@ -1,304 +0,0 @@ -package org.opendevstack.apiservice.externalservice.projectsinfoservice.service; - -import org.apache.commons.lang3.tuple.Pair; -import org.assertj.core.api.InstanceOfAssertFactories; -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.apiservice.externalservice.projectsinfoservice.client.AzureGraphClient; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.client.PlatformsYmlClient; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.client.TestingHubClient; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.config.PlatformsConfiguration; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.config.ProjectFilterConfiguration; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.dto.LinkMother; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.exception.InvalidConfigurationException; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.model.*; - -import java.util.List; -import java.util.Map; -import java.util.Set; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Mockito.when; - -@ExtendWith(MockitoExtension.class) -class PlatformServiceTest { - - @Mock - PlatformsConfiguration platformsConfiguration; - @Mock - ProjectFilterConfiguration projectFilterConfiguration; - @Mock - PlatformsYmlClient platformsYmlClient; - @Mock - AzureGraphClient azureGraphClient; - @Mock - TestingHubClient testingHubClient; - - @InjectMocks - private PlatformService platformService; - - @Test - void givenASetOfPlatforms_whenGetDisabledPlatforms_AndAzureIsWorking_AndNoPlatformAvailable_thenReturnTheExpectedList() { - // given - when(projectFilterConfiguration.getProjectRolesGroupPrefix()).thenReturn("project-roles-"); - - when(azureGraphClient.getDataHubGroups()).thenReturn(Set.of("group1", "group2")); - when(azureGraphClient.getTestingHubGroups()).thenReturn(Set.of("group2", "group3")); - when(testingHubClient.getDefaultProjects()).thenReturn(Set.of(TestingHubProjectMother.of("anyProjectKey", "1"))); - - // when - var disabledPlatforms = platformService.getDisabledPlatforms("anyProjectKey"); - - // then - assertThat(disabledPlatforms).hasSize(2) - .containsExactlyInAnyOrder("datahub", "testinghub"); - } - - @Test - void givenAHardcodedSections_whenGetSections_thenReturnTheExpectedList() { - // given - var currentCluster = "unit-test-cluster"; - var projectKey = "anyProjectKey"; - - when(platformsConfiguration.getBasePath()).thenReturn("https://any-base-path/"); - when(platformsConfiguration.getClusters()).thenReturn( - Map.of( - currentCluster, "path/to/platforms.yml" - ) - ); - var expectedUrl = "https://any-base-path/path/to/platforms.yml"; - - when(platformsYmlClient.fetchSectionsFromYaml(expectedUrl)).thenReturn(buildExpectedPlatformSections()); - - // when - var sections = platformService.getSections(projectKey, currentCluster); - - // then - assertThat(sections).isNotNull() - .hasSize(3); - - assertThat(sections.getFirst()) - .extracting("section").isEqualTo("Project Shortcuts - Application Platforms"); - - assertThat(sections.getFirst()) - .extracting("links").asInstanceOf(InstanceOfAssertFactories.LIST).isNotEmpty() - .hasSize(9) - .first().isEqualTo(LinkMother.of("JIRA", "https://www.google.com/" + projectKey, "general", "help text")); // Check that token is replaced - - assertThat(sections.get(1)) - .extracting("section").isEqualTo("Project Shortcuts - Data platform"); - - assertThat(sections.get(2)) - .extracting("section").isEqualTo("Services"); - } - - private static List buildExpectedPlatformSections() { - return List.of( - PlatformSectionService.builder() - .section("Project Shortcuts - Application Platforms") - .links( - List.of( - PlatformLinkMother.of("JIRA", "https://www.google.com/${projectKey}", "general", "help text"), - PlatformLinkMother.of("Confluence", "https://www.google.com", "general", "help text"), - PlatformLinkMother.of("SonarQube", "https://www.google.com", "general", "help text"), - PlatformLinkMother.of("Nexus", "https://www.google.com", "general", "help text"), - PlatformLinkMother.of("Jenkins", "https://www.google.com", "general", "help text"), - PlatformLinkMother.of("Artifactory", "https://www.google.com", "general", "help text"), - PlatformLinkMother.of("GitLab", "https://www.google.com", "general", "help text"), - PlatformLinkMother.of("Harbor", "https://www.google.com", "general", "help text"), - PlatformLinkMother.of("Kibana", "https://www.google.com", "general", "help text") - ) - ) - .build(), - PlatformSectionService.builder() - .section("Project Shortcuts - Data platform") - .links(java.util.Collections.emptyList()) - .build(), - PlatformSectionService.builder() - .section("Services") - .links(java.util.Collections.emptyList()) - .build() - ); - } - - @Test - void givenValidClusterAndPlatforms_whenGetPlatformLinks_thenReturnPlatformLinksMap() { - // given - var cluster = "test-cluster"; - var projectKey = "testProject"; - var basePath = "https://config.example.com/"; - var clusterPath = "clusters/test-cluster.yml"; - - when(platformsConfiguration.getClusters()).thenReturn(Map.of(cluster, clusterPath)); - when(platformsConfiguration.getBasePath()).thenReturn(basePath); - - var jiraPlatform = createPlatform("jira", "JIRA", "https://jira.example.com"); - var gitlabPlatform = createPlatform("gitlab", "GitLab", "https://gitlab.example.com/${project_key}"); - var sonarPlatform = createPlatform("sonar", "SonarQube", "https://sonar.example.com/${PROJECT_KEY}/dashboard"); - - var platforms = Pair.of("Simple title", List.of(jiraPlatform, gitlabPlatform, sonarPlatform)); - - when(platformsYmlClient.fetchPlatformsFromYaml(basePath + clusterPath)).thenReturn(platforms); - - // when - var result = platformService.getPlatforms(projectKey, cluster); - - // then - - assertThat(result).isNotNull() - .extracting("title").isEqualTo("Simple title"); - - assertThat(result.getPlatforms()).isNotNull() - .containsEntry("jira", jiraPlatform) - .containsEntry("gitlab", gitlabPlatform) - .containsEntry("sonar", sonarPlatform); - - } - - @Test - void givenPlatformWithTestingHubToken_whenGetPlatformLinks_AndProjectExists_thenReturnResolvedUrl() { - // given - var cluster = "test-cluster"; - var projectKey = "testProject"; - var basePath = "https://config.example.com/"; - var clusterPath = "clusters/test-cluster.yml"; - - when(platformsConfiguration.getClusters()).thenReturn(Map.of(cluster, clusterPath)); - when(platformsConfiguration.getBasePath()).thenReturn(basePath); - - var testingHubPlatform = createPlatform("testinghub", "TestingHub", "https://testinghub.example.com/project/${testingHubProject}"); - - var platforms = Pair.of("simple title", List.of(testingHubPlatform)); - - Set testingHubProjects = Set.of( - TestingHubProjectMother.of("testProject", "12345"), - TestingHubProjectMother.of("otherProject", "67890") - ); - - when(platformsYmlClient.fetchPlatformsFromYaml(basePath + clusterPath)).thenReturn(platforms); - when(testingHubClient.getDefaultProjects()).thenReturn(testingHubProjects); - - // when - var result = platformService.getPlatforms(projectKey, cluster); - - // then - assertThat(result.getPlatforms()).isNotNull() - .hasSize(1) - .containsEntry("testinghub", testingHubPlatform); - } - - @Test - void givenPlatformWithTestingHubToken_whenGetPlatformLinks_AndProjectNotFound_thenReturnEmptyUrl() { - // given - var cluster = "test-cluster"; - var projectKey = "nonExistentProject"; - var basePath = "https://config.example.com/"; - var clusterPath = "clusters/test-cluster.yml"; - - when(platformsConfiguration.getClusters()).thenReturn(Map.of(cluster, clusterPath)); - when(platformsConfiguration.getBasePath()).thenReturn(basePath); - - var testingHubPlatform = createPlatform("testinghub", "TestingHub", "https://testinghub.example.com/project/${testingHubProject}"); - var jiraPlatform = createPlatform("jira", "JIRA", "https://jira.example.com/${project_ey}"); - - var platforms = Pair.of("simple title", List.of(testingHubPlatform, jiraPlatform)); - - Set testingHubProjects = Set.of( - TestingHubProjectMother.of("testProject", "12345") - ); - - when(platformsYmlClient.fetchPlatformsFromYaml(basePath + clusterPath)).thenReturn(platforms); - when(testingHubClient.getDefaultProjects()).thenReturn(testingHubProjects); - - // when - var result = platformService.getPlatforms(projectKey, cluster); - - // then - assertThat(result).isNotNull(); - - assertThat(result.getPlatforms().get("testinghub").getUrl()).isEmpty(); // Empty because project not found in TestingHub - } - - @Test - void givenPlatformWithBothTokens_whenGetPlatformLinks_thenResolveBothTokens() { - // given - var cluster = "test-cluster"; - var projectKey = "testProject"; - var basePath = "https://config.example.com/"; - var clusterPath = "clusters/test-cluster.yml"; - - when(platformsConfiguration.getClusters()).thenReturn(Map.of(cluster, clusterPath)); - when(platformsConfiguration.getBasePath()).thenReturn(basePath); - - var platforms = Pair.of("title", List.of( - createPlatform("custom", "Custom", "https://custom.example.com/${testingHubProject}/${project_key}/view") - ) - ); - - Set testingHubProjects = Set.of( - TestingHubProjectMother.of("testProject", "12345") - ); - - when(platformsYmlClient.fetchPlatformsFromYaml(basePath + clusterPath)).thenReturn(platforms); - when(testingHubClient.getDefaultProjects()).thenReturn(testingHubProjects); - - // when - var result = platformService.getPlatforms(projectKey, cluster); - - // then - assertThat(result.getPlatforms()).isNotNull() - .hasSize(1); - - assertThat(result.getPlatforms().get("custom").getUrl()).isEqualTo("https://custom.example.com/12345/testproject/view"); - } - - @Test - void givenInvalidCluster_whenGetPlatformLinks_thenThrowInvalidConfigurationException() { - // given - var invalidCluster = "non-existent-cluster"; - var projectKey = "testProject"; - - when(platformsConfiguration.getClusters()).thenReturn(Map.of( - "cluster1", "path1.yml", - "cluster2", "path2.yml" - )); - - // when & then - assertThatThrownBy(() -> platformService.getPlatforms(projectKey, invalidCluster)) - .isInstanceOf(InvalidConfigurationException.class) - .hasMessageContaining("Cluster " + invalidCluster + " is not configured") - .hasMessageContaining("cluster1") - .hasMessageContaining("cluster2"); - } - - @Test - void givenEmptyPlatformList_whenGetPlatformLinks_thenReturnEmptyMap() { - // given - var cluster = "test-cluster"; - var projectKey = "testProject"; - var basePath = "https://config.example.com/"; - var clusterPath = "clusters/test-cluster.yml"; - - when(platformsConfiguration.getClusters()).thenReturn(Map.of(cluster, clusterPath)); - when(platformsConfiguration.getBasePath()).thenReturn(basePath); - when(platformsYmlClient.fetchPlatformsFromYaml(basePath + clusterPath)).thenReturn(Pair.of("", List.of())); - - // when - var result = platformService.getPlatforms(projectKey, cluster); - - // then - assertThat(result.getPlatforms()).isNotNull().isEmpty(); - } - - private Platform createPlatform(String id, String label, String url) { - Platform platform = new Platform(); - platform.setId(id); - platform.setLabel(label); - platform.setUrl(url); - return platform; - } -} diff --git a/external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/impl/ProjectsInfoServiceImplTest.java b/external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/impl/ProjectsInfoServiceImplTest.java deleted file mode 100644 index b58618a..0000000 --- a/external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/impl/ProjectsInfoServiceImplTest.java +++ /dev/null @@ -1,295 +0,0 @@ -package org.opendevstack.apiservice.externalservice.projectsinfoservice.service.impl; - -import org.opendevstack.apiservice.externalservice.projectsinfoservice.dto.ProjectPlatformsMother; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.dto.SectionMother; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.exception.ProjectsInfoServiceException; -import org.junit.jupiter.api.BeforeEach; -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; -import org.opendevstack.apiservice.externalservice.projectsinfoservice.facade.ProjectsFacade; -import org.springframework.core.ParameterizedTypeReference; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.test.util.ReflectionTestUtils; -import org.springframework.web.client.RestClientException; -import org.springframework.web.client.RestTemplate; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -/** - * Unit tests for ProjectsInfoServiceImpl. - * Tests all methods with various scenarios including success cases, error cases, and edge cases. - */ -@ExtendWith(MockitoExtension.class) -class ProjectsInfoServiceImplTest { - - @Mock - private RestTemplate restTemplate; - - @Mock - private ProjectsFacade projectsFacade; - - @Captor - private ArgumentCaptor> httpEntityCaptor; - - @Captor - private ArgumentCaptor urlCaptor; - - private ProjectsInfoServiceImpl projectsInfoService; - - private static final String BASE_URL = "http://localhost:8080/api/v2"; - private static final String PROJECT_KEY = "TEST-PROJECT"; - - @BeforeEach - void setUp() { - projectsInfoService = new ProjectsInfoServiceImpl(restTemplate, projectsFacade); - ReflectionTestUtils.setField(projectsInfoService, "baseUrl", BASE_URL); - } - - // ========== Tests for getProjectPlatforms ========== - - @Test - void testGetProjectPlatforms_Success() throws Exception { - // given - var section = SectionMother.of(); - var sections = List.of(section); - var projectPlatforms = ProjectPlatformsMother.of(sections); - - when(projectsFacade.getProjectPlatforms(PROJECT_KEY)).thenReturn(projectPlatforms); - - // when - var platforms = projectsInfoService.getProjectPlatforms(PROJECT_KEY); - - // then - assertThat(platforms).isNotNull(); - - var resultSections = platforms.getSections(); - assertThat(resultSections).isNotNull().hasSize(1); - - var resultSection = resultSections.get(0); - - assertThat(resultSection).isNotNull(); - assertThat(resultSection.getSection()).isEqualTo(section.getSection()); - assertThat(resultSection.getTooltip()).isEqualTo(section.getTooltip()); - - } - - // ========== Tests for validateConnection ========== - - @Test - void testValidateConnection_Success() { - // Arrange - Map healthResponse = Map.of("status", "UP"); - ResponseEntity> responseEntity = new ResponseEntity<>(healthResponse, HttpStatus.OK); - - when(restTemplate.exchange( - anyString(), - eq(HttpMethod.GET), - any(HttpEntity.class), - eq(new ParameterizedTypeReference>() {}) - )).thenReturn(responseEntity); - - // Act - boolean result = projectsInfoService.validateConnection(); - - // Assert - assertTrue(result); - - // Verify the correct URL was called - verify(restTemplate).exchange( - urlCaptor.capture(), - eq(HttpMethod.GET), - httpEntityCaptor.capture(), - eq(new ParameterizedTypeReference>() {}) - ); - - String capturedUrl = urlCaptor.getValue(); - assertEquals(BASE_URL + "/actuator/health", capturedUrl); - } - - @Test - void testValidateConnection_Non2xxResponse() { - // Arrange - ResponseEntity> responseEntity = new ResponseEntity<>(HttpStatus.SERVICE_UNAVAILABLE); - - when(restTemplate.exchange( - anyString(), - eq(HttpMethod.GET), - any(HttpEntity.class), - eq(new ParameterizedTypeReference>() {}) - )).thenReturn(responseEntity); - - // Act - boolean result = projectsInfoService.validateConnection(); - - // Assert - assertFalse(result); - } - - @Test - void testValidateConnection_Exception() { - // Arrange - when(restTemplate.exchange( - anyString(), - eq(HttpMethod.GET), - any(HttpEntity.class), - eq(new ParameterizedTypeReference>() {}) - )).thenThrow(new RestClientException("Connection refused")); - - // Act - boolean result = projectsInfoService.validateConnection(); - - // Assert - assertFalse(result); - } - - @Test - void testValidateConnection_RuntimeException() { - // Arrange - when(restTemplate.exchange( - anyString(), - eq(HttpMethod.GET), - any(HttpEntity.class), - eq(new ParameterizedTypeReference>() {}) - )).thenThrow(new RuntimeException("Unexpected error")); - - // Act - boolean result = projectsInfoService.validateConnection(); - - // Assert - assertFalse(result); - } - - // ========== Tests for isHealthy ========== - - @Test - void testIsHealthy_Success() { - // Arrange - Map healthResponse = Map.of("status", "UP"); - ResponseEntity> responseEntity = new ResponseEntity<>(healthResponse, HttpStatus.OK); - - when(restTemplate.exchange( - anyString(), - eq(HttpMethod.GET), - any(HttpEntity.class), - eq(new ParameterizedTypeReference>() {}) - )).thenReturn(responseEntity); - - // Act - boolean result = projectsInfoService.isHealthy(); - - // Assert - assertTrue(result); - } - - @Test - void testIsHealthy_Failure() { - // Arrange - ResponseEntity> responseEntity = new ResponseEntity<>(HttpStatus.SERVICE_UNAVAILABLE); - - when(restTemplate.exchange( - anyString(), - eq(HttpMethod.GET), - any(HttpEntity.class), - eq(new ParameterizedTypeReference>() {}) - )).thenReturn(responseEntity); - - // Act - boolean result = projectsInfoService.isHealthy(); - - // Assert - assertFalse(result); - } - - @Test - void testIsHealthy_Exception() { - // Arrange - when(restTemplate.exchange( - anyString(), - eq(HttpMethod.GET), - any(HttpEntity.class), - eq(new ParameterizedTypeReference>() {}) - )).thenThrow(new RestClientException("Connection timeout")); - - // Act - boolean result = projectsInfoService.isHealthy(); - - // Assert - assertFalse(result); - } - - @Test - void testIsHealthy_RuntimeException() { - // Arrange - when(restTemplate.exchange( - anyString(), - eq(HttpMethod.GET), - any(HttpEntity.class), - eq(new ParameterizedTypeReference>() {}) - )).thenThrow(new RuntimeException("Unexpected error")); - - // Act - boolean result = projectsInfoService.isHealthy(); - - // Assert - assertFalse(result); - } - - // ========== Helper methods ========== - - private Map createValidProjectPlatformsResponse() { - Map response = new HashMap<>(); - response.put("disabledPlatforms", java.util.List.of("platform1", "platform2")); - - Map platformLinks = new HashMap<>(); - platformLinks.put("jenkins", "https://jenkins.example.com/job/test-project"); - platformLinks.put("sonar", "https://sonar.example.com/dashboard?id=test-project"); - response.put("platformLinks", platformLinks); - - return response; - } - - private Map createComplexProjectPlatformsResponse() { - Map response = new HashMap<>(); - response.put("disabledPlatforms", java.util.List.of("platform1")); - - Map platformLinks = new HashMap<>(); - platformLinks.put("jenkins", "https://jenkins.example.com/job/test-project"); - response.put("platformLinks", platformLinks); - - // Add sections - Map section1 = new HashMap<>(); - section1.put("section", "CI/CD"); - - Map link1 = new HashMap<>(); - link1.put("name", "Build Pipeline"); - link1.put("url", "https://jenkins.example.com/job/build"); - section1.put("links", java.util.List.of(link1)); - - response.put("sections", java.util.List.of(section1)); - - return response; - } -} diff --git a/external-service-projects-info-service/static/images/application-configuration.png b/external-service-projects-info-service/static/images/application-configuration.png deleted file mode 100644 index 6b3bbe988bd85dc7240ec4fce9f7bec9d33e934e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 161707 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
    c=DV&!@<=>nK;1D-D#p~~FdNRG)2d};v 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 diff --git a/external-service-projects-info-service/static/images/configuration-tree.png b/external-service-projects-info-service/static/images/configuration-tree.png deleted file mode 100644 index 1dd17092c449f4b4f31224bd7e2892e3ec8932f7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 75864 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 o{Bx=@SJ9Ot3dw1+#{nD8 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 P0Dr`Uq(CJC+TQ;Miqjoz From 4ac9dfb8175ba0e7ec21d1395e0cdeee46bc45b7 Mon Sep 17 00:00:00 2001 From: soriaoli Date: Tue, 17 Feb 2026 10:40:21 +0100 Subject: [PATCH 03/11] [ods-api-to-projecs-info-service] - Update facade. --- .../facade/impl/ProjectsFacadeImpl.java | 14 +--- .../mapper/ProjectPlatformsMapper.java | 77 ++++++++++++++++++- .../ProjectsInfoServiceException.java | 15 ++++ .../model/PlatformSection.java | 59 ++++++++++++++ .../model/PlatformSectionLink.java | 70 +++++++++++++++++ .../projectsinfoservice/model/Platforms.java | 38 +++++++++ .../service/ProjectsInfoService.java | 37 +++++++++ .../service/impl/ProjectsInfoServiceImpl.java | 26 +++++++ 8 files changed, 325 insertions(+), 11 deletions(-) create mode 100644 external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/exception/ProjectsInfoServiceException.java create mode 100644 external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/PlatformSection.java create mode 100644 external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/PlatformSectionLink.java create mode 100644 external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/Platforms.java create mode 100644 external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/ProjectsInfoService.java create mode 100644 external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/impl/ProjectsInfoServiceImpl.java diff --git a/api-project-platform/src/main/java/org/opendevstack/apiservice/projectplatform/facade/impl/ProjectsFacadeImpl.java b/api-project-platform/src/main/java/org/opendevstack/apiservice/projectplatform/facade/impl/ProjectsFacadeImpl.java index 67724e3..ce7b9a3 100644 --- a/api-project-platform/src/main/java/org/opendevstack/apiservice/projectplatform/facade/impl/ProjectsFacadeImpl.java +++ b/api-project-platform/src/main/java/org/opendevstack/apiservice/projectplatform/facade/impl/ProjectsFacadeImpl.java @@ -1,21 +1,16 @@ package org.opendevstack.apiservice.projectplatform.facade.impl; +import org.opendevstack.apiservice.externalservice.projectsinfoservice.exception.ProjectsInfoServiceException; +import org.opendevstack.apiservice.externalservice.projectsinfoservice.model.Platforms; +import org.opendevstack.apiservice.externalservice.projectsinfoservice.service.ProjectsInfoService; import org.opendevstack.apiservice.projectplatform.exception.ProjectPlatformsException; import org.opendevstack.apiservice.projectplatform.facade.ProjectsFacade; +import org.opendevstack.apiservice.projectplatform.mapper.ProjectPlatformsMapper; import org.opendevstack.apiservice.projectplatform.model.ProjectPlatforms; import org.springframework.stereotype.Component; @Component public class ProjectsFacadeImpl implements ProjectsFacade { - @Override - public ProjectPlatforms getProjectPlatforms(String projectKey) throws ProjectPlatformsException { - return null; - } - - - - /* - FIXME: Add real code private final ProjectsInfoService projectsInfoService; private final ProjectPlatformsMapper mapper; @@ -36,5 +31,4 @@ public ProjectPlatforms getProjectPlatforms(String projectKey) throws ProjectPla } } - */ } diff --git a/api-project-platform/src/main/java/org/opendevstack/apiservice/projectplatform/mapper/ProjectPlatformsMapper.java b/api-project-platform/src/main/java/org/opendevstack/apiservice/projectplatform/mapper/ProjectPlatformsMapper.java index 6cc2156..de07a84 100644 --- a/api-project-platform/src/main/java/org/opendevstack/apiservice/projectplatform/mapper/ProjectPlatformsMapper.java +++ b/api-project-platform/src/main/java/org/opendevstack/apiservice/projectplatform/mapper/ProjectPlatformsMapper.java @@ -1,13 +1,88 @@ package org.opendevstack.apiservice.projectplatform.mapper; +import org.opendevstack.apiservice.externalservice.projectsinfoservice.model.PlatformSection; +import org.opendevstack.apiservice.externalservice.projectsinfoservice.model.PlatformSectionLink; +import org.opendevstack.apiservice.externalservice.projectsinfoservice.model.Platforms; +import org.opendevstack.apiservice.projectplatform.model.Link; +import org.opendevstack.apiservice.projectplatform.model.ProjectPlatforms; +import org.opendevstack.apiservice.projectplatform.model.Section; import org.springframework.stereotype.Component; +import java.util.stream.Collectors; + /** * Mapper class for converting between external service and API models. */ @Component public class ProjectPlatformsMapper { - // FIXME: Add proper code + /** + * Converts external service ProjectPlatforms to API ProjectPlatforms. + * + * @param externalPlatforms the external service ProjectPlatforms + * @return the API ProjectPlatforms + */ + public ProjectPlatforms toApiModel(Platforms externalPlatforms) { + + if (externalPlatforms == null) { + return null; + } + + ProjectPlatforms apiPlatforms = new ProjectPlatforms(); + + // Map sections + if (externalPlatforms.getSections() != null) { + apiPlatforms.setSections( + externalPlatforms.getSections().stream() + .map(this::toApiSection) + .collect(Collectors.toList()) + ); + } + + return apiPlatforms; + } + + /** + * Converts external service Section to API Section. + * + * @param externalSection the external service ProjectPlatformSection + * @return the API Section + */ + private Section toApiSection(PlatformSection externalSection) { + + if (externalSection == null) { + return null; + } + + Section apiSection = new Section(); + apiSection.setSection(externalSection.getSection()); + apiSection.setTooltip(externalSection.getTooltip()); + + // Map links + if (externalSection.getLinks() != null) { + apiSection.setLinks( + externalSection.getLinks().stream() + .map(this::toApiLink) + .collect(Collectors.toList()) + ); + } + + return apiSection; + } + + /** + * Converts external service Link to API Link. + * + * @param externalLink the external service ProjectPlatformSectionLink + * @return the API Link + */ + private Link toApiLink(PlatformSectionLink externalLink) { + + if (externalLink == null) { + return null; + } + + return new Link(externalLink.getLabel(), externalLink.getUrl(), externalLink.getTooltip(), externalLink.getType(), externalLink.getAbbreviation(), externalLink.getDisabled()); + } } diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/exception/ProjectsInfoServiceException.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/exception/ProjectsInfoServiceException.java new file mode 100644 index 0000000..f206ef8 --- /dev/null +++ b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/exception/ProjectsInfoServiceException.java @@ -0,0 +1,15 @@ +package org.opendevstack.apiservice.externalservice.projectsinfoservice.exception; + +/** + * Exception thrown when there are issues with projects info service operations. + */ +public class ProjectsInfoServiceException extends Exception { + + public ProjectsInfoServiceException(String message) { + super(message); + } + + public ProjectsInfoServiceException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/PlatformSection.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/PlatformSection.java new file mode 100644 index 0000000..51552e6 --- /dev/null +++ b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/PlatformSection.java @@ -0,0 +1,59 @@ +package org.opendevstack.apiservice.externalservice.projectsinfoservice.model; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import lombok.Data; +import org.opendevstack.apiservice.projects_info_service.model.Section; + +/** + * Represents a section of links from a specific platform associated to a project. + */ +@Data +public class PlatformSection { + + private String section; + private String tooltip; + private List links; + + public PlatformSection() { + // default constructor + } + + public PlatformSection(Section section) { + this.section = section.getSection(); + this.tooltip = section.getTooltip(); + this.links = section.getLinks().stream() + .map(PlatformSectionLink::new) + .toList(); + } + + /** + * Creates a ProjectPlatformSection from a raw map. + * + * @param rawSection the raw map containing section data + * @return a new ProjectPlatformSection instance + */ + public static PlatformSection fromMap(Map rawSection) { + PlatformSection section = new PlatformSection(); + + if (rawSection.containsKey("section")) { + section.setSection((String) rawSection.get("section")); + } + + if (rawSection.containsKey("tooltip")) { + section.setTooltip((String) rawSection.get("tooltip")); + } + + if (rawSection.containsKey("links")) { + @SuppressWarnings("unchecked") + List> rawLinks = (List>) rawSection.get("links"); + List links = rawLinks.stream() + .map(PlatformSectionLink::fromMap) + .collect(Collectors.toList()); + section.setLinks(links); + } + + return section; + } +} diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/PlatformSectionLink.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/PlatformSectionLink.java new file mode 100644 index 0000000..538a2cd --- /dev/null +++ b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/PlatformSectionLink.java @@ -0,0 +1,70 @@ +package org.opendevstack.apiservice.externalservice.projectsinfoservice.model; + +import lombok.Data; +import org.opendevstack.apiservice.projects_info_service.model.Link; + + +import java.util.Map; + +/** + * Represents a link from a specific platform associated to a project. + */ +@Data +public class PlatformSectionLink { + + private String label; + private String url; + private String tooltip; + private String type; + private String abbreviation; + private Boolean disabled; + + public PlatformSectionLink() { + // default constructor + } + + public PlatformSectionLink(Link link) { + this.label = link.getLabel(); + this.url = link.getUrl(); + this.tooltip = link.getTooltip(); + this.type = link.getType(); + this.abbreviation = link.getAbbreviation(); + this.disabled = link.getDisabled(); + } + + /** + * Creates a ProjectPlatformSectionLink from a raw map. + * + * @param rawLink the raw map containing link data + * @return a new ProjectPlatformSectionLink instance + */ + public static PlatformSectionLink fromMap(Map rawLink) { + PlatformSectionLink link = new PlatformSectionLink(); + + if (rawLink.containsKey("label")) { + link.setLabel((String) rawLink.get("label")); + } + + if (rawLink.containsKey("url") && rawLink.get("url") != null) { + link.setUrl((String) rawLink.get("url")); + } + + if (rawLink.containsKey("tooltip")) { + link.setTooltip((String) rawLink.get("tooltip")); + } + + if (rawLink.containsKey("type")) { + link.setType((String) rawLink.get("type")); + } + + if (rawLink.containsKey("abbreviation")) { + link.setAbbreviation((String) rawLink.get("abbreviation")); + } + + if (rawLink.containsKey("disabled")) { + link.setDisabled((Boolean) rawLink.get("disabled")); + } + + return link; + } +} diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/Platforms.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/Platforms.java new file mode 100644 index 0000000..ce5956d --- /dev/null +++ b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/Platforms.java @@ -0,0 +1,38 @@ +package org.opendevstack.apiservice.externalservice.projectsinfoservice.model; + +import java.net.URI; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import lombok.Data; + +/** + * Represents the platforms information associated to a project. + */ +@Data +public class Platforms { + + private List sections; + + /** + * Creates a ProjectPlatforms from a raw map response. + * + * @param responseBody the raw map containing platforms data + * @return a new ProjectPlatforms instance + */ + public static Platforms fromMap(Map responseBody) { + Platforms platforms = new Platforms(); + + // Map sections + if (responseBody.containsKey("sections")) { + @SuppressWarnings("unchecked") + List> rawSections = (List>) responseBody.get("sections"); + List sections = rawSections.stream() + .map(PlatformSection::fromMap) + .collect(Collectors.toList()); + platforms.setSections(sections); + } + + return platforms; + } +} diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/ProjectsInfoService.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/ProjectsInfoService.java new file mode 100644 index 0000000..0d1c36f --- /dev/null +++ b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/ProjectsInfoService.java @@ -0,0 +1,37 @@ +package org.opendevstack.apiservice.externalservice.projectsinfoservice.service; + +import org.opendevstack.apiservice.externalservice.projectsinfoservice.exception.ProjectsInfoServiceException; +import org.opendevstack.apiservice.externalservice.projectsinfoservice.model.Platforms; + +/** + * Service interface for integrating with projects info service + * This interface provides a generic way to consume the service. + */ +public interface ProjectsInfoService { + + /** + * Retrieves the platforms associated with a given project. + * + * @param projectKey the key of the project you want to check + * @return the platforms details associated with the project + * @throws ProjectsInfoServiceException if workflow execution fails + */ + Platforms getProjectPlatforms(String projectKey) throws ProjectsInfoServiceException; + + /** + * Validates connection to the projects info service. + * + * @return true if connection is valid, false otherwise + */ + boolean validateConnection(); + + /** + * Checks if the projects info service is healthy and reachable. + * This method is used by health indicators and should not throw exceptions. + * + * @return true if the service is healthy, false otherwise + */ + boolean isHealthy(); + + +} diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/impl/ProjectsInfoServiceImpl.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/impl/ProjectsInfoServiceImpl.java new file mode 100644 index 0000000..ee2dc75 --- /dev/null +++ b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/impl/ProjectsInfoServiceImpl.java @@ -0,0 +1,26 @@ +package org.opendevstack.apiservice.externalservice.projectsinfoservice.service.impl; + +import lombok.extern.slf4j.Slf4j; +import org.opendevstack.apiservice.externalservice.projectsinfoservice.exception.ProjectsInfoServiceException; +import org.opendevstack.apiservice.externalservice.projectsinfoservice.model.Platforms; +import org.opendevstack.apiservice.externalservice.projectsinfoservice.service.ProjectsInfoService; +import org.springframework.stereotype.Service; + +@Service +@Slf4j +public class ProjectsInfoServiceImpl implements ProjectsInfoService { + @Override + public Platforms getProjectPlatforms(String projectKey) throws ProjectsInfoServiceException { + return null; + } + + @Override + public boolean validateConnection() { + return false; + } + + @Override + public boolean isHealthy() { + return false; + } +} From ae60288169da618655845d1dbda53e95700bb4f3 Mon Sep 17 00:00:00 2001 From: soriaoli Date: Tue, 17 Feb 2026 10:40:21 +0100 Subject: [PATCH 04/11] [ods-api-to-projecs-info-service] - Update facade. --- .../facade/impl/ProjectsFacadeImpl.java | 14 +-- .../mapper/ProjectPlatformsMapper.java | 77 +++++++++++- .../config/ProjectsInfoServiceConfig.java | 115 ++++++++++++++++++ .../ProjectsInfoServiceSslProperties.java | 65 ++++++++++ .../ProjectsInfoServiceException.java | 15 +++ .../model/PlatformSection.java | 59 +++++++++ .../model/PlatformSectionLink.java | 70 +++++++++++ .../projectsinfoservice/model/Platforms.java | 38 ++++++ .../service/ProjectsInfoService.java | 37 ++++++ .../service/impl/ProjectsInfoServiceImpl.java | 74 +++++++++++ 10 files changed, 553 insertions(+), 11 deletions(-) create mode 100644 external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/config/ProjectsInfoServiceConfig.java create mode 100644 external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/config/ProjectsInfoServiceSslProperties.java create mode 100644 external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/exception/ProjectsInfoServiceException.java create mode 100644 external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/PlatformSection.java create mode 100644 external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/PlatformSectionLink.java create mode 100644 external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/Platforms.java create mode 100644 external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/ProjectsInfoService.java create mode 100644 external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/impl/ProjectsInfoServiceImpl.java diff --git a/api-project-platform/src/main/java/org/opendevstack/apiservice/projectplatform/facade/impl/ProjectsFacadeImpl.java b/api-project-platform/src/main/java/org/opendevstack/apiservice/projectplatform/facade/impl/ProjectsFacadeImpl.java index 67724e3..ce7b9a3 100644 --- a/api-project-platform/src/main/java/org/opendevstack/apiservice/projectplatform/facade/impl/ProjectsFacadeImpl.java +++ b/api-project-platform/src/main/java/org/opendevstack/apiservice/projectplatform/facade/impl/ProjectsFacadeImpl.java @@ -1,21 +1,16 @@ package org.opendevstack.apiservice.projectplatform.facade.impl; +import org.opendevstack.apiservice.externalservice.projectsinfoservice.exception.ProjectsInfoServiceException; +import org.opendevstack.apiservice.externalservice.projectsinfoservice.model.Platforms; +import org.opendevstack.apiservice.externalservice.projectsinfoservice.service.ProjectsInfoService; import org.opendevstack.apiservice.projectplatform.exception.ProjectPlatformsException; import org.opendevstack.apiservice.projectplatform.facade.ProjectsFacade; +import org.opendevstack.apiservice.projectplatform.mapper.ProjectPlatformsMapper; import org.opendevstack.apiservice.projectplatform.model.ProjectPlatforms; import org.springframework.stereotype.Component; @Component public class ProjectsFacadeImpl implements ProjectsFacade { - @Override - public ProjectPlatforms getProjectPlatforms(String projectKey) throws ProjectPlatformsException { - return null; - } - - - - /* - FIXME: Add real code private final ProjectsInfoService projectsInfoService; private final ProjectPlatformsMapper mapper; @@ -36,5 +31,4 @@ public ProjectPlatforms getProjectPlatforms(String projectKey) throws ProjectPla } } - */ } diff --git a/api-project-platform/src/main/java/org/opendevstack/apiservice/projectplatform/mapper/ProjectPlatformsMapper.java b/api-project-platform/src/main/java/org/opendevstack/apiservice/projectplatform/mapper/ProjectPlatformsMapper.java index 6cc2156..de07a84 100644 --- a/api-project-platform/src/main/java/org/opendevstack/apiservice/projectplatform/mapper/ProjectPlatformsMapper.java +++ b/api-project-platform/src/main/java/org/opendevstack/apiservice/projectplatform/mapper/ProjectPlatformsMapper.java @@ -1,13 +1,88 @@ package org.opendevstack.apiservice.projectplatform.mapper; +import org.opendevstack.apiservice.externalservice.projectsinfoservice.model.PlatformSection; +import org.opendevstack.apiservice.externalservice.projectsinfoservice.model.PlatformSectionLink; +import org.opendevstack.apiservice.externalservice.projectsinfoservice.model.Platforms; +import org.opendevstack.apiservice.projectplatform.model.Link; +import org.opendevstack.apiservice.projectplatform.model.ProjectPlatforms; +import org.opendevstack.apiservice.projectplatform.model.Section; import org.springframework.stereotype.Component; +import java.util.stream.Collectors; + /** * Mapper class for converting between external service and API models. */ @Component public class ProjectPlatformsMapper { - // FIXME: Add proper code + /** + * Converts external service ProjectPlatforms to API ProjectPlatforms. + * + * @param externalPlatforms the external service ProjectPlatforms + * @return the API ProjectPlatforms + */ + public ProjectPlatforms toApiModel(Platforms externalPlatforms) { + + if (externalPlatforms == null) { + return null; + } + + ProjectPlatforms apiPlatforms = new ProjectPlatforms(); + + // Map sections + if (externalPlatforms.getSections() != null) { + apiPlatforms.setSections( + externalPlatforms.getSections().stream() + .map(this::toApiSection) + .collect(Collectors.toList()) + ); + } + + return apiPlatforms; + } + + /** + * Converts external service Section to API Section. + * + * @param externalSection the external service ProjectPlatformSection + * @return the API Section + */ + private Section toApiSection(PlatformSection externalSection) { + + if (externalSection == null) { + return null; + } + + Section apiSection = new Section(); + apiSection.setSection(externalSection.getSection()); + apiSection.setTooltip(externalSection.getTooltip()); + + // Map links + if (externalSection.getLinks() != null) { + apiSection.setLinks( + externalSection.getLinks().stream() + .map(this::toApiLink) + .collect(Collectors.toList()) + ); + } + + return apiSection; + } + + /** + * Converts external service Link to API Link. + * + * @param externalLink the external service ProjectPlatformSectionLink + * @return the API Link + */ + private Link toApiLink(PlatformSectionLink externalLink) { + + if (externalLink == null) { + return null; + } + + return new Link(externalLink.getLabel(), externalLink.getUrl(), externalLink.getTooltip(), externalLink.getType(), externalLink.getAbbreviation(), externalLink.getDisabled()); + } } diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/config/ProjectsInfoServiceConfig.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/config/ProjectsInfoServiceConfig.java new file mode 100644 index 0000000..118baeb --- /dev/null +++ b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/config/ProjectsInfoServiceConfig.java @@ -0,0 +1,115 @@ +package org.opendevstack.apiservice.externalservice.projectsinfoservice.config; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.SimpleClientHttpRequestFactory; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.util.StringUtils; +import org.springframework.web.client.RestTemplate; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.security.GeneralSecurityException; +import java.security.cert.X509Certificate; + +/** + * Configuration class for external service components. + */ +@Configuration +@EnableAsync +@EnableConfigurationProperties(ProjectsInfoServiceSslProperties.class) +@Slf4j +public class ProjectsInfoServiceConfig { + + private final ProjectsInfoServiceSslProperties sslProperties; + + public ProjectsInfoServiceConfig(ProjectsInfoServiceSslProperties sslProperties) { + this.sslProperties = sslProperties; + } + + /** + * Creates a RestTemplate bean for HTTP client operations with configurable SSL settings. + * + * @return RestTemplate instance with SSL configuration + */ + @Bean + public RestTemplate projectsInfoServiceRestTemplate(RestTemplateBuilder restTemplateBuilder) { + if (!sslProperties.isVerifyCertificates()) { + log.warn("SSL certificate verification is DISABLED - this should only be used in development environments"); + return createInsecureRestTemplate(); + } else { + log.info("SSL certificate verification is ENABLED"); + return createSecureRestTemplate(); + } + } + + private RestTemplate createInsecureRestTemplate() { + try { + // Create a trust manager that accepts all certificates + // WARNING: This is insecure and should only be used in development environments + TrustManager[] trustAllCerts = new TrustManager[] { + new X509TrustManager() { + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; // Return empty array instead of null + } + public void checkClientTrusted(X509Certificate[] certs, String authType) { + // Intentionally empty - accepts all client certificates (insecure) + } + public void checkServerTrusted(X509Certificate[] certs, String authType) { + // Intentionally empty - accepts all server certificates (insecure) + } + } + }; + + // Install the all-trusting trust manager + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, trustAllCerts, new java.security.SecureRandom()); + + // Create hostname verifier that accepts all hostnames (insecure) + HostnameVerifier allHostsValid = (hostname, session) -> true; + + // Create a custom request factory that uses our SSL configuration + SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory() { + @Override + protected void prepareConnection(HttpURLConnection connection, String httpMethod) throws IOException { + if (connection instanceof HttpsURLConnection httpsConnection) { + httpsConnection.setSSLSocketFactory(sslContext.getSocketFactory()); + httpsConnection.setHostnameVerifier(allHostsValid); + } + super.prepareConnection(connection, httpMethod); + } + }; + + return new RestTemplate(requestFactory); + + } catch (GeneralSecurityException e) { + log.warn("Failed to create insecure RestTemplate, falling back to default: {}", e.getMessage()); + return new RestTemplate(); + } + } + + private RestTemplate createSecureRestTemplate() { + try { + // If custom trust store is provided, configure it + if (StringUtils.hasText(sslProperties.getTrustStorePath())) { + log.info("Custom trust store specified: {} (custom trust store support can be added in future versions)", + sslProperties.getTrustStorePath()); + } + + // Return default RestTemplate with system SSL settings + return new RestTemplate(); + + } catch (Exception e) { + log.warn("Failed to create secure RestTemplate with custom trust store, using default: {}", e.getMessage()); + return new RestTemplate(); + } + } +} diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/config/ProjectsInfoServiceSslProperties.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/config/ProjectsInfoServiceSslProperties.java new file mode 100644 index 0000000..4cef9c8 --- /dev/null +++ b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/config/ProjectsInfoServiceSslProperties.java @@ -0,0 +1,65 @@ +package org.opendevstack.apiservice.externalservice.projectsinfoservice.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Configuration properties for SSL settings in external service calls. + */ +@ConfigurationProperties(prefix = "externalservices.projects-info-service.ssl") +public class ProjectsInfoServiceSslProperties { + + /** + * Whether to verify SSL certificates when making external service calls. + * Default is true for security. + */ + private boolean verifyCertificates = true; + + /** + * Path to the trust store file for SSL certificate validation. + * Optional - if not provided, uses system default trust store. + */ + private String trustStorePath; + + /** + * Password for the trust store. + */ + private String trustStorePassword; + + /** + * Type of the trust store (JKS, PKCS12, etc.). + * Default is JKS. + */ + private String trustStoreType = "JKS"; + + public boolean isVerifyCertificates() { + return verifyCertificates; + } + + public void setVerifyCertificates(boolean verifyCertificates) { + this.verifyCertificates = verifyCertificates; + } + + public String getTrustStorePath() { + return trustStorePath; + } + + public void setTrustStorePath(String trustStorePath) { + this.trustStorePath = trustStorePath; + } + + public String getTrustStorePassword() { + return trustStorePassword; + } + + public void setTrustStorePassword(String trustStorePassword) { + this.trustStorePassword = trustStorePassword; + } + + public String getTrustStoreType() { + return trustStoreType; + } + + public void setTrustStoreType(String trustStoreType) { + this.trustStoreType = trustStoreType; + } +} \ No newline at end of file diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/exception/ProjectsInfoServiceException.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/exception/ProjectsInfoServiceException.java new file mode 100644 index 0000000..f206ef8 --- /dev/null +++ b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/exception/ProjectsInfoServiceException.java @@ -0,0 +1,15 @@ +package org.opendevstack.apiservice.externalservice.projectsinfoservice.exception; + +/** + * Exception thrown when there are issues with projects info service operations. + */ +public class ProjectsInfoServiceException extends Exception { + + public ProjectsInfoServiceException(String message) { + super(message); + } + + public ProjectsInfoServiceException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/PlatformSection.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/PlatformSection.java new file mode 100644 index 0000000..51552e6 --- /dev/null +++ b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/PlatformSection.java @@ -0,0 +1,59 @@ +package org.opendevstack.apiservice.externalservice.projectsinfoservice.model; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import lombok.Data; +import org.opendevstack.apiservice.projects_info_service.model.Section; + +/** + * Represents a section of links from a specific platform associated to a project. + */ +@Data +public class PlatformSection { + + private String section; + private String tooltip; + private List links; + + public PlatformSection() { + // default constructor + } + + public PlatformSection(Section section) { + this.section = section.getSection(); + this.tooltip = section.getTooltip(); + this.links = section.getLinks().stream() + .map(PlatformSectionLink::new) + .toList(); + } + + /** + * Creates a ProjectPlatformSection from a raw map. + * + * @param rawSection the raw map containing section data + * @return a new ProjectPlatformSection instance + */ + public static PlatformSection fromMap(Map rawSection) { + PlatformSection section = new PlatformSection(); + + if (rawSection.containsKey("section")) { + section.setSection((String) rawSection.get("section")); + } + + if (rawSection.containsKey("tooltip")) { + section.setTooltip((String) rawSection.get("tooltip")); + } + + if (rawSection.containsKey("links")) { + @SuppressWarnings("unchecked") + List> rawLinks = (List>) rawSection.get("links"); + List links = rawLinks.stream() + .map(PlatformSectionLink::fromMap) + .collect(Collectors.toList()); + section.setLinks(links); + } + + return section; + } +} diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/PlatformSectionLink.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/PlatformSectionLink.java new file mode 100644 index 0000000..538a2cd --- /dev/null +++ b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/PlatformSectionLink.java @@ -0,0 +1,70 @@ +package org.opendevstack.apiservice.externalservice.projectsinfoservice.model; + +import lombok.Data; +import org.opendevstack.apiservice.projects_info_service.model.Link; + + +import java.util.Map; + +/** + * Represents a link from a specific platform associated to a project. + */ +@Data +public class PlatformSectionLink { + + private String label; + private String url; + private String tooltip; + private String type; + private String abbreviation; + private Boolean disabled; + + public PlatformSectionLink() { + // default constructor + } + + public PlatformSectionLink(Link link) { + this.label = link.getLabel(); + this.url = link.getUrl(); + this.tooltip = link.getTooltip(); + this.type = link.getType(); + this.abbreviation = link.getAbbreviation(); + this.disabled = link.getDisabled(); + } + + /** + * Creates a ProjectPlatformSectionLink from a raw map. + * + * @param rawLink the raw map containing link data + * @return a new ProjectPlatformSectionLink instance + */ + public static PlatformSectionLink fromMap(Map rawLink) { + PlatformSectionLink link = new PlatformSectionLink(); + + if (rawLink.containsKey("label")) { + link.setLabel((String) rawLink.get("label")); + } + + if (rawLink.containsKey("url") && rawLink.get("url") != null) { + link.setUrl((String) rawLink.get("url")); + } + + if (rawLink.containsKey("tooltip")) { + link.setTooltip((String) rawLink.get("tooltip")); + } + + if (rawLink.containsKey("type")) { + link.setType((String) rawLink.get("type")); + } + + if (rawLink.containsKey("abbreviation")) { + link.setAbbreviation((String) rawLink.get("abbreviation")); + } + + if (rawLink.containsKey("disabled")) { + link.setDisabled((Boolean) rawLink.get("disabled")); + } + + return link; + } +} diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/Platforms.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/Platforms.java new file mode 100644 index 0000000..ce5956d --- /dev/null +++ b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/Platforms.java @@ -0,0 +1,38 @@ +package org.opendevstack.apiservice.externalservice.projectsinfoservice.model; + +import java.net.URI; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import lombok.Data; + +/** + * Represents the platforms information associated to a project. + */ +@Data +public class Platforms { + + private List sections; + + /** + * Creates a ProjectPlatforms from a raw map response. + * + * @param responseBody the raw map containing platforms data + * @return a new ProjectPlatforms instance + */ + public static Platforms fromMap(Map responseBody) { + Platforms platforms = new Platforms(); + + // Map sections + if (responseBody.containsKey("sections")) { + @SuppressWarnings("unchecked") + List> rawSections = (List>) responseBody.get("sections"); + List sections = rawSections.stream() + .map(PlatformSection::fromMap) + .collect(Collectors.toList()); + platforms.setSections(sections); + } + + return platforms; + } +} diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/ProjectsInfoService.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/ProjectsInfoService.java new file mode 100644 index 0000000..0d1c36f --- /dev/null +++ b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/ProjectsInfoService.java @@ -0,0 +1,37 @@ +package org.opendevstack.apiservice.externalservice.projectsinfoservice.service; + +import org.opendevstack.apiservice.externalservice.projectsinfoservice.exception.ProjectsInfoServiceException; +import org.opendevstack.apiservice.externalservice.projectsinfoservice.model.Platforms; + +/** + * Service interface for integrating with projects info service + * This interface provides a generic way to consume the service. + */ +public interface ProjectsInfoService { + + /** + * Retrieves the platforms associated with a given project. + * + * @param projectKey the key of the project you want to check + * @return the platforms details associated with the project + * @throws ProjectsInfoServiceException if workflow execution fails + */ + Platforms getProjectPlatforms(String projectKey) throws ProjectsInfoServiceException; + + /** + * Validates connection to the projects info service. + * + * @return true if connection is valid, false otherwise + */ + boolean validateConnection(); + + /** + * Checks if the projects info service is healthy and reachable. + * This method is used by health indicators and should not throw exceptions. + * + * @return true if the service is healthy, false otherwise + */ + boolean isHealthy(); + + +} diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/impl/ProjectsInfoServiceImpl.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/impl/ProjectsInfoServiceImpl.java new file mode 100644 index 0000000..4578caf --- /dev/null +++ b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/impl/ProjectsInfoServiceImpl.java @@ -0,0 +1,74 @@ +package org.opendevstack.apiservice.externalservice.projectsinfoservice.service.impl; + +import lombok.extern.slf4j.Slf4j; +import org.opendevstack.apiservice.externalservice.projectsinfoservice.exception.ProjectsInfoServiceException; +import org.opendevstack.apiservice.externalservice.projectsinfoservice.model.Platforms; +import org.opendevstack.apiservice.externalservice.projectsinfoservice.service.ProjectsInfoService; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import java.util.Map; + +@Service +@Slf4j +public class ProjectsInfoServiceImpl implements ProjectsInfoService { + + @Qualifier("projectsInfoServiceRestTemplate") + private final RestTemplate restTemplate; + + @Value("${externalservices.projects-info-service.base-url:http://localhost:8080}") + private String baseUrl; + + public ProjectsInfoServiceImpl(RestTemplate restTemplate) { + this.restTemplate = restTemplate; + } + + @Override + public Platforms getProjectPlatforms(String projectKey) throws ProjectsInfoServiceException { + return null; + } + + @Override + public boolean validateConnection() { + try { + HttpHeaders headers = createHeaders(); + HttpEntity request = new HttpEntity<>(headers); + + String url = baseUrl + "/actuator/health"; + ResponseEntity> response = restTemplate.exchange(url, HttpMethod.GET, request, new ParameterizedTypeReference<>() {}); + + boolean isValid = response.getStatusCode().is2xxSuccessful(); + log.debug("Connection validation: {}", isValid ? "successful" : "failed"); + return isValid; + + } catch (Exception e) { + log.warn("Connection validation failed: {}", e.getMessage()); + return false; + } + } + + @Override + public boolean isHealthy() { + try { + // Use validateConnection for health checks, but don't log warnings on failure + // as health checks are frequent and failures are expected to be handled by the health indicator + return validateConnection(); + } catch (Exception e) { + log.debug("Health check failed: {}", e.getMessage()); + return false; + } + } + + private HttpHeaders createHeaders() { + HttpHeaders headers = new HttpHeaders(); + headers.set("Content-Type", "application/json"); + return headers; + } +} From b93e7a71e9d0ba7a0816d4b9d1f879af6f48d3e5 Mon Sep 17 00:00:00 2001 From: soriaoli Date: Tue, 17 Feb 2026 14:23:29 +0100 Subject: [PATCH 05/11] [ods-api-to-projecs-info-service] - Use new ProjectsApi auto-generated client. --- .../pom.xml | 64 ++++++++++++++++--- .../config/ProjectsInfoServiceConfig.java | 18 ++++++ .../model/PlatformSection.java | 2 +- .../model/PlatformSectionLink.java | 2 +- .../service/impl/ProjectsInfoServiceImpl.java | 53 ++------------- 5 files changed, 80 insertions(+), 59 deletions(-) diff --git a/external-service-projects-info-service/pom.xml b/external-service-projects-info-service/pom.xml index a561873..cd427a6 100644 --- a/external-service-projects-info-service/pom.xml +++ b/external-service-projects-info-service/pom.xml @@ -78,6 +78,22 @@ 3.18.0 + + + javax.annotation + javax.annotation-api + 1.3.2 + provided + + + + + com.google.code.findbugs + jsr305 + 3.0.2 + provided + + org.springframework.boot @@ -112,31 +128,59 @@ generate - spring - ${project.basedir} - spring-boot + java + ${project.basedir}/target/generated-sources/openapi + resttemplate ${project.basedir}/openapi/openapi-projects-info-service-v1.0.0.yaml - org.opendevstack.apiservice.projects_info_service.api - org.opendevstack.apiservice.projects_info_service.model - org.opendevstack.apiservice.projects_info_service - false + org.opendevstack.apiservice.externalservice.projects_info_service.v1_0_0.client.api + org.opendevstack.apiservice.externalservice.projects_info_service.v1_0_0.client.model + org.opendevstack.apiservice.externalservice.projects_info_service.v1_0_0.client + true + true + true false false false false - false + default=defaultValue - true + false true - springdoc + true true true true + + java8 + jackson + true + + + + + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + add-source + generate-sources + + add-source + + + + ${project.basedir}/target/generated-sources/openapi/src/main/java + + org.springframework.boot spring-boot-maven-plugin diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/config/ProjectsInfoServiceConfig.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/config/ProjectsInfoServiceConfig.java index 118baeb..6881360 100644 --- a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/config/ProjectsInfoServiceConfig.java +++ b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/config/ProjectsInfoServiceConfig.java @@ -1,6 +1,10 @@ package org.opendevstack.apiservice.externalservice.projectsinfoservice.config; import lombok.extern.slf4j.Slf4j; +import org.opendevstack.apiservice.externalservice.projects_info_service.v1_0_0.client.ApiClient; +import org.opendevstack.apiservice.externalservice.projects_info_service.v1_0_0.client.api.ProjectsApi; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.context.annotation.Bean; @@ -29,6 +33,9 @@ @Slf4j public class ProjectsInfoServiceConfig { + @Value("${externalservices.projects-info-service.base-url:http://localhost:8080}") + private String baseUrl; + private final ProjectsInfoServiceSslProperties sslProperties; public ProjectsInfoServiceConfig(ProjectsInfoServiceSslProperties sslProperties) { @@ -51,6 +58,17 @@ public RestTemplate projectsInfoServiceRestTemplate(RestTemplateBuilder restTemp } } + @Bean + public ApiClient apiClient(RestTemplate restTemplate) { + return new ApiClient(restTemplate); + } + + @Qualifier("ProjectsInfoServiceApiClient") + @Bean + public ProjectsApi projectsApi(ApiClient apiClient) { + return new ProjectsApi(apiClient); + } + private RestTemplate createInsecureRestTemplate() { try { // Create a trust manager that accepts all certificates diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/PlatformSection.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/PlatformSection.java index 51552e6..bc6b434 100644 --- a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/PlatformSection.java +++ b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/PlatformSection.java @@ -4,7 +4,7 @@ import java.util.Map; import java.util.stream.Collectors; import lombok.Data; -import org.opendevstack.apiservice.projects_info_service.model.Section; +import org.opendevstack.apiservice.externalservice.projects_info_service.v1_0_0.client.model.Section; /** * Represents a section of links from a specific platform associated to a project. diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/PlatformSectionLink.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/PlatformSectionLink.java index 538a2cd..c5b9304 100644 --- a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/PlatformSectionLink.java +++ b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/PlatformSectionLink.java @@ -1,7 +1,7 @@ package org.opendevstack.apiservice.externalservice.projectsinfoservice.model; import lombok.Data; -import org.opendevstack.apiservice.projects_info_service.model.Link; +import org.opendevstack.apiservice.externalservice.projects_info_service.v1_0_0.client.model.Link; import java.util.Map; diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/impl/ProjectsInfoServiceImpl.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/impl/ProjectsInfoServiceImpl.java index 4578caf..c66d0d5 100644 --- a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/impl/ProjectsInfoServiceImpl.java +++ b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/impl/ProjectsInfoServiceImpl.java @@ -1,34 +1,19 @@ package org.opendevstack.apiservice.externalservice.projectsinfoservice.service.impl; +import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.opendevstack.apiservice.externalservice.projects_info_service.v1_0_0.client.api.ProjectsApi; import org.opendevstack.apiservice.externalservice.projectsinfoservice.exception.ProjectsInfoServiceException; import org.opendevstack.apiservice.externalservice.projectsinfoservice.model.Platforms; import org.opendevstack.apiservice.externalservice.projectsinfoservice.service.ProjectsInfoService; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.core.ParameterizedTypeReference; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; -import org.springframework.web.client.RestTemplate; - -import java.util.Map; @Service +@AllArgsConstructor @Slf4j public class ProjectsInfoServiceImpl implements ProjectsInfoService { - @Qualifier("projectsInfoServiceRestTemplate") - private final RestTemplate restTemplate; - - @Value("${externalservices.projects-info-service.base-url:http://localhost:8080}") - private String baseUrl; - - public ProjectsInfoServiceImpl(RestTemplate restTemplate) { - this.restTemplate = restTemplate; - } + private ProjectsApi projectsApi; @Override public Platforms getProjectPlatforms(String projectKey) throws ProjectsInfoServiceException { @@ -37,38 +22,12 @@ public Platforms getProjectPlatforms(String projectKey) throws ProjectsInfoServi @Override public boolean validateConnection() { - try { - HttpHeaders headers = createHeaders(); - HttpEntity request = new HttpEntity<>(headers); - - String url = baseUrl + "/actuator/health"; - ResponseEntity> response = restTemplate.exchange(url, HttpMethod.GET, request, new ParameterizedTypeReference<>() {}); - - boolean isValid = response.getStatusCode().is2xxSuccessful(); - log.debug("Connection validation: {}", isValid ? "successful" : "failed"); - return isValid; - - } catch (Exception e) { - log.warn("Connection validation failed: {}", e.getMessage()); - return false; - } + return true; } @Override public boolean isHealthy() { - try { - // Use validateConnection for health checks, but don't log warnings on failure - // as health checks are frequent and failures are expected to be handled by the health indicator - return validateConnection(); - } catch (Exception e) { - log.debug("Health check failed: {}", e.getMessage()); - return false; - } + return true; } - private HttpHeaders createHeaders() { - HttpHeaders headers = new HttpHeaders(); - headers.set("Content-Type", "application/json"); - return headers; - } } From 4db0952087c5ba65eb42ae2975751878d093b92b Mon Sep 17 00:00:00 2001 From: soriaoli Date: Tue, 17 Feb 2026 15:03:27 +0100 Subject: [PATCH 06/11] [ods-api-to-projecs-info-service] - Get token from request and use it for authorize into projectsInfoService. --- api-project-platform/pom.xml | 10 ++++ .../facade/impl/ProjectsFacadeImpl.java | 25 ++++++++- .../mapper/ProjectPlatformsMapper.java | 12 ++-- .../model/PlatformSection.java | 56 ++----------------- .../projectsinfoservice/model/Platforms.java | 31 +--------- .../service/ProjectsInfoService.java | 5 +- .../service/impl/ProjectsInfoServiceImpl.java | 16 +++++- .../service/mapper/PlatformSectionMapper.java | 20 +++++++ .../service/mapper/PlatformsMapper.java | 21 +++++++ 9 files changed, 104 insertions(+), 92 deletions(-) create mode 100644 external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/mapper/PlatformSectionMapper.java create mode 100644 external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/mapper/PlatformsMapper.java diff --git a/api-project-platform/pom.xml b/api-project-platform/pom.xml index 1c15ede..695db8a 100644 --- a/api-project-platform/pom.xml +++ b/api-project-platform/pom.xml @@ -20,6 +20,16 @@ spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + org.springframework.boot diff --git a/api-project-platform/src/main/java/org/opendevstack/apiservice/projectplatform/facade/impl/ProjectsFacadeImpl.java b/api-project-platform/src/main/java/org/opendevstack/apiservice/projectplatform/facade/impl/ProjectsFacadeImpl.java index ce7b9a3..c0ee7f2 100644 --- a/api-project-platform/src/main/java/org/opendevstack/apiservice/projectplatform/facade/impl/ProjectsFacadeImpl.java +++ b/api-project-platform/src/main/java/org/opendevstack/apiservice/projectplatform/facade/impl/ProjectsFacadeImpl.java @@ -1,5 +1,6 @@ package org.opendevstack.apiservice.projectplatform.facade.impl; +import lombok.extern.slf4j.Slf4j; import org.opendevstack.apiservice.externalservice.projectsinfoservice.exception.ProjectsInfoServiceException; import org.opendevstack.apiservice.externalservice.projectsinfoservice.model.Platforms; import org.opendevstack.apiservice.externalservice.projectsinfoservice.service.ProjectsInfoService; @@ -8,8 +9,12 @@ import org.opendevstack.apiservice.projectplatform.mapper.ProjectPlatformsMapper; import org.opendevstack.apiservice.projectplatform.model.ProjectPlatforms; import org.springframework.stereotype.Component; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthentication; @Component +@Slf4j public class ProjectsFacadeImpl implements ProjectsFacade { private final ProjectsInfoService projectsInfoService; @@ -23,12 +28,30 @@ public ProjectsFacadeImpl(ProjectsInfoService projectsInfoService, ProjectPlatfo @Override public ProjectPlatforms getProjectPlatforms(String projectKey) throws ProjectPlatformsException { try { + var idToken = getIdToken(); + Platforms externalPlatforms = - projectsInfoService.getProjectPlatforms(projectKey); + projectsInfoService.getProjectPlatforms(projectKey, idToken); return mapper.toApiModel(externalPlatforms); } catch (ProjectsInfoServiceException e) { throw new ProjectPlatformsException("Failed to retrieve project platforms", e); } } + protected String getIdToken() { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + + log.debug("Authenticated user '{}'", auth.getName()); + + var token = "INVALID token"; + + if (auth instanceof BearerTokenAuthentication bearer) { + token = bearer.getToken().getTokenValue(); + } + + log.debug("Token extracted: {}", token); + + return token; + } + } diff --git a/api-project-platform/src/main/java/org/opendevstack/apiservice/projectplatform/mapper/ProjectPlatformsMapper.java b/api-project-platform/src/main/java/org/opendevstack/apiservice/projectplatform/mapper/ProjectPlatformsMapper.java index de07a84..9b9b0af 100644 --- a/api-project-platform/src/main/java/org/opendevstack/apiservice/projectplatform/mapper/ProjectPlatformsMapper.java +++ b/api-project-platform/src/main/java/org/opendevstack/apiservice/projectplatform/mapper/ProjectPlatformsMapper.java @@ -31,9 +31,9 @@ public ProjectPlatforms toApiModel(Platforms externalPlatforms) { ProjectPlatforms apiPlatforms = new ProjectPlatforms(); // Map sections - if (externalPlatforms.getSections() != null) { + if (externalPlatforms.sections() != null) { apiPlatforms.setSections( - externalPlatforms.getSections().stream() + externalPlatforms.sections().stream() .map(this::toApiSection) .collect(Collectors.toList()) ); @@ -55,13 +55,13 @@ private Section toApiSection(PlatformSection externalSection) { } Section apiSection = new Section(); - apiSection.setSection(externalSection.getSection()); - apiSection.setTooltip(externalSection.getTooltip()); + apiSection.setSection(externalSection.section()); + apiSection.setTooltip(externalSection.tooltip()); // Map links - if (externalSection.getLinks() != null) { + if (externalSection.links() != null) { apiSection.setLinks( - externalSection.getLinks().stream() + externalSection.links().stream() .map(this::toApiLink) .collect(Collectors.toList()) ); diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/PlatformSection.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/PlatformSection.java index bc6b434..6b00f57 100644 --- a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/PlatformSection.java +++ b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/PlatformSection.java @@ -1,59 +1,13 @@ package org.opendevstack.apiservice.externalservice.projectsinfoservice.model; import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; -import lombok.Data; -import org.opendevstack.apiservice.externalservice.projects_info_service.v1_0_0.client.model.Section; /** * Represents a section of links from a specific platform associated to a project. */ -@Data -public class PlatformSection { - - private String section; - private String tooltip; - private List links; - - public PlatformSection() { - // default constructor - } - - public PlatformSection(Section section) { - this.section = section.getSection(); - this.tooltip = section.getTooltip(); - this.links = section.getLinks().stream() - .map(PlatformSectionLink::new) - .toList(); - } - - /** - * Creates a ProjectPlatformSection from a raw map. - * - * @param rawSection the raw map containing section data - * @return a new ProjectPlatformSection instance - */ - public static PlatformSection fromMap(Map rawSection) { - PlatformSection section = new PlatformSection(); - - if (rawSection.containsKey("section")) { - section.setSection((String) rawSection.get("section")); - } - - if (rawSection.containsKey("tooltip")) { - section.setTooltip((String) rawSection.get("tooltip")); - } - - if (rawSection.containsKey("links")) { - @SuppressWarnings("unchecked") - List> rawLinks = (List>) rawSection.get("links"); - List links = rawLinks.stream() - .map(PlatformSectionLink::fromMap) - .collect(Collectors.toList()); - section.setLinks(links); - } - - return section; - } +public record PlatformSection ( + String section, + String tooltip, + List links +){ } diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/Platforms.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/Platforms.java index ce5956d..8254026 100644 --- a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/Platforms.java +++ b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model/Platforms.java @@ -1,38 +1,9 @@ package org.opendevstack.apiservice.externalservice.projectsinfoservice.model; -import java.net.URI; import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; -import lombok.Data; /** * Represents the platforms information associated to a project. */ -@Data -public class Platforms { - - private List sections; - - /** - * Creates a ProjectPlatforms from a raw map response. - * - * @param responseBody the raw map containing platforms data - * @return a new ProjectPlatforms instance - */ - public static Platforms fromMap(Map responseBody) { - Platforms platforms = new Platforms(); - - // Map sections - if (responseBody.containsKey("sections")) { - @SuppressWarnings("unchecked") - List> rawSections = (List>) responseBody.get("sections"); - List sections = rawSections.stream() - .map(PlatformSection::fromMap) - .collect(Collectors.toList()); - platforms.setSections(sections); - } - - return platforms; - } +public record Platforms(List sections) { } diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/ProjectsInfoService.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/ProjectsInfoService.java index 0d1c36f..ec85c35 100644 --- a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/ProjectsInfoService.java +++ b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/ProjectsInfoService.java @@ -12,11 +12,12 @@ public interface ProjectsInfoService { /** * Retrieves the platforms associated with a given project. * - * @param projectKey the key of the project you want to check + * @param projectKey the key of the project you want to check. + * @param idToken the azure id token, used to log user into the app. * @return the platforms details associated with the project * @throws ProjectsInfoServiceException if workflow execution fails */ - Platforms getProjectPlatforms(String projectKey) throws ProjectsInfoServiceException; + Platforms getProjectPlatforms(String projectKey, String idToken) throws ProjectsInfoServiceException; /** * Validates connection to the projects info service. diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/impl/ProjectsInfoServiceImpl.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/impl/ProjectsInfoServiceImpl.java index c66d0d5..407188a 100644 --- a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/impl/ProjectsInfoServiceImpl.java +++ b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/impl/ProjectsInfoServiceImpl.java @@ -2,10 +2,13 @@ import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.opendevstack.apiservice.externalservice.projects_info_service.v1_0_0.client.ApiClient; import org.opendevstack.apiservice.externalservice.projects_info_service.v1_0_0.client.api.ProjectsApi; +import org.opendevstack.apiservice.externalservice.projects_info_service.v1_0_0.client.auth.HttpBearerAuth; import org.opendevstack.apiservice.externalservice.projectsinfoservice.exception.ProjectsInfoServiceException; import org.opendevstack.apiservice.externalservice.projectsinfoservice.model.Platforms; import org.opendevstack.apiservice.externalservice.projectsinfoservice.service.ProjectsInfoService; +import org.opendevstack.apiservice.externalservice.projectsinfoservice.service.mapper.PlatformsMapper; import org.springframework.stereotype.Service; @Service @@ -13,11 +16,20 @@ @Slf4j public class ProjectsInfoServiceImpl implements ProjectsInfoService { + private ApiClient apiClient; + private ProjectsApi projectsApi; + private PlatformsMapper platformsMapper; + @Override - public Platforms getProjectPlatforms(String projectKey) throws ProjectsInfoServiceException { - return null; + public Platforms getProjectPlatforms(String projectKey, String idToken) throws ProjectsInfoServiceException { + var auth = (HttpBearerAuth) apiClient.getAuthentication("bearerAuth"); + auth.setBearerToken(idToken); + + var projectPlatforms = projectsApi.getProjectPlatforms(projectKey); + + return platformsMapper.asPlatforms(projectPlatforms); } @Override diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/mapper/PlatformSectionMapper.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/mapper/PlatformSectionMapper.java new file mode 100644 index 0000000..817cb96 --- /dev/null +++ b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/mapper/PlatformSectionMapper.java @@ -0,0 +1,20 @@ +package org.opendevstack.apiservice.externalservice.projectsinfoservice.service.mapper; + +import lombok.AllArgsConstructor; +import org.opendevstack.apiservice.externalservice.projects_info_service.v1_0_0.client.model.Section; +import org.opendevstack.apiservice.externalservice.projectsinfoservice.model.PlatformSection; +import org.opendevstack.apiservice.externalservice.projectsinfoservice.model.PlatformSectionLink; +import org.springframework.stereotype.Service; + +@Service +@AllArgsConstructor +public class PlatformSectionMapper { + + public PlatformSection asPlatformSection(Section section) { + var links = section.getLinks().stream() + .map(PlatformSectionLink::new) + .toList(); + + return new PlatformSection(section.getSection(), section.getTooltip(), links); + } +} diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/mapper/PlatformsMapper.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/mapper/PlatformsMapper.java new file mode 100644 index 0000000..7ae5948 --- /dev/null +++ b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/mapper/PlatformsMapper.java @@ -0,0 +1,21 @@ +package org.opendevstack.apiservice.externalservice.projectsinfoservice.service.mapper; + +import lombok.AllArgsConstructor; +import org.opendevstack.apiservice.externalservice.projects_info_service.v1_0_0.client.model.ProjectPlatforms; +import org.opendevstack.apiservice.externalservice.projectsinfoservice.model.Platforms; +import org.springframework.stereotype.Service; + +@Service +@AllArgsConstructor +public class PlatformsMapper { + + private final PlatformSectionMapper platformSectionMapper; + + public Platforms asPlatforms(ProjectPlatforms projectPlatforms) { + var sections = projectPlatforms.getSections().stream() + .map(platformSectionMapper::asPlatformSection) + .toList(); + + return new Platforms(sections); + } +} From c1b82765d7cfd769e80c1a92106fafc6835c9b2c Mon Sep 17 00:00:00 2001 From: soriaoli Date: Tue, 17 Feb 2026 15:18:50 +0100 Subject: [PATCH 07/11] [ods-api-to-projecs-info-service] - Set base path. --- .../service/impl/ProjectsInfoServiceImpl.java | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/impl/ProjectsInfoServiceImpl.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/impl/ProjectsInfoServiceImpl.java index 407188a..331e9db 100644 --- a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/impl/ProjectsInfoServiceImpl.java +++ b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/impl/ProjectsInfoServiceImpl.java @@ -1,6 +1,6 @@ package org.opendevstack.apiservice.externalservice.projectsinfoservice.service.impl; -import lombok.AllArgsConstructor; +import jakarta.annotation.PostConstruct; import lombok.extern.slf4j.Slf4j; import org.opendevstack.apiservice.externalservice.projects_info_service.v1_0_0.client.ApiClient; import org.opendevstack.apiservice.externalservice.projects_info_service.v1_0_0.client.api.ProjectsApi; @@ -9,18 +9,32 @@ import org.opendevstack.apiservice.externalservice.projectsinfoservice.model.Platforms; import org.opendevstack.apiservice.externalservice.projectsinfoservice.service.ProjectsInfoService; import org.opendevstack.apiservice.externalservice.projectsinfoservice.service.mapper.PlatformsMapper; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @Service -@AllArgsConstructor @Slf4j public class ProjectsInfoServiceImpl implements ProjectsInfoService { - private ApiClient apiClient; + private final ApiClient apiClient; - private ProjectsApi projectsApi; + private final ProjectsApi projectsApi; - private PlatformsMapper platformsMapper; + private final PlatformsMapper platformsMapper; + + @Value("${externalservices.projects-info-service.base-url:http://localhost:8080}") + private String baseUrl; + + public ProjectsInfoServiceImpl(ApiClient apiClient, ProjectsApi projectsApi, PlatformsMapper platformsMapper) { + this.apiClient = apiClient; + this.projectsApi = projectsApi; + this.platformsMapper = platformsMapper; + } + + @PostConstruct + public void configureApiClient() { + this.apiClient.setBasePath(baseUrl); + } @Override public Platforms getProjectPlatforms(String projectKey, String idToken) throws ProjectsInfoServiceException { From 9679dee6364fde46f4f1c147735728998821155a Mon Sep 17 00:00:00 2001 From: soriaoli Date: Tue, 17 Feb 2026 16:09:38 +0100 Subject: [PATCH 08/11] [ods-api-to-projecs-info-service] - Add Unit tests. --- api-project-platform/pom.xml | 4 + .../facade/impl/ProjectsFacadeImplTest.java | 179 +++++++++--------- .../impl/ProjectsInfoServiceImplTest.java | 99 ++++++++++ 3 files changed, 191 insertions(+), 91 deletions(-) create mode 100644 external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/impl/ProjectsInfoServiceImplTest.java diff --git a/api-project-platform/pom.xml b/api-project-platform/pom.xml index 695db8a..b2b2b23 100644 --- a/api-project-platform/pom.xml +++ b/api-project-platform/pom.xml @@ -87,6 +87,10 @@ spring-boot-starter-test test + + org.springframework.security + spring-security-core + diff --git a/api-project-platform/src/test/java/org/opendevstack/apiservice/projectplatform/facade/impl/ProjectsFacadeImplTest.java b/api-project-platform/src/test/java/org/opendevstack/apiservice/projectplatform/facade/impl/ProjectsFacadeImplTest.java index fff4d41..8c6c007 100644 --- a/api-project-platform/src/test/java/org/opendevstack/apiservice/projectplatform/facade/impl/ProjectsFacadeImplTest.java +++ b/api-project-platform/src/test/java/org/opendevstack/apiservice/projectplatform/facade/impl/ProjectsFacadeImplTest.java @@ -1,117 +1,114 @@ package org.opendevstack.apiservice.projectplatform.facade.impl; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.junit.jupiter.MockitoExtension; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opendevstack.apiservice.externalservice.projectsinfoservice.exception.ProjectsInfoServiceException; +import org.opendevstack.apiservice.externalservice.projectsinfoservice.model.PlatformSection; +import org.opendevstack.apiservice.externalservice.projectsinfoservice.model.Platforms; +import org.opendevstack.apiservice.externalservice.projectsinfoservice.service.ProjectsInfoService; +import org.opendevstack.apiservice.projectplatform.exception.ProjectPlatformsException; +import org.opendevstack.apiservice.projectplatform.mapper.ProjectPlatformsMapper; +import org.opendevstack.apiservice.projectplatform.model.ProjectPlatforms; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthentication; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; -@ExtendWith(MockitoExtension.class) class ProjectsFacadeImplTest { - /* - FIXME: Add proper code - - @Mock private ProjectsInfoService projectsInfoService; - - @Mock private ProjectPlatformsMapper mapper; - private ProjectsFacadeImpl facade; + private ProjectsFacadeImpl sut; @BeforeEach - void setUp() { - facade = new ProjectsFacadeImpl(projectsInfoService, mapper); + void setup() { + projectsInfoService = mock(ProjectsInfoService.class); + mapper = mock(ProjectPlatformsMapper.class); + + sut = new ProjectsFacadeImpl(projectsInfoService, mapper); + + // Reset security context before each test + SecurityContextHolder.clearContext(); } - @Test - void givenAnyProjectKey_whenGetProjectPlatforms_thenGetMockProjectPlatforms() throws ProjectsInfoServiceException, ProjectPlatformsException { - // Arrange - String projectKey = "DEVSTACK"; - - // Create external service response - Platforms externalPlatforms = new Platforms(); - - // Create expected API response - ProjectPlatforms expectedPlatforms = createExpectedProjectPlatforms(); - - // Mock behavior - when(projectsInfoService.getProjectPlatforms(projectKey)).thenReturn(externalPlatforms); - when(mapper.toApiModel(externalPlatforms)).thenReturn(expectedPlatforms); - - // Act - ProjectPlatforms result = facade.getProjectPlatforms(projectKey); - - // Assert - assertNotNull(result, "Result should not be null"); - - List
    sections = result.getSections(); - assertNotNull(sections, "Sections should not be null"); - assertEquals(3, sections.size(), "There should be 3 sections"); - - // Validate first section - Section appPlatformSection = sections.get(0); - assertEquals("Project Shortcuts - Application Platform", appPlatformSection.getSection()); - assertEquals(4, appPlatformSection.getLinks().size()); - assertTrue(appPlatformSection.getLinks().stream().anyMatch(link -> link.getLabel().equals("JIRA"))); - assertTrue(appPlatformSection.getLinks().stream().allMatch(link -> link.getUrl().equals("https://www.google.com"))); - - // Validate second section - Section dataPlatformSection = sections.get(1); - assertEquals("Project Shortcuts - Data Platform", dataPlatformSection.getSection()); - assertEquals(2, dataPlatformSection.getLinks().size()); - - // Validate third section - Section servicesSection = sections.get(2); - assertEquals("Services", servicesSection.getSection()); - assertEquals(3, servicesSection.getLinks().size()); - - // Verify interactions - verify(projectsInfoService, times(1)).getProjectPlatforms(projectKey); - verify(mapper, times(1)).toApiModel(externalPlatforms); + @AfterEach + void cleanup() { + SecurityContextHolder.clearContext(); } @Test - void givenProjectsInfoServiceThrowsException_whenGetProjectPlatforms_thenRuntimeExceptionIsThrown() throws ProjectsInfoServiceException { - // Arrange - String projectKey = "DEVSTACK"; - when(projectsInfoService.getProjectPlatforms(projectKey)) - .thenThrow(new ProjectsInfoServiceException("Service error")); - - // Act & Assert - ProjectPlatformsException exception = assertThrows(ProjectPlatformsException.class, () -> facade.getProjectPlatforms(projectKey)); - - assertEquals("Failed to retrieve project platforms", exception.getMessage()); - verify(projectsInfoService, times(1)).getProjectPlatforms(projectKey); - verify(mapper, never()).toApiModel(any()); + void getProjectPlatforms_whenValidBearerToken_thenReturnMappedApiModel() throws Exception { + //given + String projectKey = "PROJ"; + String tokenValue = "id-token-123"; + + prepareMocksForTokenExtraction(tokenValue); + + List sections = List.of(); + Platforms externalPlatforms = new Platforms(sections); + when(projectsInfoService.getProjectPlatforms(projectKey, tokenValue)).thenReturn(externalPlatforms); + + ProjectPlatforms mapped = new ProjectPlatforms(); + when(mapper.toApiModel(externalPlatforms)).thenReturn(mapped); + + //when + ProjectPlatforms result = sut.getProjectPlatforms(projectKey); + + //then + verify(projectsInfoService).getProjectPlatforms(projectKey, tokenValue); + verify(mapper).toApiModel(externalPlatforms); + + assertThat(result).isSameAs(mapped); } - private ProjectPlatforms createExpectedProjectPlatforms() { - ProjectPlatforms platforms = new ProjectPlatforms(); + @Test + void getProjectPlatforms_whenInfoServiceThrowsException_thenWrapInProjectPlatformsException() throws Exception { + //given + String projectKey = "PROJ"; + String tokenValue = "id-token-123"; + + prepareMocksForTokenExtraction(tokenValue); - // Set sections - Section appPlatformSection = new Section("Project Shortcuts - Application Platform", "tooltip", List.of( - new Link("JIRA", "https://www.google.com", "tooltip", "type", "abbreviation", false), - new Link("Bitbucket", "https://www.google.com", "tooltip", "type", "abbreviation", false), - new Link("Confluence", "https://www.google.com", "tooltip", "type", "abbreviation", false), - new Link("Jenkins", "https://www.google.com", "tooltip", "type", "abbreviation", false) - )); + when(projectsInfoService.getProjectPlatforms(projectKey, tokenValue)) + .thenThrow(new ProjectsInfoServiceException("boom")); - Section dataPlatformSection = new Section("Project Shortcuts - Data Platform", "tooltip", List.of( - new Link("EKG", "https://www.google.com", "tooltip", "type", "abbreviation", false), - new Link("EDGC", "https://www.google.com", "tooltip", "type", "abbreviation", false) - )); + //when/then + assertThatThrownBy(() -> sut.getProjectPlatforms(projectKey)) + .isInstanceOf(ProjectPlatformsException.class) + .hasMessageContaining("Failed to retrieve project platforms"); + } - Section servicesSection = new Section("Services", "tooltip", List.of( - new Link("Service Onboarding", "https://www.google.com", "tooltip", "type", "abbreviation", false), - new Link("Documentation", "https://www.google.com", "tooltip", "type", "abbreviation", false), - new Link("Service Training", "https://www.google.com", "tooltip", "type", "abbreviation", false) - )); + @Test + void getIdToken_whenNoBearerAuthentication_thenReturnInvalidToken() { + //given + SecurityContextHolder.getContext().setAuthentication( + new TestingAuthenticationToken("user", "pwd") + ); - platforms.setSections(List.of(appPlatformSection, dataPlatformSection, servicesSection)); + //when + String token = sut.getIdToken(); - return platforms; + //then + assertThat(token).isEqualTo("INVALID token"); } + private void prepareMocksForTokenExtraction(String tokenValue) { + OAuth2AccessToken accessToken = mock(OAuth2AccessToken.class); + BearerTokenAuthentication authentication = mock(BearerTokenAuthentication.class); - */ -} + when(authentication.getToken()).thenReturn(accessToken); + when(accessToken.getTokenValue()).thenReturn(tokenValue); + SecurityContextHolder.getContext().setAuthentication(authentication); + } +} \ No newline at end of file diff --git a/external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/impl/ProjectsInfoServiceImplTest.java b/external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/impl/ProjectsInfoServiceImplTest.java new file mode 100644 index 0000000..b9ae0c1 --- /dev/null +++ b/external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/impl/ProjectsInfoServiceImplTest.java @@ -0,0 +1,99 @@ +package org.opendevstack.apiservice.externalservice.projectsinfoservice.service.impl; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opendevstack.apiservice.externalservice.projects_info_service.v1_0_0.client.ApiClient; +import org.opendevstack.apiservice.externalservice.projects_info_service.v1_0_0.client.api.ProjectsApi; +import org.opendevstack.apiservice.externalservice.projects_info_service.v1_0_0.client.auth.HttpBearerAuth; +import org.opendevstack.apiservice.externalservice.projects_info_service.v1_0_0.client.model.ProjectPlatforms; +import org.opendevstack.apiservice.externalservice.projectsinfoservice.model.PlatformSection; +import org.opendevstack.apiservice.externalservice.projectsinfoservice.model.Platforms; +import org.opendevstack.apiservice.externalservice.projectsinfoservice.service.mapper.PlatformsMapper; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class ProjectsInfoServiceImplTest { + + private ApiClient apiClient; + private ProjectsApi projectsApi; + private PlatformsMapper platformsMapper; + + private ProjectsInfoServiceImpl projectsInfoService; + + @BeforeEach + void setup() { + apiClient = mock(ApiClient.class); + projectsApi = mock(ProjectsApi.class); + platformsMapper = mock(PlatformsMapper.class); + + projectsInfoService = new ProjectsInfoServiceImpl(apiClient, projectsApi, platformsMapper); + } + + @Test + void configureApiClient_whenPostConstructCalled_thenBasePathIsConfigured() { + //given + String expectedBaseUrl = "http://localhost:8080"; + ReflectionTestUtils.setField(projectsInfoService, "baseUrl", expectedBaseUrl); + + //when + projectsInfoService.configureApiClient(); + + //then + verify(apiClient).setBasePath(expectedBaseUrl); + } + + @Test + void getProjectPlatforms_whenCalled_thenTokenSetAndPlatformsMapped() throws Exception { + //given + String projectKey = "PROJ"; + String idToken = "token-123"; + + var auth = mock(HttpBearerAuth.class); + when(apiClient.getAuthentication("bearerAuth")).thenReturn(auth); + + var apiResponse = new ProjectPlatforms(); + when(projectsApi.getProjectPlatforms(projectKey)).thenReturn(apiResponse); + + List sections = List.of(); + Platforms mapped = new Platforms(sections); + when(platformsMapper.asPlatforms(apiResponse)).thenReturn(mapped); + + //when + Platforms result = projectsInfoService.getProjectPlatforms(projectKey, idToken); + + //then + verify(auth).setBearerToken(idToken); + verify(projectsApi).getProjectPlatforms(projectKey); + verify(platformsMapper).asPlatforms(apiResponse); + + assertThat(result).isSameAs(mapped); + } + + @Test + void validateConnection_whenCalled_thenReturnTrue() { + //given + + //when + boolean result = projectsInfoService.validateConnection(); + + //then + assertThat(result).isTrue(); + } + + @Test + void isHealthy_whenCalled_thenReturnTrue() { + //given + + //when + boolean result = projectsInfoService.isHealthy(); + + //then + assertThat(result).isTrue(); + } +} \ No newline at end of file From baf68858dcc268397ee9dc9d74e18825f8f6002f Mon Sep 17 00:00:00 2001 From: soriaoli Date: Wed, 18 Feb 2026 09:14:57 +0100 Subject: [PATCH 09/11] [ods-api-to-projecs-info-service] - Get rid of idToken, as projects-info-service does not need it. --- .../facade/impl/ProjectsFacadeImpl.java | 23 +---------- .../facade/impl/ProjectsFacadeImplTest.java | 40 ++----------------- .../service/ProjectsInfoService.java | 3 +- .../service/impl/ProjectsInfoServiceImpl.java | 6 +-- .../impl/ProjectsInfoServiceImplTest.java | 6 +-- 5 files changed, 9 insertions(+), 69 deletions(-) diff --git a/api-project-platform/src/main/java/org/opendevstack/apiservice/projectplatform/facade/impl/ProjectsFacadeImpl.java b/api-project-platform/src/main/java/org/opendevstack/apiservice/projectplatform/facade/impl/ProjectsFacadeImpl.java index c0ee7f2..955fe90 100644 --- a/api-project-platform/src/main/java/org/opendevstack/apiservice/projectplatform/facade/impl/ProjectsFacadeImpl.java +++ b/api-project-platform/src/main/java/org/opendevstack/apiservice/projectplatform/facade/impl/ProjectsFacadeImpl.java @@ -9,9 +9,6 @@ import org.opendevstack.apiservice.projectplatform.mapper.ProjectPlatformsMapper; import org.opendevstack.apiservice.projectplatform.model.ProjectPlatforms; import org.springframework.stereotype.Component; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthentication; @Component @Slf4j @@ -28,30 +25,12 @@ public ProjectsFacadeImpl(ProjectsInfoService projectsInfoService, ProjectPlatfo @Override public ProjectPlatforms getProjectPlatforms(String projectKey) throws ProjectPlatformsException { try { - var idToken = getIdToken(); - Platforms externalPlatforms = - projectsInfoService.getProjectPlatforms(projectKey, idToken); + projectsInfoService.getProjectPlatforms(projectKey); return mapper.toApiModel(externalPlatforms); } catch (ProjectsInfoServiceException e) { throw new ProjectPlatformsException("Failed to retrieve project platforms", e); } } - protected String getIdToken() { - Authentication auth = SecurityContextHolder.getContext().getAuthentication(); - - log.debug("Authenticated user '{}'", auth.getName()); - - var token = "INVALID token"; - - if (auth instanceof BearerTokenAuthentication bearer) { - token = bearer.getToken().getTokenValue(); - } - - log.debug("Token extracted: {}", token); - - return token; - } - } diff --git a/api-project-platform/src/test/java/org/opendevstack/apiservice/projectplatform/facade/impl/ProjectsFacadeImplTest.java b/api-project-platform/src/test/java/org/opendevstack/apiservice/projectplatform/facade/impl/ProjectsFacadeImplTest.java index 8c6c007..d4e7e2b 100644 --- a/api-project-platform/src/test/java/org/opendevstack/apiservice/projectplatform/facade/impl/ProjectsFacadeImplTest.java +++ b/api-project-platform/src/test/java/org/opendevstack/apiservice/projectplatform/facade/impl/ProjectsFacadeImplTest.java @@ -10,10 +10,7 @@ import org.opendevstack.apiservice.projectplatform.exception.ProjectPlatformsException; import org.opendevstack.apiservice.projectplatform.mapper.ProjectPlatformsMapper; import org.opendevstack.apiservice.projectplatform.model.ProjectPlatforms; -import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.oauth2.core.OAuth2AccessToken; -import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthentication; import java.util.List; @@ -47,16 +44,13 @@ void cleanup() { } @Test - void getProjectPlatforms_whenValidBearerToken_thenReturnMappedApiModel() throws Exception { + void getProjectPlatforms_whenGetProjectPlatforms_thenReturnMappedApiModel() throws Exception { //given String projectKey = "PROJ"; - String tokenValue = "id-token-123"; - - prepareMocksForTokenExtraction(tokenValue); List sections = List.of(); Platforms externalPlatforms = new Platforms(sections); - when(projectsInfoService.getProjectPlatforms(projectKey, tokenValue)).thenReturn(externalPlatforms); + when(projectsInfoService.getProjectPlatforms(projectKey)).thenReturn(externalPlatforms); ProjectPlatforms mapped = new ProjectPlatforms(); when(mapper.toApiModel(externalPlatforms)).thenReturn(mapped); @@ -65,7 +59,7 @@ void getProjectPlatforms_whenValidBearerToken_thenReturnMappedApiModel() throws ProjectPlatforms result = sut.getProjectPlatforms(projectKey); //then - verify(projectsInfoService).getProjectPlatforms(projectKey, tokenValue); + verify(projectsInfoService).getProjectPlatforms(projectKey); verify(mapper).toApiModel(externalPlatforms); assertThat(result).isSameAs(mapped); @@ -75,11 +69,8 @@ void getProjectPlatforms_whenValidBearerToken_thenReturnMappedApiModel() throws void getProjectPlatforms_whenInfoServiceThrowsException_thenWrapInProjectPlatformsException() throws Exception { //given String projectKey = "PROJ"; - String tokenValue = "id-token-123"; - - prepareMocksForTokenExtraction(tokenValue); - when(projectsInfoService.getProjectPlatforms(projectKey, tokenValue)) + when(projectsInfoService.getProjectPlatforms(projectKey)) .thenThrow(new ProjectsInfoServiceException("boom")); //when/then @@ -88,27 +79,4 @@ void getProjectPlatforms_whenInfoServiceThrowsException_thenWrapInProjectPlatfor .hasMessageContaining("Failed to retrieve project platforms"); } - @Test - void getIdToken_whenNoBearerAuthentication_thenReturnInvalidToken() { - //given - SecurityContextHolder.getContext().setAuthentication( - new TestingAuthenticationToken("user", "pwd") - ); - - //when - String token = sut.getIdToken(); - - //then - assertThat(token).isEqualTo("INVALID token"); - } - - private void prepareMocksForTokenExtraction(String tokenValue) { - OAuth2AccessToken accessToken = mock(OAuth2AccessToken.class); - BearerTokenAuthentication authentication = mock(BearerTokenAuthentication.class); - - when(authentication.getToken()).thenReturn(accessToken); - when(accessToken.getTokenValue()).thenReturn(tokenValue); - - SecurityContextHolder.getContext().setAuthentication(authentication); - } } \ No newline at end of file diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/ProjectsInfoService.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/ProjectsInfoService.java index ec85c35..4935365 100644 --- a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/ProjectsInfoService.java +++ b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/ProjectsInfoService.java @@ -13,11 +13,10 @@ public interface ProjectsInfoService { * Retrieves the platforms associated with a given project. * * @param projectKey the key of the project you want to check. - * @param idToken the azure id token, used to log user into the app. * @return the platforms details associated with the project * @throws ProjectsInfoServiceException if workflow execution fails */ - Platforms getProjectPlatforms(String projectKey, String idToken) throws ProjectsInfoServiceException; + Platforms getProjectPlatforms(String projectKey) throws ProjectsInfoServiceException; /** * Validates connection to the projects info service. diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/impl/ProjectsInfoServiceImpl.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/impl/ProjectsInfoServiceImpl.java index 331e9db..b473cee 100644 --- a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/impl/ProjectsInfoServiceImpl.java +++ b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/impl/ProjectsInfoServiceImpl.java @@ -4,7 +4,6 @@ import lombok.extern.slf4j.Slf4j; import org.opendevstack.apiservice.externalservice.projects_info_service.v1_0_0.client.ApiClient; import org.opendevstack.apiservice.externalservice.projects_info_service.v1_0_0.client.api.ProjectsApi; -import org.opendevstack.apiservice.externalservice.projects_info_service.v1_0_0.client.auth.HttpBearerAuth; import org.opendevstack.apiservice.externalservice.projectsinfoservice.exception.ProjectsInfoServiceException; import org.opendevstack.apiservice.externalservice.projectsinfoservice.model.Platforms; import org.opendevstack.apiservice.externalservice.projectsinfoservice.service.ProjectsInfoService; @@ -37,10 +36,7 @@ public void configureApiClient() { } @Override - public Platforms getProjectPlatforms(String projectKey, String idToken) throws ProjectsInfoServiceException { - var auth = (HttpBearerAuth) apiClient.getAuthentication("bearerAuth"); - auth.setBearerToken(idToken); - + public Platforms getProjectPlatforms(String projectKey) throws ProjectsInfoServiceException { var projectPlatforms = projectsApi.getProjectPlatforms(projectKey); return platformsMapper.asPlatforms(projectPlatforms); diff --git a/external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/impl/ProjectsInfoServiceImplTest.java b/external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/impl/ProjectsInfoServiceImplTest.java index b9ae0c1..56b3048 100644 --- a/external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/impl/ProjectsInfoServiceImplTest.java +++ b/external-service-projects-info-service/src/test/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/impl/ProjectsInfoServiceImplTest.java @@ -49,10 +49,9 @@ void configureApiClient_whenPostConstructCalled_thenBasePathIsConfigured() { } @Test - void getProjectPlatforms_whenCalled_thenTokenSetAndPlatformsMapped() throws Exception { + void getProjectPlatforms_whenCalled_thenPlatformsMapped() throws Exception { //given String projectKey = "PROJ"; - String idToken = "token-123"; var auth = mock(HttpBearerAuth.class); when(apiClient.getAuthentication("bearerAuth")).thenReturn(auth); @@ -65,10 +64,9 @@ void getProjectPlatforms_whenCalled_thenTokenSetAndPlatformsMapped() throws Exce when(platformsMapper.asPlatforms(apiResponse)).thenReturn(mapped); //when - Platforms result = projectsInfoService.getProjectPlatforms(projectKey, idToken); + Platforms result = projectsInfoService.getProjectPlatforms(projectKey); //then - verify(auth).setBearerToken(idToken); verify(projectsApi).getProjectPlatforms(projectKey); verify(platformsMapper).asPlatforms(apiResponse); From 1033b39f989cfac272e1e89fda92eb0f4125827d Mon Sep 17 00:00:00 2001 From: soriaoli Date: Wed, 18 Feb 2026 09:39:43 +0100 Subject: [PATCH 10/11] [ods-api-to-projects-info-service] - Clean application configuration. --- application.yaml | 84 ------------------------------------------------ 1 file changed, 84 deletions(-) diff --git a/application.yaml b/application.yaml index 9ee9506..603ecc0 100644 --- a/application.yaml +++ b/application.yaml @@ -170,87 +170,3 @@ externalservices: projects-info-service: base-url: ${PROJECTS_INFO_SERVICE_BASE_URL:http://localhost:8081} - ssl: - verify-certificates: ${PROJECTS_INFO_SERVICE_SSL_VERIFY:true} - trust-store-path: ${PROJECTS_INFO_SERVICE_SSL_TRUSTSTORE_PATH:} - trust-store-password: ${PROJECTS_INFO_SERVICE_SSL_TRUSTSTORE_PASSWORD:} - trust-store-type: ${PROJECTS_INFO_SERVICE_SSL_TRUSTSTORE_TYPE:JKS} - azure: - access-token: ${PROJECTS_INFO_SERVICE_AZURE_ACCESS_TOKEN:tbc} - datahub: - group-id: ${PROJECTS_INFO_SERVICE_AZURE_DATA_HUB_GROUP_ID:tbc} - groups: - page-size: ${PROJECTS_INFO_SERVICE_AZURE_GROUPS_PAGE_SIZE:100} - testing-hub: - default: - projects: ${PROJECTS_INFO_SERVICE_TESTING_HUB_DEFAULT_PROJECTS:tbc} - api: - url: ${PROJECTS_INFO_SERVICE_TESTING_HUB_API_URL:tbc} - token: ${PROJECTS_INFO_SERVICE_TESTING_HUB_API_TOKEN:tbc} - page-size: ${PROJECTS_INFO_SERVICE_TESTING_HUB_API_PAGE_SIZE:100} - custom: - cache: - specs: - userGroups: - ttl: ${PROJECTS_INFO_SERVICE_CUSTOM_CACHE_TTL_SECONDS:60} - maxSize: ${PROJECTS_INFO_SERVICE_CUSTOM_CACHE_MAXIMUM_SIZE:100} - userGroups-fallback: - ttl: ${PROJECTS_INFO_SERVICE_CUSTOM_CACHE_FALLBACK_TTL_SECONDS:120} - maxSize: ${PROJECTS_INFO_SERVICE_CUSTOM_CACHE_FALLBACK_MAXIMUM_SIZE:100} - userEmail: - ttl: ${PROJECTS_INFO_SERVICE_CUSTOM_CACHE_TTL_SECONDS:60} - maxSize: ${PROJECTS_INFO_SERVICE_CUSTOM_CACHE_MAXIMUM_SIZE:100} - userEmail-fallback: - ttl: ${PROJECTS_INFO_SERVICE_CUSTOM_CACHE_FALLBACK_TTL_SECONDS:120} - maxSize: ${PROJECTS_INFO_SERVICE_CUSTOM_CACHE_FALLBACK_MAXIMUM_SIZE:100} - allEdpProjects: - ttl: ${PROJECTS_INFO_SERVICE_CUSTOM_CACHE_TTL_SECONDS:60} - maxSize: ${PROJECTS_INFO_SERVICE_CUSTOM_CACHE_MAXIMUM_SIZE:100} - allEdpProjects-fallback: - ttl: ${PROJECTS_INFO_SERVICE_CUSTOM_CACHE_FALLBACK_TTL_SECONDS:120} - maxSize: ${PROJECTS_INFO_SERVICE_CUSTOM_CACHE_FALLBACK_MAXIMUM_SIZE:100} - projectsInfoCache: - ttl: ${PROJECTS_INFO_SERVICE_CUSTOM_CACHE_TTL_SECONDS:60} - maxSize: ${PROJECTS_INFO_SERVICE_CUSTOM_CACHE_MAXIMUM_SIZE:100} - projectsInfoCache-fallback: - ttl: ${PROJECTS_INFO_SERVICE_CUSTOM_CACHE_FALLBACK_TTL_SECONDS:120} - maxSize: ${PROJECTS_INFO_SERVICE_CUSTOM_CACHE_FALLBACK_MAXIMUM_SIZE:100} - openshiftProjects: - ttl: ${PROJECTS_INFO_SERVICE_CUSTOM_CACHE_TTL_SECONDS:60} - maxSize: ${PROJECTS_INFO_SERVICE_CUSTOM_CACHE_MAXIMUM_SIZE:100} - openshiftProjects-fallback: - ttl: ${PROJECTS_INFO_SERVICE_CUSTOM_CACHE_FALLBACK_TTL_SECONDS:120} - maxSize: ${PROJECTS_INFO_SERVICE_CUSTOM_CACHE_FALLBACK_MAXIMUM_SIZE:100} - dataHubGroups: - ttl: ${PROJECTS_INFO_SERVICE_CUSTOM_CACHE_FALLBACK_TTL_SECONDS:120} - maxSize: ${PROJECTS_INFO_SERVICE_CUSTOM_CACHE_FALLBACK_MAXIMUM_SIZE:100} - testingHubGroups: - ttl: ${PROJECTS_INFO_SERVICE_CUSTOM_CACHE_FALLBACK_TTL_SECONDS:120} - maxSize: ${PROJECTS_INFO_SERVICE_CUSTOM_CACHE_FALLBACK_MAXIMUM_SIZE:100} - mock: - clusters: ${PROJECTS_INFO_SERVICE_MOCK_CLUSTERS:tbc} - projects: - default: ${PROJECTS_INFO_SERVICE_MOCK_DEFAULT_PROJECTS:tbc} - users: ${PROJECTS_INFO_SERVICE_MOCK_USER_PROJECTS:tbc} - openshift: - api: - clusters: - test: - url: ${PROJECTS_INFO_SERVICE_OPENSHIFT_TEST_URL:tbc} - token: ${PROJECTS_INFO_SERVICE_OPENSHIFT_TEST_TOKEN:tbc} - dev: - url: ${PROJECTS_INFO_SERVICE_OPENSHIFT_DEV_URL:tbc} - token: ${PROJECTS_INFO_SERVICE_OPENSHIFT_DEV_TOKEN:tbc} - project: - url: /apis/project.openshift.io/v1/projects - platforms: - bearer-token: ${PROJECTS_INFO_SERVICE_PLATFORMS_BEARER_TOKEN:tbc} - base-path: ${PROJECTS_INFO_SERVICE_PLATFORMS_BASE_PATH:tbc} - clusters: - test: ${PROJECTS_INFO_SERVICE_PLATFORMS_TEST_CLUSTER:tbc} - dev: ${PROJECTS_INFO_SERVICE_PLATFORMS_DEV_CLUSTER:tbc} - project: - filter: - project-roles-group-prefix: - # Properties to be used as lists cannot have leading or trailing blanks. - project-roles-group-suffixes: ROLE-A,ROLE-B From 8af87df693419d845a15da3fe458d71a542f5844 Mon Sep 17 00:00:00 2001 From: soriaoli Date: Wed, 18 Feb 2026 11:40:12 +0100 Subject: [PATCH 11/11] [ods-api-to-projects-info-service] - Add missing unit test. --- .../mapper/ProjectPlatformsMapperTest.java | 130 +++++------------- 1 file changed, 33 insertions(+), 97 deletions(-) diff --git a/api-project-platform/src/test/java/org/opendevstack/apiservice/projectplatform/mapper/ProjectPlatformsMapperTest.java b/api-project-platform/src/test/java/org/opendevstack/apiservice/projectplatform/mapper/ProjectPlatformsMapperTest.java index 2761f13..58f3a99 100644 --- a/api-project-platform/src/test/java/org/opendevstack/apiservice/projectplatform/mapper/ProjectPlatformsMapperTest.java +++ b/api-project-platform/src/test/java/org/opendevstack/apiservice/projectplatform/mapper/ProjectPlatformsMapperTest.java @@ -1,14 +1,27 @@ package org.opendevstack.apiservice.projectplatform.mapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; import org.mockito.junit.jupiter.MockitoExtension; +import org.opendevstack.apiservice.externalservice.projectsinfoservice.model.PlatformSection; +import org.opendevstack.apiservice.externalservice.projectsinfoservice.model.PlatformSectionLink; +import org.opendevstack.apiservice.externalservice.projectsinfoservice.model.Platforms; +import org.opendevstack.apiservice.projectplatform.model.ProjectPlatforms; +import org.opendevstack.apiservice.projectplatform.model.Section; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; @ExtendWith(MockitoExtension.class) class ProjectPlatformsMapperTest { - /* - FIXME: Add proper code - @InjectMocks private ProjectPlatformsMapper mapper; @@ -57,27 +70,7 @@ void testToApiModel_WithCompleteData_MapsAllFields() { @Test void testToApiModel_WithNullPlatformLinks_MapsOtherFields() { // Given - Platforms externalPlatforms = - new Platforms(); - externalPlatforms.setSections(new ArrayList<>()); - - // When - ProjectPlatforms result = mapper.toApiModel(externalPlatforms); - - // Then - assertNotNull(result); - assertNotNull(result.getSections()); - assertTrue(result.getSections().isEmpty()); - } - - @Test - void testToApiModel_WithNullDisabledPlatforms_MapsOtherFields() { - // Given - Platforms externalPlatforms = - new Platforms(); - Map platformLinks = new HashMap<>(); - platformLinks.put("github", URI.create("https://github.com")); - externalPlatforms.setSections(new ArrayList<>()); + Platforms externalPlatforms = new Platforms(new ArrayList<>()); // When ProjectPlatforms result = mapper.toApiModel(externalPlatforms); @@ -91,11 +84,7 @@ void testToApiModel_WithNullDisabledPlatforms_MapsOtherFields() { @Test void testToApiModel_WithNullSections_MapsOtherFields() { // Given - Platforms externalPlatforms = - new Platforms(); - Map platformLinks = new HashMap<>(); - platformLinks.put("github", URI.create("https://github.com")); - externalPlatforms.setSections(null); + Platforms externalPlatforms = new Platforms(null); // When ProjectPlatforms result = mapper.toApiModel(externalPlatforms); @@ -105,34 +94,11 @@ void testToApiModel_WithNullSections_MapsOtherFields() { assertTrue(result.getSections().isEmpty()); } - @Test - void testToApiModel_WithEmptyCollections_ReturnsEmptyCollections() { - // Given - Platforms externalPlatforms = - new Platforms(); - externalPlatforms.setSections(new ArrayList<>()); - - // When - ProjectPlatforms result = mapper.toApiModel(externalPlatforms); - - // Then - assertNotNull(result); - assertNotNull(result.getSections()); - assertTrue(result.getSections().isEmpty()); - } - @Test void testToApiModel_WithSectionContainingNullLinks_HandlesGracefully() { // Given - Platforms externalPlatforms = - new Platforms(); - - PlatformSection section = - new PlatformSection(); - section.setSection("Development"); - section.setLinks(null); - - externalPlatforms.setSections(List.of(section)); + PlatformSection section = new PlatformSection("Development", null, null); + Platforms externalPlatforms = new Platforms(List.of(section)); // When ProjectPlatforms result = mapper.toApiModel(externalPlatforms); @@ -148,15 +114,9 @@ void testToApiModel_WithSectionContainingNullLinks_HandlesGracefully() { @Test void testToApiModel_WithSectionContainingEmptyLinks_ReturnsEmptyLinks() { // Given - Platforms externalPlatforms = - new Platforms(); - - PlatformSection section = - new PlatformSection(); - section.setSection("Development"); - section.setLinks(new ArrayList<>()); + PlatformSection section = new PlatformSection("Development", null, new ArrayList<>()); - externalPlatforms.setSections(List.of(section)); + Platforms externalPlatforms = new Platforms(List.of(section)); // When ProjectPlatforms result = mapper.toApiModel(externalPlatforms); @@ -173,14 +133,11 @@ void testToApiModel_WithSectionContainingEmptyLinks_ReturnsEmptyLinks() { @Test void testToApiModel_WithSectionsContainingNullSection_HandlesGracefully() { // Given - Platforms externalPlatforms = - new Platforms(); - List sections = new ArrayList<>(); sections.add(null); - externalPlatforms.setSections(sections); + Platforms externalPlatforms = new Platforms(sections); // When ProjectPlatforms result = mapper.toApiModel(externalPlatforms); @@ -195,19 +152,13 @@ void testToApiModel_WithSectionsContainingNullSection_HandlesGracefully() { @Test void testToApiModel_WithLinksContainingNullLink_HandlesGracefully() { // Given - Platforms externalPlatforms = - new Platforms(); + List links = new ArrayList<>(); + links.add(null); - PlatformSection section = - new PlatformSection(); - section.setSection("Development"); + PlatformSection section = new PlatformSection("Development", null, links); - List links = - new ArrayList<>(); - links.add(null); - section.setLinks(links); + Platforms externalPlatforms = new Platforms(List.of(section)); - externalPlatforms.setSections(List.of(section)); // When ProjectPlatforms result = mapper.toApiModel(externalPlatforms); @@ -243,17 +194,7 @@ void testToApiModel_WithMultipleSectionsAndLinks_MapsCorrectly() { // Helper method to create a complete external ProjectPlatforms object private Platforms createCompleteExternalProjectPlatforms() { - Platforms externalPlatforms = - new Platforms(); - - // Setup sections - List sections = new ArrayList<>(); - // First section with 2 links - PlatformSection section1 = - new PlatformSection(); - section1.setSection("Development"); - List links1 = new ArrayList<>(); PlatformSectionLink link1 = @@ -268,14 +209,9 @@ private Platforms createCompleteExternalProjectPlatforms() { link2.setUrl("https://jira.com/board"); links1.add(link2); - section1.setLinks(links1); - sections.add(section1); + PlatformSection section1 = new PlatformSection("Development", null, links1); // Second section with 1 link - PlatformSection section2 = - new PlatformSection(); - section2.setSection("CI/CD"); - List links2 = new ArrayList<>(); PlatformSectionLink link3 = @@ -284,14 +220,14 @@ private Platforms createCompleteExternalProjectPlatforms() { link3.setUrl("https://jenkins.com/job"); links2.add(link3); - section2.setLinks(links2); - sections.add(section2); + PlatformSection section2 = + new PlatformSection("CI/CD", null, links2); - externalPlatforms.setSections(sections); + // Setup sections + List sections = new ArrayList<>(List.of(section1, section2)); - return externalPlatforms; + return new Platforms(sections); } - */ }